feat(observatory): ship modular AI inference labs
This commit is contained in:
@@ -2,17 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from threading import Lock
|
||||
from typing import Any, Literal
|
||||
from uuid import UUID
|
||||
from uuid import UUID, uuid5
|
||||
|
||||
import rerun as rr
|
||||
import rerun_bindings as bindings
|
||||
from rerun import blueprint as rrb
|
||||
|
||||
from k1link.viewer.recorded_blueprint_lifecycle import (
|
||||
BlueprintSessionReleased,
|
||||
RecordedBlueprintSessions,
|
||||
)
|
||||
from k1link.viewer.rerun_bridge import RerunSceneSettings
|
||||
|
||||
APPLICATION_ID = "nodedc_mission_core_recorded"
|
||||
@@ -48,8 +51,9 @@ class _RecordedBlueprintStream:
|
||||
"""Keep one bounded SDK blueprint source for each browser viewport.
|
||||
|
||||
Upstream 0.36.3 activates a clone, not this source store. Explicit refresh
|
||||
makes layer changes visible but cannot retain the clone's operator eye.
|
||||
It is opt-in for portable replay pending native camera-state support.
|
||||
makes layer changes visible. Ordinary layer updates deliberately omit eye
|
||||
components so the active clone retains the operator's camera. Only an
|
||||
explicit 3D/plan/follow transition may write a new spatial eye preset.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -57,8 +61,10 @@ class _RecordedBlueprintStream:
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
*,
|
||||
blueprint_session_id: str,
|
||||
view_reset_generation: Literal[0, 1],
|
||||
) -> None:
|
||||
self.blueprint_session_id = blueprint_session_id
|
||||
self.view_reset_generation = view_reset_generation
|
||||
self._lock = Lock()
|
||||
self._sequence = 0
|
||||
@@ -81,7 +87,7 @@ class _RecordedBlueprintStream:
|
||||
|
||||
def render(
|
||||
self,
|
||||
blueprint_factory: Callable[[bool, bool], rrb.Blueprint],
|
||||
blueprint_factory: Callable[[bool, bool, str], rrb.Blueprint],
|
||||
*,
|
||||
follow_trajectory: bool,
|
||||
plan_view: bool,
|
||||
@@ -94,10 +100,20 @@ class _RecordedBlueprintStream:
|
||||
update_eye_controls = self._eye_contract != eye_contract
|
||||
# Initial admission/reset uses native framing. Explicit presets
|
||||
# apply to mode transitions, after the viewer has a source cursor.
|
||||
blueprint = blueprint_factory(update_eye_controls, self._eye_contract is not None)
|
||||
# Keep one view identity for the lifetime of this browser owner.
|
||||
# Replacing the UUID on every layer toggle rebuilt both native
|
||||
# viewports, delayed a simple visibility change, and discarded the
|
||||
# operator's active interaction state. The explicit reset
|
||||
# generation already owns the one intentional identity change.
|
||||
view_instance_token = self.blueprint_session_id
|
||||
blueprint = blueprint_factory(
|
||||
update_eye_controls,
|
||||
self._eye_contract is not None,
|
||||
view_instance_token,
|
||||
)
|
||||
# Appending rows alone does not refresh upstream's active clone.
|
||||
# Keep legacy admission unchanged; portable replay opts into
|
||||
# working layer updates with an explicitly documented eye reset.
|
||||
# Reactivation makes layer changes visible; eye components remain
|
||||
# absent unless the explicit view-mode contract changed above.
|
||||
make_active = self._sequence == 0 or reactivate_updates
|
||||
self._blueprint_recording.set_time(
|
||||
"blueprint",
|
||||
@@ -127,40 +143,7 @@ class _RecordedBlueprintStream:
|
||||
self._transport_recording.disconnect()
|
||||
|
||||
|
||||
_MAX_RECORDED_BLUEPRINT_STREAMS = 32
|
||||
_recorded_blueprint_streams_lock = Lock()
|
||||
_recorded_blueprint_streams: OrderedDict[tuple[str, str, str], _RecordedBlueprintStream] = (
|
||||
OrderedDict()
|
||||
)
|
||||
|
||||
|
||||
def _stable_recorded_blueprint_stream(
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
blueprint_session_id: str,
|
||||
view_reset_generation: Literal[0, 1],
|
||||
) -> _RecordedBlueprintStream:
|
||||
key = (application_id, recording_id, blueprint_session_id)
|
||||
with _recorded_blueprint_streams_lock:
|
||||
stream = _recorded_blueprint_streams.get(key)
|
||||
if stream is not None and stream.view_reset_generation != view_reset_generation:
|
||||
stream.close()
|
||||
del _recorded_blueprint_streams[key]
|
||||
stream = None
|
||||
if stream is None:
|
||||
stream = _RecordedBlueprintStream(
|
||||
application_id,
|
||||
recording_id,
|
||||
view_reset_generation=view_reset_generation,
|
||||
)
|
||||
_recorded_blueprint_streams[key] = stream
|
||||
else:
|
||||
_recorded_blueprint_streams.move_to_end(key)
|
||||
while len(_recorded_blueprint_streams) > _MAX_RECORDED_BLUEPRINT_STREAMS:
|
||||
_, stale = _recorded_blueprint_streams.popitem(last=False)
|
||||
stale.close()
|
||||
return stream
|
||||
recorded_blueprint_sessions = RecordedBlueprintSessions[_RecordedBlueprintStream]()
|
||||
|
||||
|
||||
def recorded_blueprint(
|
||||
@@ -170,15 +153,21 @@ def recorded_blueprint(
|
||||
active_view: RecordedView = "spatial",
|
||||
view_reset_generation: Literal[0, 1] = 0,
|
||||
unified_perception: bool = False,
|
||||
unified_camera_share: float = 0.46,
|
||||
semantic_layer: Literal["city", "vegetation"] | None = None,
|
||||
plan_view: bool = False,
|
||||
show_detections_2d: bool = False,
|
||||
show_camera_image: bool = True,
|
||||
show_segmentation: bool = False,
|
||||
show_cuboids_3d: bool = False,
|
||||
show_costmap: bool = False,
|
||||
follow_trajectory: bool = False,
|
||||
update_eye_controls: bool = True,
|
||||
explicit_spatial_preset: bool = False,
|
||||
eye_position: tuple[float, float, float] | None = None,
|
||||
eye_look_target: tuple[float, float, float] | None = None,
|
||||
eye_up: tuple[float, float, float] | None = None,
|
||||
view_instance_token: str | None = None,
|
||||
) -> rrb.Blueprint:
|
||||
accumulation = max(0.0, settings.accumulation_seconds)
|
||||
time_ranges: list[rr.VisibleTimeRange] | None = None
|
||||
@@ -218,6 +207,14 @@ def recorded_blueprint(
|
||||
)
|
||||
spatial_eye_controls = (
|
||||
rrb.EyeControls3D.from_fields(
|
||||
kind=rrb.Eye3DKind.Orbital,
|
||||
position=eye_position,
|
||||
look_target=eye_look_target,
|
||||
eye_up=eye_up,
|
||||
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
|
||||
)
|
||||
if eye_position is not None and eye_look_target is not None and eye_up is not None
|
||||
else rrb.EyeControls3D.from_fields(
|
||||
kind=rrb.Eye3DKind.Orbital,
|
||||
position=[0.0, 0.0, 30.0],
|
||||
look_target=[0.0, 0.0, 0.0],
|
||||
@@ -267,15 +264,21 @@ def recorded_blueprint(
|
||||
# alone does not preserve edits in upstream's activated blueprint clone.
|
||||
eye_controls=spatial_eye_controls,
|
||||
)
|
||||
spatial_view.id = (
|
||||
RECORDED_SPATIAL_RESET_VIEW_ID if view_reset_generation else RECORDED_SPATIAL_VIEW_ID
|
||||
|
||||
def instance_id(primary: UUID, reset: UUID) -> UUID:
|
||||
base = reset if view_reset_generation else primary
|
||||
return uuid5(base, view_instance_token) if view_instance_token is not None else base
|
||||
|
||||
spatial_view.id = instance_id(
|
||||
RECORDED_SPATIAL_VIEW_ID,
|
||||
RECORDED_SPATIAL_RESET_VIEW_ID,
|
||||
)
|
||||
camera_view = rrb.Spatial2DView(
|
||||
origin="/perception/camera",
|
||||
name="Оригинальное видео · слои AI",
|
||||
background=[7, 8, 10, 255],
|
||||
overrides={
|
||||
"/perception/camera/image": rrb.EntityBehavior(visible=True),
|
||||
"/perception/camera/image": rrb.EntityBehavior(visible=show_camera_image),
|
||||
"/perception/camera/detections": rrb.EntityBehavior(
|
||||
visible=show_detections_2d,
|
||||
),
|
||||
@@ -290,8 +293,9 @@ def recorded_blueprint(
|
||||
),
|
||||
},
|
||||
)
|
||||
camera_view.id = (
|
||||
RECORDED_CAMERA_RESET_VIEW_ID if view_reset_generation else RECORDED_CAMERA_VIEW_ID
|
||||
camera_view.id = instance_id(
|
||||
RECORDED_CAMERA_VIEW_ID,
|
||||
RECORDED_CAMERA_RESET_VIEW_ID,
|
||||
)
|
||||
perception_3d_view = rrb.Spatial3DView(
|
||||
# Cuboids are expressed in the same calibrated world frame as the
|
||||
@@ -325,10 +329,9 @@ def recorded_blueprint(
|
||||
},
|
||||
eye_controls=spatial_eye_controls,
|
||||
)
|
||||
perception_3d_view.id = (
|
||||
RECORDED_PERCEPTION_3D_RESET_VIEW_ID
|
||||
if view_reset_generation
|
||||
else RECORDED_PERCEPTION_3D_VIEW_ID
|
||||
perception_3d_view.id = instance_id(
|
||||
RECORDED_PERCEPTION_3D_VIEW_ID,
|
||||
RECORDED_PERCEPTION_3D_RESET_VIEW_ID,
|
||||
)
|
||||
metrics_view = rrb.TimeSeriesView(
|
||||
origin="/metrics/device",
|
||||
@@ -357,16 +360,16 @@ def recorded_blueprint(
|
||||
spatial_view.visualizer_overrides["/world/perception/boxes3d"] = [
|
||||
rrb.EntityBehavior(visible=show_cuboids_3d),
|
||||
]
|
||||
camera_share = min(0.9, max(0.1, unified_camera_share))
|
||||
root_container = rrb.Horizontal(
|
||||
camera_view,
|
||||
spatial_view,
|
||||
column_shares=[0.46, 0.54],
|
||||
column_shares=[camera_share, 1.0 - camera_share],
|
||||
name="Единая сцена восприятия",
|
||||
)
|
||||
root_container.id = (
|
||||
RECORDED_UNIFIED_RESET_ROOT_CONTAINER_ID
|
||||
if view_reset_generation
|
||||
else RECORDED_UNIFIED_ROOT_CONTAINER_ID
|
||||
root_container.id = instance_id(
|
||||
RECORDED_UNIFIED_ROOT_CONTAINER_ID,
|
||||
RECORDED_UNIFIED_RESET_ROOT_CONTAINER_ID,
|
||||
)
|
||||
else:
|
||||
# Keep operator video and 3D cuboids as direct root views. Rerun's nested
|
||||
@@ -384,7 +387,7 @@ def recorded_blueprint(
|
||||
# replace it from a later blueprint message. Give every operator mode (and
|
||||
# its explicit reset generation) a stable root identity so the requested
|
||||
# child is authoritative instead of inheriting a previously visited tab.
|
||||
root_container.id = {
|
||||
base_root_id = {
|
||||
("spatial", 0): RECORDED_ROOT_CONTAINER_ID,
|
||||
("spatial", 1): RECORDED_SPATIAL_RESET_ROOT_CONTAINER_ID,
|
||||
("perception", 0): RECORDED_PERCEPTION_ROOT_CONTAINER_ID,
|
||||
@@ -394,6 +397,11 @@ def recorded_blueprint(
|
||||
("metrics", 0): RECORDED_METRICS_ROOT_CONTAINER_ID,
|
||||
("metrics", 1): RECORDED_METRICS_RESET_ROOT_CONTAINER_ID,
|
||||
}[(active_view, view_reset_generation)]
|
||||
root_container.id = (
|
||||
uuid5(base_root_id, view_instance_token)
|
||||
if view_instance_token is not None
|
||||
else base_root_id
|
||||
)
|
||||
|
||||
if include_initial_playback_state:
|
||||
return rrb.Blueprint(
|
||||
@@ -424,19 +432,26 @@ def recorded_blueprint_rrd(
|
||||
active_view: RecordedView = "spatial",
|
||||
view_reset_generation: Literal[0, 1] = 0,
|
||||
unified_perception: bool = False,
|
||||
unified_camera_share: float = 0.46,
|
||||
semantic_layer: Literal["city", "vegetation"] | None = None,
|
||||
plan_view: bool = False,
|
||||
show_detections_2d: bool = False,
|
||||
show_camera_image: bool = True,
|
||||
show_segmentation: bool = False,
|
||||
show_cuboids_3d: bool = False,
|
||||
show_costmap: bool = False,
|
||||
follow_trajectory: bool = False,
|
||||
reactivate_updates: bool = False,
|
||||
eye_position: tuple[float, float, float] | None = None,
|
||||
eye_look_target: tuple[float, float, float] | None = None,
|
||||
eye_up: tuple[float, float, float] | None = None,
|
||||
) -> bytes:
|
||||
"""Serialize a bounded active blueprint update without recorded data."""
|
||||
|
||||
def build_blueprint(
|
||||
update_eye_controls: bool, use_spatial_preset: bool = False
|
||||
update_eye_controls: bool,
|
||||
use_spatial_preset: bool = False,
|
||||
view_instance_token: str | None = None,
|
||||
) -> rrb.Blueprint:
|
||||
return recorded_blueprint(
|
||||
settings,
|
||||
@@ -444,32 +459,43 @@ def recorded_blueprint_rrd(
|
||||
active_view=active_view,
|
||||
view_reset_generation=view_reset_generation,
|
||||
unified_perception=unified_perception,
|
||||
unified_camera_share=unified_camera_share,
|
||||
semantic_layer=semantic_layer,
|
||||
plan_view=plan_view,
|
||||
show_detections_2d=show_detections_2d,
|
||||
show_camera_image=show_camera_image,
|
||||
show_segmentation=show_segmentation,
|
||||
show_cuboids_3d=show_cuboids_3d,
|
||||
show_costmap=show_costmap,
|
||||
follow_trajectory=follow_trajectory,
|
||||
update_eye_controls=update_eye_controls,
|
||||
explicit_spatial_preset=reactivate_updates and use_spatial_preset,
|
||||
explicit_spatial_preset=use_spatial_preset and update_eye_controls,
|
||||
eye_position=eye_position,
|
||||
eye_look_target=eye_look_target,
|
||||
eye_up=eye_up,
|
||||
view_instance_token=view_instance_token,
|
||||
)
|
||||
|
||||
payload: bytes | None
|
||||
if blueprint_session_id is not None:
|
||||
try:
|
||||
payload = _stable_recorded_blueprint_stream(
|
||||
application_id=application_id,
|
||||
recording_id=recording_id,
|
||||
blueprint_session_id=blueprint_session_id,
|
||||
view_reset_generation=view_reset_generation,
|
||||
).render(
|
||||
build_blueprint,
|
||||
follow_trajectory=follow_trajectory,
|
||||
plan_view=plan_view,
|
||||
reactivate_updates=reactivate_updates,
|
||||
payload = recorded_blueprint_sessions.use(
|
||||
(application_id, recording_id, blueprint_session_id),
|
||||
view_reset_generation,
|
||||
lambda: _RecordedBlueprintStream(
|
||||
application_id,
|
||||
recording_id,
|
||||
blueprint_session_id=blueprint_session_id,
|
||||
view_reset_generation=view_reset_generation,
|
||||
),
|
||||
lambda stream: stream.render(
|
||||
build_blueprint,
|
||||
follow_trajectory=follow_trajectory,
|
||||
plan_view=plan_view,
|
||||
reactivate_updates=reactivate_updates,
|
||||
),
|
||||
)
|
||||
except RecordedBlueprintError:
|
||||
except (RecordedBlueprintError, BlueprintSessionReleased):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise RecordedBlueprintError("failed to serialize stable recorded blueprint") from exc
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Ephemeral viewport resources: renew while mounted, release at termination."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from threading import RLock
|
||||
from typing import Protocol
|
||||
|
||||
BlueprintKey = tuple[str, str, str]
|
||||
|
||||
|
||||
class Closable(Protocol):
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
class BlueprintSessionReleased(RuntimeError):
|
||||
"""A late update cannot resurrect an explicitly released viewport."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Entry[Resource: Closable]:
|
||||
resource: Resource
|
||||
generation: int
|
||||
last_used: float
|
||||
|
||||
|
||||
class RecordedBlueprintSessions[Resource: Closable]:
|
||||
"""No TTL applies while a render is running; idle owners renew separately.
|
||||
|
||||
The small registry lock also serializes release with render. Thus the
|
||||
release acknowledgement means native resources have actually been closed,
|
||||
and a previously queued update is fenced by a lightweight tombstone.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
ttl_seconds: float = 300.0,
|
||||
max_entries: int = 32,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
) -> None:
|
||||
self._ttl = ttl_seconds
|
||||
self._max_entries = max_entries
|
||||
self._clock = clock
|
||||
self._lock = RLock()
|
||||
self._entries: OrderedDict[BlueprintKey, _Entry[Resource]] = OrderedDict()
|
||||
self._released: OrderedDict[BlueprintKey, float] = OrderedDict()
|
||||
|
||||
def _expire(self, now: float) -> None:
|
||||
for key, entry in list(self._entries.items()):
|
||||
if now - entry.last_used >= self._ttl:
|
||||
del self._entries[key]
|
||||
entry.resource.close()
|
||||
for key, expires in list(self._released.items()):
|
||||
if expires <= now:
|
||||
del self._released[key]
|
||||
|
||||
def use[Result](
|
||||
self,
|
||||
key: BlueprintKey,
|
||||
generation: int,
|
||||
create: Callable[[], Resource],
|
||||
render: Callable[[Resource], Result],
|
||||
) -> Result:
|
||||
with self._lock:
|
||||
now = self._clock()
|
||||
self._expire(now)
|
||||
if key in self._released:
|
||||
raise BlueprintSessionReleased("recorded viewport has been released")
|
||||
entry = self._entries.get(key)
|
||||
if entry is not None and entry.generation != generation:
|
||||
del self._entries[key]
|
||||
entry.resource.close()
|
||||
entry = None
|
||||
if entry is None:
|
||||
entry = _Entry(create(), generation, now)
|
||||
self._entries[key] = entry
|
||||
self._entries.move_to_end(key)
|
||||
while len(self._entries) > self._max_entries:
|
||||
_, stale = self._entries.popitem(last=False)
|
||||
stale.resource.close()
|
||||
try:
|
||||
return render(entry.resource)
|
||||
except BaseException:
|
||||
# A failed operation must not retain its partial native store.
|
||||
del self._entries[key]
|
||||
entry.resource.close()
|
||||
raise
|
||||
finally:
|
||||
entry.last_used = self._clock()
|
||||
|
||||
def renew(self, key: BlueprintKey) -> bool:
|
||||
with self._lock:
|
||||
now = self._clock()
|
||||
self._expire(now)
|
||||
entry = self._entries.get(key)
|
||||
if entry is None:
|
||||
return False
|
||||
entry.last_used = now
|
||||
self._entries.move_to_end(key)
|
||||
return True
|
||||
|
||||
def release(self, key: BlueprintKey) -> None:
|
||||
with self._lock:
|
||||
now = self._clock()
|
||||
self._expire(now)
|
||||
entry = self._entries.pop(key, None)
|
||||
self._released[key] = now + self._ttl
|
||||
self._released.move_to_end(key)
|
||||
# Tombstones contain identities only, never SDK resources or data.
|
||||
while len(self._released) > 4096:
|
||||
self._released.popitem(last=False)
|
||||
if entry is not None:
|
||||
entry.resource.close()
|
||||
|
||||
def expire(self) -> None:
|
||||
with self._lock:
|
||||
self._expire(self._clock())
|
||||
|
||||
def close(self) -> None:
|
||||
with self._lock:
|
||||
entries = list(self._entries.values())
|
||||
self._entries.clear()
|
||||
self._released.clear()
|
||||
for entry in entries:
|
||||
entry.resource.close()
|
||||
@@ -0,0 +1,187 @@
|
||||
"""Scene-derived orbital camera bounds for recorded Rerun views."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pyarrow.compute as pc
|
||||
import rerun_bindings as bindings
|
||||
|
||||
SESSION_TIMELINE = "session_time"
|
||||
MIN_ORBIT_DISTANCE = 0.02
|
||||
MAX_ORBITAL_ZOOM_OUT_FACTOR = 5.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _SpatialBoundsSeries:
|
||||
times_ns: np.ndarray
|
||||
lower: np.ndarray
|
||||
upper: np.ndarray
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _recorded_spatial_bounds_index(
|
||||
path_text: str,
|
||||
byte_length: int,
|
||||
modified_ns: int,
|
||||
) -> dict[str, _SpatialBoundsSeries]:
|
||||
"""Build a small temporal bounds index for one immutable RRD generation.
|
||||
|
||||
Layer and follow buttons only change a blueprint. Re-decoding the complete
|
||||
source RRD for every click made those controls wait on archive I/O. The
|
||||
cache key includes the file generation fingerprint; cached values contain
|
||||
only timestamps and six floats per sample, never point-cloud payloads.
|
||||
"""
|
||||
|
||||
del byte_length, modified_ns # Generation identity is carried by the cache key.
|
||||
reader = bindings.RrdReaderInternal(path_text)
|
||||
recording = next(
|
||||
(entry for entry in reader.store_entries() if entry.kind == "recording"),
|
||||
None,
|
||||
)
|
||||
if recording is None:
|
||||
raise ValueError("recorded camera source has no recording store")
|
||||
|
||||
rows: dict[str, list[tuple[int, np.ndarray, np.ndarray]]] = {
|
||||
"/world/points": [],
|
||||
"/world/trajectory": [],
|
||||
}
|
||||
for chunk in reader.stream(recording):
|
||||
entity_path = str(chunk.entity_path)
|
||||
if entity_path not in rows:
|
||||
continue
|
||||
batch = chunk.to_record_batch()
|
||||
names = batch.column_names
|
||||
if SESSION_TIMELINE not in names:
|
||||
continue
|
||||
component = (
|
||||
"Points3D:positions" if entity_path == "/world/points" else "LineStrips3D:strips"
|
||||
)
|
||||
if component not in names:
|
||||
continue
|
||||
times = np.asarray(
|
||||
batch.column(names.index(SESSION_TIMELINE)).cast(pa.int64()),
|
||||
dtype=np.int64,
|
||||
)
|
||||
spatial = batch.column(names.index(component))
|
||||
for index, timestamp in enumerate(times):
|
||||
values = _spatial_values(
|
||||
spatial.slice(index, 1),
|
||||
nested=entity_path == "/world/trajectory",
|
||||
)
|
||||
if not values.size:
|
||||
continue
|
||||
finite = values[np.isfinite(values).all(axis=1)]
|
||||
if not finite.size:
|
||||
continue
|
||||
rows[entity_path].append(
|
||||
(
|
||||
int(timestamp),
|
||||
np.min(finite, axis=0),
|
||||
np.max(finite, axis=0),
|
||||
)
|
||||
)
|
||||
|
||||
result: dict[str, _SpatialBoundsSeries] = {}
|
||||
for entity_path, samples in rows.items():
|
||||
if not samples:
|
||||
continue
|
||||
samples.sort(key=lambda sample: sample[0])
|
||||
result[entity_path] = _SpatialBoundsSeries(
|
||||
times_ns=np.asarray([sample[0] for sample in samples], dtype=np.int64),
|
||||
lower=np.asarray([sample[1] for sample in samples], dtype=np.float32),
|
||||
upper=np.asarray([sample[2] for sample in samples], dtype=np.float32),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def recorded_orbital_radius_limit(
|
||||
recording_path: Path,
|
||||
*,
|
||||
current_time_ns: int,
|
||||
accumulation_seconds: float,
|
||||
show_points: bool,
|
||||
show_trajectory: bool,
|
||||
) -> float | None:
|
||||
"""Match Rerun 0.36.3's current scene-diagonal zoom-out limit.
|
||||
|
||||
The LAB 3D view roots its native mapping data at ``/world``. Points and
|
||||
trajectory are the source entities whose visible-time query changes with
|
||||
the operator's accumulation setting. Derived layers are deliberately not
|
||||
folded into this source contract: they use the same calibrated world frame
|
||||
and do not own navigation.
|
||||
"""
|
||||
|
||||
if current_time_ns < 0 or not math.isfinite(accumulation_seconds) or accumulation_seconds < 0:
|
||||
raise ValueError("invalid recorded camera query")
|
||||
wanted = {
|
||||
*(("/world/points",) if show_points else ()),
|
||||
*(("/world/trajectory",) if show_trajectory else ()),
|
||||
}
|
||||
if not wanted:
|
||||
return None
|
||||
path = recording_path.expanduser().absolute()
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("recorded camera source is unavailable")
|
||||
|
||||
stat = path.stat()
|
||||
series_by_entity = _recorded_spatial_bounds_index(
|
||||
str(path),
|
||||
stat.st_size,
|
||||
stat.st_mtime_ns,
|
||||
)
|
||||
|
||||
lower_ns = current_time_ns - round(accumulation_seconds * 1_000_000_000)
|
||||
lower = np.array([np.inf, np.inf, np.inf], dtype=np.float32)
|
||||
upper = np.array([-np.inf, -np.inf, -np.inf], dtype=np.float32)
|
||||
found = False
|
||||
for entity_path in wanted:
|
||||
series = series_by_entity.get(entity_path)
|
||||
if series is None:
|
||||
continue
|
||||
if accumulation_seconds > 0:
|
||||
selected = (series.times_ns >= lower_ns) & (series.times_ns <= current_time_ns)
|
||||
else:
|
||||
eligible_end = int(np.searchsorted(series.times_ns, current_time_ns, side="right"))
|
||||
if eligible_end == 0:
|
||||
continue
|
||||
latest_time = series.times_ns[eligible_end - 1]
|
||||
selected = series.times_ns == latest_time
|
||||
if not selected.any():
|
||||
continue
|
||||
lower = np.minimum(lower, np.min(series.lower[selected], axis=0))
|
||||
upper = np.maximum(upper, np.max(series.upper[selected], axis=0))
|
||||
found = True
|
||||
if not found:
|
||||
return None
|
||||
|
||||
# macaw::BoundingBox and Rerun's eye controller operate in f32.
|
||||
diagonal = np.float32(np.linalg.norm((upper - lower).astype(np.float32)))
|
||||
if not np.isfinite(diagonal) or diagonal <= 0:
|
||||
return None
|
||||
return float(
|
||||
max(
|
||||
np.float32(MIN_ORBIT_DISTANCE),
|
||||
np.float32(diagonal * np.float32(MAX_ORBITAL_ZOOM_OUT_FACTOR)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _spatial_values(column: pa.Array, *, nested: bool) -> np.ndarray:
|
||||
flattened = pc.list_flatten(column)
|
||||
if nested:
|
||||
flattened = pc.list_flatten(flattened)
|
||||
if not pa.types.is_fixed_size_list(flattened.type) or flattened.type.list_size != 3:
|
||||
raise ValueError("recorded spatial component is not a 3D vector")
|
||||
# Flatten through Arrow rather than reading ``FixedSizeListArray.values``:
|
||||
# the latter exposes the complete backing buffer and ignores a sliced
|
||||
# array's logical offset.
|
||||
values = pc.list_flatten(flattened).to_numpy(zero_copy_only=False)
|
||||
if values.size == 0:
|
||||
return np.empty((0, 3), dtype=np.float32)
|
||||
return np.asarray(values, dtype=np.float32).reshape((-1, 3))
|
||||
Reference in New Issue
Block a user