Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a15d3ee
Improve processor discovery and logging
C-Achard Jul 1, 2026
b8cd04d
Add processors package exports
C-Achard Jul 1, 2026
ae8764b
Move example socket processors to examples module
C-Achard Jul 1, 2026
a88720e
Skip socket base module in processor scan
C-Achard Jul 1, 2026
16845bc
Update processor_utils.py
C-Achard Jul 1, 2026
ca297f6
Warn on duplicate processor registration
C-Achard Jul 1, 2026
b8a4fab
Extract processor registry into new module
C-Achard Jul 1, 2026
303380f
Normalize recording container handling
C-Achard Jul 2, 2026
00b2869
Add DLC timing instrumentation in GUI path
C-Achard Jul 2, 2026
6a40f90
Prioritize latest frame when queue is full
C-Achard Jul 2, 2026
042afd1
Lower camera backend logs to debug
C-Achard Jul 2, 2026
5f3a537
Harden DLC worker startup and timing logs
C-Achard Jul 2, 2026
940d977
Disable pose latency debug logging
C-Achard Jul 2, 2026
49185ed
Fix shutdown order in camera preview stop
C-Achard Jul 2, 2026
c7e37f4
Add mono-preserving output mode to GenTL
C-Achard Jul 2, 2026
c936c1c
Async recording stop and queued frame dispatch
C-Achard Jul 2, 2026
057f84a
Make fps value float for write gear
C-Achard Jul 2, 2026
e0d4f87
Update recording_manager.py
C-Achard Jul 3, 2026
fb8c1cb
Lock processor settings during DLC inference
C-Achard Jun 30, 2026
340bd89
Improve processor discovery and logging
C-Achard Jul 1, 2026
622a584
Add processors package exports
C-Achard Jul 1, 2026
c635f49
Move example socket processors to examples module
C-Achard Jul 1, 2026
6675299
Skip socket base module in processor scan
C-Achard Jul 1, 2026
c734674
Update processor_utils.py
C-Achard Jul 1, 2026
ba0df48
Warn on duplicate processor registration
C-Achard Jul 1, 2026
4599836
Fix dlclive Processor import paths
C-Achard Jul 1, 2026
8bcff9b
Extract processor registry into new module
C-Achard Jul 1, 2026
d6a41e2
Refactor camera worker and recording pipeline
C-Achard Jul 2, 2026
b9a1f59
Route recording frames through recording sink
C-Achard Jul 2, 2026
eac8fd3
Update recording manager test imports
C-Achard Jul 2, 2026
16b4bce
Propagate capture metadata in single-camera flow
C-Achard Jul 2, 2026
fc27c52
Add timestamp metadata to frame signal
C-Achard Jul 3, 2026
7133353
Comment previous signals
C-Achard Jul 3, 2026
b419684
Fix dispatcher lifecycle and add flush API
C-Achard Jul 3, 2026
1723913
Update tests for CapturedFrame integration
C-Achard Jul 3, 2026
5382131
Stabilize recording and camera tests
C-Achard Jul 3, 2026
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
26 changes: 13 additions & 13 deletions dlclivegui/cameras/backends/basler_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ def _configure_frame_rate(self) -> None:

fps = self._positive_float(getattr(self.settings, "fps", 0.0))
if fps is None:
LOG.info("[Basler] FPS: auto/free-run, not forcing AcquisitionFrameRate")
LOG.debug("[Basler] FPS: auto/free-run, not forcing AcquisitionFrameRate")
return

enable = self._feature("AcquisitionFrameRateEnable")
Expand All @@ -465,7 +465,7 @@ def _configure_frame_rate(self) -> None:
try:
min_v = rate.GetMin()
max_v = rate.GetMax()
LOG.info("[Basler] AcquisitionFrameRate range: min=%s max=%s requested=%s", min_v, max_v, fps)
LOG.debug("[Basler] AcquisitionFrameRate range: min=%s max=%s requested=%s", min_v, max_v, fps)
except Exception:
pass

Expand Down Expand Up @@ -496,7 +496,7 @@ def _configure_frame_rate(self) -> None:
if feature is not None:
readbacks[name] = self._feature_value(feature, None)

LOG.info("[Basler] FPS readback requested=%s values=%s", fps, readbacks)
LOG.debug("[Basler] Readback requested=%s values=%s", fps, readbacks)

try:
self._actual_fps = float(readbacks.get("AcquisitionFrameRate"))
Expand All @@ -521,14 +521,14 @@ def _configure_converter(self) -> None:

if self._should_output_mono():
self._converter.OutputPixelFormat = pylon.PixelType_Mono8
LOG.info(
LOG.debug(
"[Basler] Converter configured for Mono8 output (camera PixelFormat=%s preserve_mono=%s)",
camera_pixel_format,
self._preserve_mono,
)
else:
self._converter.OutputPixelFormat = pylon.PixelType_BGR8packed
LOG.info(
LOG.debug(
"[Basler] Converter configured for BGR8 output (camera PixelFormat=%s preserve_mono=%s)",
camera_pixel_format,
self._preserve_mono,
Expand Down Expand Up @@ -559,7 +559,7 @@ def open(self) -> None:
self._camera.ExposureTime.SetValue(float(self.settings.exposure))
if hasattr(self._camera, "ExposureTimeAbs"):
self._camera.ExposureTimeAbs.SetValue(float(self.settings.exposure))
LOG.info("[Basler] Exposure set to %s us (auto off)", self.settings.exposure)
LOG.debug("[Basler] Exposure set to %s us (auto off)", self.settings.exposure)
except Exception as exc:
LOG.warning("[Basler] Failed to set exposure: %s", exc)

Expand All @@ -569,7 +569,7 @@ def open(self) -> None:
if hasattr(self._camera, "GainAuto"):
self._camera.GainAuto.SetValue("Off")
self._camera.Gain.SetValue(float(self.settings.gain))
LOG.info("[Basler] Gain set to %s dB (auto off)", self.settings.gain)
LOG.debug("[Basler] Gain set to %s dB (auto off)", self.settings.gain)
except Exception as exc:
LOG.warning("[Basler] Failed to set gain: %s", exc)

Expand Down Expand Up @@ -649,15 +649,15 @@ def open(self) -> None:
# pylon.GrabStrategy_LatestImageOnly,
pylon.GrabStrategy_OneByOne,
)
LOG.info(
LOG.debug(
"[Basler] grabbing=%s max_buffers=%s",
self._camera.IsGrabbing(),
self._camera.MaxNumBuffer.GetValue() if hasattr(self._camera, "MaxNumBuffer") else "N/A",
)
else:
LOG.debug("Fast-start probe: skipping StartGrabbing and converter")

LOG.info(
LOG.debug(
"[Basler] open device_id=%s index=%s fast_start=%s requested=(%sx%s @ %s fps exp=%s gain=%s)",
getattr(self, "_device_id", None),
getattr(self.settings, "index", None),
Expand Down Expand Up @@ -767,7 +767,7 @@ def read(self) -> CapturedFrame:

if not self._logged_first_frame:
self._logged_first_frame = True
LOG.info(
LOG.debug(
"[Basler] first frame device_id=%s shape=%s dtype=%s nbytes=%.2f MB "
"camera_pixel_format=%s output_format=%s preserve_mono=%s",
self._device_id,
Expand Down Expand Up @@ -814,7 +814,7 @@ def read(self) -> CapturedFrame:
raise RuntimeError("Failed to retrieve image from Basler camera.") from exc

def close(self) -> None:
LOG.info(
LOG.debug(
"[Basler] close called camera_exists=%s grabbing=%s open=%s",
self._camera is not None,
bool(self._camera and self._camera.IsGrabbing()),
Expand Down Expand Up @@ -1176,7 +1176,7 @@ def _configure_trigger_input(self, cfg, *, strict: bool = False) -> None:
self._trigger = CameraTriggerSettings()
return

LOG.info(
LOG.debug(
"Basler trigger input configured: role=%s selector=%s source=%s activation=%s "
"selector_ok=%s source_ok=%s activation_ok=%s",
role,
Expand Down Expand Up @@ -1241,7 +1241,7 @@ def _configure_trigger_master(self, cfg, *, strict: bool = False) -> None:
source_ok = self._set_enum_feature("LineSource", output_source, strict=strict)

if mode_ok and source_ok:
LOG.info(
LOG.debug(
"Basler trigger master configured via Line*: output_line=%s output_source=%s",
output_line,
output_source,
Expand Down
72 changes: 61 additions & 11 deletions dlclivegui/cameras/backends/gentl_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ def __init__(self, settings):
ns = {}

self._fast_start: bool = bool(ns.get("fast_start", False))
self._preserve_mono: bool = bool(getattr(settings, "preserve_mono", False) or ns.get("preserve_mono", False))
self._logged_first_frame: bool = False

raw_device_id = ns.get("device_id") or props.get("device_id")
legacy_serial = ns.get("serial_number") or ns.get("serial") or props.get("serial_number") or props.get("serial")
Expand Down Expand Up @@ -177,10 +179,20 @@ def actual_pixel_format(self) -> str | None:
"""Camera/native pixel format selected on the GenICam PixelFormat node."""
return self._camera_pixel_format or (self._pixel_format if self._pixel_format != "auto" else None)

@property
def recommended_preserve_mono(self) -> bool | None:
if not self._camera_pixel_format:
return None
return self._is_camera_mono()

@property
def actual_output_format(self) -> str | None:
"""Current GenTL backend emits OpenCV-native BGR uint8 frames."""
return self._actual_output_format or "BGR8"
"""Backend output frame format emitted to the app, e.g. 'Mono8' or 'BGR8'."""
if self._actual_output_format:
return self._actual_output_format
if not self._camera_pixel_format:
return None
return "Mono8" if self._should_output_mono() else "BGR8"

@classmethod
def is_available(cls) -> bool:
Expand All @@ -196,6 +208,7 @@ def static_capabilities(cls) -> dict[str, SupportLevel]:
"device_discovery": SupportLevel.SUPPORTED,
"stable_identity": SupportLevel.SUPPORTED,
"hardware_trigger": SupportLevel.BEST_EFFORT,
"preserve_mono": SupportLevel.SUPPORTED,
}

def _debug_trigger_nodes(self, node_map, *, context: str = "") -> None:
Expand Down Expand Up @@ -606,6 +619,13 @@ def waits_for_hardware_trigger(self) -> bool:
role = str(self._trigger_attr(getattr(self, "_trigger", None), "role", "off") or "off").lower()
return role in {"external", "follower"}

def _is_camera_mono(self) -> bool:
fmt = str(self._camera_pixel_format or self._pixel_format or "").strip()
return fmt.startswith("Mono")

def _should_output_mono(self) -> bool:
return bool(self._preserve_mono and self._is_camera_mono())

@staticmethod
def _output_format_for_frame(frame: np.ndarray) -> str:
if frame.ndim == 2:
Expand Down Expand Up @@ -660,6 +680,25 @@ def read(self) -> CapturedFrame:
except Exception:
pass
self._actual_output_format = self._output_format_for_frame(frame)
try:
ns = self._ensure_settings_ns()
ns["actual_output_format"] = self._actual_output_format
ns["preserve_mono"] = self._preserve_mono
except Exception:
pass
if not self._logged_first_frame:
self._logged_first_frame = True
LOG.info(
"[GenTL] first frame device_id=%s shape=%s dtype=%s nbytes=%.2f MB "
"camera_pixel_format=%s output_format=%s preserve_mono=%s",
self._device_id,
frame.shape,
frame.dtype,
frame.nbytes / (1024 * 1024),
self._camera_pixel_format,
self.actual_output_format,
self._preserve_mono,
)

return CapturedFrame(
frame=frame,
Expand Down Expand Up @@ -1286,7 +1325,7 @@ def _resolve_trigger_source(self, node_map, requested: str, *, strict: bool) ->
if requested.lower() == "auto":
for candidate in ("Line0", "Line1", "Line2", "Any"):
if candidate in available:
LOG.info(
LOG.debug(
"GenTL TriggerSource auto-selected '%s'. Available: %s",
candidate,
available,
Expand Down Expand Up @@ -1374,6 +1413,14 @@ def _configure_pixel_format(self, node_map) -> None:
pixel_format_node.value = selected
self._pixel_format = str(pixel_format_node.value)
self._camera_pixel_format = self._pixel_format
try:
ns = self._ensure_settings_ns()
ns["actual_pixel_format"] = self._camera_pixel_format
ns["detected_pixel_format"] = self._camera_pixel_format
ns["actual_output_format"] = self.actual_output_format
ns["preserve_mono"] = self._preserve_mono
except Exception:
pass

LOG.debug("GenTL pixel format selected: %s", self._pixel_format)

Expand Down Expand Up @@ -1475,7 +1522,7 @@ def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> No
self._trigger = CameraTriggerSettings()
return

LOG.info(
LOG.debug(
"GenTL trigger input configured: role=%s selector=%s source_requested=%s "
"source=%s activation=%s selector_ok=%s source_ok=%s activation_ok=%s",
role,
Expand Down Expand Up @@ -1536,7 +1583,7 @@ def _configure_trigger_master(self, node_map, cfg, *, strict: bool = False) -> N
node = self._node(node_map, "StrobeDuration")
if node is not None:
node.value = int(strobe_duration)
LOG.info("Configured GenTL StrobeDuration=%s", int(strobe_duration))
LOG.debug("Configured GenTL StrobeDuration=%s", int(strobe_duration))
except Exception as exc:
if strict:
raise RuntimeError(f"Failed to set StrobeDuration={strobe_duration}: {exc}") from exc
Expand All @@ -1547,7 +1594,7 @@ def _configure_trigger_master(self, node_map, cfg, *, strict: bool = False) -> N
node = self._node(node_map, "StrobeDelay")
if node is not None:
node.value = int(strobe_delay)
LOG.info("Configured GenTL StrobeDelay=%s", int(strobe_delay))
LOG.debug("Configured GenTL StrobeDelay=%s", int(strobe_delay))
except Exception as exc:
if strict:
raise RuntimeError(f"Failed to set StrobeDelay={strobe_delay}: {exc}") from exc
Expand All @@ -1561,7 +1608,7 @@ def _configure_trigger_master(self, node_map, cfg, *, strict: bool = False) -> N
)

if enable_ok:
LOG.info(
LOG.debug(
"GenTL trigger master configured via Strobe*: "
"StrobeEnable=On StrobePolarity=%s polarity_ok=%s "
"StrobeOperation=%s operation_ok=%s",
Expand Down Expand Up @@ -1601,7 +1648,7 @@ def _configure_trigger_master(self, node_map, cfg, *, strict: bool = False) -> N
source_ok = self._set_enum_node(node_map, "LineSource", output_source, strict=strict)

if mode_ok and source_ok:
LOG.info(
LOG.debug(
"GenTL trigger master configured via Line*: output_line=%s output_source=%s",
output_line,
output_source,
Expand Down Expand Up @@ -1720,15 +1767,15 @@ def _configure_frame_rate(self, node_map) -> None:
return

target = float(self.settings.fps)
LOG.info("Configuring GenTL frame rate: requested %.3f FPS", target)
LOG.debug("Configuring GenTL frame rate: requested %.3f FPS", target)

for attr in ("AcquisitionFrameRateEnable", "AcquisitionFrameRateControlEnable"):
try:
node = getattr(node_map, attr)
before = getattr(node, "value", None)
node.value = True
after = getattr(node, "value", None)
LOG.info("Enabled GenTL %s: before=%r after=%r", attr, before, after)
LOG.debug("Enabled GenTL %s: before=%r after=%r", attr, before, after)
break
except Exception:
pass
Expand All @@ -1740,7 +1787,7 @@ def _configure_frame_rate(self, node_map) -> None:
node.value = target
after = getattr(node, "value", None)

LOG.info(
LOG.debug(
"Set GenTL %s: before=%r requested=%.3f after=%r",
attr,
before,
Expand Down Expand Up @@ -1879,6 +1926,9 @@ def _convert_frame(self, frame: np.ndarray) -> np.ndarray:
frame = cv2.cvtColor(frame, cv2.COLOR_BayerGR2BGR)
elif fmt == "BayerBG8":
frame = cv2.cvtColor(frame, cv2.COLOR_BayerBG2BGR)
elif self._should_output_mono():
# Keep Mono* cameras as 2D uint8 frames when explicitly requested.
pass
else:
frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)

Expand Down
8 changes: 6 additions & 2 deletions dlclivegui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@
# Global settings
## GUI
GUI_MAX_DISPLAY_FPS: float = 30.0
## Recording
ALLOWED_VIDEO_CONTAINERS: set[str] = {"mp4", "avi", "mov"}
DEFAULT_RECORDING_CONTAINER: str = "mp4"


## Debug
### Timing logs
SINGLE_CAMERA_WORKER_DO_LOG_TIMING: bool = False
MULTI_CAMERA_WORKER_DO_LOG_TIMING: bool = False
REC_DO_LOG_TIMING: bool = False
DLC_DO_LOG_TIMING: bool = True
Comment thread
C-Achard marked this conversation as resolved.
# MAIN_WINDOW_DO_LOG_TIMING: bool = False
Comment on lines 30 to 34
#### Backends
BASLER_DO_LOG_TIMING: bool = False
Expand Down Expand Up @@ -512,7 +516,7 @@ class RecordingSettings(BaseModel):
enabled: bool = False
directory: str = Field(default_factory=lambda: str(Path.home() / "Videos" / "deeplabcut-live"))
filename: str = "session.mp4"
container: Literal["mp4", "avi", "mov"] = "mp4"
container: Literal["mp4", "avi", "mov"] = DEFAULT_RECORDING_CONTAINER
codec: str = "libx264"
crf: int = Field(default=23, ge=0, le=51)
fast_encoding: bool = False
Expand Down Expand Up @@ -554,7 +558,7 @@ def writegear_options(self, fps: float | None) -> dict[str, Any]:
crf_value = int(self.crf) if self.crf is not None else 23

opts: dict[str, Any] = {
"-input_framerate": f"{fps_value:.6f}",
"-input_framerate": float(fps_value),
"-vcodec": codec_value,
"-crf": str(crf_value),
}
Expand Down
5 changes: 3 additions & 2 deletions dlclivegui/gui/camera_config/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from PySide6.QtCore import QTimer

from ...services.camera_controller import SingleCameraWorker
from ...services.multi_camera_controller import MultiCameraController

if TYPE_CHECKING:
Expand Down Expand Up @@ -56,7 +57,7 @@ class PreviewSession:


def apply_rotation(frame, rotation):
return MultiCameraController.apply_rotation(frame, rotation)
return SingleCameraWorker.apply_rotation(frame, rotation)


def apply_crop(frame, x0, y0, x1, y1):
Expand All @@ -66,7 +67,7 @@ def apply_crop(frame, x0, y0, x1, y1):
x1 = max(x0, min(x1, w))
y1 = max(y0, min(y1, h))

return MultiCameraController.apply_crop(frame, (x0, y0, x1, y1))
return SingleCameraWorker.apply_crop(frame, (x0, y0, x1, y1))


def resize_to_fit(frame, max_w=400, max_h=300):
Expand Down
Loading
Loading