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 7cabada5..36bd44e5 100644 --- a/labscript_devices/SpinnakerCamera/blacs_workers.py +++ b/labscript_devices/SpinnakerCamera/blacs_workers.py @@ -19,6 +19,18 @@ 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 @@ -251,26 +263,258 @@ def close(self): self.camList.Clear() self.system.ReleaseInstance() +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""" + 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 + 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): + """Initialize the camera object""" + self.camera = cam + self.camera.Init() + + # 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 + + assert self.continuous_thread is None + + # Start thread FIRST + self.continuous_thread = threading.Thread( + target=self.continuous_loop, + args=(dt,), + daemon=True, + ) + self.continuous_thread.start() + + # 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: + 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 -class SpinnakerCameraWorker(IMAQdxCameraWorker): - """Spinnaker API Camera Worker. - - 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 + img_array = image.GetNDArray() + self._send_image_to_parent(img_array) + image.Release() + + 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: + 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): + 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 + + + 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) + #imageio.imwrite('camera_test/snap.png', img_array) + self._send_image_to_parent(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.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() + + 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): +# """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