diff --git a/pymeasure/instruments/thorlabs/__init__.py b/pymeasure/instruments/thorlabs/__init__.py index 70cf06c2dc..926476c679 100644 --- a/pymeasure/instruments/thorlabs/__init__.py +++ b/pymeasure/instruments/thorlabs/__init__.py @@ -24,4 +24,4 @@ from .thorlabspm100usb import ThorlabsPM100USB from .thorlabspro8000 import ThorlabsPro8000 -from .thorlabskst201 import ThorlabsKST201 +from .imageacquisition import CS165MUM, Image_Parameters, ImageAcquisitionThread \ No newline at end of file diff --git a/pymeasure/instruments/thorlabs/thorlabscs165mum.py b/pymeasure/instruments/thorlabs/thorlabscs165mum.py new file mode 100644 index 0000000000..e9796924dd --- /dev/null +++ b/pymeasure/instruments/thorlabs/thorlabscs165mum.py @@ -0,0 +1,155 @@ +# Author: Amelie Deshazer +# Data: 6/18/24 +# Purpose: Communicate with the ThorLabs CS165MU/M color camera. Three classes are defined in this script to control image acquisition, image processing, and camera paramters. +# Saves image as a tif file in a specified directory. + + +try: + # if on Windows, use the provided setup script to add the DLLs folder to the PATH + from windows_setup import configure_path + configure_path() +except ImportError: + configure_path = None + +import logging +import time +import queue +from thorlabs_tsi_sdk.tl_camera import TLCameraSDK, Frame +from thorlabs_tsi_sdk.tl_camera_enums import SENSOR_TYPE +from thorlabs_tsi_sdk.tl_mono_to_color_processor import MonoToColorProcessorSDK +from PIL import Image + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +class CS165MUM(): + def __init__(self, i_camera): + self.sdk = TLCameraSDK() + #self.close = TLCamera() + camera_serial_number = self.sdk.discover_available_cameras() + if camera_serial_number != 0: + print("Camera Detected") + log.info(camera_serial_number) + else: + print("No camera detected") + + self.camera = self.sdk.open_camera(camera_serial_number[i_camera]) + self.camera.frames_per_trigger_zero_for_unlimited = 0 + + def set_exposure_time_us(self, exposure_time_us): + """Sets the exposure time in microseconds.""" + min_exposure, max_exposure = self.camera.exposure_time_range_us # fix to truncated range + if not min_exposure <= exposure_time_us <= max_exposure: + raise ValueError(f"Exposure time must be between {min_exposure} and {max_exposure} microseconds.") + + self.camera.exposure_time_us = exposure_time_us + time.sleep(0.5) + print(f"Exposure time set to {self.camera.exposure_time_us} microseconds.") + + def get_exposure_time_us(self): + """Gets the exposure time in microseconds.""" + exposure_time = self.camera.exposure_time_us + print(f"Current exposure time is {exposure_time} microseconds.") + return exposure_time + + + def initialize_camera(self): + self.camera.arm(2) + self.camera.issue_software_trigger() + + + def image_acquire(self): + acquisition_thread = ImageAcquisitionThread(self.camera) + acquisition_thread.run() + print("Image acquisition finished") + return acquisition_thread.run() + + + def save_image(self, image_data, file_path): + image_data.save(file_path, format = 'TIFF') + print(f"Image saved to {file_path}") + + + + def dispose(self): + self.camera.disarm() + log.info("Camera disarmed") + self.camera.dispose() #After called, the camera object is no longer valid and should not be used. + logging.info("Camera disposed.") + + + +class ImageAcquisitionThread(): + + def __init__(self, camera): + super().__init__() + self._camera = camera + self._previous_timestamp = 0 + self.image_data = None #added this line + + # setup color processing if necessary + if self._camera.camera_sensor_type != SENSOR_TYPE.BAYER: + # Sensor type is not compatible with the color processing library + self._is_color = False + else: + self._mono_to_color_sdk = MonoToColorProcessorSDK() + self._image_width = self._camera.image_width_pixels + self._image_height = self._camera.image_height_pixels + self._mono_to_color_processor = self._mono_to_color_sdk.create_mono_to_color_processor( + SENSOR_TYPE.BAYER, + self._camera.color_filter_array_phase, + self._camera.get_color_correction_matrix(), + self._camera.get_default_white_balance_matrix(), + self._camera.bit_depth + ) + self._is_color = True + + self._bit_depth = camera.bit_depth + self._camera.image_poll_timeout_ms = 0 # Do not want to block for long periods of time + + def get_output_queue(self): + # type: (type[None]) -> queue.Queue + return self._image_queue + + def _get_color_image(self, frame): + # type: (Frame) -> Image + # verify the image size + width = frame.image_buffer.shape[1] + height = frame.image_buffer.shape[0] + if (width != self._image_width) or (height != self._image_height): + self._image_width = width + self._image_height = height + print("Image dimension change detected, image acquisition thread was updated") + # color the image. transform_to_24 will scale to 8 bits per channel + color_image_data = self._mono_to_color_processor.transform_to_24(frame.image_buffer, + self._image_width, + self._image_height) + color_image_data = color_image_data.reshape(self._image_height, self._image_width, 3) + print("color_image_data =", color_image_data) + + # return PIL Image object + return Image.fromarray(color_image_data, mode='RGB') + + def _get_image(self, frame): + # type: (Frame) -> Image + # no coloring, just scale down image to 8 bpp and place into PIL Image object + scaled_image = frame.image_buffer >> (self._bit_depth - 8) + return Image.fromarray(scaled_image) + + def run(self): + frame = self._camera.get_pending_frame_or_null() + if frame is not None: + if self._is_color: + pil_image = self._get_color_image(frame) + self.image_data = pil_image + print("color image") + else: + pil_image = self._get_image(frame) + print("not color image") + + self._mono_to_color_processor.dispose() + self._mono_to_color_sdk.dispose() + print("Disposed") + return self.image_data + + \ No newline at end of file diff --git a/pymeasure/instruments/thorlabs/thorlabskst201.py b/pymeasure/instruments/thorlabs/thorlabskst201.py deleted file mode 100644 index 78cc1b0122..0000000000 --- a/pymeasure/instruments/thorlabs/thorlabskst201.py +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is part of the PyMeasure package. -# -# Copyright (c) 2013-2024 PyMeasure Developers -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# - -from pymeasure.instruments import Instrument -from pymeasure.instruments.validators import strict_discrete_set - -""" -Required DLLs -Thorlabs.MotionControl.DeviceManager.dll -Thorlabs.MotionControl.KCube.StepperMotor.dll -Thorlabs.MotionControl.KCube.StepperMotor.h -Thorlabs.MotionControl.KCube.StepperMotor.lib - -See an example with ctypes: -``` -/home/daichi/OneDrive/DatasheetsManuals/Thorlabs/Motion_Control_Examples/Python/KCube/KDC101/kdc101_example.py -``` -""" - -clr.AddReference("C:\\Program Files\\Thorlabs\\Kinesis\\Thorlabs.MotionControl.DeviceManagerCLI.dll") -clr.AddReference("C:\\Program Files\\Thorlabs\\Kinesis\\Thorlabs.MotionControl.GenericMotorCLI.dll") -clr.AddReference("C:\\Program Files\\Thorlabs\\Kinesis\\ThorLabs.MotionControl.KCube.DCServoCLI.dll") -from Thorlabs.MotionControl.DeviceManagerCLI import * -from Thorlabs.MotionControl.GenericMotorCLI import * -from Thorlabs.MotionControl.KCube.DCServoCLI import * -from System import Decimal - - -class ThorlabsKST201(Instrument): - """Control the Thorlabs KST201 K-Cube Stepper Motor Controller.""" - - def __init__(self, adapter, name="Thorlabs KST201", **kwargs): - super().__init__( - adapter, - name, - **kwargs - ) diff --git a/pymeasure/instruments/thorlabs/windows_setup.py b/pymeasure/instruments/thorlabs/windows_setup.py new file mode 100644 index 0000000000..e52487487e --- /dev/null +++ b/pymeasure/instruments/thorlabs/windows_setup.py @@ -0,0 +1,45 @@ +""" +windows_setup.py + +In order for the Thorlabs Python examples to work, they need visibility of the directory containing the Thorlabs TSI +Native DLLs. This setup function changes the PATH environment variable (Just for the current process, not the system +PATH variable) by adding the directory containing the DLLs. This function is written specifically to work for the +Thorlabs Python SDK examples on Windows, but can be adjusted to work with custom programs. Changing the PATH variable +of a running application is just one way of making the DLLs visible to the program. The following methods could +be used instead: + +- Use the os module to adjust the program's current directory to be the directory containing the DLLs. +- Manually copy the DLLs into the working directory of your application. +- Manually add the path to the directory containing the DLLs to the system PATH environment variable. + +""" +import os +import sys + +def configure_path(): + is_64bits = sys.maxsize > 2**32 + relative_path_to_dlls = '..' + os.sep + 'dlls' + os.sep + + if is_64bits: + relative_path_to_dlls += '64_lib' + else: + relative_path_to_dlls += '32_lib' + + absolute_path_to_file_directory = os.path.dirname(os.path.abspath(__file__)) + + + #absolute_path_to_dlls = os.path.abspath(os.path.join(absolute_path_to_file_directory, 'thorlabs folder', relative_path_to_dlls)) + #absolute_path_to_dlls = r"C:\Program Files\Thorlabs\Scientific Imaging\ThorCam" + + #must change path to absolute_path_to_dlls. Current is 64 bit library + absolute_path_to_dlls = r"C:\Users\desha\Codes\Scientific_Camera_Interfaces_Windows-2.1\Scientific Camera Interfaces\SDK\Python Toolkit\dlls\64_lib" + print("absolute_path_to_dlls: ", absolute_path_to_dlls) + os.environ['PATH'] = absolute_path_to_dlls + os.pathsep + os.environ['PATH'] + + #os.environ['PATH'] = absolute_path_to_dlls + os.pathsep + os.environ['PATH'] + + try: + # Python 3.8 introduces a new method to specify dll directory + os.add_dll_directory(absolute_path_to_dlls) + except AttributeError: + pass