Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b346e67
Improve processor discovery and logging
C-Achard Jul 1, 2026
0e7a12f
Add processors package exports
C-Achard Jul 1, 2026
df17f15
Move example socket processors to examples module
C-Achard Jul 1, 2026
4c5373e
Skip socket base module in processor scan
C-Achard Jul 1, 2026
70531ac
Update processor_utils.py
C-Achard Jul 1, 2026
1bf0be7
Warn on duplicate processor registration
C-Achard Jul 1, 2026
da45dbc
Extract processor registry into new module
C-Achard Jul 1, 2026
e55d78d
Normalize recording container handling
C-Achard Jul 2, 2026
8ba7cd4
Add DLC timing instrumentation in GUI path
C-Achard Jul 2, 2026
eddba6e
Prioritize latest frame when queue is full
C-Achard Jul 2, 2026
b23565a
Lower camera backend logs to debug
C-Achard Jul 2, 2026
0aae19f
Harden DLC worker startup and timing logs
C-Achard Jul 2, 2026
2770045
Disable pose latency debug logging
C-Achard Jul 2, 2026
b3d5915
Fix shutdown order in camera preview stop
C-Achard Jul 2, 2026
7d5d39c
Add mono-preserving output mode to GenTL
C-Achard Jul 2, 2026
12b059a
Async recording stop and queued frame dispatch
C-Achard Jul 2, 2026
275aedc
Update recording_manager.py
C-Achard Jul 3, 2026
f3d3118
Use runner pose directly in DLC processor
C-Achard Aug 13, 2026
8cdbd7a
Split timing loggers across worker thread
C-Achard Aug 13, 2026
4e4a4a9
pre-commit
C-Achard Aug 13, 2026
35a3541
Remove duplicate node value reader
C-Achard Aug 14, 2026
3ae10c8
Pass timestamp metadata to frame writer
C-Achard Aug 18, 2026
0005643
Stabilize rec manager async write tests
C-Achard Aug 18, 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 @@ -446,7 +446,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 @@ -463,7 +463,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 @@ -494,7 +494,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 @@ -519,14 +519,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 @@ -557,7 +557,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 @@ -567,7 +567,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 @@ -647,15 +647,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 @@ -765,7 +765,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 @@ -812,7 +812,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 @@ -1174,7 +1174,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 @@ -1239,7 +1239,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
85 changes: 61 additions & 24 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 @@ -609,6 +622,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 @@ -664,6 +684,25 @@ def read(self) -> CapturedFrame:
pass
if self._actual_output_format is None:
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 @@ -1178,19 +1217,6 @@ def _node_symbolics(node) -> list[str]:
except Exception:
return []

@staticmethod
def _node_value(node_map, name: str, default=None):
"""Best-effort read of a GenICam node value."""
try:
node = getattr(node_map, name)
except Exception:
return default

try:
return node.value
except Exception:
return default

@classmethod
def _node_float(cls, node_map, *names: str, allow_zero: bool = False) -> float | None:
"""Return the first positive float value from a list of GenICam node names."""
Expand Down Expand Up @@ -1290,7 +1316,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 @@ -1378,6 +1404,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 @@ -1479,7 +1513,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 @@ -1540,7 +1574,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 @@ -1551,7 +1585,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 @@ -1565,7 +1599,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 @@ -1605,7 +1639,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 @@ -1724,15 +1758,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 @@ -1744,7 +1778,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 @@ -1883,6 +1917,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
Comment thread
C-Achard marked this conversation as resolved.
else:
frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)

Expand Down
6 changes: 5 additions & 1 deletion dlclivegui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@
GUI_MAX_DISPLAY_FPS: float = 30.0
## Recording
DEFAULT_RECORDING_FPS: float = 30.0
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder

### Trigger debug logging
DEBUG_TRIGGER_LOGS = False
# MAIN_WINDOW_DO_LOG_TIMING: bool = False
Expand Down Expand Up @@ -508,7 +512,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
Loading