feat(perception): add diagnostic semantic SLAM replay
This commit is contained in:
@@ -83,6 +83,22 @@ class GeometryFrame:
|
||||
return int(self.points_map.shape[0])
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordedFrameTemporalBinding:
|
||||
"""Digest-bound recorded timing evidence for one camera-indexed increment.
|
||||
|
||||
The shared session time binds the camera ordinal to the E10 pack entry. The
|
||||
LiDAR and pose deltas retain their admitted E6 meaning: nearest host-arrival
|
||||
best effort, not hardware synchronization.
|
||||
"""
|
||||
|
||||
frame_index: int
|
||||
source_time_ns: int
|
||||
source_available: bool
|
||||
lidar_camera_delta_ms: float | None
|
||||
pose_point_delta_ms: float | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReplayBodyFrameInputs:
|
||||
"""Verified inputs required to derive one replay-only virtual body frame."""
|
||||
@@ -222,13 +238,29 @@ class RecordedGeometryStore:
|
||||
or pose_reference.frame_index != envelope.sequence
|
||||
):
|
||||
raise GeometryProviderError("packet geometry references are not source-bound")
|
||||
frame_index = envelope.sequence
|
||||
frame = self.frame_for_index(envelope.sequence)
|
||||
if frame is None:
|
||||
raise GeometryProviderError("packet claims unavailable source geometry as current")
|
||||
return frame
|
||||
|
||||
def frame_for_index(self, frame_index: int) -> GeometryFrame | None:
|
||||
"""Expose one verified source increment with its pose and KB4 calibration.
|
||||
|
||||
This read-only seam is intentionally narrower than the source archive. It
|
||||
exists for deterministic replay diagnostics which must project the exact
|
||||
frame-local point index space without manufacturing a ``SourcePacket``.
|
||||
An unavailable recorded increment remains ``None``; surface validity is
|
||||
retained on the returned frame rather than silently filtering its points.
|
||||
"""
|
||||
|
||||
if not isinstance(frame_index, int) or isinstance(frame_index, bool):
|
||||
raise GeometryProviderError("replay geometry frame index is invalid")
|
||||
if not 0 <= frame_index < self.profile.frame_count:
|
||||
raise GeometryProviderError("packet geometry frame index is outside the profile")
|
||||
raise GeometryProviderError("replay geometry frame is outside the profile")
|
||||
if int(self._source["frame_indices"][frame_index]) != frame_index:
|
||||
raise GeometryProviderError("source pack frame sequence changed")
|
||||
if not bool(self._source["sample_available"][frame_index]):
|
||||
raise GeometryProviderError("packet claims unavailable source geometry as current")
|
||||
return None
|
||||
offsets = self._source["cloud_offsets"]
|
||||
start, end = int(offsets[frame_index]), int(offsets[frame_index + 1])
|
||||
return GeometryFrame(
|
||||
@@ -247,6 +279,42 @@ class RecordedGeometryStore:
|
||||
surface_valid=bool(self._surface["frame_valid"][frame_index]),
|
||||
)
|
||||
|
||||
def temporal_binding_for_index(self, frame_index: int) -> RecordedFrameTemporalBinding:
|
||||
"""Return the sealed ordinal/session binding and admitted best-effort deltas."""
|
||||
|
||||
if not isinstance(frame_index, int) or isinstance(frame_index, bool):
|
||||
raise GeometryProviderError("replay temporal frame index is invalid")
|
||||
if not 0 <= frame_index < self.profile.frame_count:
|
||||
raise GeometryProviderError("replay temporal frame is outside the profile")
|
||||
if (
|
||||
int(self._source["frame_indices"][frame_index]) != frame_index
|
||||
or int(self._source["source_frame_indices"][frame_index]) != frame_index
|
||||
):
|
||||
raise GeometryProviderError("source pack temporal sequence changed")
|
||||
session_seconds = float(self._source["session_seconds"][frame_index])
|
||||
if not math.isfinite(session_seconds) or session_seconds < 0.0:
|
||||
raise GeometryProviderError("source pack session time is invalid")
|
||||
source_available = bool(self._source["sample_available"][frame_index])
|
||||
lidar_delta = float(self._source["lidar_camera_delta_ms"][frame_index])
|
||||
pose_delta = float(self._source["pose_point_delta_ms"][frame_index])
|
||||
if source_available:
|
||||
if not math.isfinite(lidar_delta) or not math.isfinite(pose_delta):
|
||||
raise GeometryProviderError("available source temporal deltas are invalid")
|
||||
lidar_value: float | None = lidar_delta
|
||||
pose_value: float | None = pose_delta
|
||||
else:
|
||||
if not math.isnan(lidar_delta) or not math.isnan(pose_delta):
|
||||
raise GeometryProviderError("unavailable source carries temporal deltas")
|
||||
lidar_value = None
|
||||
pose_value = None
|
||||
return RecordedFrameTemporalBinding(
|
||||
frame_index=frame_index,
|
||||
source_time_ns=round(session_seconds * 1_000_000_000),
|
||||
source_available=source_available,
|
||||
lidar_camera_delta_ms=lidar_value,
|
||||
pose_point_delta_ms=pose_value,
|
||||
)
|
||||
|
||||
def current_points(self, packet: SourcePacket) -> FloatArray | None:
|
||||
"""Expose the verified frame-local point index space to temporal occupancy."""
|
||||
|
||||
@@ -393,6 +461,8 @@ class RecordedGeometryStore:
|
||||
def _validate(self) -> None:
|
||||
source_required = {
|
||||
"frame_indices",
|
||||
"source_frame_indices",
|
||||
"session_seconds",
|
||||
"sample_available",
|
||||
"cloud_offsets",
|
||||
"cloud_points_map",
|
||||
@@ -401,6 +471,8 @@ class RecordedGeometryStore:
|
||||
"intrinsic_fx_fy_cx_cy",
|
||||
"distortion_kb4",
|
||||
"t_camera_from_lidar",
|
||||
"lidar_camera_delta_ms",
|
||||
"pose_point_delta_ms",
|
||||
}
|
||||
surface_required = {"frame_valid", "point_class"}
|
||||
if not source_required.issubset(self._source):
|
||||
@@ -411,6 +483,8 @@ class RecordedGeometryStore:
|
||||
points = self.profile.point_count
|
||||
shapes = {
|
||||
"frame_indices": (frames,),
|
||||
"source_frame_indices": (frames,),
|
||||
"session_seconds": (frames,),
|
||||
"sample_available": (frames,),
|
||||
"cloud_offsets": (frames + 1,),
|
||||
"cloud_points_map": (points, 3),
|
||||
@@ -419,6 +493,8 @@ class RecordedGeometryStore:
|
||||
"intrinsic_fx_fy_cx_cy": (4,),
|
||||
"distortion_kb4": (4,),
|
||||
"t_camera_from_lidar": (4, 4),
|
||||
"lidar_camera_delta_ms": (frames,),
|
||||
"pose_point_delta_ms": (frames,),
|
||||
}
|
||||
if any(self._source[name].shape != shape for name, shape in shapes.items()):
|
||||
raise GeometryProviderError("source pack array shapes changed")
|
||||
@@ -428,6 +504,26 @@ class RecordedGeometryStore:
|
||||
raise GeometryProviderError("local surface point shape changed")
|
||||
if int(self._source["cloud_offsets"][-1]) != points:
|
||||
raise GeometryProviderError("source point offsets do not close")
|
||||
expected_indices = np.arange(frames, dtype=np.int64)
|
||||
session_seconds = np.asarray(self._source["session_seconds"], dtype=np.float64)
|
||||
if (
|
||||
not np.array_equal(self._source["frame_indices"], expected_indices)
|
||||
or not np.array_equal(self._source["source_frame_indices"], expected_indices)
|
||||
or not np.isfinite(session_seconds).all()
|
||||
or np.any(session_seconds < 0.0)
|
||||
or np.any(np.diff(session_seconds) <= 0.0)
|
||||
):
|
||||
raise GeometryProviderError("source temporal index changed")
|
||||
available = np.asarray(self._source["sample_available"], dtype=np.bool_)
|
||||
lidar_deltas = np.asarray(self._source["lidar_camera_delta_ms"], dtype=np.float64)
|
||||
pose_deltas = np.asarray(self._source["pose_point_delta_ms"], dtype=np.float64)
|
||||
if (
|
||||
not np.isfinite(lidar_deltas[available]).all()
|
||||
or not np.isfinite(pose_deltas[available]).all()
|
||||
or not np.isnan(lidar_deltas[~available]).all()
|
||||
or not np.isnan(pose_deltas[~available]).all()
|
||||
):
|
||||
raise GeometryProviderError("source temporal delta availability changed")
|
||||
if (
|
||||
int(np.count_nonzero(self._source["sample_available"]))
|
||||
!= self.profile.valid_frame_count
|
||||
@@ -1030,6 +1126,7 @@ __all__ = [
|
||||
"GeometryProviderError",
|
||||
"GeometryProviderSnapshot",
|
||||
"Ravnoves00GeometryAssociationProvider",
|
||||
"RecordedFrameTemporalBinding",
|
||||
"RecordedGeometryStore",
|
||||
"load_geometry_profile",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user