wip unify the live mode on cameras

This commit is contained in:
2025-12-16 00:19:52 +01:00
parent d5c199cbd7
commit f5180d5cab
4 changed files with 101 additions and 53 deletions

View File

@@ -6,7 +6,7 @@ from enum import IntEnum
import numpy as np
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Component as Cpt, Kind, Signal
from ophyd.areadetector import ADComponent as ADCpt
from ophyd.areadetector import DetectorBase
from ophyd_devices import PreviewSignal
@@ -44,6 +44,14 @@ class AlliedVisionCamera(PSIDeviceBase, DetectorBase):
doc="Preview signal of the AlliedVision camera.",
)
live_mode_enabled = Cpt(
Signal,
name="live_mode_enabled",
value=False,
doc="Enable or disable live mode.",
kind=Kind.config,
)
def __init__(
self,
*,
@@ -68,24 +76,36 @@ class AlliedVisionCamera(PSIDeviceBase, DetectorBase):
self._poll_rate = poll_rate
self._unique_array_id = 0
self._pv_timeout = 2.0
self.r_lock = threading.RLock()
self.image: ImagePlugin
self._live_mode_lock = threading.RLock()
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
def start_live_mode(self) -> None:
"""Start live mode."""
if not self._poll_start_event.is_set():
self._poll_start_event.set()
self.cam.acquire.put(ACQUIRE_MODES.ACQUIRING.value) # Start acquisition
else:
logger.info(f"Live mode already started for {self.name}.")
self.live_mode_enabled.put(True)
def stop_live_mode(self) -> None:
"""Stop live mode."""
if self._poll_start_event.is_set():
self._poll_start_event.clear()
self.cam.acquire.put(ACQUIRE_MODES.DONE.value) # Stop acquisition
else:
logger.info(f"Live mode already stopped for {self.name}.")
self.live_mode_enabled.put(False)
def _on_live_mode_enabled_changed(self, *args, value, **kwargs) -> None:
self._apply_live_mode(bool(value))
def _apply_live_mode(self, enabled: bool) -> None:
with self._live_mode_lock:
if enabled:
if not self._poll_start_event.is_set():
self._poll_start_event.set()
self.cam.acquire.put(ACQUIRE_MODES.ACQUIRING.value) # Start acquisition
else:
logger.info(f"Live mode already started for {self.name}.")
return
if self._poll_start_event.is_set():
self._poll_start_event.clear()
self.cam.acquire.put(ACQUIRE_MODES.DONE.value) # Stop acquisition
else:
logger.info(f"Live mode already stopped for {self.name}.")
def on_connected(self):
"""Reset the unique array ID on connection."""

View File

@@ -156,7 +156,6 @@ class Camera:
camera_id (int): The ID of the camera device.
m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera.
bits_per_pixel (Literal[8, 24]): Number of bits per pixel for the camera.
live_mode (bool): Whether to enable live mode for the camera.
"""
def __init__(

View File

@@ -43,9 +43,15 @@ class IDSCamera(PSIDeviceBase):
doc="Signal for the region of interest (ROI).",
async_update={"type": "add", "max_shape": [None]},
)
live_mode_enabled = Cpt(Signal, name="live_mode_enabled", value=False ,doc="Enable or disable live mode.",kind=Kind.config)
live_mode_enabled = Cpt(
Signal,
name="live_mode_enabled",
value=False,
doc="Enable or disable live mode.",
kind=Kind.config,
)
USER_ACCESS = ["live_mode", "mask", "set_rect_roi", "get_last_image"]
USER_ACCESS = ["start_live_mode", "stop_live_mode", "mask", "set_rect_roi", "get_last_image"]
def __init__(
self,
@@ -87,11 +93,17 @@ class IDSCamera(PSIDeviceBase):
self.image.num_rotation_90 = num_rotation_90
self.image.transpose = transpose
self._force_monochrome = force_monochrome
self.live_mode_enabled.subscribe(self.on_live_mode_changed,run=False)
self.live_mode_enabled.put(live_mode)
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
self.live_mode_enabled.put(bool(live_mode))
############## Live Mode Methods ##############
def start_live_mode(self) -> None:
self.live_mode_enabled.put(True)
def stop_live_mode(self) -> None:
self.live_mode_enabled.put(False)
@property
def mask(self) -> np.ndarray:
"""Return the current region of interest (ROI) for the camera."""
@@ -114,23 +126,12 @@ class IDSCamera(PSIDeviceBase):
)
self._mask = value
@property
def live_mode(self) -> bool:
"""Return whether the camera is in live mode."""
return bool(self.live_mode_enabled.get())
@live_mode.setter
def live_mode(self, value: bool):
"""Set the live mode for the camera."""
if value != bool(self.live_mode_enabled.get()):
self.live_mode_enabled.put(bool(value))
def on_live_mode_changed(self, *args, value, **kwargs):
def _on_live_mode_enabled_changed(self, *args, value, **kwargs):
"""Callback for when live mode is changed."""
if self.cam._connected is False: # $ pylint: disable=protected-access
enabled = bool(value)
if enabled and self.cam._connected is False: # pylint: disable=protected-access
self.cam.on_connect()
self.live_mode_enabled.put(bool(value))
if value:
if enabled:
self._start_live()
else:
self._stop_live()
@@ -200,7 +201,7 @@ class IDSCamera(PSIDeviceBase):
"""Connect to the camera."""
self.cam.force_monochrome = self._force_monochrome
self.cam.on_connect()
self.live_mode = self._inputs.get("live_mode", False)
self.live_mode_enabled.put(bool(self._inputs.get("live_mode", False)))
self.set_rect_roi(0, 0, self.cam.cam.width.value, self.cam.cam.height.value)
def on_destroy(self):
@@ -210,7 +211,7 @@ class IDSCamera(PSIDeviceBase):
def on_trigger(self):
"""Handle the trigger event."""
if not self.live_mode:
if not bool(self.live_mode_enabled.get()):
return
image = self.image.get()
if image is not None:

View File

@@ -2,7 +2,7 @@ import requests
import threading
import cv2
import numpy as np
from ophyd import Device, Component as Cpt
from ophyd import Device, Component as Cpt, Kind, Signal
from ophyd_devices import PreviewSignal
import traceback
@@ -13,6 +13,13 @@ logger = bec_logger.logger
class WebcamViewer(Device):
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
preview = Cpt(PreviewSignal, ndim=2, num_rotation_90=0, transpose=False)
live_mode_enabled = Cpt(
Signal,
name="live_mode_enabled",
value=False,
doc="Enable or disable live mode.",
kind=Kind.config,
)
def __init__(self, url:str, name:str, num_rotation_90=0, transpose=False, **kwargs) -> None:
super().__init__(name=name, **kwargs)
@@ -21,20 +28,54 @@ class WebcamViewer(Device):
self._update_thread = None
self._buffer = b""
self._shutdown_event = threading.Event()
self._live_mode_lock = threading.RLock()
self.preview.num_rotation_90 = num_rotation_90
self.preview.transpose = transpose
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
def start_live_mode(self) -> None:
if self._connection is not None:
return
self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
self._update_thread.start()
self.live_mode_enabled.put(True)
def stop_live_mode(self) -> None:
self.live_mode_enabled.put(False)
def _on_live_mode_enabled_changed(self, *args, value, **kwargs) -> None:
self._apply_live_mode(bool(value))
def _apply_live_mode(self, enabled: bool) -> None:
with self._live_mode_lock:
if enabled:
if self._update_thread is not None and self._update_thread.is_alive():
return
self._shutdown_event.clear()
self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
self._update_thread.start()
return
if self._update_thread is None:
return
self._shutdown_event.set()
if self._connection is not None:
try:
self._connection.close()
except Exception: # pylint: disable=broad-except
pass
self._connection = None
self._update_thread.join(timeout=2)
if self._update_thread.is_alive():
logger.warning("Webcam live mode thread did not stop within timeout.")
return
self._update_thread = None
self._buffer = b""
self._shutdown_event.clear()
def _update_loop(self) -> None:
while not self._shutdown_event.is_set():
try:
self._connection = requests.get(self.url, stream=True)
self._connection = requests.get(self.url, stream=True, timeout=5)
for chunk in self._connection.iter_content(chunk_size=1024):
if self._shutdown_event.is_set():
break
self._buffer += chunk
start = self._buffer.find(b'\xff\xd8') # JPEG start
end = self._buffer.find(b'\xff\xd9') # JPEG end
@@ -50,16 +91,3 @@ class WebcamViewer(Device):
except Exception as exc:
content = traceback.format_exc()
logger.error(f"Image update loop failed: {content}")
def stop_live_mode(self) -> None:
if self._connection is None:
return
self._shutdown_event.set()
if self._connection is not None:
self._connection.close()
self._connection = None
if self._update_thread is not None:
self._update_thread.join()
self._update_thread = None
self._shutdown_event.clear()