From 7a8008a9dfeb27bd375660bbca4ddd1734d75477 Mon Sep 17 00:00:00 2001 From: SinclairQuantumLab Date: Fri, 5 Dec 2025 15:54:04 -0600 Subject: [PATCH 1/5] Initialized SpinnakerCameraWorker --- .../SpinnakerCamera/blacs_workers.py | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/labscript_devices/SpinnakerCamera/blacs_workers.py b/labscript_devices/SpinnakerCamera/blacs_workers.py index 7cabada5..8f14d896 100644 --- a/labscript_devices/SpinnakerCamera/blacs_workers.py +++ b/labscript_devices/SpinnakerCamera/blacs_workers.py @@ -258,19 +258,35 @@ class SpinnakerCameraWorker(IMAQdxCameraWorker): Inherits from IMAQdxCameraWorker.""" interface_class = Spinnaker_Camera - #def continuous_loop(self, dt): - # """Acquire continuously in a loop, with minimum repetition interval dt""" - # self.camera.trigger() - # while True: - # if dt is not None: - # t = perf_counter() - # image = self.camera.grab() - # self.camera.trigger() - # self._send_image_to_parent(image) - # if dt is None: - # timeout = 0 - # else: - # timeout = t + dt - perf_counter() - # if self.continuous_stop.wait(timeout): - # self.continuous_stop.clear() - # break + def init(self): + print("Spinnaker Worker Called") + try: + self.camera = self.interface_class(self.serial_number) + self.camera.configure_acquisition() + print(f"Successfully initialized camera {self.serial_number}") + except Exception as e: + print(f"Error initializing camera: {e}") + raise + + def shutdown(self): + try: + self.camera.close() + except Exception as e: + print(f"Error shutting down camera: {e}") + + def continuous_loop(self, dt): + """Acquire continuously in a loop, with minimum repetition interval dt""" + self.camera.trigger() + while True: + if dt is not None: + t = perf_counter() + image = self.camera.grab() + self.camera.trigger() + self._send_image_to_parent(image) + if dt is None: + timeout = 0 + else: + timeout = t + dt - perf_counter() + if self.continuous_stop.wait(timeout): + self.continuous_stop.clear() + break From 0b1d519d025c4fc98e441367498df345e1b6c400 Mon Sep 17 00:00:00 2001 From: SinclairQuantumLab Date: Fri, 5 Dec 2025 15:54:04 -0600 Subject: [PATCH 2/5] Initialized SpinnakerCameraWorker --- .../SpinnakerCamera/blacs_workers.py | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/labscript_devices/SpinnakerCamera/blacs_workers.py b/labscript_devices/SpinnakerCamera/blacs_workers.py index 7cabada5..8f14d896 100644 --- a/labscript_devices/SpinnakerCamera/blacs_workers.py +++ b/labscript_devices/SpinnakerCamera/blacs_workers.py @@ -258,19 +258,35 @@ class SpinnakerCameraWorker(IMAQdxCameraWorker): Inherits from IMAQdxCameraWorker.""" interface_class = Spinnaker_Camera - #def continuous_loop(self, dt): - # """Acquire continuously in a loop, with minimum repetition interval dt""" - # self.camera.trigger() - # while True: - # if dt is not None: - # t = perf_counter() - # image = self.camera.grab() - # self.camera.trigger() - # self._send_image_to_parent(image) - # if dt is None: - # timeout = 0 - # else: - # timeout = t + dt - perf_counter() - # if self.continuous_stop.wait(timeout): - # self.continuous_stop.clear() - # break + def init(self): + print("Spinnaker Worker Called") + try: + self.camera = self.interface_class(self.serial_number) + self.camera.configure_acquisition() + print(f"Successfully initialized camera {self.serial_number}") + except Exception as e: + print(f"Error initializing camera: {e}") + raise + + def shutdown(self): + try: + self.camera.close() + except Exception as e: + print(f"Error shutting down camera: {e}") + + def continuous_loop(self, dt): + """Acquire continuously in a loop, with minimum repetition interval dt""" + self.camera.trigger() + while True: + if dt is not None: + t = perf_counter() + image = self.camera.grab() + self.camera.trigger() + self._send_image_to_parent(image) + if dt is None: + timeout = 0 + else: + timeout = t + dt - perf_counter() + if self.continuous_stop.wait(timeout): + self.continuous_stop.clear() + break From cfb368f9eb6302d920b0960c6c876c74c2b61982 Mon Sep 17 00:00:00 2001 From: SinclairQuantumLab Date: Mon, 15 Dec 2025 12:58:43 -0600 Subject: [PATCH 3/5] Updated SpinnakerCameraWorker --- .../SpinnakerCamera/blacs_workers.py | 178 ++++++++++++++---- 1 file changed, 145 insertions(+), 33 deletions(-) diff --git a/labscript_devices/SpinnakerCamera/blacs_workers.py b/labscript_devices/SpinnakerCamera/blacs_workers.py index 8f14d896..f86bea77 100644 --- a/labscript_devices/SpinnakerCamera/blacs_workers.py +++ b/labscript_devices/SpinnakerCamera/blacs_workers.py @@ -19,6 +19,11 @@ from labscript_utils import dedent from enum import IntEnum from time import sleep, perf_counter +import threading +import time +from blacs.tab_base_classes import ImageWorker +import PySpin + from labscript_devices.IMAQdxCamera.blacs_workers import IMAQdxCameraWorker @@ -251,42 +256,149 @@ def close(self): self.camList.Clear() self.system.ReleaseInstance() +class SpinnakerCameraWorker(ImageWorker): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.continuous_thread = None + self._stop_event = threading.Event() + self.camera = None + + def init(self): + """Initialize the camera object""" + system = PySpin.System.GetInstance() + cam_list = system.GetCameras() + if cam_list.GetSize() == 0: + raise RuntimeError("No cameras detected") + self.camera = cam_list[0] # choose the first camera + self.camera.Init() + self.system = system + self.cam_list = cam_list + + + def init_camera(self, cam): + """Initialize the camera object""" + self.camera = cam + self.camera.Init() -class SpinnakerCameraWorker(IMAQdxCameraWorker): - """Spinnaker API Camera Worker. + def start_continuous(self, *args, **kwargs): + """Start continuous acquisition in a separate thread""" + if self.camera.IsStreaming(): + print("Camera already streaming. Skipping start_continuous.") + return - Inherits from IMAQdxCameraWorker.""" - interface_class = Spinnaker_Camera + if self.continuous_thread is not None: + # Thread already running + return - def init(self): - print("Spinnaker Worker Called") + self._stop_event.clear() + self.continuous_thread = threading.Thread(target=self._continuous_acquisition) + self.continuous_thread.start() + + def _continuous_acquisition(self): try: - self.camera = self.interface_class(self.serial_number) - self.camera.configure_acquisition() - print(f"Successfully initialized camera {self.serial_number}") - except Exception as e: - print(f"Error initializing camera: {e}") - raise - + self.camera.BeginAcquisition() + while not self._stop_event.is_set(): + image = self.camera.GetNextImage(1000) + if not image.IsIncomplete(): + img_array = image.GetNDArray() + # Send live frame to BLACS GUI + self.send_image(img_array) + image.Release() + except PySpin.SpinnakerException as ex: + print(f"Spinnaker exception: {ex}") + finally: + try: + if self.camera.IsStreaming(): + self.camera.EndAcquisition() + except PySpin.SpinnakerException: + pass + + def stop_continuous(self): + """Stop continuous acquisition""" + if self.continuous_thread is not None: + self._stop_event.set() + self.continuous_thread.join() + self.continuous_thread = None + + def snap(self, *args, **kwargs): + if self.camera is None: + raise RuntimeError("Camera not initialized") + + try: + if self.camera.IsStreaming(): + self.camera.EndAcquisition() + self.camera.BeginAcquisition() + + image = self.camera.GetNextImage(1000) + if not image.IsIncomplete(): + img_array = image.GetNDArray() + # Send to BLACS GUI + self.send_image(img_array) + image.Release() + + self.camera.EndAcquisition() + except PySpin.SpinnakerException as ex: + print(f"Spinnaker exception during snap: {ex}") + def shutdown(self): + """Clean up camera on BLACS exit""" try: - self.camera.close() - except Exception as e: - print(f"Error shutting down camera: {e}") - - def continuous_loop(self, dt): - """Acquire continuously in a loop, with minimum repetition interval dt""" - self.camera.trigger() - while True: - if dt is not None: - t = perf_counter() - image = self.camera.grab() - self.camera.trigger() - self._send_image_to_parent(image) - if dt is None: - timeout = 0 - else: - timeout = t + dt - perf_counter() - if self.continuous_stop.wait(timeout): - self.continuous_stop.clear() - break + self.stop_continuous() + if self.camera is not None: + if self.camera.IsStreaming(): + self.camera.EndAcquisition() + self.camera.DeInit() + del self.camera + except PySpin.SpinnakerException as ex: + print(f"Spinnaker exception during shutdown: {ex}") + def program_manual(self, *args, **kwargs): + """ + Called by BLACS when switching to manual mode. + For now, this can be a no-op since we handle snapping via snap(). + """ + pass + + def abort(self): + """Called by BLACS when aborting an experiment.""" + self.stop_continuous() + + +# class SpinnakerCameraWorker(IMAQdxCameraWorker): +# """Spinnaker API Camera Worker. + +# Inherits from IMAQdxCameraWorker.""" +# interface_class = Spinnaker_Camera + +# def init(self): +# self.continuous_thread = None +# print("Spinnaker Worker Called") +# try: +# self.camera = self.interface_class(self.serial_number) +# self.camera.configure_acquisition() +# print(f"Successfully initialized camera {self.serial_number}") +# except Exception as e: +# print(f"Error initializing camera: {e}") +# raise + +# def shutdown(self): +# try: +# self.camera.close() +# except Exception as e: +# print(f"Error shutting down camera: {e}") + +# def continuous_loop(self, dt): +# """Acquire continuously in a loop, with minimum repetition interval dt""" +# self.camera.trigger() +# while True: +# if dt is not None: +# t = perf_counter() +# image = self.camera.grab() +# self.camera.trigger() +# self._send_image_to_parent(image) +# if dt is None: +# timeout = 0 +# else: +# timeout = t + dt - perf_counter() +# if self.continuous_stop.wait(timeout): +# self.continuous_stop.clear() +# break From 1903dac397e69168a489732f9e6b3b840929ef23 Mon Sep 17 00:00:00 2001 From: SinclairQuantumLab Date: Mon, 15 Dec 2025 16:34:23 -0600 Subject: [PATCH 4/5] Blackfly Camera Taking Photos --- .../SpinnakerCamera/blacs_tabs.py | 18 ++++++++++++++++++ .../SpinnakerCamera/blacs_workers.py | 10 ++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/labscript_devices/SpinnakerCamera/blacs_tabs.py b/labscript_devices/SpinnakerCamera/blacs_tabs.py index e63e8c93..06903c11 100644 --- a/labscript_devices/SpinnakerCamera/blacs_tabs.py +++ b/labscript_devices/SpinnakerCamera/blacs_tabs.py @@ -12,8 +12,26 @@ ##################################################################### from labscript_devices.IMAQdxCamera.blacs_tabs import IMAQdxCameraTab +from qtpy import QtWidgets, QtGui + class SpinnakerCameraTab(IMAQdxCameraTab): # override worker class worker_class = 'labscript_devices.SpinnakerCamera.blacs_workers.SpinnakerCameraWorker' + # def initialise_GUI(self): + # self.image_label = QtWidgets.QLabel(self) + # self.image_label.setFixedSize(640, 480) # adjust to your camera + # self.layout().addWidget(self.image_label) + + # def new_image(self, img_array): + # # Convert NumPy array to QImage + # h, w = img_array.shape + # q_img = QtGui.QImage(img_array.data, w, h, w, QtGui.QImage.Format_Grayscale8) + # pixmap = QtGui.QPixmap.fromImage(q_img) + # self.image_label.setPixmap(pixmap) + + # def initialise_workers(self): + # # This is called after the worker is initialized + # # Connect the worker signal 'new_image' to the GUI update + # self.camera_worker.register_event('new_image', self.new_image) diff --git a/labscript_devices/SpinnakerCamera/blacs_workers.py b/labscript_devices/SpinnakerCamera/blacs_workers.py index f86bea77..db041efb 100644 --- a/labscript_devices/SpinnakerCamera/blacs_workers.py +++ b/labscript_devices/SpinnakerCamera/blacs_workers.py @@ -21,8 +21,8 @@ from time import sleep, perf_counter import threading import time -from blacs.tab_base_classes import ImageWorker import PySpin +import imageio from labscript_devices.IMAQdxCamera.blacs_workers import IMAQdxCameraWorker @@ -256,7 +256,7 @@ def close(self): self.camList.Clear() self.system.ReleaseInstance() -class SpinnakerCameraWorker(ImageWorker): +class SpinnakerCameraWorker(IMAQdxCameraWorker): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.continuous_thread = None @@ -302,7 +302,8 @@ def _continuous_acquisition(self): if not image.IsIncomplete(): img_array = image.GetNDArray() # Send live frame to BLACS GUI - self.send_image(img_array) + #self.send_image(img_array) + imageio.imwrite('camera_test/live.png', img_array) image.Release() except PySpin.SpinnakerException as ex: print(f"Spinnaker exception: {ex}") @@ -333,7 +334,8 @@ def snap(self, *args, **kwargs): if not image.IsIncomplete(): img_array = image.GetNDArray() # Send to BLACS GUI - self.send_image(img_array) + #self.send_image(img_array) + imageio.imwrite('camera_test/snap.png', img_array) image.Release() self.camera.EndAcquisition() From 9df3c393028eed7355b1b1c781c566cc4d386b5a Mon Sep 17 00:00:00 2001 From: SinclairQuantumLab Date: Wed, 17 Dec 2025 15:50:36 -0600 Subject: [PATCH 5/5] SpinnakerCamera working with GUI --- .../SpinnakerCamera/blacs_workers.py | 182 ++++++++++++++---- 1 file changed, 148 insertions(+), 34 deletions(-) diff --git a/labscript_devices/SpinnakerCamera/blacs_workers.py b/labscript_devices/SpinnakerCamera/blacs_workers.py index db041efb..36bd44e5 100644 --- a/labscript_devices/SpinnakerCamera/blacs_workers.py +++ b/labscript_devices/SpinnakerCamera/blacs_workers.py @@ -19,10 +19,17 @@ from labscript_utils import dedent from enum import IntEnum from time import sleep, perf_counter +from blacs.tab_base_classes import Worker import threading import time import PySpin import imageio +import zmq + +from labscript_utils.ls_zprocess import Context +from labscript_utils.shared_drive import path_to_local +from labscript_utils.properties import set_attributes + from labscript_devices.IMAQdxCamera.blacs_workers import IMAQdxCameraWorker @@ -256,12 +263,30 @@ def close(self): self.camList.Clear() self.system.ReleaseInstance() -class SpinnakerCameraWorker(IMAQdxCameraWorker): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.continuous_thread = None - self._stop_event = threading.Event() - self.camera = None +class SpinnakerCameraWorker(Worker): + # def __init__(self, *args, **kwargs): + # # self.camera = self.get_camera() + # # print("Setting attributes...") + # # self.smart_cache = {} + # # self.set_attributes_smart(self.camera_attributes) + # # self.set_attributes_smart(self.manual_mode_camera_attributes) + # # print("Initialisation complete") + # # self.images = None + # # self.n_images = None + # # self.attributes_to_save = None + # # self.exposures = None + # # self.acquisition_thread = None + # # self.h5_filepath = None + # # self.stop_acquisition_timeout = None + # # self.exception_on_failed_shot = None + # # self.continuous_stop = threading.Event() + # # self.continuous_thread = None + # # self.continuous_dt = None + # # self.image_socket = Context().socket(zmq.REQ) + # # self.image_socket.connect( + # # f'tcp://{self.parent_host}:{self.image_receiver_port}' + # # ) + # super.__init__(*args,) def init(self): """Initialize the camera object""" @@ -273,6 +298,13 @@ def init(self): self.camera.Init() self.system = system self.cam_list = cam_list + self.continuous_stop = threading.Event() + self.continuous_thread = None + self.continuous_dt = None + self.image_socket = Context().socket(zmq.REQ) + self.image_socket.connect( + f'tcp://{self.parent_host}:{self.image_receiver_port}' + ) def init_camera(self, cam): @@ -280,46 +312,120 @@ def init_camera(self, cam): self.camera = cam self.camera.Init() - def start_continuous(self, *args, **kwargs): - """Start continuous acquisition in a separate thread""" - if self.camera.IsStreaming(): - print("Camera already streaming. Skipping start_continuous.") - return + # def start_continuous(self, dt): + # self.continuous_stop.set() + # if self.camera.IsStreaming(): + # self.camera.EndAcquisition() + # self.camera.BeginAcquisition() + + # time.sleep(0.05) + # assert self.continuous_thread is None + # # self.camera.configure_acquisition() + # self.continuous_thread = threading.Thread( + # target=self.continuous_loop, args=(dt,), daemon=True + # ) + # self.continuous_thread.start() + # self.continuous_dt = dt + def start_continuous(self, dt): + # Ensure fully stopped + self.stop_continuous() + + self.continuous_stop.clear() + self.continuous_dt = dt - if self.continuous_thread is not None: - # Thread already running - return + assert self.continuous_thread is None - self._stop_event.clear() - self.continuous_thread = threading.Thread(target=self._continuous_acquisition) + # Start thread FIRST + self.continuous_thread = threading.Thread( + target=self.continuous_loop, + args=(dt,), + daemon=True, + ) self.continuous_thread.start() - def _continuous_acquisition(self): + # Give thread a moment to start blocking on GetNextImage + time.sleep(0.01) + + # THEN start acquisition + self.camera.BeginAcquisition() + + + # def continuous_loop(self, dt): + # """Acquire continuously in a loop, with minimum repetition interval dt""" + # while True: + # if dt is not None: + # t = perf_counter() + # try: + # image = self.camera.GetNextImage(1000) + # img_array = image.GetNDArray() + # self._send_image_to_parent(img_array) + # image.Release() + # except: + # pass + # if dt is None: + # timeout = 0 + # else: + # timeout = t + dt - perf_counter() + # if self.continuous_stop.wait(timeout): + # self.continuous_stop.clear() + # break + + + # self.stop_continuous() + + def continuous_loop(self, dt): try: - self.camera.BeginAcquisition() - while not self._stop_event.is_set(): - image = self.camera.GetNextImage(1000) - if not image.IsIncomplete(): - img_array = image.GetNDArray() - # Send live frame to BLACS GUI - #self.send_image(img_array) - imageio.imwrite('camera_test/live.png', img_array) + while not self.continuous_stop.is_set(): + if dt is not None: + t = perf_counter() + + try: + image = self.camera.GetNextImage(1000) + except PySpin.SpinnakerException: + continue + + if image.IsIncomplete(): + image.Release() + continue + + img_array = image.GetNDArray() + self._send_image_to_parent(img_array) image.Release() - except PySpin.SpinnakerException as ex: - print(f"Spinnaker exception: {ex}") + + if dt is not None: + timeout = max(0, t + dt - perf_counter()) + self.continuous_stop.wait(timeout) + finally: + # Stop acquisition HERE, in the acquisition thread try: - if self.camera.IsStreaming(): - self.camera.EndAcquisition() + self.camera.EndAcquisition() except PySpin.SpinnakerException: pass + + # def stop_continuous(self): + # self.continuous_stop.set() + # try: + # self.continuous_thread.join() + # except: + # pass + # self.continuous_thread = None + # if self.camera.IsStreaming(): + # self.camera.EndAcquisition() + def stop_continuous(self): - """Stop continuous acquisition""" - if self.continuous_thread is not None: - self._stop_event.set() + self.continuous_stop.set() + + if ( + self.continuous_thread is not None + and self.continuous_thread.is_alive() + and threading.current_thread() is not self.continuous_thread + ): self.continuous_thread.join() - self.continuous_thread = None + + self.continuous_thread = None + def snap(self, *args, **kwargs): if self.camera is None: @@ -335,7 +441,8 @@ def snap(self, *args, **kwargs): img_array = image.GetNDArray() # Send to BLACS GUI #self.send_image(img_array) - imageio.imwrite('camera_test/snap.png', img_array) + #imageio.imwrite('camera_test/snap.png', img_array) + self._send_image_to_parent(img_array) image.Release() self.camera.EndAcquisition() @@ -363,6 +470,13 @@ def program_manual(self, *args, **kwargs): def abort(self): """Called by BLACS when aborting an experiment.""" self.stop_continuous() + + def _send_image_to_parent(self, image): + metadata = dict(dtype=str(image.dtype), shape=image.shape) + self.image_socket.send_json(metadata, zmq.SNDMORE) + self.image_socket.send(image, copy=False) + response = self.image_socket.recv() + assert response == b'ok', response # class SpinnakerCameraWorker(IMAQdxCameraWorker):