added calibration data handling functions
This commit is contained in:
+58
-22
@@ -19,6 +19,7 @@ from typing import Optional, Tuple
|
||||
from .Stitcher import Stitcher
|
||||
from .tracker import Tracker
|
||||
from .hhd import nHHD
|
||||
from . import calibration as cal_mod
|
||||
from . import configs
|
||||
from .get_depth import Contact3D
|
||||
from .backend_availability import (check_cupy_available, check_opencl_available)
|
||||
@@ -26,8 +27,6 @@ from .frame_processors import create_frame_processor
|
||||
from .signal_processing import (
|
||||
DEFAULT_SAMPLE_RATE_HZ,
|
||||
LowpassFilter,
|
||||
calibrate_normal_force,
|
||||
calibrate_shear_components,
|
||||
)
|
||||
from .capture import (
|
||||
CaptureConfig,
|
||||
@@ -87,7 +86,7 @@ class Sensor:
|
||||
isstitch: Enable stitching mode for multi-tile sensor (default: False)
|
||||
verbose: Print initialization logs (default: True)
|
||||
config_name: Configuration name (default: "prototype")
|
||||
cal_path: Path to calibration file (default: None)
|
||||
cal_path: Explicit force_calibration.json path (overrides bundle default)
|
||||
override: Skip auto-detection, use provided box (default: False)
|
||||
box: Manual bounding boxes when override=True (default: None)
|
||||
backend: Poisson backend (default: "auto")
|
||||
@@ -152,7 +151,9 @@ class Sensor:
|
||||
self.n_boxes = None
|
||||
self.fnormal = self.fshearx = self.fsheary = self.ftor = 0.0
|
||||
self._force_raw = (0.0, 0.0, 0.0)
|
||||
self._force_cal_sensor_name = self._resolve_force_calibration_sensor_name()
|
||||
self.calibration: Optional[cal_mod.Calibration] = None
|
||||
self._force_cal_sensor_name: Optional[str] = None
|
||||
self._init_force_calibration()
|
||||
self._lowpass_enabled = False
|
||||
self._lowpass_cutoff_hz = 0.0
|
||||
self._lowpass_sample_rate_hz = float(DEFAULT_SAMPLE_RATE_HZ)
|
||||
@@ -164,7 +165,9 @@ class Sensor:
|
||||
n_channels=3,
|
||||
)
|
||||
if self.verbose:
|
||||
if self._force_cal_sensor_name:
|
||||
if self.calibration is not None and self.calibration.source_path is not None:
|
||||
print(f"Force calibration: {self.calibration.source_path}")
|
||||
elif self._force_cal_sensor_name:
|
||||
print(
|
||||
f"Force calibration: sensors/{self._force_cal_sensor_name}/force_calibration.json"
|
||||
)
|
||||
@@ -802,17 +805,11 @@ class Sensor:
|
||||
self._update_hhd_mask_cache()
|
||||
return self
|
||||
|
||||
def _resolve_force_calibration_sensor_name(self) -> Optional[str]:
|
||||
"""Return sensors/<name> when force_calibration.json exists; else None.
|
||||
|
||||
Lookup order:
|
||||
1. ``config_name`` stem / product alias (e.g. ``box``)
|
||||
2. ``sensors/<name>/...`` path segment in ``config_name``
|
||||
Missing JSON means ``FNORMAL/FSHEAR*`` stay raw (no hardcoded fallback).
|
||||
"""
|
||||
def _bundle_name_candidates(self) -> list[str]:
|
||||
"""Product bundle names implied by ``config_name``."""
|
||||
text = str(self.config_name or "").strip().replace("\\", "/")
|
||||
if not text:
|
||||
return None
|
||||
return []
|
||||
|
||||
candidates: list[str] = []
|
||||
stem = Path(text).stem if text else ""
|
||||
@@ -828,15 +825,55 @@ class Sensor:
|
||||
candidates.append(name)
|
||||
|
||||
seen: set[str] = set()
|
||||
ordered: list[str] = []
|
||||
for candidate in candidates:
|
||||
alias = configs.CONFIG_ALIASES.get(candidate, candidate)
|
||||
for name in (alias, candidate):
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
if configs.load_force_calibration(name) is not None:
|
||||
return name
|
||||
return None
|
||||
ordered.append(name)
|
||||
return ordered
|
||||
|
||||
def _init_force_calibration(self) -> None:
|
||||
"""Load force calibration from ``cal_path`` or the product bundle."""
|
||||
self.calibration = None
|
||||
self._force_cal_sensor_name = None
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
for name in self._bundle_name_candidates():
|
||||
cal = cal_mod.load(sensor_type=name)
|
||||
if cal is not None:
|
||||
self.calibration = cal
|
||||
self._force_cal_sensor_name = name
|
||||
return
|
||||
|
||||
@property
|
||||
def force_calibrated(self) -> bool:
|
||||
"""True when at least one channel curve is loaded."""
|
||||
return self.calibration is not None and bool(self.calibration.channels)
|
||||
|
||||
@property
|
||||
def force_unit(self) -> str:
|
||||
return "N" if self.force_calibrated else "raw"
|
||||
|
||||
def set_calibration(
|
||||
self,
|
||||
cal: "cal_mod.Calibration | str | Path | None",
|
||||
) -> None:
|
||||
"""Attach or replace the active force calibration (live swap after save)."""
|
||||
if cal is None:
|
||||
self.calibration = None
|
||||
self._force_cal_sensor_name = None
|
||||
return
|
||||
if isinstance(cal, cal_mod.Calibration):
|
||||
self.calibration = cal
|
||||
return
|
||||
self.calibration = cal_mod.load(cal, strict=True)
|
||||
|
||||
def _apply_force_filter(self):
|
||||
raw = np.asarray(self._force_raw, dtype=np.float64)
|
||||
@@ -846,12 +883,11 @@ class Sensor:
|
||||
else:
|
||||
fn, fx, fy = float(raw[0]), float(raw[1]), float(raw[2])
|
||||
|
||||
# Apply sensors/<name>/force_calibration.json when present; otherwise keep raw.
|
||||
name = self._force_cal_sensor_name
|
||||
if name is not None:
|
||||
fn = float(calibrate_normal_force(fn, sensor_name=name))
|
||||
fx, fy = calibrate_shear_components(fx, fy, sensor_name=name)
|
||||
# Apply loaded force_calibration.json when present; otherwise keep raw.
|
||||
if self.calibration is not None:
|
||||
fn, fx, fy = self.calibration.convert(fn, fx, fy)
|
||||
fx, fy = float(fx), float(fy)
|
||||
fn = float(fn)
|
||||
|
||||
self.fnormal, self.fshearx, self.fsheary = fn, fx, fy
|
||||
|
||||
|
||||
Reference in New Issue
Block a user