Skip to content

Commit 4f77655

Browse files
committed
Basler: better exposure/gain and stream reset
Basler backend: simplify and harden exposure/gain configuration by only applying positive values, turning off auto modes when available, and logging failures as warnings. Harden stream lifecycle for previews by stopping grabbing if needed, creating the ImageFormatConverter before StartGrabbing, forcing MaxNumBuffer, starting grabbing reliably and logging grab state; also wrap StopGrabbing on shutdown to ignore errors. Camera config UI: refine property merge and UI behavior — avoid reloading the form after merge, safely refresh camera labels by guarding null lists and blocking signals, ignore redundant selection events, and add explicit preview restart/start helpers (_restart_preview_for_camera and _start_preview_with_camera). Ensure previews never use fast_start mode and improve logging around selection and preview startup.
1 parent 55e5c52 commit 4f77655

2 files changed

Lines changed: 132 additions & 28 deletions

File tree

dlclivegui/cameras/backends/basler_backend.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -329,27 +329,25 @@ def open(self) -> None:
329329
self._camera = pylon.InstantCamera(pylon.TlFactory.GetInstance().CreateDevice(device))
330330
self._camera.Open()
331331

332-
# ----------------------------
333-
# Exposure (0 = Auto → do not set)
334-
# ----------------------------
335-
exposure = self._positive_float(getattr(self.settings, "exposure", 0))
336-
337-
if exposure is not None:
332+
# Exposure
333+
if getattr(self.settings, "exposure", 0) > 0:
338334
try:
339-
self._camera.ExposureTime.SetValue(exposure)
340-
except Exception:
341-
LOG.debug("ExposureTime not writable or not supported", exc_info=True)
342-
343-
# ----------------------------
344-
# Gain (0 = Auto → do not set)
345-
# ----------------------------
346-
gain = self._positive_float(getattr(self.settings, "gain", 0))
347-
348-
if gain is not None:
335+
if hasattr(self._camera, "ExposureAuto"):
336+
self._camera.ExposureAuto.SetValue("Off")
337+
self._camera.ExposureTime.SetValue(float(self.settings.exposure))
338+
LOG.info("[Basler] Exposure set to %s us (auto off)", self.settings.exposure)
339+
except Exception as exc:
340+
LOG.warning("[Basler] Failed to set exposure: %s", exc)
341+
342+
# Gain
343+
if getattr(self.settings, "gain", 0.0) > 0.0:
349344
try:
350-
self._camera.Gain.SetValue(gain)
351-
except Exception:
352-
LOG.debug("Gain not writable or not supported", exc_info=True)
345+
if hasattr(self._camera, "GainAuto"):
346+
self._camera.GainAuto.SetValue("Off")
347+
self._camera.Gain.SetValue(float(self.settings.gain))
348+
LOG.info("[Basler] Gain set to %s dB (auto off)", self.settings.gain)
349+
except Exception as exc:
350+
LOG.warning("[Basler] Failed to set gain: %s", exc)
353351

354352
# ----------------------------
355353
# Resolution (None → device default)
@@ -403,11 +401,33 @@ def open(self) -> None:
403401
# Start acquisition (skip for fast probe)
404402
# ----------------------------
405403
if not self._fast_start:
406-
self._camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly)
404+
# --- HARD RESET of stream state (critical after fast-start probe) ---
405+
try:
406+
if hasattr(self._camera, "StopGrabbing") and self._camera.IsGrabbing():
407+
self._camera.StopGrabbing()
408+
except Exception:
409+
pass
407410

411+
# Converter BEFORE StartGrabbing
408412
self._converter = pylon.ImageFormatConverter()
409413
self._converter.OutputPixelFormat = pylon.PixelType_BGR8packed
410414
self._converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned
415+
416+
# Force stream configuration reset
417+
try:
418+
if hasattr(self._camera, "MaxNumBuffer"):
419+
self._camera.MaxNumBuffer.SetValue(10)
420+
except Exception:
421+
pass
422+
423+
self._camera.StartGrabbing(
424+
pylon.GrabStrategy_LatestImageOnly,
425+
)
426+
LOG.info(
427+
"[Basler] grabbing=%s max_buffers=%s",
428+
self._camera.IsGrabbing(),
429+
self._camera.MaxNumBuffer.GetValue() if hasattr(self._camera, "MaxNumBuffer") else "N/A",
430+
)
411431
else:
412432
LOG.debug("Fast-start probe: skipping StartGrabbing and converter")
413433

@@ -469,7 +489,10 @@ def close(self) -> None:
469489
)
470490
if self._camera is not None:
471491
if self._camera.IsGrabbing():
472-
self._camera.StopGrabbing()
492+
try:
493+
self._camera.StopGrabbing()
494+
except Exception:
495+
pass
473496
if self._camera.IsOpen():
474497
self._camera.Close()
475498
self._camera = None

dlclivegui/gui/camera_config_dialog.py

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,9 @@ def _merge_backend_settings_back(self, opened_settings: CameraSettings) -> None:
298298
except Exception:
299299
pass
300300

301-
# Merge properties (especially stable IDs) back
302301
if isinstance(opened_settings.properties, dict):
303302
if not isinstance(target.properties, dict):
304303
target.properties = {}
305-
# shallow merge is ok; backend namespaces are nested dicts
306304
for k, v in opened_settings.properties.items():
307305
if isinstance(v, dict) and isinstance(target.properties.get(k), dict):
308306
target.properties[k].update(v)
@@ -311,7 +309,6 @@ def _merge_backend_settings_back(self, opened_settings: CameraSettings) -> None:
311309

312310
# Update UI list item text to reflect any changes
313311
self._update_active_list_item(row, target)
314-
self._load_camera_to_form(target)
315312

316313
# -------------------------------
317314
# UI setup
@@ -858,12 +855,18 @@ def _format_camera_label(self, cam: CameraSettings, index: int = -1) -> str:
858855

859856
def _refresh_camera_labels(self) -> None:
860857
cam_list = getattr(self, "active_cameras_list", None)
861-
if cam_list:
858+
if not cam_list:
859+
return
860+
861+
cam_list.blockSignals(True) # prevent unwanted selection change events during update
862+
try:
862863
for i in range(cam_list.count()):
863864
item = cam_list.item(i)
864865
cam = item.data(Qt.ItemDataRole.UserRole)
865866
if cam:
866867
item.setText(self._format_camera_label(cam, i))
868+
finally:
869+
cam_list.blockSignals(False)
867870

868871
def _on_backend_changed(self, _index: int) -> None:
869872
self._refresh_available_cameras()
@@ -990,8 +993,20 @@ def _on_available_camera_double_clicked(self, item: QListWidgetItem) -> None:
990993
self._add_selected_camera()
991994

992995
def _on_active_camera_selected(self, row: int) -> None:
996+
LOGGER.info(
997+
"[Select] row=%s prev=%s preview_active=%s loading_active=%s",
998+
row,
999+
self._current_edit_index,
1000+
self._preview_active,
1001+
self._loading_active,
1002+
)
9931003
prev_row = self._current_edit_index
9941004

1005+
# If row is the same, ignore
1006+
if prev_row is not None and prev_row == row:
1007+
LOGGER.debug("[Selection] Redundant currentRowChanged to same index %d; ignoring.", row)
1008+
return
1009+
9951010
# If switching away from a previous camera, commit pending edits first
9961011
if prev_row is not None and prev_row != row:
9971012
if not self._commit_pending_edits(reason="before switching camera selection"):
@@ -1551,8 +1566,7 @@ def _cam_diff(old: CameraSettings, new: CameraSettings) -> dict:
15511566
if self._preview_active:
15521567
if restart:
15531568
self._append_status("[Apply] Restarting preview to apply camera settings…")
1554-
self._stop_preview()
1555-
QTimer.singleShot(100, self._start_preview) # small delay for drivers (Basler/Pylon)
1569+
QTimer.singleShot(0, lambda cam=new_model: self._restart_preview_for_camera(cam))
15561570
else:
15571571
self._append_status("[Apply] Applied without restart (crop/rotation update is live).")
15581572

@@ -1627,10 +1641,72 @@ def _toggle_preview(self) -> None:
16271641
else:
16281642
self._start_preview()
16291643

1644+
def _restart_preview_for_camera(self, cam: CameraSettings) -> None:
1645+
"""Restart preview for a specific camera, independent of UI selection."""
1646+
LOGGER.info(
1647+
"[Preview] restarting explicitly for backend=%s idx=%s",
1648+
cam.backend,
1649+
cam.index,
1650+
)
1651+
1652+
# Stop any running preview cleanly
1653+
self._stop_preview()
1654+
1655+
# Force preview-safe backend flags
1656+
if isinstance(cam.properties, dict):
1657+
ns = cam.properties.setdefault((cam.backend or "").lower(), {})
1658+
if isinstance(ns, dict):
1659+
ns["fast_start"] = False
1660+
1661+
# Start preview without relying on selection state
1662+
self._start_preview_with_camera(cam)
1663+
1664+
def _start_preview_with_camera(self, cam: CameraSettings) -> None:
1665+
"""Start preview for a given CameraSettings object."""
1666+
LOGGER.info(
1667+
"[Preview] start (explicit) backend=%s idx=%s name=%s",
1668+
cam.backend,
1669+
cam.index,
1670+
cam.name,
1671+
)
1672+
1673+
# Create loader directly from camera
1674+
self._loader = CameraLoadWorker(cam, self)
1675+
self._loader.progress.connect(self._on_loader_progress)
1676+
self._loader.success.connect(self._on_loader_success)
1677+
self._loader.error.connect(self._on_loader_error)
1678+
self._loader.canceled.connect(self._on_loader_canceled)
1679+
self._loader.finished.connect(self._on_loader_finished)
1680+
1681+
self._loading_active = True
1682+
self._update_button_states()
1683+
1684+
# Prepare UI
1685+
self.preview_group.setVisible(True)
1686+
self.preview_label.setText("No preview")
1687+
self.preview_status.clear()
1688+
self._show_loading_overlay("Loading camera…")
1689+
self._set_preview_button_loading(True)
1690+
1691+
self._loader.start()
1692+
16301693
def _start_preview(self) -> None:
16311694
"""Start camera preview asynchronously (no UI freeze)."""
1632-
if self._current_edit_index is None or self._current_edit_index < 0:
1695+
row = self._current_edit_index
1696+
if row is None or row < 0:
1697+
row = self.active_cameras_list.currentRow()
1698+
1699+
if row is None or row < 0:
1700+
LOGGER.warning("[Preview] No camera selected to start preview.")
16331701
return
1702+
1703+
self._current_edit_index = row
1704+
LOGGER.info(
1705+
"[Preview] resolved start row=%s active_row=%s",
1706+
self._current_edit_index,
1707+
self.active_cameras_list.currentRow(),
1708+
)
1709+
16341710
item = self.active_cameras_list.item(self._current_edit_index)
16351711
if not item:
16361712
return
@@ -1651,6 +1727,11 @@ def _start_preview(self) -> None:
16511727
self._stop_preview()
16521728
# if self._loader and self._loader.isRunning():
16531729
# self._loader.request_cancel()
1730+
# Never use probe or fast_start mode
1731+
if isinstance(cam.properties, dict):
1732+
ns = cam.properties.get((cam.backend or "").lower(), {})
1733+
if isinstance(ns, dict):
1734+
ns["fast_start"] = False
16541735
# Create worker
16551736
self._loader = CameraLoadWorker(cam, self)
16561737
self._loader.progress.connect(self._on_loader_progress)

0 commit comments

Comments
 (0)