接入力标定 schema 3 与多点空间增益模型
保留旧格式和 cal_path,校验 SN、特征身份及坐标附件,按帧返回逐轴状态。相关测试 263 项通过,已核对存量预测并实测 180 帧录像。
This commit is contained in:
@@ -17,7 +17,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
CURRENT_SCHEMA = 2
|
||||
SUPPORTED_SCHEMAS = (1, 2)
|
||||
SUPPORTED_SCHEMAS = (1, 2, 3)
|
||||
CHANNEL_IDS: tuple[str, ...] = ("fn", "fx", "fy")
|
||||
|
||||
CHANNEL_ALIASES: dict[str, str] = {
|
||||
@@ -500,6 +500,17 @@ def check(doc: dict | Calibration, *, pipeline: dict | None = None) -> CheckResu
|
||||
warns: list[str] = []
|
||||
notes: list[str] = []
|
||||
|
||||
if getattr(doc, "schema_version", None) == 3 or (isinstance(doc, dict) and doc.get("schema_version") == 3):
|
||||
from .calibration_models import parse_model
|
||||
|
||||
try:
|
||||
selected = parse_model(doc) if isinstance(doc, dict) else doc
|
||||
if pipeline is not None:
|
||||
selected.validate_for_context(pipeline)
|
||||
except (CalibrationError, ValueError, TypeError) as exc:
|
||||
return CheckResult(ok=False, errors=[str(exc)])
|
||||
return CheckResult(ok=True, errors=[], warnings=[])
|
||||
|
||||
if isinstance(doc, Calibration):
|
||||
data = doc.to_dict()
|
||||
channels_obj = doc.channels
|
||||
@@ -605,6 +616,14 @@ def parse(data: dict, *, source_path: Path | None = None) -> Calibration:
|
||||
if schema not in SUPPORTED_SCHEMAS:
|
||||
raise CalibrationError(f"unsupported schema_version: {schema}")
|
||||
|
||||
if schema == 3:
|
||||
from .calibration_models import parse_model
|
||||
|
||||
try:
|
||||
return parse_model(data, source_path=source_path)
|
||||
except (ValueError, TypeError, KeyError) as exc:
|
||||
raise CalibrationError(f"invalid schema 3 calibration: {exc}") from exc
|
||||
|
||||
unit = str(data.get("unit", "N"))
|
||||
channels_raw = data.get("channels")
|
||||
if not isinstance(channels_raw, dict):
|
||||
@@ -766,7 +785,7 @@ def load(
|
||||
explicit = source is not None and str(source).strip() != ""
|
||||
|
||||
if explicit:
|
||||
path = Path(str(source)).expanduser()
|
||||
path = select_calibration_path(cal_path=source)
|
||||
if not path.is_file():
|
||||
msg = f"calibration file not found: {path}"
|
||||
if strict or explicit:
|
||||
@@ -801,7 +820,7 @@ def load(
|
||||
try:
|
||||
return parse(data, source_path=path)
|
||||
except CalibrationError:
|
||||
if strict or explicit:
|
||||
if strict or explicit or (isinstance(data, dict) and (data.get("schema_version") == 3 or "model" in data)):
|
||||
raise
|
||||
return None
|
||||
|
||||
@@ -814,6 +833,26 @@ def load_for_device(
|
||||
pipeline: Mapping[str, Any] | None = None,
|
||||
) -> Calibration | None:
|
||||
"""Load ``<directory>/<sensor_type>/<device_id>.json``; missing file means raw."""
|
||||
path = select_calibration_path(calibration_dir=calibration_dir, sensor_type=sensor_type, device_id=device_id)
|
||||
if path is None:
|
||||
return None
|
||||
cal = load(path, strict=True)
|
||||
assert cal is not None
|
||||
return validate_for_device(cal, sensor_type, device_id, pipeline=pipeline)
|
||||
|
||||
|
||||
def select_calibration_path(*, cal_path=None, calibration_dir=None, sensor_type=None,
|
||||
device_id=None, default_path=None) -> Path | None:
|
||||
"""Select a file without guessing a model or falling back across device identities."""
|
||||
explicit = cal_path is not None and bool(str(cal_path).strip())
|
||||
if explicit and calibration_dir is not None:
|
||||
raise CalibrationError("cal_path and calibration_dir are mutually exclusive")
|
||||
if device_id is not None and not explicit and calibration_dir is None:
|
||||
raise CalibrationError("device_id requires calibration_dir or cal_path")
|
||||
if explicit:
|
||||
return Path(cal_path).expanduser()
|
||||
if calibration_dir is None:
|
||||
return Path(default_path) if default_path is not None else None
|
||||
root = Path(calibration_dir).expanduser()
|
||||
if not root.is_dir():
|
||||
raise CalibrationError(f"calibration directory not found: {root}")
|
||||
@@ -827,9 +866,7 @@ def load_for_device(
|
||||
return None
|
||||
if not path.is_file():
|
||||
raise CalibrationError(f"calibration path is not a file: {path}")
|
||||
cal = load(path, strict=True)
|
||||
assert cal is not None
|
||||
return validate_for_device(cal, sensor_type, device_id, pipeline=pipeline)
|
||||
return path
|
||||
|
||||
|
||||
def validate_for_device(
|
||||
@@ -843,6 +880,19 @@ def validate_for_device(
|
||||
path = cal.source_path or "<in-memory calibration>"
|
||||
if cal.identity.get("sensor_type") != sensor_type or cal.identity.get("sn") != device_id:
|
||||
raise CalibrationError(f"calibration identity mismatch: {path}")
|
||||
if cal.schema_version == 3:
|
||||
if cal.unit != "N" or not cal.channels:
|
||||
raise CalibrationError(f"calibration requires declared force axes in N: {path}")
|
||||
# Full feature/geometry validation happens when the processor binds its
|
||||
# actual context. Device construction only knows this legacy subset.
|
||||
if pipeline is not None:
|
||||
for key in ("stitch_resolution", "dis_mode", "hhd_downsample"):
|
||||
if pipeline.get(key) is not None and cal.pipeline.get(key) != pipeline[key]:
|
||||
raise CalibrationError(f"calibration pipeline mismatch ({key}): {path}")
|
||||
measurement, _ = filter_mismatches(cal.pipeline.get("filters"), pipeline.get("filters"))
|
||||
if measurement:
|
||||
raise CalibrationError(f"calibration pipeline mismatch ({'; '.join(measurement)}): {path}")
|
||||
return cal
|
||||
if cal.unit != "N" or any(cal.channels[axis].unit != "N" for axis in cal.channels):
|
||||
raise CalibrationError(f"calibration unit must be N: {path}")
|
||||
if set(CHANNEL_IDS) - cal.channels.keys():
|
||||
@@ -1050,10 +1100,13 @@ def write(
|
||||
path = Path(path)
|
||||
if path.exists() and not overwrite:
|
||||
raise FileExistsError(f"calibration file exists: {path}")
|
||||
if cal.schema_version == 3:
|
||||
# A moved manifest must not acquire unresolved or different attachments.
|
||||
parse(cal.to_dict(), source_path=path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc = cal.to_dict()
|
||||
# Always emit schema 2 for new writes.
|
||||
doc["schema_version"] = CURRENT_SCHEMA
|
||||
# Preserve an explicitly selected model format; legacy builders still emit v2.
|
||||
doc["schema_version"] = 3 if cal.schema_version == 3 else CURRENT_SCHEMA
|
||||
path.write_text(
|
||||
json.dumps(doc, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
|
||||
Reference in New Issue
Block a user