Skip to content

Commit 3cf45a1

Browse files
authored
Fix and allow streamable server cameras (#143)
* Allow server cameras to be streamable by removing precaution checks * Unify interface, naming between get_microscope/camera(_class) * Remove unused _init_attr_dict/get_attrs from microscope client/server * Do not force server-side attributes to be callable, use them first: these 3 lines took ~3h * EAFP: Allow camera to call functions whether they are registered or not * EAFP: Allow camera to call unregistered functions - fixes server cameras * Remove unnecessary print debug statement * Add **streamable** description to `config.md` documentation
1 parent 61a3bdb commit 3cf45a1

File tree

10 files changed

+44
-30
lines changed

10 files changed

+44
-30
lines changed

docs/config.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ This file holds the specifications of the camera. This file is must be located t
230230
**camera_rotation_vs_stage_xy**
231231
: 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).
232232

233+
**streamable**
234+
: 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.
235+
233236
**stretch_amplitude**
234237
: 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.
235238

src/instamatic/camera/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from __future__ import annotations
22

3-
from .camera import Camera
3+
from .camera import get_camera, get_camera_class
44
from .videostream import VideoStream

src/instamatic/camera/camera.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import logging
44
from pathlib import Path
5+
from typing import Optional
56

67
from instamatic import config
8+
from instamatic.camera.camera_base import CameraBase
79

810
logger = logging.getLogger(__name__)
911

10-
__all__ = ['Camera']
12+
__all__ = ['get_camera', 'get_camera_class']
1113

1214
default_cam_interface = config.camera.interface
1315

1416

15-
def get_cam(interface: str = None):
16-
"""Grabs the camera object defined by `interface`"""
17+
def get_camera_class(interface: str) -> type[CameraBase]:
18+
"""Grabs the camera class with the specific `interface`"""
1719

1820
simulate = config.settings.simulate
1921

@@ -39,10 +41,24 @@ def get_cam(interface: str = None):
3941
return cam
4042

4143

42-
def Camera(name: str = None, as_stream: bool = False, use_server: bool = False):
44+
def get_camera(
45+
name: Optional[str] = None,
46+
as_stream: bool = False,
47+
use_server: bool = False,
48+
) -> CameraBase:
4349
"""Initialize the camera identified by the 'name' parameter if `as_stream`
4450
is True, it will return a VideoStream object if `as_stream` is False, it
45-
will return the raw Camera object."""
51+
will return the raw Camera object.
52+
53+
name: Optional[str]
54+
Specify which camera to use, must be implemented in `instamatic.camera`
55+
as_stream: bool
56+
If True (default False), allow streaming this camera image live.
57+
use_server: bool
58+
Connect to camera server running on the host/port defined in the config
59+
60+
returns: Camera interface class instance
61+
"""
4662

4763
if name is None:
4864
name = config.camera.name
@@ -56,9 +72,8 @@ def Camera(name: str = None, as_stream: bool = False, use_server: bool = False):
5672
from instamatic.camera.camera_client import CamClient
5773

5874
cam = CamClient(name=name, interface=interface)
59-
as_stream = False # precaution
6075
else:
61-
cam_cls = get_cam(interface)
76+
cam_cls = get_camera_class(interface)
6277

6378
if interface in ('timepix', 'pytimepix'):
6479
tpx_config = (
@@ -221,7 +236,7 @@ def main_entry():
221236

222237
if __name__ == '__main__':
223238
# main_entry()
224-
cam = Camera(use_server=True)
239+
cam = get_camera(use_server=True)
225240
arr = cam.get_image(exposure=0.1)
226241
print(arr)
227242
print(arr.shape)

src/instamatic/camera/camera_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def __init__(
5555
self.name = name
5656
self.interface = interface
5757
self._bufsize = BUFSIZE
58-
self.streamable = False # overrides cam settings
5958
self.verbose = False
6059

6160
try:
@@ -83,6 +82,7 @@ def __init__(
8382
self.buffers: Dict[str, np.ndarray] = {}
8483
self.shms = {}
8584

85+
self._attr_dct: dict = {}
8686
self._init_dict()
8787
self._init_attr_dict()
8888

@@ -104,14 +104,14 @@ def connect(self):
104104

105105
def __getattr__(self, attr_name):
106106
if attr_name in self._dct:
107+
if attr_name in object.__getattribute__(self, '_attr_dct'):
108+
return self._eval_dct({'attr_name': attr_name})
107109
wrapped = self._dct[attr_name]
108110
elif attr_name in self._attr_dct:
109111
dct = {'attr_name': attr_name}
110112
return self._eval_dct(dct)
111113
else:
112-
raise AttributeError(
113-
f'`{self.__class__.__name__}` object has no attribute `{attr_name}`'
114-
)
114+
wrapped = None # AFAIK can't wrap with None, can cause odd errors
115115

116116
@wraps(wrapped)
117117
def wrapper(*args, **kwargs):
@@ -156,9 +156,9 @@ def _eval_dct(self, dct):
156156
def _init_dict(self):
157157
"""Get list of functions and their doc strings from the uninitialized
158158
class."""
159-
from instamatic.camera.camera import get_cam
159+
from instamatic.camera.camera import get_camera_class
160160

161-
cam = get_cam(self.interface)
161+
cam = get_camera_class(self.interface)
162162

163163
self._dct = {
164164
key: value for key, value in cam.__dict__.items() if not key.startswith('_')

src/instamatic/camera/videostream.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import numpy as np
1212

13-
from instamatic.camera import Camera
13+
from instamatic.camera import get_camera
1414
from instamatic.camera.camera_base import CameraBase
1515
from instamatic.image_utils import autoscale
1616

@@ -115,15 +115,15 @@ def from_any(
115115
cls: Type[VideoStream_T], cam: Union[CameraBase, str] = 'simulate'
116116
) -> VideoStream_T:
117117
"""Create a subclass based on passed cam or cam-str stream-ability."""
118-
cam: CameraBase = Camera(name=cam) if isinstance(cam, str) else cam
118+
cam: CameraBase = get_camera(name=cam) if isinstance(cam, str) else cam
119119
if cls is VideoStream:
120120
return (LiveVideoStream if cam.streamable else FakeVideoStream)(cam)
121121
return cls(cam)
122122

123123
def __init__(self, cam: Union[CameraBase, str] = 'simulate') -> None:
124124
threading.Thread.__init__(self)
125125

126-
self.cam: CameraBase = Camera(name=cam) if isinstance(cam, str) else cam
126+
self.cam: CameraBase = get_camera(name=cam) if isinstance(cam, str) else cam
127127
self.lock = threading.Lock()
128128

129129
self.default_exposure = self.cam.default_exposure

src/instamatic/config/autoconfig.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,12 @@ def main():
125125
cam_connect = False
126126
cam_name = None
127127

128-
from instamatic.camera.camera import get_cam
128+
from instamatic.camera.camera import get_camera_class
129129
from instamatic.controller import TEMController
130130
from instamatic.microscope import get_microscope_class
131131

132132
if cam_connect:
133-
cam = get_cam(cam_name)() if cam_name else None
133+
cam = get_camera_class(cam_name)() if cam_name else None
134134
else:
135135
cam = None
136136

src/instamatic/controller.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import yaml
1010

1111
from instamatic import config
12-
from instamatic.camera import Camera
12+
from instamatic.camera import get_camera
1313
from instamatic.camera.camera_base import CameraBase
1414
from instamatic.exceptions import TEMControllerError
1515
from instamatic.formats import write_tiff
@@ -61,7 +61,7 @@ def initialize(
6161

6262
print(f'Camera : {cam_name}{cam_tag}')
6363

64-
cam = Camera(cam_name, as_stream=stream, use_server=use_cam_server)
64+
cam = get_camera(cam_name, as_stream=stream, use_server=use_cam_server)
6565
else:
6666
cam = None
6767

src/instamatic/microscope/client.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,6 @@ def _init_dict(self) -> None:
125125
}
126126
self._dct['get_attrs'] = None
127127

128-
def _init_attr_dict(self):
129-
"""Get list of attrs and their types."""
130-
self._attr_dct = self.get_attrs()
131-
132128
def __dir__(self) -> list:
133129
return list(self._dct.keys())
134130

src/instamatic/microscope/microscope.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
__all__ = ['get_microscope', 'get_microscope_class']
1111

1212

13-
def get_microscope_class(interface: str) -> 'type[MicroscopeBase]':
13+
def get_microscope_class(interface: str) -> type[MicroscopeBase]:
1414
"""Grab tem class with the specific 'interface'."""
1515
simulate = config.settings.simulate
1616

@@ -42,7 +42,7 @@ def get_microscope(name: Optional[str] = None, use_server: bool = False) -> Micr
4242
use_server: bool
4343
Connect to microscope server running on the host/port defined in the config file
4444
45-
returns: TEM interface class
45+
returns: TEM interface class instance
4646
"""
4747
if name is None:
4848
interface = default_tem_interface

src/instamatic/server/cam_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import numpy as np
1212

1313
from instamatic import config
14-
from instamatic.camera import Camera
14+
from instamatic.camera import get_camera
1515
from instamatic.utils import high_precision_timers
1616

1717
from .serializer import dumper, loader
@@ -81,7 +81,7 @@ def copy_data_to_shared_buffer(self, arr):
8181

8282
def run(self):
8383
"""Start server thread."""
84-
self.cam = Camera(name=self._name, use_server=False)
84+
self.cam = get_camera(name=self._name, use_server=False)
8585
self.cam.get_attrs = self.get_attrs
8686

8787
print(f'Initialized camera: {self.cam.interface}')

0 commit comments

Comments
 (0)