Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,8 @@ venv.bak/
!dlclivegui/config.py
# uv package files
uv.lock

# profiling
profile*.svg
scalene*.json
scalene*.html
62 changes: 59 additions & 3 deletions dlclivegui/cameras/backends/basler_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import numpy as np

from ...config import BASLER_DO_LOG_TIMING, CameraTriggerSettings
from ...config import BASLER_DO_LOG_TIMING, DEBUG_TRIGGER_LOGS, CameraTriggerSettings
from ...utils.stats import WorkerTimingStats
from ..base import CameraBackend, SupportLevel, register_backend

Expand Down Expand Up @@ -51,6 +51,7 @@ def __init__(self, settings):
)
self._camera_pixel_format: str | None = None
self._logged_first_frame: bool = False
self._debug_last_acquis_stats_log: float = 0.0

# Optional fast-start hint for probe workers
# (may skip StartGrabbing and converter setup for faster capability probing; not suitable for normal capture)
Expand Down Expand Up @@ -638,7 +639,8 @@ def open(self) -> None:
pass

self._camera.StartGrabbing(
pylon.GrabStrategy_LatestImageOnly,
# pylon.GrabStrategy_LatestImageOnly,
pylon.GrabStrategy_OneByOne,
)
LOG.info(
Comment on lines 640 to 645

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like a big unconditional change which we should give some consideration.

I understand that we want to prevent discarding intermediate frames (which is mostly useful for viewing, but not recording), but processing all frames in arrival order also has it's risks/downsides. Some remarks/questions:

  • If the buffer is full (currently 10 -> ~100ms ?), does this mean that new incoming frames are lost?
  • Does unconditionally adopting this new strategy mean that viewing (not recording) operates on a growing backlog of stale frames?
  • Do we have proper diagnostics on the acquisition-side, not the recorder side? i.e. do we know the number of ready frames in the buffer, if frames are dropped, etc? or does the recorder never see this?

@C-Achard C-Achard Aug 13, 2026

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.

  • (Note that this is true at 100 FPS only) Yes, but the alternative also drops frames, and more readily
  • No, viewing will (should) not consume the backlog frame by frame. Even when acquisition uses GrabStrategy_OneByOne, display and inference have their own "latest frame only" policies with queue size one. However for recording I was explicitely asked to try and preserve every acquired frame, which matters for experiments
  • No this is missing currently, as it would require per-backend reporting and may not be applicable in some cases (?). But it is a true source of frame drops we currently cannot truly know about, aside from the estimated FPS dropping, so this is worth flagging

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ok after discussing in real life: we don't know what the buffer exactly looks like, but as they recommend it in production (see https://docs.baslerweb.com/pylonapi/pylon-sdk-samples-manual) we can assume it is the safest way to process and we can probably assume a FIFO principle.

Still, might be good to have diagnostics in case they are available. Then we can always see empirically in practice.

"[Basler] grabbing=%s max_buffers=%s",
Expand All @@ -661,7 +663,7 @@ def open(self) -> None:
)

# ----------------------------
# Persist stable identity into namespace (migration-safe)
# Persist stable identity into namespace
# ----------------------------
try:
serial = device.GetSerialNumber()
Expand Down Expand Up @@ -722,6 +724,8 @@ def read(self) -> tuple[np.ndarray, float]:
grab_result.Release()
grab_result = None

self._debug_log_acquisition_stats(context="after frame read")

if self._actual_width is None or self._actual_height is None:
h, w = frame.shape[:2]
self._actual_width = int(w)
Expand All @@ -746,6 +750,8 @@ def read(self) -> tuple[np.ndarray, float]:

self._timing.note_error()
self._timing.maybe_log()
self._debug_log_acquisition_stats(context=f"after frame read error: {type(exc).__name__}")

raise RuntimeError("Failed to retrieve image from Basler camera.") from exc

def close(self) -> None:
Expand Down Expand Up @@ -957,7 +963,57 @@ def _set_numeric_feature(self, name: str, value, *, strict: bool = False) -> boo
LOG.warning("Failed to set Basler feature '%s' to '%s': %s", name, value, exc)
return False

def _debug_log_acquisition_stats(
self,
*,
context: str,
force: bool = False,
) -> None:
if not BASLER_DO_LOG_TIMING or not LOG.isEnabledFor(logging.DEBUG):
return

now = time.monotonic()
if not force and now - self._debug_last_acquisition_stats_log < 1.0:
return

self._debug_last_acquisition_stats_log = now
stats = self._debug_read_acquisition_stats()
if stats:
LOG.debug(
"[Basler] acquisition stats context=%s values=%s",
context,
stats,
)

def _debug_read_acquisition_stats(self) -> dict[str, int]:
cam = self._camera
if cam is None or not cam.IsGrabbing():
return {}

stats: dict[str, int] = {}
for name in ("NumReadyBuffers", "NumQueuedBuffers", "MaxNumBuffer"):
try:
stats[name] = int(getattr(cam, name).GetValue())
except Exception:
pass

try:
sg = cam.StreamGrabber
for name in (
"Statistic_Buffer_Underrun_Count",
"Statistic_Missed_Frame_Count",
"Statistic_Failed_Buffer_Count",
):
stats[name] = int(getattr(sg, name).GetValue())
except Exception:
pass

return stats

def _debug_trigger_nodes(self, *, context: str = "") -> None:
if not LOG.isEnabledFor(logging.DEBUG) or not DEBUG_TRIGGER_LOGS:
return

names = (
"TriggerSelector",
"TriggerMode",
Expand Down
5 changes: 4 additions & 1 deletion dlclivegui/cameras/backends/gentl_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import cv2
import numpy as np

from ...config import CameraTriggerSettings
from ...config import DEBUG_TRIGGER_LOGS, CameraTriggerSettings
from ..base import CameraBackend, SupportLevel, register_backend
from ..factory import DetectedCamera
from .utils import gentl_discovery as cti_finder
Expand Down Expand Up @@ -199,6 +199,9 @@ def static_capabilities(cls) -> dict[str, SupportLevel]:
}

def _debug_trigger_nodes(self, node_map, *, context: str = "") -> None:
if not LOG.isEnabledFor(logging.DEBUG) or not DEBUG_TRIGGER_LOGS:
return

names = (
"TriggerMode",
"TriggerSelector",
Expand Down
2 changes: 2 additions & 0 deletions dlclivegui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
SINGLE_CAMERA_WORKER_DO_LOG_TIMING: bool = False
MULTI_CAMERA_WORKER_DO_LOG_TIMING: bool = False
REC_DO_LOG_TIMING: bool = False
### Trigger debug logging
DEBUG_TRIGGER_LOGS = False
# MAIN_WINDOW_DO_LOG_TIMING: bool = False
#### Backends
BASLER_DO_LOG_TIMING: bool = False
Expand Down
Loading