ee830d8505
把同帧测量、空载归零、模型推断与共用后处理收进一个 SDK 会话,Studio 显示与记录使用同一样本;旧 Sensor 读数接口保持兼容。以 force session 与受影响 SDK 套件、lint 与文档门禁验证,硬件与真实载荷精度未验证。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CpCfJF6SvonMKmPZhzKZHb
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
"""Measurement identities shared by SDK runtime and offline calibration exporters."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from .calibration import filters_fingerprint
|
|
|
|
|
|
def _feature_signature(identity) -> str:
|
|
encoded = json.dumps(identity, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
|
|
|
|
|
class CalibrationContext(dict):
|
|
"""Actual pipeline fields plus the runtime inputs needed to compare HHD settings.
|
|
|
|
JSON serialization contains only the actual fields. Retaining the signature
|
|
inputs lets old files differ in downsample without ignoring other hashed settings.
|
|
"""
|
|
|
|
def __init__(self, fields, feature_identity):
|
|
super().__init__(fields)
|
|
self._feature_identity = deepcopy(feature_identity)
|
|
|
|
def signature_for_downsample(self, downsample: int) -> str:
|
|
"""Recompute the runtime identity with only the requested HHD resolution."""
|
|
identity = deepcopy(self._feature_identity)
|
|
identity["tracker"]["hhd_downsample"] = downsample
|
|
return _feature_signature(identity)
|
|
|
|
@property
|
|
def actual_downsample(self) -> int:
|
|
"""The HHD downsample this runtime context was actually built with."""
|
|
raw = self.get("hhd_downsample", 1)
|
|
try:
|
|
return max(1, int(raw)) if raw is not None else 1
|
|
except (TypeError, ValueError):
|
|
return 1
|
|
|
|
|
|
def sha256_file(path) -> str:
|
|
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
|
|
|
|
def domain_fingerprint(domain) -> str:
|
|
"""Bind a coordinate lookup to the actual unroll, not just its dimensions."""
|
|
data = domain.to_dict() if hasattr(domain, "to_dict") else domain
|
|
digest = hashlib.sha256()
|
|
for key, dtype in (("map_x", "<f4"), ("map_y", "<f4"), ("valid_mask", "u1"), ("world_xyz", "<f4")):
|
|
array = np.ascontiguousarray(data[key], dtype=dtype)
|
|
digest.update(key.encode("ascii"))
|
|
digest.update(str(array.shape).encode("ascii"))
|
|
digest.update(array.tobytes())
|
|
return digest.hexdigest()
|
|
|
|
|
|
def build_context(config, filters=None, *, geometry_sha256=None, source_scale=None,
|
|
coordinates=None, output_domain_sha256=None, processing_backend=None,
|
|
preprocessing=None, field=None) -> dict:
|
|
"""Build the closed v1 feature identity from effective values, excluding readout filters.
|
|
|
|
``field`` carries the closed profile identity for field models (shape, dtype,
|
|
unit, mask and lifecycle). It is omitted for scalar profiles so their
|
|
signatures stay byte-identical to previously exported files.
|
|
"""
|
|
camera = config.get("camera", {})
|
|
tracker = dict(config.get("tracker", {}))
|
|
raw_downsample = tracker.get("hhd_downsample", 1)
|
|
try:
|
|
downsample = max(1, int(raw_downsample)) if raw_downsample is not None else 1
|
|
except (TypeError, ValueError):
|
|
downsample = 1
|
|
tracker["hhd_downsample"] = downsample
|
|
if source_scale is not None:
|
|
source_scale = float(source_scale)
|
|
measurement = filters_fingerprint(config.get("filters", {}) if filters is None else filters).get("measurement", {})
|
|
# A missing EMA setting has the same effective meaning as the SDK default.
|
|
measurement.setdefault("flow_ema_alpha", 1.0)
|
|
result = {
|
|
"stitch_resolution": camera.get("resolution_stitch"),
|
|
"dis_mode": tracker.get("dis_mode"),
|
|
"hhd_downsample": downsample,
|
|
"filters": {"measurement": measurement},
|
|
}
|
|
if geometry_sha256 is not None:
|
|
result["geometry_sha256"] = geometry_sha256
|
|
if source_scale is not None:
|
|
result["source_scale"] = float(source_scale)
|
|
if coordinates is not None:
|
|
result["coordinates"] = coordinates
|
|
if output_domain_sha256 is not None:
|
|
result["output_domain_sha256"] = output_domain_sha256
|
|
effective = {
|
|
"feature_version": "orisys_hhd_raw/1",
|
|
"processing_backend": processing_backend or config.get("type", "compound_eye"),
|
|
"camera": {key: camera.get(key) for key in ("resolution_raw", "resolution_stitch")},
|
|
"tracker": tracker,
|
|
"contact": config.get("contact", {}),
|
|
"measurement": measurement,
|
|
"geometry_sha256": geometry_sha256,
|
|
"output_domain_sha256": output_domain_sha256,
|
|
"source_scale": source_scale,
|
|
"preprocessing": preprocessing or {},
|
|
}
|
|
if field is not None:
|
|
result["field"] = deepcopy(field)
|
|
effective["field"] = deepcopy(field)
|
|
result["feature_signature_sha256"] = _feature_signature(effective)
|
|
return CalibrationContext(result, effective)
|