添加按设备 ID 选择和校验标定文件

避免多台传感器误用默认标定,缺文件时保持 raw。已通过 SDK 回归测试和文档检查。
This commit is contained in:
ykcheungab
2026-10-05 12:21:04 +08:00
parent 230b04a79d
commit 80f7da5a20
6 changed files with 261 additions and 10 deletions
+63 -5
View File
@@ -143,6 +143,7 @@ class Sensor:
backend='auto', capture_config: CaptureConfig | None = None,
auto_reconnect: bool = False, capture_mode: str = "latest",
capture_queue_size: int = 1, capture_overflow: str = "drop_oldest",
calibration_dir: str | Path | None = None, device_id: str | None = None,
**_deprecated_capture_kwargs):
"""
Initialize Sensor with video source and configuration.
@@ -154,6 +155,8 @@ class Sensor:
verbose: Print initialization logs (default: True)
config_name: Configuration name (default: "prototype")
cal_path: Explicit force_calibration.json path (overrides bundle default)
calibration_dir: User-managed root containing <product>/<device_id>.json.
device_id: Manually entered or hardware-reported device identity.
override: Skip auto-detection, use provided box (default: False)
box: Manual bounding boxes when override=True (default: None)
backend: Poisson backend (default: "auto")
@@ -184,6 +187,15 @@ class Sensor:
self.debug = debug
self.config_name = config_name
self.cal_path = cal_path
if calibration_dir is not None and device_id is None:
raise cal_mod.CalibrationError("device_id is required with calibration_dir")
if device_id is not None and calibration_dir is None and cal_path is None:
raise cal_mod.CalibrationError("device_id requires calibration_dir or cal_path")
if calibration_dir is not None and cal_path is not None:
raise cal_mod.CalibrationError("cal_path and calibration_dir are mutually exclusive")
self.calibration_dir = calibration_dir
self.device_id = device_id
self.device_calibration_missing = False
self.mirror = mirror
self._capture_config_override = capture_config
self._auto_reconnect = bool(auto_reconnect)
@@ -1273,6 +1285,11 @@ class Sensor:
bundle from another root and stopped there without ever asking whether
that root had coefficients.
"""
if getattr(self, "device_calibration_missing", False):
return (
f"Force calibration: raw (no file for {self.device_id!r} "
f"under {self.calibration_dir})"
)
attempts = self._force_cal_attempts or []
# ``found_root`` is keyed even when nothing was found (valued None), so
# test its value, not the key.
@@ -1306,14 +1323,30 @@ class Sensor:
return "\n".join(lines)
def _init_force_calibration(self) -> None:
"""Load force calibration from ``cal_path`` or the product bundle."""
"""Load the selected force calibration before processing frames."""
self.calibration = None
self._force_cal_sensor_name = None
self._force_cal_attempts = None
family = None
pipeline = None
if self.device_id is not None:
family, pipeline = self._device_calibration_expectations()
if self.cal_path is not None and str(self.cal_path).strip():
# Explicit path: missing/invalid raises (no silent fallback).
self.calibration = cal_mod.load(self.cal_path, strict=True)
if self.device_id is not None:
self.calibration = cal_mod.validate_for_device(
self.calibration, family, self.device_id, pipeline=pipeline
)
return
if self.calibration_dir is not None:
self.calibration = cal_mod.load_for_device(
self.calibration_dir, family, self.device_id, pipeline=pipeline
)
self.device_calibration_missing = self.calibration is None
return
attempts: list[dict] = []
@@ -1333,6 +1366,24 @@ class Sensor:
# without coefficients is not that.
self._force_cal_attempts = attempts or None
def _device_calibration_expectations(self):
family = next(
(name for name in self._bundle_name_candidates() if name in configs.PRODUCT_BUNDLES),
None,
)
if family is None:
raise cal_mod.CalibrationError(
f"cannot determine product family from config_name={self.config_name!r}"
)
camera = self._config.get("camera", {})
tracker = self._config.get("tracker", {})
return family, {
"stitch_resolution": camera.get("resolution_stitch"),
"dis_mode": tracker.get("dis_mode"),
"hhd_downsample": tracker.get("hhd_downsample"),
"filters": cal_mod.filters_fingerprint(self._config.get("filters")),
}
@property
def force_calibrated(self) -> bool:
"""True when at least one channel curve is loaded."""
@@ -1349,14 +1400,21 @@ class Sensor:
"""Attach or replace the active force calibration (live swap after save)."""
if cal is None:
self.calibration = None
self.device_calibration_missing = False
self._force_cal_sensor_name = None
self._force_cal_attempts = None
return
if isinstance(cal, cal_mod.Calibration):
self.calibration = cal
self._force_cal_attempts = None
return
self.calibration = cal_mod.load(cal, strict=True)
selected = cal
else:
selected = cal_mod.load(cal, strict=True)
if self.device_id is not None:
family, pipeline = self._device_calibration_expectations()
selected = cal_mod.validate_for_device(
selected, family, self.device_id, pipeline=pipeline
)
self.calibration = selected
self.device_calibration_missing = False
self._force_cal_attempts = None
def _apply_force_filter(self):