Skip to content

Commit 7f5de83

Browse files
committed
Add exposure/gain tracking and auto handling
Expose and track actual exposure and gain for Basler and GenTL backends, and treat non-positive values as "Auto" (do not set). Basler: add actual_exposure/actual_gain properties, read/write exposure/gain only when positive, probe actual FPS after opening. Introduce _settings_value(treat_nonpositive_as_none=True) to centralize zero-as-auto logic. GenTL: parse settings so 0 means auto, add actual_exposure/actual_gain fields and properties, and attempt to read those values from the device/node_map. Includes defensive error handling when reading or setting camera parameters.
1 parent b57f02b commit 7f5de83

2 files changed

Lines changed: 116 additions & 12 deletions

File tree

dlclivegui/cameras/backends/basler_backend.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def __init__(self, settings):
4343
self._actual_width: int | None = None
4444
self._actual_height: int | None = None
4545
self._actual_fps: float | None = None
46+
self._actual_exposure: float | None = None
47+
self._actual_gain: float | None = None
4648

4749
@property
4850
def actual_resolution(self) -> tuple[int, int] | None:
@@ -54,6 +56,14 @@ def actual_resolution(self) -> tuple[int, int] | None:
5456
def actual_fps(self) -> float | None:
5557
return self._actual_fps
5658

59+
@property
60+
def actual_exposure(self) -> float | None:
61+
return self._actual_exposure
62+
63+
@property
64+
def actual_gain(self) -> float | None:
65+
return self._actual_gain
66+
5767
@classmethod
5868
def is_available(cls) -> bool:
5969
return pylon is not None
@@ -85,16 +95,20 @@ def open(self) -> None:
8595
self._camera = pylon.InstantCamera(pylon.TlFactory.GetInstance().CreateDevice(device))
8696
self._camera.Open()
8797

88-
# Exposure
89-
exposure = self._settings_value("exposure", self.settings.properties)
98+
# Exposure (0 = Auto -> do not set)
99+
exposure = self._settings_value(
100+
"exposure", self.settings.properties, fallback=self.settings.exposure, treat_nonpositive_as_none=True
101+
)
90102
if exposure is not None:
91103
try:
92104
self._camera.ExposureTime.SetValue(float(exposure))
93105
except Exception:
94106
pass
95107

96-
# Gain
97-
gain = self._settings_value("gain", self.settings.properties)
108+
# Gain (0 = Auto -> do not set)
109+
gain = self._settings_value(
110+
"gain", self.settings.properties, fallback=self.settings.gain, treat_nonpositive_as_none=True
111+
)
98112
if gain is not None:
99113
try:
100114
self._camera.Gain.SetValue(float(gain))
@@ -104,15 +118,22 @@ def open(self) -> None:
104118
# Resolution (device default if None)
105119
self._configure_resolution()
106120

107-
# Frame rate
108-
fps = self._settings_value("fps", self.settings.properties, fallback=self.settings.fps)
121+
# Frame rate (0.0 = Auto -> do not set)
122+
fps = self._settings_value(
123+
"fps", self.settings.properties, fallback=self.settings.fps, treat_nonpositive_as_none=True
124+
)
109125
if fps is not None:
110126
try:
111127
self._camera.AcquisitionFrameRateEnable.SetValue(True)
112128
self._camera.AcquisitionFrameRate.SetValue(float(fps))
113-
self._actual_fps = float(self._camera.AcquisitionFrameRate.GetValue())
114129
except Exception:
115-
self._actual_fps = None
130+
pass
131+
132+
# Always try to read actual FPS for probing / GUI
133+
try:
134+
self._actual_fps = float(self._camera.AcquisitionFrameRate.GetValue())
135+
except Exception:
136+
self._actual_fps = None
116137

117138
# Capture actual resolution even when using defaults
118139
try:
@@ -121,6 +142,16 @@ def open(self) -> None:
121142
except Exception:
122143
pass
123144

145+
try:
146+
self._actual_exposure = float(self._camera.ExposureTime.GetValue())
147+
except Exception:
148+
self._actual_exposure = None
149+
150+
try:
151+
self._actual_gain = float(self._camera.Gain.GetValue())
152+
except Exception:
153+
self._actual_gain = None
154+
124155
self._camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly)
125156

126157
self._converter = pylon.ImageFormatConverter()
@@ -250,6 +281,28 @@ def _configure_resolution(self) -> None:
250281
LOG.warning(f"Failed to set resolution to {req_w}x{req_h}: {exc}")
251282

252283
@staticmethod
253-
def _settings_value(key: str, source: dict, fallback: float | None = None):
284+
def _settings_value(
285+
key: str, source: dict, fallback: float | None = None, *, treat_nonpositive_as_none: bool = True
286+
):
287+
"""
288+
Fetch setting from a dict with an optional fallback.
289+
290+
If treat_nonpositive_as_none is True:
291+
- numeric values <= 0 are treated as "Auto" and returned as None
292+
"""
254293
value = source.get(key, fallback)
255-
return None if value is None else value
294+
295+
if value is None:
296+
return None
297+
298+
# Treat 0 / <=0 as Auto by default
299+
if treat_nonpositive_as_none and isinstance(value, (int, float)):
300+
try:
301+
fv = float(value)
302+
if fv <= 0.0:
303+
return None
304+
return fv
305+
except Exception:
306+
return None
307+
308+
return value

dlclivegui/cameras/backends/gentl_backend.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,29 @@ def __init__(self, settings):
5757
self._rotate: int = int(ns.get("rotate", props.get("rotate", 0))) % 360
5858
self._crop: tuple[int, int, int, int] | None = self._parse_crop(ns.get("crop", props.get("crop")))
5959

60+
# Exposure / Gain: 0 means Auto (do not set)
61+
exp_val = getattr(settings, "exposure", 0)
62+
gain_val = getattr(settings, "gain", 0.0)
63+
6064
self._exposure: float | None = (
61-
settings.exposure if settings.exposure else ns.get("exposure", props.get("exposure"))
65+
float(exp_val) if isinstance(exp_val, (int, float)) and float(exp_val) > 0 else None
6266
)
63-
self._gain: float | None = settings.gain if settings.gain else ns.get("gain", props.get("gain"))
67+
if self._exposure is None:
68+
v = ns.get("exposure", props.get("exposure"))
69+
try:
70+
self._exposure = float(v) if v is not None and float(v) > 0 else None
71+
except Exception:
72+
self._exposure = None
73+
74+
self._gain: float | None = (
75+
float(gain_val) if isinstance(gain_val, (int, float)) and float(gain_val) > 0 else None
76+
)
77+
if self._gain is None:
78+
v = ns.get("gain", props.get("gain"))
79+
try:
80+
self._gain = float(v) if v is not None and float(v) > 0 else None
81+
except Exception:
82+
self._gain = None
6483

6584
self._timeout: float = float(ns.get("timeout", props.get("timeout", 2.0)))
6685
self._cti_search_paths: tuple[str, ...] = self._parse_cti_paths(
@@ -74,6 +93,8 @@ def __init__(self, settings):
7493
self._actual_width: int | None = None
7594
self._actual_height: int | None = None
7695
self._actual_fps: float | None = None
96+
self._actual_gain: float | None = None
97+
self._actual_exposure: float | None = None
7798

7899
self._harvester = None
79100
self._acquirer = None
@@ -89,6 +110,14 @@ def actual_resolution(self) -> tuple[int, int] | None:
89110
def actual_fps(self) -> float | None:
90111
return self._actual_fps
91112

113+
@property
114+
def actual_exposure(self) -> float | None:
115+
return self._actual_exposure
116+
117+
@property
118+
def actual_gain(self) -> float | None:
119+
return self._actual_gain
120+
92121
@classmethod
93122
def is_available(cls) -> bool:
94123
return Harvester is not None
@@ -213,6 +242,16 @@ def open(self) -> None:
213242
except Exception:
214243
self._actual_fps = None
215244

245+
try:
246+
self._actual_exposure = float(node_map.ExposureTime.value)
247+
except Exception:
248+
self._actual_exposure = None
249+
250+
try:
251+
self._actual_gain = float(node_map.Gain.value)
252+
except Exception:
253+
self._actual_gain = None
254+
216255
self._acquirer.start()
217256

218257
def read(self) -> tuple[np.ndarray, float]:
@@ -245,6 +284,18 @@ def read(self) -> tuple[np.ndarray, float]:
245284
self._actual_width = int(w)
246285
self._actual_height = int(h)
247286

287+
if self._actual_exposure is None:
288+
try:
289+
self._actual_exposure = float(self._acquirer.node_map.ExposureTime.value)
290+
except Exception:
291+
self._actual_exposure = None
292+
293+
if self._actual_gain is None:
294+
try:
295+
self._actual_gain = float(self._acquirer.node_map.Gain.value)
296+
except Exception:
297+
self._actual_gain = None
298+
248299
return frame, timestamp
249300

250301
def stop(self) -> None:

0 commit comments

Comments
 (0)