added calibration data handling functions

This commit is contained in:
ykcheungab
2026-09-10 21:47:36 +08:00
parent 9cbba08d68
commit 96f4428230
13 changed files with 1399 additions and 175 deletions
+58 -22
View File
@@ -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