From edd0db0c69c2458f0ea56d8460f954b4fa1eeaa0 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Mon, 4 Mar 2024 19:42:16 +0100 Subject: [PATCH 01/25] Create HamamatsuCamera() --- .../hamamatsu_camera/hamamatsu_camera.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 catkit2/services/hamamatsu_camera/hamamatsu_camera.py diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py new file mode 100644 index 000000000..9fd95b695 --- /dev/null +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -0,0 +1,192 @@ +""" +This module contains a service for Hamamatsu digital cameras. + +This service is a wrapper around the DCAM-SDK4. +It provides a simple interface to control the camera and acquire images. +""" +import threading +import numpy as np +from catkit2.testbed.service import Service + +try: + sdk_path = os.environ.get('CATKIT_DCAM_SDK_PATH') + if sdk_path is not None: + sys.path.append(sdk_path) + + import dcam +except ImportError: + print('To use Hamamatsu cameras, you need to set the CATKIT_DCAM_SDK_PATH environment variable.') + raise + + +class HamamatsuCamera(Service): + """ + Service for Hamamatsu cameras. + + This service is a wrapper around the DCAM-SDK4. + It provides a simple interface to control the camera and acquire images. + + Attributes + ---------- + cam : Camera + The camera to control. + pixel_formats : dict + A dictionary to store the pixel format and the corresponding numpy dtype and dcam pixel format. + current_pixel_format : str + The current pixel format to use. + temperature_thread : threading.Thread + A thread to monitor the temperature of the camera. + temperature : DataStream + A data stream to submit the temperature of the camera. + images : DataStream + A data stream to submit the images from the camera. + is_acquiring : DataStream + A data stream to submit whether the camera is currently acquiring images. + should_be_acquiring : threading.Event + An event to signal whether the camera should be acquiring images. + NUM_FRAMES : int + The number of frames to allocate for the data streams. + + Methods + ------- + open() + Open the service. + main() + The main function of the service. + close() + Close the service. + acquisition_loop() + The main acquisition loop. + monitor_temperature() + Monitor the temperature of the camera. + start_acquisition() + Start the acquisition loop. + end_acquisition() + End the acquisition loop. + get_temperature() + Get the temperature of the camera. + """ + NUM_FRAMES = 20 + + def __init__(self): + """ + Create a new HamamatsuCamera service. + """ + super().__init__('hamamatsu_camera') + + self.cam = None + + # Dictionary to store the pixel format and the corresponding numpy dtype and dcam pixel format + self.pixel_formats = { + "Mono8": DCAM_PIXELTYPE.Mono8, + "Mono16": DCAM_PIXELTYPE.Mono16, + } + self.current_pixel_format = None + self.temperature_thread = None + self.temperature = None + self.images = None + self.is_acquiring = None + + self.should_be_acquiring = threading.Event() + self.should_be_acquiring.set() + + def open(self): + """ + Open the service. + + This function is called when the service is opened. + It initializes the camera and creates the data streams and properties. + + Raises + ------ + ValueError + If the pixel format is invalid. + """ + dcam.Dcamapi.init() + + camera_id = self.config.get('camera_id', 0) + self.cam = dcam.Dcam(camera_id) + self.log.info('Using camera with ID %s', camera_id) + self.cam.dev_open() + + self.current_pixel_format = self.config.get('pixel_format', 'Mono16') + if self.current_pixel_format not in self.pixel_formats: + raise ValueError('Invalid pixel format: ' + + self.current_pixel_format + + ', must be one of ' + + str(list(self.pixel_formats.keys()))) + self.log.info('Using pixel format: %s', self.current_pixel_format) + # self.cam.set_pixel_format(self.pixel_formats[self.current_pixel_format]) # TODO + + # Set device values from config file (set width and height before offsets) + offset_x = self.config.get('offset_x', 0) + offset_y = self.config.get('offset_y', 0) + + self.width = self.config.get('width', self.sensor_width - offset_x) + self.height = self.config.get('height', self.sensor_height - offset_y) + self.offset_x = offset_x + self.offset_y = offset_y + + self.gain = self.config.get('gain', 0) + self.exposure_time = self.config.get('exposure_time', 1000) + self.temperature = self.make_data_stream('temperature', 'float64', [1], 20) + + # Create datastreams + # Use the full sensor size here to always allocate enough shared memory. + self.images = self.make_data_stream('images', 'float32', [self.sensor_height, self.sensor_width], self.NUM_FRAMES) + + self.is_acquiring = self.make_data_stream('is_acquiring', 'int8', [1], self.NUM_FRAMES) + self.is_acquiring.submit_data(np.array([0], dtype='int8')) + + # Create properties + def make_property_helper(name, read_only=False): + if read_only: + self.make_property(name, lambda: getattr(self, name)) + else: + self.make_property(name, lambda: getattr(self, name), lambda val: setattr(self, name, val)) + + make_property_helper('exposure_time') + make_property_helper('gain') + make_property_helper('brightness') + + make_property_helper('width') + make_property_helper('height') + make_property_helper('offset_x') + make_property_helper('offset_y') + + make_property_helper('sensor_width', read_only=True) + make_property_helper('sensor_height', read_only=True) + + make_property_helper('device_name', read_only=True) + + self.make_command('start_acquisition', self.start_acquisition) + self.make_command('end_acquisition', self.end_acquisition) + + self.temperature_thread = threading.Thread(target=self.monitor_temperature) + self.temperature_thread.start() + + def main(self): + """ + The main function of the service. + + This function is called when the service is started. + It starts the acquisition loop and waits for incoming frames. + """ + while not self.should_shut_down: + if self.should_be_acquiring.wait(0.05): + self.acquisition_loop() + + def close(self): + """ + Close the service. + + This function is called when the service is closed. + It stops the acquisition loop and cleans up the camera and data streams. + """ + self.cam.dev_close() + self.cam = None + + +if __name__ == '__main__': + service = HamamatsuCamera() + service.run() From 26160af62b348764399c27255b295bf626660498 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Mon, 4 Mar 2024 19:57:45 +0100 Subject: [PATCH 02/25] Add missing methods (WIP) --- .../hamamatsu_camera/hamamatsu_camera.py | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 9fd95b695..6b7a876eb 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -186,6 +186,302 @@ def close(self): self.cam.dev_close() self.cam = None + dcam.Dcamapi.uninit() + + def acquisition_loop(self): + """ + The main acquisition loop. + + This function is the main loop for acquiring images from the camera. + It starts the acquisition and then waits for incoming frames. + """ + # Make sure the data stream has the right size and datatype. + has_correct_parameters = np.allclose(self.images.shape, [self.height, self.width]) + if not has_correct_parameters: + self.images.update_parameters('float32', [self.height, self.width], self.NUM_FRAMES) + + # Start acquisition. + self.cam.buf_alloc(self.NUM_FRAMES) + self.cam.cap_start() + self.is_acquiring.submit_data(np.array([1], dtype='int8')) + + try: + while self.should_be_acquiring.is_set() and not self.should_shut_down: + img = self.cam.buf_getlastframedata() + + # TODO: there might be some image manipulation needed here + finally: + # Stop acquisition. + self.cam.cap_stop() + self.cam.buf_release() + self.is_acquiring.submit_data(np.array([0], dtype='int8')) + + def monitor_temperature(self): + """ + Monitor the temperature of the camera. + + This function is a separate thread that monitors the temperature of + the camera and submits the data to the temperature data stream. + """ + while not self.should_shut_down: + temperature = self.get_temperature() + self.temperature.submit_data(np.array([temperature])) + + self.sleep(0.1) + + def start_acquisition(self): + """ + Start the acquisition loop. + + This function starts the acquisition loop. + """ + self.should_be_acquiring.set() + + def end_acquisition(self): + """ + End the acquisition loop. + + This function ends the acquisition loop. + """ + self.should_be_acquiring.clear() + + def get_temperature(self): + """ + Get the temperature of the camera. + + This function gets the temperature of the camera. + + Returns + ------- + float: + The temperature of the camera in degrees Celsius. + """ + return self.cam.DeviceTemperature.get() # TODO + + @property + def exposure_time(self): + """ + The exposure time in microseconds. + + This property can be used to get the exposure time of the camera. + + Returns: + -------- + int: + The exposure time in microseconds. # TODO + """ + return self.cam.ExposureTime.get() # TODO + + @exposure_time.setter + def exposure_time(self, exposure_time: int): + """ + Set the exposure time in microseconds. + + This property can be used to set the exposure time of the camera. + + Parameters + ---------- + exposure_time : int + The exposure time in microseconds. # TODO + """ + self.cam.ExposureTime.set(exposure_time) # TODO + + @property + def gain(self): + """ + The gain of the camera. + + This property can be used to get the gain of the camera. + + Returns: + -------- + int: + The gain of the camera. + """ + return self.cam.Gain.get() # TODO + + @gain.setter + def gain(self, gain: int): + """ + Set the gain of the camera. + + This property can be used to set the gain of the camera. + + Parameters + ---------- + gain : int + The gain of the camera. + """ + self.cam.Gain.set(gain) # TODO + + @property + def brightness(self): + """ + The brightness of the camera. + + This property can be used to get the brightness of the camera. + + Returns: + -------- + int: + The brightness of the camera. + """ + return self.cam.Brightness.get() # TODO + + @brightness.setter + def brightness(self, brightness: int): + """ + Set the brightness of the camera. + + This property can be used to set the brightness of the camera. + + Parameters + ---------- + brightness : int + The brightness of the camera. + """ + self.cam.Brightness.set(brightness) # TODO + + @property + def sensor_width(self): + """ + The width of the sensor in pixels. + + This property can be used to get the width of the sensor in pixels. + + Returns: + -------- + int: + The width of the sensor in pixels. + """ + return self.cam.SensorWidth.get() # TODO + + @property + def sensor_height(self): + """ + The height of the sensor in pixels. + + This property can be used to get the height of the sensor in pixels. + + Returns: + -------- + int: + The height of the sensor in pixels. + """ + return self.cam.SensorHeight.get() # TODO + + @property + def width(self): + """ + The width of the image in pixels. + + This property can be used to get the width of the image in pixels. + + Returns: + -------- + int: + The width of the image in pixels. + """ + return self.cam.Width.get() # TODO + + @width.setter + def width(self, width: int): + """ + Set the width of the image in pixels. + + This property can be used to set the width of the image in pixels. + + Parameters + ---------- + width : int + The width of the image in pixels. + """ + self.cam.Width.set(width) # TODO + + @property + def height(self): + """ + The height of the image in pixels. + + This property can be used to get the height of the image in pixels. + + Returns: + -------- + int: + The height of the image in pixels. + """ + return self.cam.Height.get() # TODO + + @height.setter + def height(self, height: int): + """ + Set the height of the image in pixels. + + This property can be used to set the height of the image in pixels. + + Parameters + ---------- + height : int + The height of the image in pixels. + """ + self.cam.Height.set(height) # TODO + + @property + def offset_x(self): + """ + The x offset of the image in pixels. + + This property can be used to get the x offset of the image in pixels. + + Returns: + -------- + int: + The x offset of the image in pixels. + """ + return self.cam.OffsetX.get() # TODO + + @offset_x.setter + def offset_x(self, offset_x: int): + """ + Set the x offset of the image in pixels. + + This property can be used to set the x offset of the image in pixels. + + Parameters + ---------- + offset_x : int + The x offset of the image in pixels. + """ + self.cam.OffsetX.set(offset_x) # TODO + + @property + def offset_y(self): + """ + The y offset of the image in pixels. + + This property can be used to get the y offset of the image in pixels. + + Returns: + -------- + int: + The y offset of the image in pixels. + """ + return self.cam.OffsetY.get() # TODO + + @offset_y.setter + def offset_y(self, offset_y: int): + """ + Set the y offset of the image in pixels. + + This property can be used to set the y offset of the image in pixels. + + Parameters + ---------- + offset_y : int + The y offset of the image in pixels. + """ + self.cam.OffsetY.set(offset_y) # TODO + if __name__ == '__main__': service = HamamatsuCamera() From 61bc82a2ad3b3f7da2f095f7a7ebd310b4f81536 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Mon, 4 Mar 2024 20:01:40 +0100 Subject: [PATCH 03/25] Add missing imports --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 6b7a876eb..fda9b95ba 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -4,6 +4,8 @@ This service is a wrapper around the DCAM-SDK4. It provides a simple interface to control the camera and acquire images. """ +import os +import sys import threading import numpy as np from catkit2.testbed.service import Service @@ -78,8 +80,8 @@ def __init__(self): # Dictionary to store the pixel format and the corresponding numpy dtype and dcam pixel format self.pixel_formats = { - "Mono8": DCAM_PIXELTYPE.Mono8, - "Mono16": DCAM_PIXELTYPE.Mono16, + "Mono8": dcam.DCAM_PIXELTYPE.Mono8, + "Mono16": dcam.DCAM_PIXELTYPE.Mono16, } self.current_pixel_format = None self.temperature_thread = None From 784337d45c58a00f94fb627e8cb574df4095fb45 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Mon, 4 Mar 2024 21:53:01 +0100 Subject: [PATCH 04/25] Get and set some of the properties --- .../hamamatsu_camera/hamamatsu_camera.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index fda9b95ba..06f9c167f 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -118,7 +118,7 @@ def open(self): ', must be one of ' + str(list(self.pixel_formats.keys()))) self.log.info('Using pixel format: %s', self.current_pixel_format) - # self.cam.set_pixel_format(self.pixel_formats[self.current_pixel_format]) # TODO + self.cam.prop_setvalue(dcam.DCAM_IDPROP.IMAGE_PIXELTYPE, self.pixel_formats[self.current_pixel_format]) # Set device values from config file (set width and height before offsets) offset_x = self.config.get('offset_x', 0) @@ -258,7 +258,7 @@ def get_temperature(self): float: The temperature of the camera in degrees Celsius. """ - return self.cam.DeviceTemperature.get() # TODO + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SENSORTEMPERATURE) @property def exposure_time(self): @@ -269,13 +269,13 @@ def exposure_time(self): Returns: -------- - int: - The exposure time in microseconds. # TODO + float: + The exposure time in seconds. """ - return self.cam.ExposureTime.get() # TODO + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.EXPOSURETIME) @exposure_time.setter - def exposure_time(self, exposure_time: int): + def exposure_time(self, exposure_time: float): """ Set the exposure time in microseconds. @@ -283,10 +283,10 @@ def exposure_time(self, exposure_time: int): Parameters ---------- - exposure_time : int - The exposure time in microseconds. # TODO + exposure_time : float + The exposure time in seconds. """ - self.cam.ExposureTime.set(exposure_time) # TODO + self.cam.prop_setvalue(dcam.DCAM_IDPROP.EXPOSURETIME, exposure_time) @property def gain(self): @@ -356,7 +356,7 @@ def sensor_width(self): int: The width of the sensor in pixels. """ - return self.cam.SensorWidth.get() # TODO + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_WIDTH) # TODO: Is this sensor or image width? @property def sensor_height(self): @@ -370,7 +370,7 @@ def sensor_height(self): int: The height of the sensor in pixels. """ - return self.cam.SensorHeight.get() # TODO + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_HEIGHT) # TODO: Is this sensor or image height? @property def width(self): From 89f52d3a56cc07ec8d9b9b10b912c459b417e0bb Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Wed, 17 Apr 2024 16:47:31 +0200 Subject: [PATCH 05/25] Update service after initial lab tests --- .../hamamatsu_camera/hamamatsu_camera.py | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 06f9c167f..b087fdc22 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -104,12 +104,23 @@ def open(self): ValueError If the pixel format is invalid. """ - dcam.Dcamapi.init() + if dcam.Dcamapi.init() is False: + raise RuntimeError(f'Dcamapi.init() fails with error {dcam.Dcamapi.lasterr()}') camera_id = self.config.get('camera_id', 0) self.cam = dcam.Dcam(camera_id) self.log.info('Using camera with ID %s', camera_id) - self.cam.dev_open() + if self.cam.dev_open() is False: + raise RuntimeError(f'Dcam.dev_open() fails with error {self.cam.lasterr()}') + + # Set subarray mode + self.cam.prop_setvalue(4202832, 2.0) # TODO: ? - 2.0 for subarray mode on + + # Set binning + self.cam.prop_setvalue(4198672, 1.0) # TODO: find DCAM keyword for this + + # Set camera mode + dcam.prop_setvalue(4194576, 2.0) # TODO: find DCAM keyword for this - 2.0 for "standard", 1.0 for "ultraquiet" self.current_pixel_format = self.config.get('pixel_format', 'Mono16') if self.current_pixel_format not in self.pixel_formats: @@ -130,7 +141,7 @@ def open(self): self.offset_y = offset_y self.gain = self.config.get('gain', 0) - self.exposure_time = self.config.get('exposure_time', 1000) + self.exposure_time = self.config.get('exposure_time', 1) self.temperature = self.make_data_stream('temperature', 'float64', [1], 20) # Create datastreams @@ -203,15 +214,22 @@ def acquisition_loop(self): self.images.update_parameters('float32', [self.height, self.width], self.NUM_FRAMES) # Start acquisition. - self.cam.buf_alloc(self.NUM_FRAMES) - self.cam.cap_start() + if self.cam.buf_alloc(self.NUM_FRAMES) is False: + raise RuntimeError(f'Dcam.buf_alloc() fails with error {self.cam.lasterr()}') + if self.cam.cap_start() is False: + raise RuntimeError(f'Dcam.cap_start() fails with error {self.cam.lasterr()}') self.is_acquiring.submit_data(np.array([1], dtype='int8')) + timeout_millisec = 2000 try: while self.should_be_acquiring.is_set() and not self.should_shut_down: + if self.cam.lasterr().is_timeout(): + sself.log.warning('Timeout while waiting for frame') + if self.cam.wait_capevent_frameready(timeout_millisec) is False: + raise RuntimeError(f'Dcam.wait_capevent_frameready() fails with error {self.cam.lasterr()}') img = self.cam.buf_getlastframedata() + self.images.submit_data(img) - # TODO: there might be some image manipulation needed here finally: # Stop acquisition. self.cam.cap_stop() @@ -263,7 +281,7 @@ def get_temperature(self): @property def exposure_time(self): """ - The exposure time in microseconds. + The exposure time in seconds. This property can be used to get the exposure time of the camera. @@ -277,7 +295,7 @@ def exposure_time(self): @exposure_time.setter def exposure_time(self, exposure_time: float): """ - Set the exposure time in microseconds. + Set the exposure time in seconds. This property can be used to set the exposure time of the camera. @@ -357,6 +375,7 @@ def sensor_width(self): The width of the sensor in pixels. """ return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_WIDTH) # TODO: Is this sensor or image width? + # TODO: needs to be 4325904 (sensor width) or 4325920 (sensor height) @property def sensor_height(self): @@ -371,6 +390,7 @@ def sensor_height(self): The height of the sensor in pixels. """ return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_HEIGHT) # TODO: Is this sensor or image height? + # TODO: needs to be 4325904 (sensor width) or 4325920 (sensor height) @property def width(self): @@ -384,7 +404,7 @@ def width(self): int: The width of the image in pixels. """ - return self.cam.Width.get() # TODO + return self.cam.prop_getvalue(4202784) # TODO: find DCAM keyword for this @width.setter def width(self, width: int): @@ -398,7 +418,7 @@ def width(self, width: int): width : int The width of the image in pixels. """ - self.cam.Width.set(width) # TODO + self.cam.prop_setvalue(4202784, width) # TODO: find DCAM keyword for this @property def height(self): @@ -412,7 +432,7 @@ def height(self): int: The height of the image in pixels. """ - return self.cam.Height.get() # TODO + return self.cam.prop_getvalue(4202816) # TODO: find DCAM keyword for this @height.setter def height(self, height: int): @@ -426,7 +446,7 @@ def height(self, height: int): height : int The height of the image in pixels. """ - self.cam.Height.set(height) # TODO + self.cam.prop_setvalue(4202816, height) # TODO: find DCAM keyword for this @property def offset_x(self): @@ -440,7 +460,7 @@ def offset_x(self): int: The x offset of the image in pixels. """ - return self.cam.OffsetX.get() # TODO + return self.cam.prop_getvalue(4202800) # TODO: find DCAM keyword for this @offset_x.setter def offset_x(self, offset_x: int): @@ -454,7 +474,7 @@ def offset_x(self, offset_x: int): offset_x : int The x offset of the image in pixels. """ - self.cam.OffsetX.set(offset_x) # TODO + self.cam.prop_setvalue(4202800, offset_x) # TODO: find DCAM keyword for this @property def offset_y(self): @@ -468,7 +488,7 @@ def offset_y(self): int: The y offset of the image in pixels. """ - return self.cam.OffsetY.get() # TODO + return self.cam.prop_getvalue(4202768) # TODO: find DCAM keyword for this @offset_y.setter def offset_y(self, offset_y: int): @@ -482,7 +502,7 @@ def offset_y(self, offset_y: int): offset_y : int The y offset of the image in pixels. """ - self.cam.OffsetY.set(offset_y) # TODO + self.cam.prop_setvalue(4202768, offset_x) # TODO: find DCAM keyword for this if __name__ == '__main__': From e0df7f22cd4da6c3ea153233c0df181f3b53bb73 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Wed, 17 Apr 2024 22:24:38 +0200 Subject: [PATCH 06/25] Replace property id numbers with named properties from DCAM_IDPROP --- .../hamamatsu_camera/hamamatsu_camera.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index b087fdc22..992e9f57e 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -114,13 +114,13 @@ def open(self): raise RuntimeError(f'Dcam.dev_open() fails with error {self.cam.lasterr()}') # Set subarray mode - self.cam.prop_setvalue(4202832, 2.0) # TODO: ? - 2.0 for subarray mode on + self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYMODE, 2.0) # TODO: ? - 2.0 for subarray mode on # Set binning - self.cam.prop_setvalue(4198672, 1.0) # TODO: find DCAM keyword for this + self.cam.prop_setvalue(dcam.DCAM_IDPROP.BINNING, 1.0) # TODO: read from config # Set camera mode - dcam.prop_setvalue(4194576, 2.0) # TODO: find DCAM keyword for this - 2.0 for "standard", 1.0 for "ultraquiet" + dcam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, 2.0) # TODO: read from config - 2.0 for "standard", 1.0 for "ultraquiet" self.current_pixel_format = self.config.get('pixel_format', 'Mono16') if self.current_pixel_format not in self.pixel_formats: @@ -224,7 +224,7 @@ def acquisition_loop(self): try: while self.should_be_acquiring.is_set() and not self.should_shut_down: if self.cam.lasterr().is_timeout(): - sself.log.warning('Timeout while waiting for frame') + self.log.warning('Timeout while waiting for frame') if self.cam.wait_capevent_frameready(timeout_millisec) is False: raise RuntimeError(f'Dcam.wait_capevent_frameready() fails with error {self.cam.lasterr()}') img = self.cam.buf_getlastframedata() @@ -374,8 +374,7 @@ def sensor_width(self): int: The width of the sensor in pixels. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_WIDTH) # TODO: Is this sensor or image width? - # TODO: needs to be 4325904 (sensor width) or 4325920 (sensor height) + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_WIDTH) @property def sensor_height(self): @@ -389,8 +388,7 @@ def sensor_height(self): int: The height of the sensor in pixels. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_HEIGHT) # TODO: Is this sensor or image height? - # TODO: needs to be 4325904 (sensor width) or 4325920 (sensor height) + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_HEIGHT) @property def width(self): @@ -404,7 +402,7 @@ def width(self): int: The width of the image in pixels. """ - return self.cam.prop_getvalue(4202784) # TODO: find DCAM keyword for this + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYHSIZE) @width.setter def width(self, width: int): @@ -418,7 +416,7 @@ def width(self, width: int): width : int The width of the image in pixels. """ - self.cam.prop_setvalue(4202784, width) # TODO: find DCAM keyword for this + self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYHSIZE, width) @property def height(self): @@ -432,7 +430,7 @@ def height(self): int: The height of the image in pixels. """ - return self.cam.prop_getvalue(4202816) # TODO: find DCAM keyword for this + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYVSIZE) @height.setter def height(self, height: int): @@ -446,7 +444,7 @@ def height(self, height: int): height : int The height of the image in pixels. """ - self.cam.prop_setvalue(4202816, height) # TODO: find DCAM keyword for this + self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYVSIZE, height) @property def offset_x(self): @@ -460,7 +458,7 @@ def offset_x(self): int: The x offset of the image in pixels. """ - return self.cam.prop_getvalue(4202800) # TODO: find DCAM keyword for this + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYVPOS) @offset_x.setter def offset_x(self, offset_x: int): @@ -474,7 +472,7 @@ def offset_x(self, offset_x: int): offset_x : int The x offset of the image in pixels. """ - self.cam.prop_setvalue(4202800, offset_x) # TODO: find DCAM keyword for this + self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYVPOS, offset_x) @property def offset_y(self): @@ -488,7 +486,7 @@ def offset_y(self): int: The y offset of the image in pixels. """ - return self.cam.prop_getvalue(4202768) # TODO: find DCAM keyword for this + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYHPOS) @offset_y.setter def offset_y(self, offset_y: int): @@ -502,7 +500,7 @@ def offset_y(self, offset_y: int): offset_y : int The y offset of the image in pixels. """ - self.cam.prop_setvalue(4202768, offset_x) # TODO: find DCAM keyword for this + self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYHPOS, offset_y) if __name__ == '__main__': From 42ec86d87719d2d6cc9752b432724f2753c95d75 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Wed, 17 Apr 2024 22:27:11 +0200 Subject: [PATCH 07/25] Add comment --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 992e9f57e..00738c385 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -103,6 +103,8 @@ def open(self): ------ ValueError If the pixel format is invalid. + RuntimeError + For a Dcamapi or Dcam error when initializing the library or when opening the camera. """ if dcam.Dcamapi.init() is False: raise RuntimeError(f'Dcamapi.init() fails with error {dcam.Dcamapi.lasterr()}') From 5614629282cb85cadb40ae1bf19e307bac2a671b Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Wed, 17 Apr 2024 22:42:56 +0200 Subject: [PATCH 08/25] Code sneak: remove property device_name from AV cam since unused --- catkit2/services/allied_vision_camera/allied_vision_camera.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/catkit2/services/allied_vision_camera/allied_vision_camera.py b/catkit2/services/allied_vision_camera/allied_vision_camera.py index d2856adcb..2d529e593 100644 --- a/catkit2/services/allied_vision_camera/allied_vision_camera.py +++ b/catkit2/services/allied_vision_camera/allied_vision_camera.py @@ -243,8 +243,6 @@ def make_property_helper(name, read_only=False): make_property_helper('sensor_width', read_only=True) make_property_helper('sensor_height', read_only=True) - make_property_helper('device_name', read_only=True) - self.make_command('start_acquisition', self.start_acquisition) self.make_command('end_acquisition', self.end_acquisition) From 529d17731851599d63dda83238385e620cab2ef5 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Wed, 17 Apr 2024 22:43:23 +0200 Subject: [PATCH 09/25] Simplify docstrings --- .../hamamatsu_camera/hamamatsu_camera.py | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 00738c385..fa6d98e07 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -48,25 +48,6 @@ class HamamatsuCamera(Service): An event to signal whether the camera should be acquiring images. NUM_FRAMES : int The number of frames to allocate for the data streams. - - Methods - ------- - open() - Open the service. - main() - The main function of the service. - close() - Close the service. - acquisition_loop() - The main acquisition loop. - monitor_temperature() - Monitor the temperature of the camera. - start_acquisition() - Start the acquisition loop. - end_acquisition() - End the acquisition loop. - get_temperature() - Get the temperature of the camera. """ NUM_FRAMES = 20 @@ -192,12 +173,6 @@ def main(self): self.acquisition_loop() def close(self): - """ - Close the service. - - This function is called when the service is closed. - It stops the acquisition loop and cleans up the camera and data streams. - """ self.cam.dev_close() self.cam = None From 77935e8171bff442084dc0076b9d369e6eef743b Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Wed, 17 Apr 2024 22:43:37 +0200 Subject: [PATCH 10/25] Some cleaning --- .../hamamatsu_camera/hamamatsu_camera.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index fa6d98e07..d09a9c1e2 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -100,17 +100,15 @@ def open(self): self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYMODE, 2.0) # TODO: ? - 2.0 for subarray mode on # Set binning - self.cam.prop_setvalue(dcam.DCAM_IDPROP.BINNING, 1.0) # TODO: read from config + binning = self.config.get('binning', 1) + self.cam.prop_setvalue(dcam.DCAM_IDPROP.BINNING, binning) - # Set camera mode - dcam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, 2.0) # TODO: read from config - 2.0 for "standard", 1.0 for "ultraquiet" + # Set camera mode to "ultraquiet" (1.0) rather than "standard" (2.0) + dcam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, 1.0) self.current_pixel_format = self.config.get('pixel_format', 'Mono16') if self.current_pixel_format not in self.pixel_formats: - raise ValueError('Invalid pixel format: ' + - self.current_pixel_format + - ', must be one of ' + - str(list(self.pixel_formats.keys()))) + raise ValueError(f'Invalid pixel format: {self.current_pixel_format}, must be one of {str(list(self.pixel_formats.keys()))}') self.log.info('Using pixel format: %s', self.current_pixel_format) self.cam.prop_setvalue(dcam.DCAM_IDPROP.IMAGE_PIXELTYPE, self.pixel_formats[self.current_pixel_format]) @@ -153,8 +151,6 @@ def make_property_helper(name, read_only=False): make_property_helper('sensor_width', read_only=True) make_property_helper('sensor_height', read_only=True) - make_property_helper('device_name', read_only=True) - self.make_command('start_acquisition', self.start_acquisition) self.make_command('end_acquisition', self.end_acquisition) @@ -203,7 +199,7 @@ def acquisition_loop(self): if self.cam.lasterr().is_timeout(): self.log.warning('Timeout while waiting for frame') if self.cam.wait_capevent_frameready(timeout_millisec) is False: - raise RuntimeError(f'Dcam.wait_capevent_frameready() fails with error {self.cam.lasterr()}') + raise RuntimeError(f'Dcam.wait_capevent_frameready({timeout_millisec}) fails with error {self.cam.lasterr()}') img = self.cam.buf_getlastframedata() self.images.submit_data(img) @@ -295,7 +291,7 @@ def gain(self): int: The gain of the camera. """ - return self.cam.Gain.get() # TODO + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.CONTRASTGAIN) # TODO: verify this @gain.setter def gain(self, gain: int): @@ -309,7 +305,7 @@ def gain(self, gain: int): gain : int The gain of the camera. """ - self.cam.Gain.set(gain) # TODO + self.cam.prop_setvalue(dcam.DCAM_IDPROP.CONTRASTGAIN, gain) # TODO: verify this @property def brightness(self): @@ -323,7 +319,7 @@ def brightness(self): int: The brightness of the camera. """ - return self.cam.Brightness.get() # TODO + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.XXX) # TODO: find correct property @brightness.setter def brightness(self, brightness: int): @@ -337,7 +333,7 @@ def brightness(self, brightness: int): brightness : int The brightness of the camera. """ - self.cam.Brightness.set(brightness) # TODO + self.cam.prop_setvalue(dcam.DCAM_IDPROP.XXX, brightness) # TODO: find correct property @property def sensor_width(self): From bee35d51b190950a54fc9186fe901f1361e8044b Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 13:15:56 +0200 Subject: [PATCH 11/25] Fix typo in pixel type --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index d09a9c1e2..d6291ecef 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -61,8 +61,8 @@ def __init__(self): # Dictionary to store the pixel format and the corresponding numpy dtype and dcam pixel format self.pixel_formats = { - "Mono8": dcam.DCAM_PIXELTYPE.Mono8, - "Mono16": dcam.DCAM_PIXELTYPE.Mono16, + "Mono8": dcam.DCAM_PIXELTYPE.MONO8, + "Mono16": dcam.DCAM_PIXELTYPE.MONO16, } self.current_pixel_format = None self.temperature_thread = None From f930523a35bc9d9d95fb47053585a44b1b5fc3a0 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 13:24:03 +0200 Subject: [PATCH 12/25] Call prop_setvalue() on camera, not on library --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index d6291ecef..66234c186 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -104,7 +104,7 @@ def open(self): self.cam.prop_setvalue(dcam.DCAM_IDPROP.BINNING, binning) # Set camera mode to "ultraquiet" (1.0) rather than "standard" (2.0) - dcam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, 1.0) + self.cam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, 1.0) self.current_pixel_format = self.config.get('pixel_format', 'Mono16') if self.current_pixel_format not in self.pixel_formats: From a204ee94b66848ea66077e14e2613779b2a3552b Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 13:33:54 +0200 Subject: [PATCH 13/25] Return sensor width and height as int --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 66234c186..9f8b434e6 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -347,7 +347,7 @@ def sensor_width(self): int: The width of the sensor in pixels. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_WIDTH) + return int(self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_WIDTH)) @property def sensor_height(self): @@ -361,7 +361,7 @@ def sensor_height(self): int: The height of the sensor in pixels. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_HEIGHT) + return int(self.cam.prop_getvalue(dcam.DCAM_IDPROP.IMAGE_HEIGHT)) @property def width(self): From 527d1f93d3bf5c8582097c1e502f371df2089875 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 13:40:23 +0200 Subject: [PATCH 14/25] Define image dtype before submitting to data stream --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 9f8b434e6..8b3e6b392 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -201,7 +201,7 @@ def acquisition_loop(self): if self.cam.wait_capevent_frameready(timeout_millisec) is False: raise RuntimeError(f'Dcam.wait_capevent_frameready({timeout_millisec}) fails with error {self.cam.lasterr()}') img = self.cam.buf_getlastframedata() - self.images.submit_data(img) + self.images.submit_data(img.astype('float32')) finally: # Stop acquisition. From a734a0a40cc9de5fbbca632cff6471b3b623a2b5 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 13:40:51 +0200 Subject: [PATCH 15/25] Return ROI width and height as int --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 8b3e6b392..f20465e5d 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -375,7 +375,7 @@ def width(self): int: The width of the image in pixels. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYHSIZE) + return int(self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYHSIZE)) @width.setter def width(self, width: int): @@ -403,7 +403,7 @@ def height(self): int: The height of the image in pixels. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYVSIZE) + return int(self.cam.prop_getvalue(dcam.DCAM_IDPROP.SUBARRAYVSIZE)) @height.setter def height(self, height: int): From fb4c4424fbd0a0410c2d01558c27a160686713f4 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 13:46:29 +0200 Subject: [PATCH 16/25] Stick in SENSITIVITY keyword for brightness --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index f20465e5d..5e6fbccdd 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -291,7 +291,7 @@ def gain(self): int: The gain of the camera. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.CONTRASTGAIN) # TODO: verify this + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.CONTRASTGAIN) @gain.setter def gain(self, gain: int): @@ -305,7 +305,7 @@ def gain(self, gain: int): gain : int The gain of the camera. """ - self.cam.prop_setvalue(dcam.DCAM_IDPROP.CONTRASTGAIN, gain) # TODO: verify this + self.cam.prop_setvalue(dcam.DCAM_IDPROP.CONTRASTGAIN, gain) @property def brightness(self): @@ -319,7 +319,7 @@ def brightness(self): int: The brightness of the camera. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.XXX) # TODO: find correct property + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.SENSITIVITY) @brightness.setter def brightness(self, brightness: int): @@ -333,7 +333,7 @@ def brightness(self, brightness: int): brightness : int The brightness of the camera. """ - self.cam.prop_setvalue(dcam.DCAM_IDPROP.XXX, brightness) # TODO: find correct property + self.cam.prop_setvalue(dcam.DCAM_IDPROP.SENSITIVITY, brightness) @property def sensor_width(self): From d2f978f81dbd455ac518f42c831178b7dfa37c94 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 13:52:28 +0200 Subject: [PATCH 17/25] Clarify subarray mode setting --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 5e6fbccdd..f35615777 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -96,8 +96,8 @@ def open(self): if self.cam.dev_open() is False: raise RuntimeError(f'Dcam.dev_open() fails with error {self.cam.lasterr()}') - # Set subarray mode - self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYMODE, 2.0) # TODO: ? - 2.0 for subarray mode on + # Set subarray mode to on so that it checks subarray compatibility when picking ROI + self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYMODE, 2.0) # Set binning binning = self.config.get('binning', 1) From c2b7356a42e3b19737c72be3e4e117df6f3ddb8e Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 18 Apr 2024 14:08:59 +0200 Subject: [PATCH 18/25] Read camera mode from config --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index f35615777..a817ce004 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -103,8 +103,15 @@ def open(self): binning = self.config.get('binning', 1) self.cam.prop_setvalue(dcam.DCAM_IDPROP.BINNING, binning) - # Set camera mode to "ultraquiet" (1.0) rather than "standard" (2.0) - self.cam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, 1.0) + # Set camera mode + camera_mode = self.config.get('camera_mode', "standard") + if camera_mode == "ultraquiet": + self.camera_mode = 1.0 + elif camera_mode == "standard": + self.camera_mode = 2.0 + else: + raise ValueError(f'Invalid camera mode: {camera_mode}, must be one of ["ultraquiet", "standard"]') + self.cam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, self.camera_mode) self.current_pixel_format = self.config.get('pixel_format', 'Mono16') if self.current_pixel_format not in self.pixel_formats: From a9199a2b0d35ef1189e3ed2ec2c9fc6e2d77da34 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Mon, 22 Apr 2024 14:35:11 +0200 Subject: [PATCH 19/25] Drop first frame in acquisition loop --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index a817ce004..b9714f7a5 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -207,6 +207,12 @@ def acquisition_loop(self): self.log.warning('Timeout while waiting for frame') if self.cam.wait_capevent_frameready(timeout_millisec) is False: raise RuntimeError(f'Dcam.wait_capevent_frameready({timeout_millisec}) fails with error {self.cam.lasterr()}') + + if self.cam.buf_getframedata(-2) is False: + # If there is no second to last frame, it means we just acquired the first frame. + # In this case, drop the current frame since it always contains systematic errors. + continue + img = self.cam.buf_getlastframedata() self.images.submit_data(img.astype('float32')) From 8f3307f1cea23e91e2dedf433bb5c90bcf5c93ed Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Mon, 22 Apr 2024 23:14:47 +0200 Subject: [PATCH 20/25] Add log message when dropping first frame --- catkit2/services/hamamatsu_camera/hamamatsu_camera.py | 1 + 1 file changed, 1 insertion(+) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index b9714f7a5..8d27f6268 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -211,6 +211,7 @@ def acquisition_loop(self): if self.cam.buf_getframedata(-2) is False: # If there is no second to last frame, it means we just acquired the first frame. # In this case, drop the current frame since it always contains systematic errors. + self.log.info('Dropping first camera frame') continue img = self.cam.buf_getlastframedata() From ae3448378b071030dd6474f1aca1682c196f5b0d Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Mon, 22 Apr 2024 23:20:51 +0200 Subject: [PATCH 21/25] Handle exposure time in microseconds --- .../services/hamamatsu_camera/hamamatsu_camera.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 8d27f6268..8247d603d 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -129,7 +129,7 @@ def open(self): self.offset_y = offset_y self.gain = self.config.get('gain', 0) - self.exposure_time = self.config.get('exposure_time', 1) + self.exposure_time = self.config.get('exposure_time', 1000) self.temperature = self.make_data_stream('temperature', 'float64', [1], 20) # Create datastreams @@ -268,30 +268,30 @@ def get_temperature(self): @property def exposure_time(self): """ - The exposure time in seconds. + The exposure time in microseconds. This property can be used to get the exposure time of the camera. Returns: -------- float: - The exposure time in seconds. + The exposure time in microseconds. """ - return self.cam.prop_getvalue(dcam.DCAM_IDPROP.EXPOSURETIME) + return self.cam.prop_getvalue(dcam.DCAM_IDPROP.EXPOSURETIME) * 1e6 @exposure_time.setter def exposure_time(self, exposure_time: float): """ - Set the exposure time in seconds. + Set the exposure time in microseconds. This property can be used to set the exposure time of the camera. Parameters ---------- exposure_time : float - The exposure time in seconds. + The exposure time in microseconds. """ - self.cam.prop_setvalue(dcam.DCAM_IDPROP.EXPOSURETIME, exposure_time) + self.cam.prop_setvalue(dcam.DCAM_IDPROP.EXPOSURETIME, exposure_time / 1e6) @property def gain(self): From 243b5ca553f1c14e510f46f074465bad7e7307d1 Mon Sep 17 00:00:00 2001 From: THD2-team Date: Wed, 24 Apr 2024 10:54:12 +0200 Subject: [PATCH 22/25] Fix first frame dropping --- .../services/hamamatsu_camera/hamamatsu_camera.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index 8247d603d..a7b33b8b3 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -202,21 +202,25 @@ def acquisition_loop(self): timeout_millisec = 2000 try: + i = 0 while self.should_be_acquiring.is_set() and not self.should_shut_down: if self.cam.lasterr().is_timeout(): self.log.warning('Timeout while waiting for frame') if self.cam.wait_capevent_frameready(timeout_millisec) is False: raise RuntimeError(f'Dcam.wait_capevent_frameready({timeout_millisec}) fails with error {self.cam.lasterr()}') - if self.cam.buf_getframedata(-2) is False: - # If there is no second to last frame, it means we just acquired the first frame. - # In this case, drop the current frame since it always contains systematic errors. + img = self.cam.buf_getlastframedata() + + if i == 0: + # The first frame often contains systematic errors, so drop it. self.log.info('Dropping first camera frame') + i += 1 continue - img = self.cam.buf_getlastframedata() self.images.submit_data(img.astype('float32')) + i += 1 + finally: # Stop acquisition. self.cam.cap_stop() From 568bf8f99dc7e8cfcc9dacdb6cfef2c64f48aae1 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Fri, 26 Apr 2024 14:18:24 +0200 Subject: [PATCH 23/25] Add a service doc for the Hamamatsu camera --- docs/services/hamamatsu_camera.rst | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/services/hamamatsu_camera.rst diff --git a/docs/services/hamamatsu_camera.rst b/docs/services/hamamatsu_camera.rst new file mode 100644 index 000000000..3de65e414 --- /dev/null +++ b/docs/services/hamamatsu_camera.rst @@ -0,0 +1,69 @@ +Hamamatsu Camera +==================== + +This service controls a Hamamatsu camera. It is a wrapper around the DCAM SDK, which is distributed on the manufacturer +website together with their Python API ``dcam``: `https://www.hamamatsu.com/eu/en/product/cameras/software/driver-software.html `_ + +The service requires the definition of an environment variable ``CATKIT_DCAM_SDK_PATH`` that points to the +``python`` directory within the DCAM SDK installation. + +The service has been successfully tested with the following camera models: +- Hamamatsu ORCA-Quest C15550-20UP + +Configuration +------------- + +.. code-block:: YAML + + camera1: + service_type: hamamatsu_camera + simulated_service_type: camera_sim + requires_safety: false + + camera_id: 0 + camera_mode: 'ultraquiet' + pixel_format: Mono16 + binning: 1 + + offset_x: 0 + offset_y: 0 + width: 400 + height: 400 + sensor_width: 4096 + sensor_height: 2304 + exposure_time: 8294.4 + gain: 0 + +Properties +---------- +``exposure_time``: Exposure time of the camera. + +``gain``: Gain of the camera. + +``brightness``: Brightness of the camera. + +``width``: The width of the camera. + +``height``: The height of the camera. + +``offset_x``: The x offset of the camera. + +``offset_y``: The y offset of the camera. + +``sensor_width``: The width of the sensor. + +``sensor_height``: The height of the sensor. + +Commands +-------- +``start_acquisition()``: This starts the acquisition of images from the camera. + +``end_acquisition()``: This ends the acquisition of images from the camera. + +Datastreams +----------- +``temperature``: The temperature as measured by this sensor in Celsius. + +``images``: The images acquired by the camera. + +``is_acquiring``: Whether the camera is currently acquiring images. \ No newline at end of file From 028928dca3099ac1c00839cb692973a0fc050dac Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Fri, 26 Apr 2024 15:13:36 +0200 Subject: [PATCH 24/25] Specify exposure time units --- docs/services/allied_vision_camera.rst | 2 +- docs/services/hamamatsu_camera.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/services/allied_vision_camera.rst b/docs/services/allied_vision_camera.rst index 2d81800f3..cf2b32d3e 100644 --- a/docs/services/allied_vision_camera.rst +++ b/docs/services/allied_vision_camera.rst @@ -41,7 +41,7 @@ Configuration Properties ---------- -``exposure_time``: Exposure time of the camera. +``exposure_time``: Exposure time of the camera in microseconds. ``gain``: Gain of the camera. diff --git a/docs/services/hamamatsu_camera.rst b/docs/services/hamamatsu_camera.rst index 3de65e414..507535984 100644 --- a/docs/services/hamamatsu_camera.rst +++ b/docs/services/hamamatsu_camera.rst @@ -36,7 +36,7 @@ Configuration Properties ---------- -``exposure_time``: Exposure time of the camera. +``exposure_time``: Exposure time of the camera in microseconds. ``gain``: Gain of the camera. From d7edf0fe9d88f3273697c93e87d2ea29d09f7f99 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Thu, 23 May 2024 14:43:55 +0200 Subject: [PATCH 25/25] Add setting of detector- and hot pixel correction modes from config --- .../hamamatsu_camera/hamamatsu_camera.py | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py index a7b33b8b3..b77f8b69c 100644 --- a/catkit2/services/hamamatsu_camera/hamamatsu_camera.py +++ b/catkit2/services/hamamatsu_camera/hamamatsu_camera.py @@ -99,19 +99,31 @@ def open(self): # Set subarray mode to on so that it checks subarray compatibility when picking ROI self.cam.prop_setvalue(dcam.DCAM_IDPROP.SUBARRAYMODE, 2.0) - # Set binning binning = self.config.get('binning', 1) self.cam.prop_setvalue(dcam.DCAM_IDPROP.BINNING, binning) - # Set camera mode - camera_mode = self.config.get('camera_mode', "standard") - if camera_mode == "ultraquiet": - self.camera_mode = 1.0 - elif camera_mode == "standard": - self.camera_mode = 2.0 + detector_correction = 2.0 if self.config.get('detector_correction', True) else 1.0 + self.cam.prop_setvalue(dcam.DCAM_IDPROP.DEFECTCORRECT_MODE, detector_correction) + + self.hot_pixel_correction = self.config.get('hot_pixel_correction', 'standard') + if self.hot_pixel_correction == "standard": + hot_pixel_correction = 1.0 + elif self.hot_pixel_correction == "minimum": + hot_pixel_correction = 2.0 + elif self.hot_pixel_correction == "aggressive": + hot_pixel_correction = 3.0 + else: + raise ValueError(f'Invalid hot pixel correction: {self.hot_pixel_correction}, must be one of ["standard", "minimum", "aggressive"]') + self.cam.prop_setvalue(self.cam.DCAM_IDPROP.HOTPIXELCORRECT_LEVEL, hot_pixel_correction) + + self.camera_mode = self.config.get('camera_mode', "standard") + if self.camera_mode == "ultraquiet": + camera_mode = 1.0 + elif self.camera_mode == "standard": + camera_mode = 2.0 else: - raise ValueError(f'Invalid camera mode: {camera_mode}, must be one of ["ultraquiet", "standard"]') - self.cam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, self.camera_mode) + raise ValueError(f'Invalid camera mode: {self.camera_mode}, must be one of ["ultraquiet", "standard"]') + self.cam.prop_setvalue(dcam.DCAM_IDPROP.READOUTSPEED, camera_mode) self.current_pixel_format = self.config.get('pixel_format', 'Mono16') if self.current_pixel_format not in self.pixel_formats: