-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve ASI detector support and reduce excessive logging #111
base: main
Are you sure you want to change the base?
Changes from all commits
f202a34
8ccdef1
8f0deed
1ef5a36
6e18b2c
d8cbb8e
ea83287
81a6ba6
5dab929
1c5febb
a6aa242
63afa09
31eca62
0d10902
dcea143
5d96d30
d0bc19b
3a811bf
089f905
d9db13e
0e784ba
d3a67fe
2df07b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,13 +2,14 @@ | |||||||||||||||||||
|
||||||||||||||||||||
import atexit | ||||||||||||||||||||
import logging | ||||||||||||||||||||
from pathlib import Path | ||||||||||||||||||||
from typing import Tuple | ||||||||||||||||||||
import math | ||||||||||||||||||||
import threading | ||||||||||||||||||||
from functools import wraps | ||||||||||||||||||||
from typing import Any, Callable, Tuple, TypeVar | ||||||||||||||||||||
|
||||||||||||||||||||
import numpy as np | ||||||||||||||||||||
from serval_toolkit.camera import Camera as ServalCamera | ||||||||||||||||||||
|
||||||||||||||||||||
from instamatic import config | ||||||||||||||||||||
from instamatic.camera.camera_base import CameraBase | ||||||||||||||||||||
|
||||||||||||||||||||
logger = logging.getLogger(__name__) | ||||||||||||||||||||
|
@@ -19,20 +20,40 @@ | |||||||||||||||||||
# 3. launch `instamatic` | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
Decorated = TypeVar('Decorated', bound=Callable[..., Any]) | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
def synchronized(lock: threading.Lock) -> Callable: | ||||||||||||||||||||
"""Decorator: only one function decorated with given lock can run at time""" | ||||||||||||||||||||
|
||||||||||||||||||||
def decorator(func: Decorated) -> Decorated: | ||||||||||||||||||||
@wraps(func) | ||||||||||||||||||||
def wrapper(*args, **kwargs) -> Any: | ||||||||||||||||||||
with lock: | ||||||||||||||||||||
return func(*args, **kwargs) | ||||||||||||||||||||
|
||||||||||||||||||||
return wrapper | ||||||||||||||||||||
|
||||||||||||||||||||
return decorator | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
class CameraServal(CameraBase): | ||||||||||||||||||||
"""Interfaces with Serval from ASI.""" | ||||||||||||||||||||
|
||||||||||||||||||||
lock = threading.Lock() | ||||||||||||||||||||
streamable = True | ||||||||||||||||||||
MIN_EXPOSURE = 0.000001 | ||||||||||||||||||||
MAX_EXPOSURE = 10.0 | ||||||||||||||||||||
BAD_EXPOSURE_MSG = 'Requested exposure exceeds native Serval support (>0–10s)' | ||||||||||||||||||||
|
||||||||||||||||||||
def __init__(self, name='serval'): | ||||||||||||||||||||
"""Initialize camera module.""" | ||||||||||||||||||||
super().__init__(name) | ||||||||||||||||||||
|
||||||||||||||||||||
self.establish_connection() | ||||||||||||||||||||
|
||||||||||||||||||||
msg = f'Camera {self.get_name()} initialized' | ||||||||||||||||||||
logger.info(msg) | ||||||||||||||||||||
|
||||||||||||||||||||
self.dead_time = ( | ||||||||||||||||||||
self.detector_config['TriggerPeriod'] - self.detector_config['ExposureTime'] | ||||||||||||||||||||
) | ||||||||||||||||||||
logger.info(f'Camera {self.get_name()} initialized') | ||||||||||||||||||||
atexit.register(self.release_connection) | ||||||||||||||||||||
|
||||||||||||||||||||
def get_image(self, exposure=None, binsize=None, **kwargs) -> np.ndarray: | ||||||||||||||||||||
|
@@ -46,12 +67,32 @@ def get_image(self, exposure=None, binsize=None, **kwargs) -> np.ndarray: | |||||||||||||||||||
""" | ||||||||||||||||||||
if exposure is None: | ||||||||||||||||||||
exposure = self.default_exposure | ||||||||||||||||||||
if not binsize: | ||||||||||||||||||||
binsize = self.default_binsize | ||||||||||||||||||||
|
||||||||||||||||||||
if exposure < self.MIN_EXPOSURE: | ||||||||||||||||||||
logger.warning(f'{self.BAD_EXPOSURE_MSG}: {exposure}') | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider deferring the formatting until necessary: https://docs.python.org/3/howto/logging.html#optimization
Suggested change
|
||||||||||||||||||||
return self._get_image_null() | ||||||||||||||||||||
elif self.MIN_EXPOSURE <= exposure <= self.MAX_EXPOSURE: | ||||||||||||||||||||
return self._get_image_single(exposure, binsize, **kwargs) | ||||||||||||||||||||
else: # if exposure > self.MAX_EXPOSURE | ||||||||||||||||||||
Comment on lines
+73
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imo, logically _get_image_single should be the default. This also simplifies the logic a bit.
Suggested change
|
||||||||||||||||||||
logger.warning(f'{self.BAD_EXPOSURE_MSG}: {exposure}') | ||||||||||||||||||||
n_triggers = math.ceil(exposure / self.MAX_EXPOSURE) | ||||||||||||||||||||
exposure1 = (exposure + self.dead_time) / n_triggers - self.dead_time | ||||||||||||||||||||
arrays = self.get_movie(n_triggers, exposure1, binsize, **kwargs) | ||||||||||||||||||||
array_sum = sum(arrays, np.zeros_like(arrays[0])) | ||||||||||||||||||||
scaling_factor = exposure / exposure1 * n_triggers # account for dead time | ||||||||||||||||||||
return (array_sum * scaling_factor).astype(array_sum.dtype) | ||||||||||||||||||||
Comment on lines
+76
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider moving this bit to its own function to be in line with the other 2 options:
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
@synchronized(lock) | ||||||||||||||||||||
def _get_image_null(self, exposure=None, binsize=None, **kwargs) -> np.ndarray: | ||||||||||||||||||||
logger.debug('Creating a synthetic image with zero counts') | ||||||||||||||||||||
return np.zeros(shape=self.get_image_dimensions(), dtype=np.int32) | ||||||||||||||||||||
|
||||||||||||||||||||
@synchronized(lock) | ||||||||||||||||||||
def _get_image_single(self, exposure=None, binsize=None, **kwargs) -> np.ndarray: | ||||||||||||||||||||
logger.debug(f'Collecting a single image with exposure {exposure} s') | ||||||||||||||||||||
# Upload exposure settings (Note: will do nothing if no change in settings) | ||||||||||||||||||||
self.conn.set_detector_config( | ||||||||||||||||||||
ExposureTime=exposure, TriggerPeriod=exposure + 0.00050001 | ||||||||||||||||||||
ExposureTime=exposure, | ||||||||||||||||||||
TriggerPeriod=exposure + self.dead_time, | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
# Check if measurement is running. If not: start | ||||||||||||||||||||
|
@@ -67,6 +108,7 @@ def get_image(self, exposure=None, binsize=None, **kwargs) -> np.ndarray: | |||||||||||||||||||
arr = np.array(img) | ||||||||||||||||||||
return arr | ||||||||||||||||||||
|
||||||||||||||||||||
@synchronized(lock) | ||||||||||||||||||||
def get_movie(self, n_frames, exposure=None, binsize=None, **kwargs): | ||||||||||||||||||||
"""Movie acquisition routine. If the exposure and binsize are not | ||||||||||||||||||||
given, the default values are read from the config file. | ||||||||||||||||||||
|
@@ -80,18 +122,21 @@ def get_movie(self, n_frames, exposure=None, binsize=None, **kwargs): | |||||||||||||||||||
""" | ||||||||||||||||||||
if exposure is None: | ||||||||||||||||||||
exposure = self.default_exposure | ||||||||||||||||||||
if not binsize: | ||||||||||||||||||||
binsize = self.default_binsize | ||||||||||||||||||||
|
||||||||||||||||||||
self.conn.set_detector_config(TriggerMode='CONTINUOUS') | ||||||||||||||||||||
|
||||||||||||||||||||
arr = self.conn.get_images( | ||||||||||||||||||||
nTriggers=n_frames, | ||||||||||||||||||||
logger.debug(f'Collecting {n_frames} images with exposure {exposure} s') | ||||||||||||||||||||
mode = 'AUTOTRIGSTART_TIMERSTOP' if self.dead_time else 'CONTINUOUS' | ||||||||||||||||||||
self.conn.measurement_stop() | ||||||||||||||||||||
previous_config = self.conn.detector_config | ||||||||||||||||||||
self.conn.set_detector_config( | ||||||||||||||||||||
TriggerMode=mode, | ||||||||||||||||||||
ExposureTime=exposure, | ||||||||||||||||||||
TriggerPeriod=exposure, | ||||||||||||||||||||
TriggerPeriod=exposure + self.dead_time, | ||||||||||||||||||||
nTriggers=n_frames, | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
return arr | ||||||||||||||||||||
self.conn.measurement_start() | ||||||||||||||||||||
images = self.conn.get_image_stream(nTriggers=n_frames, disable_tqdm=True) | ||||||||||||||||||||
self.conn.measurement_stop() | ||||||||||||||||||||
self.conn.set_detector_config(**previous_config) | ||||||||||||||||||||
return images | ||||||||||||||||||||
|
||||||||||||||||||||
def get_image_dimensions(self) -> Tuple[int, int]: | ||||||||||||||||||||
"""Get the binned dimensions reported by the camera.""" | ||||||||||||||||||||
|
@@ -111,21 +156,14 @@ def establish_connection(self) -> None: | |||||||||||||||||||
bpc_file_path=self.bpc_file_path, dacs_file_path=self.dacs_file_path | ||||||||||||||||||||
) | ||||||||||||||||||||
self.conn.set_detector_config(**self.detector_config) | ||||||||||||||||||||
# Check pixel depth. If 24 bit mode is used, the pgm format does not work | ||||||||||||||||||||
# (has support up to 16 bits) so use tiff in that case. In other cases (1, 6, 12 bits) | ||||||||||||||||||||
# use pgm since it is more efficient | ||||||||||||||||||||
self.pixel_depth = self.conn.detector_config['PixelDepth'] | ||||||||||||||||||||
if self.pixel_depth == 24: | ||||||||||||||||||||
file_format = 'tiff' | ||||||||||||||||||||
else: | ||||||||||||||||||||
file_format = 'pgm' | ||||||||||||||||||||
|
||||||||||||||||||||
self.conn.destination = { | ||||||||||||||||||||
'Image': [ | ||||||||||||||||||||
{ | ||||||||||||||||||||
# Where to place the preview files (HTTP end-point: GET localhost:8080/measurement/image) | ||||||||||||||||||||
'Base': 'http://localhost', | ||||||||||||||||||||
# What (image) format to provide the files in. | ||||||||||||||||||||
'Format': file_format, | ||||||||||||||||||||
'Format': 'tiff', | ||||||||||||||||||||
# What data to build a frame from | ||||||||||||||||||||
'Mode': 'count', | ||||||||||||||||||||
} | ||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,39 @@ | ||
camera_rotation_vs_stage_xy: 0.0 | ||
default_binsize: 1 | ||
default_exposure: 0.02 | ||
|
||
# Double-check that your dimensions match the ones from your detector. | ||
dimensions: [512, 512] | ||
|
||
dynamic_range: 11800 | ||
interface: serval | ||
|
||
physical_pixelsize: 0.055 | ||
possible_binsizes: [1] | ||
|
||
stretch_amplitude: 0.0 | ||
stretch_azimuth: 0.0 | ||
|
||
# This configuration can be tuned based on your needs. | ||
# Please refer to the Serval manual. | ||
# For Serval 3.3.x, it is in section 4.5.7 - Detector Config JSON structure. | ||
detector_config: | ||
BiasVoltage: 100 | ||
BiasEnabled: True | ||
TriggerMode: SOFTWARESTART_TIMERSTOP | ||
TriggerMode: SOFTWARESTART_TIMERSTOP # Currently only this mode is supported | ||
ExposureTime: 1.0 | ||
TriggerPeriod: 1.001 | ||
TriggerPeriod: 1.002 | ||
nTriggers: 1000000000 | ||
GainMode: HGM | ||
# TriggerPeriod and ExposureTime are used to derive the dead/cooldown | ||
# time of the detector. Consult Serval user manual to determine | ||
# the optimal cooldown time for requested TriggerMode on your detector. | ||
# Remove the following lines if you are using CheeTah T3 variants (timepix3), | ||
# as they do not support these settings. | ||
GainMode: HGM # Only for Medipix3 | ||
PixelDepth: 24 # Set to 12 for 12-bit (normal) mode - only Medipix3 | ||
BothCounters: False # Set to True for 12-bit mode - only Medipix3 | ||
|
||
# Change this to the location of your actual factory settings | ||
bpc_file_path: '/home/asi/Desktop/Factory_settings/SPM-HGM/config.bpc' | ||
dacs_file_path: '/home/asi/Desktop/Factory_settings/SPM-HGM/config.dacs' | ||
url: 'http://localhost:8080' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Putting the lock at the class level initiates it when the code is loaded at runtime. Move this to
__init__()
.Actually, I don't understand why this lock is needed at all. The camera does not know if it is running in a thread, so it should not be the one locking. I think the code can be simpler if the lock is moved to
get_image
, if absolutely necessary.