Skip to content
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 @@ -184,10 +186,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 @@ -203,6 +215,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 @@ -600,6 +613,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 @@ -654,6 +674,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 @@ -1275,7 +1314,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 @@ -1346,6 +1385,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 @@ -1447,7 +1494,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 @@ -1508,7 +1555,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 @@ -1519,7 +1566,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 @@ -1533,7 +1580,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 @@ -1573,7 +1620,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 @@ -1692,15 +1739,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 @@ -1712,7 +1759,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 @@ -1851,6 +1898,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
# MAIN_WINDOW_DO_LOG_TIMING: bool = False
#### 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
Loading
Loading