Skip to content

Commit a5ca077

Browse files
committed
WIP Add gentl options namespace and actuals tracking
Parse gentl-specific settings from a dedicated 'gentl' namespace while keeping backward compatibility with legacy properties. Introduce OPTIONS_KEY and robust property parsing (cti_file, serial_number, pixel_format, rotate, crop, exposure, gain, timeout, cti_search_paths). Add SupportLevel import and static_capabilities describing supported features. Track requested vs actual resolution via _get_requested_resolution_or_none and _configure_resolution (respecting node increments and logging mismatches), and capture actual width/height/fps (properties actual_resolution and actual_fps) during configuration and read. Misc: minor import additions and defensive handling of settings properties.
1 parent e421029 commit a5ca077

1 file changed

Lines changed: 141 additions & 29 deletions

File tree

dlclivegui/cameras/backends/gentl_backend.py

Lines changed: 141 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
"""GenTL backend implemented using the Harvesters library."""
22

3+
# dlclivegui/cameras/backends/gentl_backend.py
34
from __future__ import annotations
45

56
import glob
67
import logging
78
import os
89
import time
910
from collections.abc import Iterable
11+
from typing import ClassVar
1012

1113
import cv2
1214
import numpy as np
1315

14-
from ..base import CameraBackend, register_backend
16+
from ..base import CameraBackend, SupportLevel, register_backend
1517

1618
LOG = logging.getLogger(__name__)
1719

@@ -31,6 +33,7 @@
3133
class GenTLCameraBackend(CameraBackend):
3234
"""Capture frames from GenTL-compatible devices via Harvesters."""
3335

36+
OPTIONS_KEY: ClassVar[str] = "gentl"
3437
_DEFAULT_CTI_PATTERNS: tuple[str, ...] = (
3538
r"C:\\Program Files\\The Imaging Source Europe GmbH\\IC4 GenTL Driver for USB3Vision Devices *\\bin\\*.cti",
3639
r"C:\\Program Files\\The Imaging Source Europe GmbH\\TIS Grabber\\bin\\win64_x64\\*.cti",
@@ -40,28 +43,67 @@ class GenTLCameraBackend(CameraBackend):
4043

4144
def __init__(self, settings):
4245
super().__init__(settings)
43-
props = settings.properties
44-
self._cti_file: str | None = props.get("cti_file")
45-
self._serial_number: str | None = props.get("serial_number") or props.get("serial")
46-
self._pixel_format: str = props.get("pixel_format", "Mono8")
47-
self._rotate: int = int(props.get("rotate", 0)) % 360
48-
self._crop: tuple[int, int, int, int] | None = self._parse_crop(props.get("crop"))
49-
# Check settings first (from config), then properties (for backward compatibility)
50-
self._exposure: float | None = settings.exposure if settings.exposure else props.get("exposure")
51-
self._gain: float | None = settings.gain if settings.gain else props.get("gain")
52-
self._timeout: float = float(props.get("timeout", 2.0))
53-
self._cti_search_paths: tuple[str, ...] = self._parse_cti_paths(props.get("cti_search_paths"))
54-
# Parse resolution (width, height) with defaults
55-
self._resolution: tuple[int, int] | None = self._parse_resolution(props.get("resolution"))
46+
47+
props = settings.properties if isinstance(settings.properties, dict) else {}
48+
ns = props.get(self.OPTIONS_KEY, {})
49+
if not isinstance(ns, dict):
50+
ns = {}
51+
52+
self._cti_file: str | None = ns.get("cti_file") or props.get("cti_file")
53+
self._serial_number: str | None = (
54+
ns.get("serial_number") or ns.get("serial") or props.get("serial_number") or props.get("serial")
55+
)
56+
self._pixel_format: str = ns.get("pixel_format") or props.get("pixel_format", "Mono8")
57+
self._rotate: int = int(ns.get("rotate", props.get("rotate", 0))) % 360
58+
self._crop: tuple[int, int, int, int] | None = self._parse_crop(ns.get("crop", props.get("crop")))
59+
60+
self._exposure: float | None = (
61+
settings.exposure if settings.exposure else ns.get("exposure", props.get("exposure"))
62+
)
63+
self._gain: float | None = settings.gain if settings.gain else ns.get("gain", props.get("gain"))
64+
65+
self._timeout: float = float(ns.get("timeout", props.get("timeout", 2.0)))
66+
self._cti_search_paths: tuple[str, ...] = self._parse_cti_paths(
67+
ns.get("cti_search_paths", props.get("cti_search_paths"))
68+
)
69+
70+
# Resolution request (None = device default)
71+
self._requested_resolution: tuple[int, int] | None = self._get_requested_resolution_or_none()
72+
73+
# Actuals for GUI
74+
self._actual_width: int | None = None
75+
self._actual_height: int | None = None
76+
self._actual_fps: float | None = None
5677

5778
self._harvester = None
5879
self._acquirer = None
5980
self._device_label: str | None = None
6081

82+
@property
83+
def actual_resolution(self) -> tuple[int, int] | None:
84+
if self._actual_width and self._actual_height:
85+
return (self._actual_width, self._actual_height)
86+
return None
87+
88+
@property
89+
def actual_fps(self) -> float | None:
90+
return self._actual_fps
91+
6192
@classmethod
6293
def is_available(cls) -> bool:
6394
return Harvester is not None
6495

96+
@classmethod
97+
def static_capabilities(cls) -> dict[str, SupportLevel]:
98+
return {
99+
"set_resolution": SupportLevel.SUPPORTED,
100+
"set_fps": SupportLevel.SUPPORTED,
101+
"set_exposure": SupportLevel.SUPPORTED,
102+
"set_gain": SupportLevel.SUPPORTED,
103+
"device_discovery": SupportLevel.SUPPORTED,
104+
"stable_identity": SupportLevel.SUPPORTED,
105+
}
106+
65107
@classmethod
66108
def get_device_count(cls) -> int:
67109
"""Get the actual number of GenTL devices detected by Harvester.
@@ -158,6 +200,19 @@ def open(self) -> None:
158200
self._configure_gain(node_map)
159201
self._configure_frame_rate(node_map)
160202

203+
# Capture actual resolution even when using defaults
204+
try:
205+
self._actual_width = int(node_map.Width.value)
206+
self._actual_height = int(node_map.Height.value)
207+
except Exception:
208+
pass
209+
210+
# Capture actual FPS if available
211+
try:
212+
self._actual_fps = float(node_map.ResultingFrameRate.value)
213+
except Exception:
214+
self._actual_fps = None
215+
161216
self._acquirer.start()
162217

163218
def read(self) -> tuple[np.ndarray, float]:
@@ -184,6 +239,12 @@ def read(self) -> tuple[np.ndarray, float]:
184239

185240
frame = self._convert_frame(frame)
186241
timestamp = time.time()
242+
243+
if self._actual_width is None or self._actual_height is None:
244+
h, w = frame.shape[:2]
245+
self._actual_width = int(w)
246+
self._actual_height = int(h)
247+
187248
return frame, timestamp
188249

189250
def stop(self) -> None:
@@ -232,26 +293,77 @@ def _parse_crop(self, crop) -> tuple[int, int, int, int] | None:
232293
return tuple(int(v) for v in crop)
233294
return None
234295

235-
def _parse_resolution(self, resolution) -> tuple[int, int] | None:
236-
"""Parse resolution setting.
296+
def _get_requested_resolution_or_none(self) -> tuple[int, int] | None:
297+
"""
298+
Return (w, h) if user explicitly requested a resolution.
299+
Return None to keep device defaults.
300+
"""
301+
props = self.settings.properties if isinstance(self.settings.properties, dict) else {}
302+
303+
legacy = props.get("resolution")
304+
if isinstance(legacy, (list, tuple)) and len(legacy) == 2:
305+
try:
306+
w, h = int(legacy[0]), int(legacy[1])
307+
if w > 0 and h > 0:
308+
return (w, h)
309+
except Exception:
310+
pass
311+
312+
try:
313+
w = int(getattr(self.settings, "width", 0) or 0)
314+
h = int(getattr(self.settings, "height", 0) or 0)
315+
if w > 0 and h > 0:
316+
return (w, h)
317+
except Exception:
318+
pass
237319

238-
Args:
239-
resolution: Can be a tuple/list [width, height], or None
320+
return None
240321

241-
Returns:
242-
Tuple of (width, height) or None if not specified
243-
Default is (720, 540) if parsing fails but value is provided
322+
def _configure_resolution(self, node_map) -> None:
323+
"""
324+
Configure camera resolution only if explicitly requested.
325+
If None, keep device defaults.
244326
"""
245-
if resolution is None:
246-
return (720, 540) # Default resolution
327+
req = self._requested_resolution
328+
if req is None:
329+
LOG.info("Resolution: using device default.")
330+
return
247331

248-
if isinstance(resolution, (list, tuple)) and len(resolution) == 2:
249-
try:
250-
return (int(resolution[0]), int(resolution[1]))
251-
except (ValueError, TypeError):
252-
return (720, 540)
332+
requested_width, requested_height = req
333+
actual_width, actual_height = None, None
334+
335+
# Width
336+
try:
337+
node = node_map.Width
338+
min_w, max_w = node.min, node.max
339+
inc_w = getattr(node, "inc", 1)
340+
width = self._adjust_to_increment(requested_width, min_w, max_w, inc_w)
341+
node.value = int(width)
342+
actual_width = node.value
343+
except Exception as e:
344+
LOG.warning(f"Failed to set width: {e}")
253345

254-
return (720, 540)
346+
# Height
347+
try:
348+
node = node_map.Height
349+
min_h, max_h = node.min, node.max
350+
inc_h = getattr(node, "inc", 1)
351+
height = self._adjust_to_increment(requested_height, min_h, max_h, inc_h)
352+
node.value = int(height)
353+
actual_height = node.value
354+
except Exception as e:
355+
LOG.warning(f"Failed to set height: {e}")
356+
357+
if actual_width is not None and actual_height is not None:
358+
self._actual_width = int(actual_width)
359+
self._actual_height = int(actual_height)
360+
if (actual_width, actual_height) != (requested_width, requested_height):
361+
LOG.warning(
362+
f"Resolution mismatch: requested {requested_width}x{requested_height}, "
363+
f"got {actual_width}x{actual_height}"
364+
)
365+
else:
366+
LOG.info(f"Resolution set to {actual_width}x{actual_height}")
255367

256368
@staticmethod
257369
def _search_cti_file(patterns: tuple[str, ...]) -> str | None:

0 commit comments

Comments
 (0)