scene_service.state¶
scene_service.state — object registry and data association.
All shared mutable state lives here behind a single asyncio.Lock owned
by ObjectRegistry. Geometric relations moved to
scene_graph/geometry.py (judgment) + scene_graph/geometric_loop.py
(runtime), which feed the Pilot-facing scene-graph store.
- class scene_service.state.BBox3D(size_x: float = 0.1, size_y: float = 0.1, size_z: float = 0.1, yaw: float = 0.0, frame_id: str = 'map')[source]¶
Bases:
objectAxis-aligned bounding box in frame_id, centered on the object pose.
- frame_id: str = 'map'¶
- property half_x: float¶
- property half_y: float¶
- property half_z: float¶
- size_x: float = 0.1¶
- size_y: float = 0.1¶
- size_z: float = 0.1¶
- yaw: float = 0.0¶
- class scene_service.state.Detection(cls: str, pose: Pose3D, bbox: BBox3D, confidence: float, source: str = 'perception')[source]¶
Bases:
objectOne per-frame perception output. Stable id is NOT supplied — it’s this layer’s job to assign / find one. pose is in map frame (ingest does the TF transform before producing Detection).
- cls: str¶
- confidence: float¶
- source: str = 'perception'¶
- class scene_service.state.ObjectRegistry(*, grace_period_s: float = 5.0)[source]¶
Bases:
objectAsync-safe object + surface store. All read/write paths go through with reg.lock(): …; readers take an atomic snapshot via await reg.snapshot() if they want to release the lock fast.
Stable id allocation is a per-class monotonic counter.
- all_objects() Iterable[SceneObject][source]¶
- all_surfaces() Iterable[SceneSurface][source]¶
- clear_objects() int[source]¶
Drop every object and surface. Caller MUST hold lock().
Used by the lifecycle linkage when the map frame epoch changes (mapping reset / re-init): every stored map-frame coordinate is no longer anchored, and this state is derived — re-observation rebuilds it in the new frame. Id counters are NOT reset, so ids stay unique across the flush (restored/old ids never collide with new ones). Returns the number of objects dropped.
- find_rebindable(cls: str, pose: Pose3D, max_d: float, *, only_missing: bool = False, exclude_oids: Iterable[str] | None = None) SceneObject | None[source]¶
Nearest non-robot, non-restored record of class cls whose centroid is within max_d of pose, else None.
With only_missing=True only soft-evicted (missing) records are considered — the cross-tick re-bind case, where a re-detected object reclaims the id it held before a cull. exclude_oids skips records already claimed this tick. Restored records are excluded (the perception layer re-binds those via its own warm-restore path). Caller must hold the lock.
- get_object(oid: str) SceneObject | None[source]¶
- insert_object(cls: str, pose: Pose3D, bbox: BBox3D, confidence: float, now: float, *, is_robot: bool = False, source: str = 'perception') SceneObject[source]¶
Allocate a new SceneObject. Caller must hold self._lock.
- insert_or_update_surface(pose: Pose3D, normal: tuple[float, float, float], extent_x: float, extent_y: float, now: float, *, merge_dist_m: float = 0.3) SceneSurface[source]¶
If an existing surface lives within merge_dist_m of pose AND has a near-parallel normal, update it; otherwise allocate a new one. Avoids spamming hundreds of nearly-identical planes when plane_extract sees the same table every frame.
Caller must hold the lock.
- mark_stale(now: float) int[source]¶
Set missing=True on objects past the grace period. Returns how many transitioned this tick (0 most of the time). Never deletes — Pilot may still ask about missing objects.
Caller must hold the lock.
- prune_expired(now: float, ttl_s: float) list[str][source]¶
Hard-delete missing perception records whose last_seen is older than ttl_s; returns the deleted object_ids. Bounds growth of soft-evicted records — dedup survivors’ stale twins and objects that truly left the scene. The robot self-record and warm-restored records (kept indefinitely until re-seen) are never pruned here. Caller must hold the lock.
- restore_object(obj: SceneObject) None[source]¶
Re-insert a persisted object verbatim under its existing object_id (warm restore at boot). Caller must hold self._lock.
Advances the per-class id counter past the restored numeric suffix so a later _alloc_id(cls) can never collide with — or reuse — a restored id. A new object of the same class therefore continues numbering after the highest restored one. Objects with an unparseable id (not the scene.object.<cls>_<NNN> shape) are still stored; only the counter bump is skipped for them.
The restored object is a remembered-but-unseen record: its persisted cg_uuid belonged to a now-dead perception process and is meaningless to the new MapObjectList, so it is dropped and the object is flagged restored. That flag tells the perception reconcile not to evict it on the uuid-membership rule until a live detection re-binds it by class+pose (see ConceptGraphsDetector._apply_snapshot).
- async snapshot() tuple[dict[str, SceneObject], dict[str, SceneSurface]][source]¶
Atomic shallow copy. Cheap because dataclasses are referenced, not copied — callers must NOT mutate returned values.
- soft_evict(obj: SceneObject) None[source]¶
Mark a perception record missing and release its concept-graphs uuid binding instead of deleting it, so a later re-detection can re-bind the same id (and accumulated observation_count) via find_rebindable. Bounded by prune_expired. Caller must hold the lock.
- update_object_pose(obj: SceneObject, new_pose: Pose3D, new_confidence: float, now: float, *, ema_pose: float = 0.3, ema_conf: float = 0.3) None[source]¶
EMA-blend new observation into the existing record. Caller must hold the lock. Yaw is averaged on the unit circle to avoid wrap-around; confidence is bounded to [0, 1].
- class scene_service.state.Pose3D(x: 'float', y: 'float', z: 'float', yaw: 'float' = 0.0, frame_id: 'str' = 'map')[source]¶
Bases:
object- frame_id: str = 'map'¶
- x: float¶
- y: float¶
- yaw: float = 0.0¶
- z: float¶
- class scene_service.state.SceneObject(object_id: str, cls: str, pose: Pose3D, bbox: BBox3D, confidence: float, first_seen: float, last_seen: float, observation_count: int = 1, missing: bool = False, attributes: dict[str, object]=<factory>)[source]¶
Bases:
objectStable object record. id format: scene.object.<cls>_<NNN>.
Pose is in map frame after ingest does the TF transform; never raw sensor frame. Confidence is an EMA over per-observation confidences; pose is updated by the data_assoc layer with EMA alpha=0.3 toward each new pose. last_seen is wall-clock unix seconds (Chronos TODO).
- attributes: dict[str, object]¶
- cls: str¶
- confidence: float¶
- first_seen: float¶
- last_seen: float¶
- missing: bool = False¶
- object_id: str¶
- observation_count: int = 1¶
- class scene_service.state.SceneSurface(surface_id: str, pose: Pose3D, normal: tuple[float, float, float], extent_x: float, extent_y: float, last_seen: float)[source]¶
Bases:
objectPlanar surface registered by geom/plane_extract. Same id-namespace rules as SceneObject (scene.surface.<NNN>); we expose surfaces in snapshots so on(cup, table) can resolve via plane lookups when no bounding-box “table” object is present.
- extent_x: float¶
- extent_y: float¶
- last_seen: float¶
- normal: tuple[float, float, float]¶
- surface_id: str¶
- scene_service.state.associate(registry: ObjectRegistry, detections: list[Detection], *, now: float | None = None) tuple[list[str], list[str]][source]¶
Resolve detections against the registry. Caller must hold registry.lock().
Returns (matched_ids, new_ids) for logging / metrics. The registry is mutated in place: matched detections EMA-update existing records, unmatched detections allocate new ones, unmatched objects are NOT touched (mark_stale runs separately on a periodic tick).
Modules
Associate per-frame detections with persistent scene objects. |
|
SceneObject registry — the canonical store for everything system/scene tracks about the world. |