Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ class CAMERA_CONFIG:
AWB_RATIOS_R = 1.375
AWB_RATIOS_G = 1
AWB_RATIOS_B = 1.4141
USE_GLOBAL_RESET_MODE = False


class AF:
Expand Down Expand Up @@ -1466,6 +1467,11 @@ class SlackNotifications:
"yes",
)
log.info(f"Loaded ENABLE_MEMORY_PROFILING={ENABLE_MEMORY_PROFILING} from config")
if _general_config.has_option("GENERAL", "use_global_reset_mode"):
CAMERA_CONFIG.USE_GLOBAL_RESET_MODE = _general_config.get(
"GENERAL", "use_global_reset_mode"
).lower() in ("true", "1", "yes")
log.info(f"Loaded USE_GLOBAL_RESET_MODE={CAMERA_CONFIG.USE_GLOBAL_RESET_MODE} from config")
except Exception as e:
log.warning(f"Failed to load GENERAL settings from config: {e}")

Expand Down
35 changes: 34 additions & 1 deletion software/control/camera_hamamatsu.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ def __init__(
self._camera: Dcam = camera
self._capabilities: HamamatsuCapabilities = capabilities
self._is_streaming = threading.Event()
self._is_global_reset = False

# Configure global reset (global shutter) mode if requested
if self._config.use_global_reset_mode:
self.set_global_reset_mode(True)

# We store exposure time so we don't need to worry about backing out strobe time from the
# time stored on the camera.
Expand All @@ -105,6 +110,20 @@ def __init__(
self._exposure_time_ms: int = 20
self.set_exposure_time(self._exposure_time_ms)

def supports_global_reset_mode(self) -> bool:
attr = self._camera.prop_getattr(DCAM_IDPROP.SHUTTER_MODE)
return attr is not None and not isinstance(attr, bool)

def set_global_reset_mode(self, enabled: bool):
mode = DCAMPROP.SHUTTER_MODE.GLOBAL if enabled else DCAMPROP.SHUTTER_MODE.ROLLING
mode_name = "global" if enabled else "rolling"
if self._set_prop(DCAM_IDPROP.SHUTTER_MODE, mode):
self._is_global_reset = enabled
self._log.info(f"Shutter mode set to {mode_name}")
self.set_exposure_time(self.get_exposure_time())
else:
self._log.warning(f"Failed to set shutter mode to {mode_name}, camera may not support it")

def close(self):
self._cleanup_read_thread()

Expand Down Expand Up @@ -208,7 +227,21 @@ def get_strobe_time(self) -> float:
if isinstance(line_interval_s, bool) or isinstance(trigger_delay_s, bool):
raise CameraError("Failed to get strobe delay properties from camera")

return (line_interval_s + trigger_delay_s) * 1000.0
if self._is_global_reset:
# Global reset: all rows expose simultaneously, no rolling readout delay.
# Only the trigger delay applies.
strobe_time_ms = trigger_delay_s * 1000.0
self._log.debug(
f"Strobe time (global reset): {strobe_time_ms:.3f} ms "
f"(line_interval={line_interval_s*1000:.3f} ms skipped, trigger_delay={trigger_delay_s*1000:.3f} ms, resolution={resolution})"
)
else:
strobe_time_ms = (line_interval_s + trigger_delay_s) * 1000.0
self._log.debug(
f"Strobe time: {strobe_time_ms:.3f} ms "
f"(line_interval={line_interval_s*1000:.3f} ms, trigger_delay={trigger_delay_s*1000:.3f} ms, resolution={resolution})"
)
return strobe_time_ms

def set_frame_format(self, frame_format: CameraFrameFormat):
if frame_format != CameraFrameFormat.RAW:
Expand Down
50 changes: 49 additions & 1 deletion software/control/camera_toupcam.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ToupCamCapabilities(pydantic.BaseModel):
has_TEC: bool
has_low_noise_mode: bool
has_black_level: bool
has_global_shutter: bool


class StrobeInfo(pydantic.BaseModel):
Expand Down Expand Up @@ -66,9 +67,14 @@ def _tdib_width_bytes(w):

@staticmethod
def _calculate_strobe_info(
camera: toupcam.Toupcam, pixel_size: int, exposure_time_ms: float, capabilities: ToupCamCapabilities
camera: toupcam.Toupcam,
pixel_size: int,
exposure_time_ms: float,
capabilities: ToupCamCapabilities,
is_global_reset: bool = False,
) -> StrobeInfo:
log = squid.logging.get_logger("ToupcamCamera._calculate_strobe_delay")

# use camera arguments such as resolutuon, ROI, exposure time, set max FPS, bandwidth to calculate the trigger delay time

pixel_bits = pixel_size * 8
Expand Down Expand Up @@ -156,6 +162,16 @@ def _calculate_strobe_info(
trigger_delay_us = (shr * line_length) / 72
strobe_time = int(vheight * row_time)

if is_global_reset:
# Global reset: all rows expose simultaneously, so no rolling readout delay.
# trigger_delay_us is kept as-is — TODO: verify against Toupcam global reset docs,
# the actual trigger delay in global reset mode may differ from rolling shutter.
log.debug(
f"Global reset mode: strobe_time_us=0, trigger_delay_us={trigger_delay_us}. "
f"{resolution_width=}, {resolution_height=}, {pixel_bits=}, {line_length=}, {low_noise=}, {vheight=}"
)
return StrobeInfo(strobe_time_us=0, trigger_delay_us=trigger_delay_us)

log.debug(
f"New strobe time calculated as {strobe_time} [us]. {resolution_width=}, {resolution_height=}, {pixel_bits=}, {line_length=}, {low_noise=}, {vheight=}, {trigger_delay_us=}"
)
Expand Down Expand Up @@ -215,6 +231,7 @@ def _open(index=None, sn=None) -> Tuple[toupcam.Toupcam, ToupCamCapabilities]:
has_TEC=(devices[index].model.flag & toupcam.TOUPCAM_FLAG_TEC_ONOFF) > 0,
has_low_noise_mode=(devices[index].model.flag & toupcam.TOUPCAM_FLAG_LOW_NOISE) > 0,
has_black_level=(devices[index].model.flag & toupcam.TOUPCAM_FLAG_BLACKLEVEL) > 0,
has_global_shutter=(devices[index].model.flag & toupcam.TOUPCAM_FLAG_GLOBALSHUTTER) > 0,
)

return camera, capabilities
Expand All @@ -238,6 +255,7 @@ def __init__(self, config: CameraConfig, hw_trigger_fn, hw_set_strobe_delay_ms_f
self._raw_camera_stream_started = False
self._raw_frame_callback_lock = threading.Lock()
(self._camera, self._capabilities) = ToupcamCamera._open(index=0)
self._is_global_reset = False
self._pixel_format = self._config.default_pixel_format
self._binning = self._config.default_binning

Expand Down Expand Up @@ -354,6 +372,7 @@ def _update_internal_settings(self, send_exposure=True):
pixel_size=self._get_pixel_size_in_bytes(),
exposure_time_ms=camera_exposure_time_ms,
capabilities=self._capabilities,
is_global_reset=self._is_global_reset,
)
if self._hw_set_strobe_delay_ms_fn and self.get_acquisition_mode() == CameraAcquisitionMode.HARDWARE_TRIGGER:
self._hw_set_strobe_delay_ms_fn(self.get_strobe_time())
Expand Down Expand Up @@ -393,6 +412,10 @@ def _configure_camera(self):
else:
self.set_temperature(self._config.default_temperature)

# Configure global reset mode if requested
if self._config.use_global_reset_mode:
self.set_global_reset_mode(True)

self._raw_set_frame_format(CameraFrameFormat.RAW)
self._raw_set_pixel_format(self._pixel_format) # 'MONO8'
try:
Expand All @@ -407,6 +430,31 @@ def _configure_camera(self):

# TODO: Do hardware cropping here (set ROI)

def supports_global_reset_mode(self) -> bool:
return self._capabilities.has_global_shutter

def set_global_reset_mode(self, enabled: bool):
if enabled:
if not self._capabilities.has_global_shutter:
self._log.warning("Global reset mode requested but camera does not support global shutter")
return
try:
self._camera.put_Option(toupcam.TOUPCAM_OPTION_GLOBAL_RESET_MODE, 1)
self._is_global_reset = True
self._log.info("Global reset mode enabled")
except toupcam.HRESULTException as ex:
self._log.error(f"Failed to enable global reset mode: hr=0x{ex.hr:x}")
return
else:
try:
self._camera.put_Option(toupcam.TOUPCAM_OPTION_GLOBAL_RESET_MODE, 0)
self._is_global_reset = False
self._log.info("Global reset mode disabled")
except toupcam.HRESULTException as ex:
self._log.error(f"Failed to disable global reset mode: hr=0x{ex.hr:x}")
return
self._update_internal_settings()

def set_temperature_reading_callback(self, func):
self.temperature_reading_callback = func

Expand Down
14 changes: 14 additions & 0 deletions software/control/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,10 @@ def _create_advanced_tab(self):
self.illumination_factor.setValue(self._get_config_float("GENERAL", "illumination_intensity_factor", 0.6))
hw_layout.addRow("Illumination Intensity Factor:", self.illumination_factor)

self.global_reset_checkbox = QCheckBox()
self.global_reset_checkbox.setChecked(self._get_config_bool("GENERAL", "use_global_reset_mode", False))
hw_layout.addRow("Use Global Reset Mode *:", self.global_reset_checkbox)

hw_group.content.addLayout(hw_layout)
layout.addWidget(hw_group)

Expand Down Expand Up @@ -1837,6 +1841,11 @@ def _apply_settings(self) -> bool:
self.config.set("GENERAL", "led_matrix_g_factor", str(self.led_g_factor.value()))
self.config.set("GENERAL", "led_matrix_b_factor", str(self.led_b_factor.value()))
self.config.set("GENERAL", "illumination_intensity_factor", str(self.illumination_factor.value()))
self.config.set(
"GENERAL",
"use_global_reset_mode",
"true" if self.global_reset_checkbox.isChecked() else "false",
)

# Advanced - Development Settings
self.config.set(
Expand Down Expand Up @@ -2202,6 +2211,11 @@ def _get_changes(self):
if not self._floats_equal(old_val, new_val):
changes.append(("Illumination Intensity Factor", str(old_val), str(new_val), False))

old_val = self._get_config_bool("GENERAL", "use_global_reset_mode", False)
new_val = self.global_reset_checkbox.isChecked()
if old_val != new_val:
changes.append(("Use Global Reset Mode", str(old_val), str(new_val), True))

# Advanced - Development Settings
# Enable/disable requires restart (for warning banner/dialog), but speed/compression
# take effect on next acquisition since each acquisition starts a fresh subprocess
Expand Down
12 changes: 12 additions & 0 deletions software/squid/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,18 @@ def set_temperature_reading_callback(self, callback: Callable):
"""
pass

def supports_global_reset_mode(self) -> bool:
"""Whether the camera supports global reset (global shutter) mode."""
return False

def set_global_reset_mode(self, enabled: bool):
"""Enable or disable global reset (global shutter) mode.

Override in subclasses that support this feature.
"""
if enabled:
self._log.warning(f"{self.__class__.__name__} does not support global reset mode")

@abc.abstractmethod
def close(self):
"""
Expand Down
6 changes: 6 additions & 0 deletions software/squid/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,11 @@ class CameraConfig(pydantic.BaseModel):
# After initialization, set the white balance gains to this once. Only valid for color cameras.
default_white_balance_gains: Optional[RGBValue] = None

# Enable global reset mode (global shutter) if the camera supports it.
# When enabled, all sensor rows are exposed simultaneously instead of sequentially (rolling shutter).
# If the camera does not support global reset, a warning is logged and rolling shutter is used.
use_global_reset_mode: bool = False


def _old_camera_variant_to_enum(old_string) -> CameraVariant:
if old_string == "Toupcam":
Expand Down Expand Up @@ -578,6 +583,7 @@ def _old_camera_variant_to_enum(old_string) -> CameraVariant:
default_white_balance_gains=RGBValue(
r=_def.CAMERA_CONFIG.AWB_RATIOS_R, g=_def.CAMERA_CONFIG.AWB_RATIOS_G, b=_def.CAMERA_CONFIG.AWB_RATIOS_B
),
use_global_reset_mode=_def.CAMERA_CONFIG.USE_GLOBAL_RESET_MODE,
)


Expand Down