Source code for csaxs_bec.devices.ids_cameras.base_integration.camera

"""
This module provides a Camera class for handling IDS cameras using the pyueye library,
that links to the vendors C++ SDK. Details about the camera's C++ SDK API can be found
in the IDS Software Suite 4.96.1 documentation:
(https://www.1stvision.com/cameras/IDS/IDS-manuals/uEye_Manual/sdk_einleitung_schnellstart.html)

Here, we follow a procedure to set up the camera, configure its basic parameters and
allow automated capturing of images. The IDSCameraObject class is the low-level interface,
and requires the pyueye library and appropriate DLL files on the system. The Camera class
provides a high level interface which only creates the IDSCameraObject instance when the
on_connect method is called. This allows for lazy initialization of the camera, and
CI/CD pipelines can run without the pyueye library or the related DLLs installed on the system.
"""

from __future__ import annotations

import atexit
import time
from typing import Literal

import numpy as np
from bec_lib.logger import bec_logger

from csaxs_bec.devices.ids_cameras.base_integration.utils import check_error

logger = bec_logger.logger

try:
    from pyueye import ueye
except ImportError as exc:
    logger.warning(f"The pyueye library is not properly installed : {exc}")
    ueye = None  # type: ignore[assignment]


[docs] class IDSCameraObject: """Low-level base class for IDS Camera object. Args: device_id (int): The ID of the camera device. # e.g. 201; check idscamera tool m_n_colormode (int): Color mode for the camera. # 1 for cSAXS color cameras bits_per_pixel (int): Number of bits per pixel for the camera. # 24 for color cameras, 8 for monochrome cameras """ def __init__(self, device_id: int, m_n_colormode, bits_per_pixel): if ueye is None: raise ImportError( "The pyueye library is not installed or library files are missing. Please check your Python environment or library paths." ) self.ueye = ueye self._device_id = device_id self.h_cam = ueye.HIDS(device_id) self.s_info = ueye.SENSORINFO() self.c_info = ueye.CAMINFO() self.rect_roi = ueye.IS_RECT() self.pc_image_mem = ueye.c_mem_p() self.mem_id = ueye.int() self.pitch = ueye.INT() self.m_n_colormode = ueye.INT(m_n_colormode) self.n_bits_per_pixel = ueye.INT(bits_per_pixel) self.bytes_per_pixel = int(self.n_bits_per_pixel / 8) # Sequence to initialize the camera check_error(ueye.is_InitCamera(self.h_cam, None), "IDSCameraObject") check_error(ueye.is_GetSensorInfo(self.h_cam, self.s_info), "IDSCameraObject") check_error(ueye.is_GetCameraInfo(self.h_cam, self.c_info), "IDSCameraObject") check_error(ueye.is_ResetToDefault(self.h_cam), "IDSCameraObject") check_error(ueye.is_SetDisplayMode(self.h_cam, ueye.IS_SET_DM_DIB), "IDSCameraObject") if ( int.from_bytes(self.s_info.nColorMode.value, byteorder="big") == self.ueye.IS_COLORMODE_BAYER ): logger.info("Bayer color mode detected.") # setup the color depth to the current windows setting self.ueye.is_GetColorDepth( self.h_cam, self.n_bits_per_pixel, self.m_n_colormode ) # TODO This raises an error - maybe check the m_n_colormode value self.bytes_per_pixel = int(self.n_bits_per_pixel / 8) elif ( int.from_bytes(self.s_info.nColorMode.value, byteorder="big") == self.ueye.IS_COLORMODE_CBYCRY ): # for color camera models use RGB32 mode self.m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED self.n_bits_per_pixel = self.ueye.INT(32) self.bytes_per_pixel = int(self.n_bits_per_pixel / 8) elif ( int.from_bytes(self.s_info.nColorMode.value, byteorder="big") == self.ueye.IS_COLORMODE_MONOCHROME ): # for color camera models use RGB32 mode self.m_n_colormode = self.ueye.IS_CM_MONO8 self.n_bits_per_pixel = self.ueye.INT(8) self.bytes_per_pixel = int(self.n_bits_per_pixel / 8) else: # for monochrome camera models use Y8 mode self.m_n_colormode = self.ueye.IS_CM_MONO8 self.n_bits_per_pixel = self.ueye.INT(8) self.bytes_per_pixel = int(self.n_bits_per_pixel / 8) logger.info("Monochrome camera mode detected.") # Can be used to set the size and position of an "area of interest"(AOI) within an image check_error( self.ueye.is_AOI( self.h_cam, self.ueye.IS_AOI_IMAGE_GET_AOI, self.rect_roi, self.ueye.sizeof(self.rect_roi), ), "IDSCameraObject", ) self.width = self.rect_roi.s32Width self.height = self.rect_roi.s32Height check_error( self.ueye.is_AllocImageMem( self.h_cam, self.width, self.height, self.n_bits_per_pixel, self.pc_image_mem, self.mem_id, ), "IDSCameraObject", ) check_error( self.ueye.is_SetImageMem(self.h_cam, self.pc_image_mem, self.mem_id), "IDSCameraObject" ) check_error(self.ueye.is_SetColorMode(self.h_cam, self.m_n_colormode), "IDSCameraObject") check_error( self.ueye.is_CaptureVideo(self.h_cam, self.ueye.IS_DONT_WAIT), "IDSCameraObject" ) check_error( self.ueye.is_InquireImageMem( self.h_cam, self.pc_image_mem, self.mem_id, self.width, self.height, self.n_bits_per_pixel, self.pitch, ), "IDSCameraObject", ) def __repr__(self): return f"IDSCameraObject\n\ndevice_id={self._device_id},\ns_info={self.s_info},\nc_info={self.c_info},\nrect_roi={self.rect_roi},\npc_image_mem={self.pc_image_mem},\nmem_id={self.mem_id},\npitch={self.pitch},\nm_n_colormode={self.m_n_colormode},\nn_bits_per_pixel={self.n_bits_per_pixel},\nbytes_per_pixel={self.bytes_per_pixel}"
[docs] class Camera: """High level camera base class for IDS cameras. Args: 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. """ def __init__( self, camera_id: int, m_n_colormode: Literal[0, 1, 2, 3] = 1, bits_per_pixel: int = 24, connect: bool = True, force_monochrome: bool = False, ): self.ueye = ueye self.camera_id = camera_id self._inputs = {"m_n_colormode": m_n_colormode, "bits_per_pixel": bits_per_pixel} self.force_monochrome = force_monochrome self._connected = False self.cam = None atexit.register(self.on_disconnect) self._enable_warning_rate_limit: bool = False self._last_rate_limited_log: float = 0 self._warning_log_rate_limit_s: float = 10 if connect: self.on_connect()
[docs] def set_roi(self, x: int, y: int, width: int, height: int): """Set the region of interest (ROI) for the camera.""" rect_roi = ueye.IS_RECT() rect_roi.s32X = x rect_roi.s32Y = y rect_roi.s32Width = width rect_roi.s32Height = height ret = self.ueye.is_AOI( self.cam.h_cam, self.ueye.IS_AOI_IMAGE_SET_AOI, rect_roi, self.ueye.sizeof(rect_roi) ) check_error(ret, "IDSCameraObject") logger.info(f"ROI set to: {rect_roi}")
[docs] def on_connect(self): """Connect to the camera and initialize it.""" if self._connected: logger.warning("Camera is already connected.") return self.cam = IDSCameraObject(self.camera_id, **self._inputs) self._connected = True
[docs] def on_disconnect(self, delay_after: float = 0.3): """Disconnect from the camera and optionally wait a short time for driver cleanup.""" try: if self.cam and self.cam.h_cam: check_error(self.ueye.is_ExitCamera(self.cam.h_cam), "IDSCameraObject") self._connected = False self.cam = None if delay_after > 0: time.sleep(delay_after) logger.debug(f"Waited {delay_after:.2f}s after camera disconnect for cleanup.") except Exception as e: logger.info(f"Error during camera disconnection: {e}")
@property def exposure_time(self) -> float: """Get the exposure time of the camera.""" exposure = ueye.c_double() ret = self.ueye.is_Exposure(self.cam.h_cam, ueye.IS_EXPOSURE_CMD_GET_EXPOSURE, exposure, 8) check_error(ret, "IDSCameraObject") return exposure.value @exposure_time.setter def exposure_time(self, value: float): """Set the exposure time of the camera.""" exposure = ueye.c_double(value) check_error( self.ueye.is_Exposure(self.cam.h_cam, ueye.IS_EXPOSURE_CMD_SET_EXPOSURE, exposure, 8), "IDSCameraObject", )
[docs] def set_auto_gain(self, enable: bool): """Enable or disable auto gain.""" enable = ueye.c_int(1) if enable else ueye.c_int(0) value_to_return = ueye.c_double() check_error( self.ueye.is_SetAutoParameter( self.cam.h_cam, ueye.IS_SET_ENABLE_AUTO_GAIN, enable, value_to_return ), "IDSCameraObject", )
[docs] def set_auto_shutter(self, enable: bool): """Enable or disable auto exposure.""" enable = ueye.c_int(1) if enable else ueye.c_int(0) value_to_return = ueye.c_double() check_error( self.ueye.is_SetAutoParameter( self.cam.h_cam, ueye.IS_SET_ENABLE_AUTO_SHUTTER, enable, value_to_return ), "IDSCameraObject", )
[docs] def get_image_data(self) -> np.ndarray | None: """Get the image data from the camera.""" if not self._connected: self._rate_limited_warning_log(f"Camera with id {self.camera_id} is not connected.") return None array = self.ueye.get_data( self.cam.pc_image_mem, self.cam.width, self.cam.height, self.cam.n_bits_per_pixel, self.cam.pitch, copy=False, ) if array is None: logger.error("Failed to get image data from the camera.") return None img = np.reshape( array, (self.cam.height.value, self.cam.width.value, self.cam.bytes_per_pixel) ) # If RGB image (H, W, 3), reshuffle channels from BGR → RGB if img.ndim == 3 and img.shape[2] == 3: img = img[:, :, ::-1] if self.force_monochrome: gray = np.dot(img[..., :3], [0.2989, 0.5870, 0.1140]).astype(np.uint8) # expand to 3D shape (H, W, 1) for consistency with real mono cams img = np.expand_dims(gray, axis=-1) img = np.ascontiguousarray(img) return img
def set_camera_rate_limiting(self, enabled: bool, rate_limit_s: float | None = None): if rate_limit_s is not None: if rate_limit_s <= 0: raise ValueError(f"Invalid rate limit: {rate_limit_s}, must be positive nonzero.") self._warning_log_rate_limit_s = rate_limit_s self._enable_warning_rate_limit = enabled def _rate_limited_warning_log(self, msg: "str"): if ( self._enable_warning_rate_limit and time.monotonic() < self._last_rate_limited_log + self._warning_log_rate_limit_s ): return self._last_rate_limited_log = time.monotonic() logger.warning(msg)
if __name__ == "__main__": # Example usage camera = Camera(camera_id=201) camera.on_connect()