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
7fc7868
Improve processor discovery and logging
C-Achard Jul 1, 2026
9027511
Add processors package exports
C-Achard Jul 1, 2026
566a2d7
Move example socket processors to examples module
C-Achard Jul 1, 2026
9034694
Skip socket base module in processor scan
C-Achard Jul 1, 2026
d8dd6e1
Update processor_utils.py
C-Achard Jul 1, 2026
59edd4b
Warn on duplicate processor registration
C-Achard Jul 1, 2026
b4e470d
Extract processor registry into new module
C-Achard Jul 1, 2026
0eaa565
Normalize recording container handling
C-Achard Jul 2, 2026
ad39628
Add DLC timing instrumentation in GUI path
C-Achard Jul 2, 2026
7f64ec7
Prioritize latest frame when queue is full
C-Achard Jul 2, 2026
a218a01
Lower camera backend logs to debug
C-Achard Jul 2, 2026
c9052f5
Harden DLC worker startup and timing logs
C-Achard Jul 2, 2026
19df818
Disable pose latency debug logging
C-Achard Jul 2, 2026
a717d78
Fix shutdown order in camera preview stop
C-Achard Jul 2, 2026
351af18
Add mono-preserving output mode to GenTL
C-Achard Jul 2, 2026
cd872fe
Async recording stop and queued frame dispatch
C-Achard Jul 2, 2026
ab959a8
Make fps value float for write gear
C-Achard Jul 2, 2026
c10ce05
Update recording_manager.py
C-Achard Jul 3, 2026
7d3b9f2
Lock processor settings during DLC inference
C-Achard Jun 30, 2026
a3b6a29
Improve processor discovery and logging
C-Achard Jul 1, 2026
24852a8
Add processors package exports
C-Achard Jul 1, 2026
77d545e
Move example socket processors to examples module
C-Achard Jul 1, 2026
908b763
Skip socket base module in processor scan
C-Achard Jul 1, 2026
2cb85a6
Update processor_utils.py
C-Achard Jul 1, 2026
8ae7ef5
Warn on duplicate processor registration
C-Achard Jul 1, 2026
ccc2735
Fix dlclive Processor import paths
C-Achard Jul 1, 2026
d7f87b1
Extract processor registry into new module
C-Achard Jul 1, 2026
371f567
Refactor camera worker and recording pipeline
C-Achard Jul 2, 2026
1530fa1
Route recording frames through recording sink
C-Achard Jul 2, 2026
70e3aa8
Update recording manager test imports
C-Achard Jul 2, 2026
e0f093c
Propagate capture metadata in single-camera flow
C-Achard Jul 2, 2026
6cf60ef
Add timestamp metadata to frame signal
C-Achard Jul 3, 2026
b9841a7
Comment previous signals
C-Achard Jul 3, 2026
9d94fa0
Fix dispatcher lifecycle and add flush API
C-Achard Jul 3, 2026
b217fe2
Update tests for CapturedFrame integration
C-Achard Jul 3, 2026
cbcc659
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 @@ -437,7 +437,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 @@ -454,7 +454,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 @@ -485,7 +485,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 @@ -510,14 +510,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 @@ -548,7 +548,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 @@ -558,7 +558,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 @@ -638,15 +638,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 @@ -756,7 +756,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 @@ -803,7 +803,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 @@ -1164,7 +1164,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 @@ -1229,7 +1229,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