From 5986b8da72538d1a554d2da94a1325d3d8870c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 13:19:22 +0200 Subject: [PATCH 1/8] Allow server cameras to be streamable by removing precaution checks --- src/instamatic/camera/camera.py | 1 - src/instamatic/camera/camera_client.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/instamatic/camera/camera.py b/src/instamatic/camera/camera.py index fa806b07..d7085628 100644 --- a/src/instamatic/camera/camera.py +++ b/src/instamatic/camera/camera.py @@ -56,7 +56,6 @@ def Camera(name: str = None, as_stream: bool = False, use_server: bool = False): from instamatic.camera.camera_client import CamClient cam = CamClient(name=name, interface=interface) - as_stream = False # precaution else: cam_cls = get_cam(interface) diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index dcb63442..c184ae9c 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -55,7 +55,6 @@ def __init__( self.name = name self.interface = interface self._bufsize = BUFSIZE - self.streamable = False # overrides cam settings self.verbose = False try: From 1267ac43a385d27b5e6582db881d6a9f98ae2456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 14:14:48 +0200 Subject: [PATCH 2/8] Unify interface, naming between get_microscope/camera(_class) --- src/instamatic/camera/__init__.py | 2 +- src/instamatic/camera/camera.py | 30 +++++++++++++++++++------ src/instamatic/camera/camera_client.py | 4 ++-- src/instamatic/camera/videostream.py | 6 ++--- src/instamatic/config/autoconfig.py | 4 ++-- src/instamatic/controller.py | 4 ++-- src/instamatic/microscope/microscope.py | 4 ++-- src/instamatic/server/cam_server.py | 4 ++-- 8 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/instamatic/camera/__init__.py b/src/instamatic/camera/__init__.py index 950ec6da..d4db84a7 100644 --- a/src/instamatic/camera/__init__.py +++ b/src/instamatic/camera/__init__.py @@ -1,4 +1,4 @@ from __future__ import annotations -from .camera import Camera +from .camera import get_camera, get_camera_class from .videostream import VideoStream diff --git a/src/instamatic/camera/camera.py b/src/instamatic/camera/camera.py index d7085628..4deba61f 100644 --- a/src/instamatic/camera/camera.py +++ b/src/instamatic/camera/camera.py @@ -2,18 +2,20 @@ import logging from pathlib import Path +from typing import Optional from instamatic import config +from instamatic.camera.camera_base import CameraBase logger = logging.getLogger(__name__) -__all__ = ['Camera'] +__all__ = ['get_camera', 'get_camera_class'] default_cam_interface = config.camera.interface -def get_cam(interface: str = None): - """Grabs the camera object defined by `interface`""" +def get_camera_class(interface: str) -> type[CameraBase]: + """Grabs the camera class with the specific `interface`""" simulate = config.settings.simulate @@ -39,10 +41,24 @@ def get_cam(interface: str = None): return cam -def Camera(name: str = None, as_stream: bool = False, use_server: bool = False): +def get_camera( + name: Optional[str] = None, + as_stream: bool = False, + use_server: bool = False, +) -> CameraBase: """Initialize the camera identified by the 'name' parameter if `as_stream` is True, it will return a VideoStream object if `as_stream` is False, it - will return the raw Camera object.""" + will return the raw Camera object. + + name: Optional[str] + Specify which camera to use, must be implemented in `instamatic.camera` + as_stream: bool + If True (default False), allow streaming this camera image live. + use_server: bool + Connect to camera server running on the host/port defined in the config + + returns: Camera interface class instance + """ if name is None: name = config.camera.name @@ -57,7 +73,7 @@ def Camera(name: str = None, as_stream: bool = False, use_server: bool = False): cam = CamClient(name=name, interface=interface) else: - cam_cls = get_cam(interface) + cam_cls = get_camera_class(interface) if interface in ('timepix', 'pytimepix'): tpx_config = ( @@ -220,7 +236,7 @@ def main_entry(): if __name__ == '__main__': # main_entry() - cam = Camera(use_server=True) + cam = get_camera(use_server=True) arr = cam.get_image(exposure=0.1) print(arr) print(arr.shape) diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index c184ae9c..622f4cc6 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -155,9 +155,9 @@ def _eval_dct(self, dct): def _init_dict(self): """Get list of functions and their doc strings from the uninitialized class.""" - from instamatic.camera.camera import get_cam + from instamatic.camera.camera import get_camera_class - cam = get_cam(self.interface) + cam = get_camera_class(self.interface) self._dct = { key: value for key, value in cam.__dict__.items() if not key.startswith('_') diff --git a/src/instamatic/camera/videostream.py b/src/instamatic/camera/videostream.py index 84e4ed3b..13a4d7dd 100644 --- a/src/instamatic/camera/videostream.py +++ b/src/instamatic/camera/videostream.py @@ -10,7 +10,7 @@ import numpy as np -from instamatic.camera import Camera +from instamatic.camera import get_camera from instamatic.camera.camera_base import CameraBase from instamatic.image_utils import autoscale @@ -115,7 +115,7 @@ def from_any( cls: Type[VideoStream_T], cam: Union[CameraBase, str] = 'simulate' ) -> VideoStream_T: """Create a subclass based on passed cam or cam-str stream-ability.""" - cam: CameraBase = Camera(name=cam) if isinstance(cam, str) else cam + cam: CameraBase = get_camera(name=cam) if isinstance(cam, str) else cam if cls is VideoStream: return (LiveVideoStream if cam.streamable else FakeVideoStream)(cam) return cls(cam) @@ -123,7 +123,7 @@ def from_any( def __init__(self, cam: Union[CameraBase, str] = 'simulate') -> None: threading.Thread.__init__(self) - self.cam: CameraBase = Camera(name=cam) if isinstance(cam, str) else cam + self.cam: CameraBase = get_camera(name=cam) if isinstance(cam, str) else cam self.lock = threading.Lock() self.default_exposure = self.cam.default_exposure diff --git a/src/instamatic/config/autoconfig.py b/src/instamatic/config/autoconfig.py index b21c5930..96ef2fe2 100644 --- a/src/instamatic/config/autoconfig.py +++ b/src/instamatic/config/autoconfig.py @@ -125,12 +125,12 @@ def main(): cam_connect = False cam_name = None - from instamatic.camera.camera import get_cam + from instamatic.camera.camera import get_camera_class from instamatic.controller import TEMController from instamatic.microscope import get_microscope_class if cam_connect: - cam = get_cam(cam_name)() if cam_name else None + cam = get_camera_class(cam_name)() if cam_name else None else: cam = None diff --git a/src/instamatic/controller.py b/src/instamatic/controller.py index 2aa69fba..dea63ff2 100644 --- a/src/instamatic/controller.py +++ b/src/instamatic/controller.py @@ -9,7 +9,7 @@ import yaml from instamatic import config -from instamatic.camera import Camera +from instamatic.camera import get_camera from instamatic.camera.camera_base import CameraBase from instamatic.exceptions import TEMControllerError from instamatic.formats import write_tiff @@ -61,7 +61,7 @@ def initialize( print(f'Camera : {cam_name}{cam_tag}') - cam = Camera(cam_name, as_stream=stream, use_server=use_cam_server) + cam = get_camera(cam_name, as_stream=stream, use_server=use_cam_server) else: cam = None diff --git a/src/instamatic/microscope/microscope.py b/src/instamatic/microscope/microscope.py index 6f44791e..ac83d1c1 100644 --- a/src/instamatic/microscope/microscope.py +++ b/src/instamatic/microscope/microscope.py @@ -10,7 +10,7 @@ __all__ = ['get_microscope', 'get_microscope_class'] -def get_microscope_class(interface: str) -> 'type[MicroscopeBase]': +def get_microscope_class(interface: str) -> type[MicroscopeBase]: """Grab tem class with the specific 'interface'.""" simulate = config.settings.simulate @@ -42,7 +42,7 @@ def get_microscope(name: Optional[str] = None, use_server: bool = False) -> Micr use_server: bool Connect to microscope server running on the host/port defined in the config file - returns: TEM interface class + returns: TEM interface class instance """ if name is None: interface = default_tem_interface diff --git a/src/instamatic/server/cam_server.py b/src/instamatic/server/cam_server.py index bba3dc8d..2d3c926f 100644 --- a/src/instamatic/server/cam_server.py +++ b/src/instamatic/server/cam_server.py @@ -11,7 +11,7 @@ import numpy as np from instamatic import config -from instamatic.camera import Camera +from instamatic.camera import get_camera from instamatic.utils import high_precision_timers from .serializer import dumper, loader @@ -81,7 +81,7 @@ def copy_data_to_shared_buffer(self, arr): def run(self): """Start server thread.""" - self.cam = Camera(name=self._name, use_server=False) + self.cam = get_camera(name=self._name, use_server=False) self.cam.get_attrs = self.get_attrs print(f'Initialized camera: {self.cam.interface}') From 65a2707b0a39d62e79c509589a19e1b239ca485f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 14:19:16 +0200 Subject: [PATCH 3/8] Remove unused _init_attr_dict/get_attrs from microscope client/server --- src/instamatic/microscope/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/instamatic/microscope/client.py b/src/instamatic/microscope/client.py index 1d34d27c..52150412 100644 --- a/src/instamatic/microscope/client.py +++ b/src/instamatic/microscope/client.py @@ -125,10 +125,6 @@ def _init_dict(self) -> None: } self._dct['get_attrs'] = None - def _init_attr_dict(self): - """Get list of attrs and their types.""" - self._attr_dct = self.get_attrs() - def __dir__(self) -> list: return list(self._dct.keys()) From e64c51fa1f880a1e8744bfa6be9c6fb14409a266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 18:08:42 +0200 Subject: [PATCH 4/8] Do not force server-side attributes to be callable, use them first: these 3 lines took ~3h --- src/instamatic/camera/camera_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index 622f4cc6..ebdf5125 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -82,6 +82,7 @@ def __init__( self.buffers: Dict[str, np.ndarray] = {} self.shms = {} + self._attr_dct: dict = {} self._init_dict() self._init_attr_dict() @@ -103,6 +104,8 @@ def connect(self): def __getattr__(self, attr_name): if attr_name in self._dct: + if attr_name in object.__getattribute__(self, '_attr_dct'): + return self._eval_dct({'attr_name': attr_name}) wrapped = self._dct[attr_name] elif attr_name in self._attr_dct: dct = {'attr_name': attr_name} From 81b099c36ae254fe10ab938401912282c25b11d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 18:20:42 +0200 Subject: [PATCH 5/8] EAFP: Allow camera to call functions whether they are registered or not --- src/instamatic/camera/camera_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index ebdf5125..6b320657 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -111,9 +111,7 @@ def __getattr__(self, attr_name): dct = {'attr_name': attr_name} return self._eval_dct(dct) else: - raise AttributeError( - f'`{self.__class__.__name__}` object has no attribute `{attr_name}`' - ) + wrapped = None # AFAIK can't wrap with None, can cause odd errors @wraps(wrapped) def wrapper(*args, **kwargs): From 69210a15322063cd056577f6137812b116d27260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 18:23:54 +0200 Subject: [PATCH 6/8] EAFP: Allow camera to call unregistered functions - fixes server cameras --- src/instamatic/camera/camera_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index 6b320657..952d26c4 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -103,6 +103,7 @@ def connect(self): print(f'Connected to CAM server ({HOST}:{PORT})') def __getattr__(self, attr_name): + print(self._dct) if attr_name in self._dct: if attr_name in object.__getattribute__(self, '_attr_dct'): return self._eval_dct({'attr_name': attr_name}) From 2322ece5f6daf4ba01592c63c3da32418b4f79ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 19:33:18 +0200 Subject: [PATCH 7/8] Remove unnecessary print debug statement --- src/instamatic/camera/camera_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index 952d26c4..6b320657 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -103,7 +103,6 @@ def connect(self): print(f'Connected to CAM server ({HOST}:{PORT})') def __getattr__(self, attr_name): - print(self._dct) if attr_name in self._dct: if attr_name in object.__getattribute__(self, '_attr_dct'): return self._eval_dct({'attr_name': attr_name}) From 1f5ecd21195ab6a4a523e943225d6c523ad95b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 26 Sep 2025 20:04:09 +0200 Subject: [PATCH 8/8] Add **streamable** description to `config.md` documentation --- docs/config.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/config.md b/docs/config.md index 14984e40..276022a3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -230,6 +230,9 @@ This file holds the specifications of the camera. This file is must be located t **camera_rotation_vs_stage_xy** : In radians, give here the rotation of the position of the rotation axis with respect to the horizontal. Used for diffraction only. Corresponds to the rotation axis in RED and PETS, for example: `-2.24`. You can find the rotation axis for your setup using the script `edtools.find_rotation_axis` available from [here](https://github.com/instamatic-dev/edtools#find_rotation_axispy). +**streamable** +: Boolean value. If present, overwrites the default behavior as implemented in each camera interface class to force the camera to stream (if `True`) or prevent it from streaming (if `False`) all collected data live directly to the GUI. + **stretch_amplitude** : Use `instamatic.stretch_correction` to characterize the lens distortion. The numbers here are used to calculate the XCORR/YCORR maps. The amplitude is the percentage difference between the maximum and minimum eigenvectors of the ellipsoid, i.e. if the amplitude is `2.43`, eig(max)/eig(min) = 1.0243. You can use the program `instamatic.stretch_correction` available [here](https://github.com/instamatic-dev/instamatic/blob/main/docs/programs.md#instamaticstretch_correction) on some powder patterns to define these numbers.