scene_service.state.object_registry

SceneObject registry — the canonical store for everything system/scene tracks about the world. Pure-Python in-memory at runtime. A boot starts fresh; stable objects come back via restore_object() when the operator Loads a saved map in the map UI (or, in the legacy SCENE_RESTORE_ON_START mode, at boot) — see scene_service/persistence.py — instead of being re-accumulated through the min_observations filter from scratch.

Functions

now_unix()

Wall-clock unix seconds.

Classes

BBox3D([size_x, size_y, size_z, yaw, frame_id])

Axis-aligned bounding box in frame_id, centered on the object pose.

ObjectRegistry(*[, grace_period_s])

Async-safe object + surface store.

Pose3D(x, y, z[, yaw, frame_id])

SceneObject(object_id, cls, pose, bbox, ...)

Stable object record.

SceneSurface(surface_id, pose, normal, ...)

Planar surface registered by geom/plane_extract.

class scene_service.state.object_registry.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: object

Axis-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.object_registry.ObjectRegistry(*, grace_period_s: float = 5.0)[source]

Bases: object

Async-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.

lock() Lock[source]
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.

stats() dict[str, int][source]
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.object_registry.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.object_registry.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: object

Stable 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]
bbox: BBox3D
cls: str
confidence: float
first_seen: float
last_seen: float
missing: bool = False
object_id: str
observation_count: int = 1
pose: Pose3D
class scene_service.state.object_registry.SceneSurface(surface_id: str, pose: Pose3D, normal: tuple[float, float, float], extent_x: float, extent_y: float, last_seen: float)[source]

Bases: object

Planar 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]
pose: Pose3D
surface_id: str
scene_service.state.object_registry.now_unix() float[source]

Wall-clock unix seconds. TODO(chronos) replace with the unified time service once it lands.