Skip to content

Commit 61eecda

Browse files
kongchen1992meta-codesync[bot]
authored andcommitted
{BugFix} Visualization - Show optional viewer panels only when recorded
Summary: Explanation: `aria_rerun_viewer` always rendered the Neural Band tabs (EMG, EMG volts, accel, gyro) and the GPS map view for Gen2 recordings, even for files that contain neither sensor. The panels stayed empty for the whole session while still consuming layout space. Two causes, both in `tools/aria_rerun_viewer/aria_rerun_viewer.py`: - `enable_neural_band_batch` was derived from `get_stream_id_from_label("emg")`, which is not a presence check. For Gen2 the label mapper answers from a mostly-static device-model table, so it returns an id for every label the glasses *could* record (`emg` -> 241-1, `gps` -> 281-2, ...) whether or not the file has that stream. The condition was therefore true for every Gen2 recording. - `enable_gps` was hardcoded to `True`. Adds `get_recorded_stream_id()`, which resolves a label only when the recording really carries that sensor: the label must map to a stream id, the id must appear in `get_all_streams()`, and the stream must hold at least one data record. The last check also covers streams that are declared in the file but never written to. Both optional panels are now driven by it, and the Neural Band calibration lookup reuses the resolved id instead of querying the label a second time. Only blueprint composition changes. Stream delivery already filtered against `DeliverQueuedOptions.get_stream_ids()`, so no sensor data was ever plotted for absent streams and playback behavior is unchanged. Reproducibility: Verified on a Gen2 recording with no Neural Band stream by dumping the blueprint with `--rrd-output-path` before and after the change: the four Neural Band views disappear while the GPS view is retained, since that recording does contain GPS. Unit tests cover a recorded stream, a label that maps to an absent stream, a stream with zero data records, and the GPS variants. ___ Differential Revision: D115492740 fbshipit-source-id: e843378fb840be533ce8d1c76e4d832941d75d36
1 parent 5f3dd47 commit 61eecda

2 files changed

Lines changed: 161 additions & 37 deletions

File tree

projectaria_tools/tools/aria_rerun_viewer/aria_rerun_viewer.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
)
3030
from projectaria_tools.core.data_provider import DeliverQueuedOptions, VrsDataProvider
3131
from projectaria_tools.core.sensor_data import SensorDataType, TimeDomain, TimeSyncMode
32+
from projectaria_tools.core.stream_id import StreamId
3233
from projectaria_tools.tools.aria_rerun_viewer.aria_data_plotter import (
3334
AriaDataViewer,
3435
AriaDataViewerConfig,
@@ -137,6 +138,27 @@ def parse_args():
137138
return parser.parse_args()
138139

139140

141+
def get_recorded_stream_id(vrs_data_provider, label: str) -> Optional[StreamId]:
142+
"""
143+
Get the stream id for `label`, or None if this recording carries no data for it.
144+
145+
`get_stream_id_from_label()` on its own is not a presence check: for Gen2 it
146+
answers from a mostly-static device-model table, so it returns an id for
147+
every label the glasses *could* record (`emg` -> 241-1, `gps` -> 281-2, ...)
148+
whether or not this particular file has that stream. A panel keyed off it
149+
alone would render permanently empty. Streams can also be declared with zero
150+
data records, so the record count is checked as well.
151+
"""
152+
maybe_stream_id = vrs_data_provider.get_stream_id_from_label(label)
153+
if maybe_stream_id is None:
154+
return None
155+
if maybe_stream_id not in vrs_data_provider.get_all_streams():
156+
return None
157+
if vrs_data_provider.get_num_data(maybe_stream_id) == 0:
158+
return None
159+
return maybe_stream_id
160+
161+
140162
def get_deliver_option(
141163
vrs_data_provider,
142164
enabled_stream_labels: list[str] = None,
@@ -339,13 +361,17 @@ def log_vrs_to_rerun(
339361
else:
340362
raise ValueError(f" Unsupported Aria device version: {device_version}")
341363

342-
# Step 3: Create config
364+
# Step 3: Create config. Optional panels are turned on only for sensors this
365+
# recording actually carries, so the layout never reserves space for a panel
366+
# that would stay empty for the whole session.
343367
viewer_config: AriaDataViewerConfig = AriaDataViewerConfig()
344-
viewer_config.enable_gps = True
345-
# VRS stream label is still `emg` (unchanged writer format).
346-
viewer_config.enable_neural_band_batch = (
347-
vrs_data_provider.get_stream_id_from_label("emg") is not None
368+
viewer_config.enable_gps = any(
369+
get_recorded_stream_id(vrs_data_provider, label) is not None
370+
for label in ("gps", "gps-app")
348371
)
372+
# VRS stream label is still `emg` (unchanged writer format).
373+
emg_stream_id: Optional[StreamId] = get_recorded_stream_id(vrs_data_provider, "emg")
374+
viewer_config.enable_neural_band_batch = emg_stream_id is not None
349375

350376
# Step 4: Get configured deliver options
351377
parsed_subsample_rates: dict[str, int] = (
@@ -370,22 +396,18 @@ def log_vrs_to_rerun(
370396
)
371397
aria_data_viewer.plot_device_extrinsics()
372398

373-
if viewer_config.enable_neural_band_batch:
374-
emg_stream_id = vrs_data_provider.get_stream_id_from_label("emg")
375-
if emg_stream_id is not None:
376-
sensor_calib = vrs_data_provider.get_sensor_calibration(emg_stream_id)
377-
if (
378-
sensor_calib is not None
379-
and sensor_calib.sensor_calibration_type()
380-
== SensorCalibrationType.NEURAL_BAND_BATCH_CALIBRATION
381-
):
382-
batch_calib = sensor_calib.neural_band_batch_calibration()
383-
if batch_calib.emg_calib is not None:
384-
aria_data_viewer.set_neural_band_emg_calibration(
385-
batch_calib.emg_calib
386-
)
387-
# TODO: forward batch_calib.imu_calib once the viewer surfaces
388-
# rectified accel/gyro (raw accel/gyro tabs already show wire values).
399+
if emg_stream_id is not None:
400+
sensor_calib = vrs_data_provider.get_sensor_calibration(emg_stream_id)
401+
if (
402+
sensor_calib is not None
403+
and sensor_calib.sensor_calibration_type()
404+
== SensorCalibrationType.NEURAL_BAND_BATCH_CALIBRATION
405+
):
406+
batch_calib = sensor_calib.neural_band_batch_calibration()
407+
if batch_calib.emg_calib is not None:
408+
aria_data_viewer.set_neural_band_emg_calibration(batch_calib.emg_calib)
409+
# TODO: forward batch_calib.imu_calib once the viewer surfaces
410+
# rectified accel/gyro (raw accel/gyro tabs already show wire values).
389411

390412
# Step 6: Plot queued sensor data
391413
plot_queued_sensor_data(

projectaria_tools/tools/aria_rerun_viewer/tests/test_aria_rerun_viewer.py

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ALL_STREAM_LABELS_GEN1,
2929
ALL_STREAM_LABELS_GEN2,
3030
get_deliver_option,
31+
get_recorded_stream_id,
3132
log_vrs_to_rerun,
3233
main,
3334
MAX_IMU_BATCH_SIZE,
@@ -158,6 +159,47 @@ def test_rejects_invalid_enabled_stream_choice(self) -> None:
158159
parse_args()
159160

160161

162+
class GetRecordedStreamIdTest(unittest.TestCase):
163+
"""Tests for `get_recorded_stream_id()`."""
164+
165+
def _make_provider(
166+
self, label_to_id: dict, recorded: list, num_data: int = 1
167+
) -> MagicMock:
168+
provider = MagicMock()
169+
provider.get_stream_id_from_label.side_effect = label_to_id.get
170+
provider.get_all_streams.return_value = recorded
171+
provider.get_num_data.return_value = num_data
172+
return provider
173+
174+
def test_returns_id_for_recorded_stream_with_data(self) -> None:
175+
stream_id = MagicMock(name="emg_id")
176+
provider = self._make_provider({"emg": stream_id}, recorded=[stream_id])
177+
178+
self.assertIs(get_recorded_stream_id(provider, "emg"), stream_id)
179+
180+
def test_returns_none_when_label_is_unknown(self) -> None:
181+
provider = self._make_provider({}, recorded=[])
182+
183+
self.assertIsNone(get_recorded_stream_id(provider, "emg"))
184+
185+
def test_returns_none_when_stream_absent_from_recording(self) -> None:
186+
# The device-model table resolves the label, but the file has no such
187+
# stream -- the exact case that left the Neural Band panel always on.
188+
stream_id = MagicMock(name="emg_id")
189+
provider = self._make_provider({"emg": stream_id}, recorded=[])
190+
191+
self.assertIsNone(get_recorded_stream_id(provider, "emg"))
192+
provider.get_num_data.assert_not_called()
193+
194+
def test_returns_none_when_stream_has_no_data_records(self) -> None:
195+
stream_id = MagicMock(name="emg_id")
196+
provider = self._make_provider(
197+
{"emg": stream_id}, recorded=[stream_id], num_data=0
198+
)
199+
200+
self.assertIsNone(get_recorded_stream_id(provider, "emg"))
201+
202+
161203
class GetDeliverOptionTest(unittest.TestCase):
162204
"""Tests for `get_deliver_option()`."""
163205

@@ -578,17 +620,29 @@ def _setup_provider(
578620
self,
579621
device_version: DeviceVersion,
580622
stream_labels: list = None,
623+
phantom_labels: list = None,
624+
empty_labels: list = None,
581625
) -> MagicMock:
582626
"""
583627
Build a mock VRS provider whose helpers cooperate with the real
584628
`get_deliver_option` implementation. Each label in `stream_labels`
585629
gets a distinct mock StreamId that flows through both
586630
`get_stream_id_from_label` and `deliver_options.get_stream_ids()`.
631+
632+
`phantom_labels` models the Gen2 device-model table in the external
633+
StreamIdLabelMapper: the label resolves to a StreamId, but that stream
634+
is absent from the recording. `empty_labels` models a stream that is
635+
present but carries zero data records.
587636
"""
588637
stream_labels = stream_labels or []
638+
phantom_labels = phantom_labels or []
639+
empty_labels = empty_labels or []
640+
# Phantom labels resolve to an id but never appear in the file's streams.
589641
self.stream_id_map = {
590-
label: MagicMock(name=f"stream_id_{label}") for label in stream_labels
642+
label: MagicMock(name=f"stream_id_{label}")
643+
for label in list(stream_labels) + list(phantom_labels)
591644
}
645+
recorded_ids = [self.stream_id_map[label] for label in stream_labels]
592646

593647
provider = MagicMock()
594648
(
@@ -597,11 +651,14 @@ def _setup_provider(
597651
provider.get_stream_id_from_label.side_effect = (
598652
lambda label: self.stream_id_map.get(label)
599653
)
654+
provider.get_all_streams.return_value = recorded_ids
655+
empty_ids = {self.stream_id_map[label] for label in empty_labels}
656+
provider.get_num_data.side_effect = (
657+
lambda stream_id: 0 if stream_id in empty_ids else 1
658+
)
600659

601660
self.deliver_options = MagicMock()
602-
self.deliver_options.get_stream_ids.return_value = list(
603-
self.stream_id_map.values()
604-
)
661+
self.deliver_options.get_stream_ids.return_value = recorded_ids
605662
provider.get_default_deliver_queued_options.return_value = self.deliver_options
606663
return provider
607664

@@ -686,20 +743,65 @@ def test_default_vio_hf_subsample_rate_is_applied(self) -> None:
686743
AriaDataViewerConfig.vio_high_freq_subsample_rate,
687744
)
688745

689-
def test_neural_band_batch_enabled_only_when_emg_stream_present(self) -> None:
690-
# VRS stream label is `emg` on the wire.
691-
with_emg = self._setup_provider(DeviceVersion.Gen2, stream_labels=["emg"])
692-
self.mock_create_provider.return_value = with_emg
693-
log_vrs_to_rerun("/some.vrs")
694-
with_emg_kwargs = self.mock_viewer_cls.call_args.kwargs
695-
self.assertTrue(with_emg_kwargs["config"].enable_neural_band_batch)
696-
746+
def _config_for(self, provider: MagicMock):
747+
"""Run the viewer against `provider` and return the config it was built with."""
697748
self.mock_viewer_cls.reset_mock()
698-
no_emg = self._setup_provider(DeviceVersion.Gen2, stream_labels=["camera-rgb"])
699-
self.mock_create_provider.return_value = no_emg
749+
self.mock_create_provider.return_value = provider
700750
log_vrs_to_rerun("/some.vrs")
701-
no_emg_kwargs = self.mock_viewer_cls.call_args.kwargs
702-
self.assertFalse(no_emg_kwargs["config"].enable_neural_band_batch)
751+
return self.mock_viewer_cls.call_args.kwargs["config"]
752+
753+
def test_neural_band_batch_enabled_when_emg_stream_recorded(self) -> None:
754+
# VRS stream label is `emg` on the wire.
755+
config = self._config_for(
756+
self._setup_provider(DeviceVersion.Gen2, stream_labels=["emg"])
757+
)
758+
759+
self.assertTrue(config.enable_neural_band_batch)
760+
761+
def test_neural_band_batch_disabled_when_label_maps_to_absent_stream(self) -> None:
762+
# The external Gen2 StreamIdLabelMapper resolves `emg` from a static
763+
# device-model table, so it hands back 241-1 even for a recording that
764+
# has no Neural Band stream. Presence in the file is what must decide.
765+
config = self._config_for(
766+
self._setup_provider(
767+
DeviceVersion.Gen2,
768+
stream_labels=["camera-rgb"],
769+
phantom_labels=["emg"],
770+
)
771+
)
772+
773+
self.assertFalse(config.enable_neural_band_batch)
774+
775+
def test_neural_band_batch_disabled_when_stream_has_no_records(self) -> None:
776+
# A declared-but-empty stream would render a panel that never fills.
777+
config = self._config_for(
778+
self._setup_provider(
779+
DeviceVersion.Gen2,
780+
stream_labels=["camera-rgb", "emg"],
781+
empty_labels=["emg"],
782+
)
783+
)
784+
785+
self.assertFalse(config.enable_neural_band_batch)
786+
787+
def test_gps_enabled_only_when_a_gps_stream_is_recorded(self) -> None:
788+
# `gps` and `gps-app` are separate streams; either one earns the map panel.
789+
for labels in (["gps"], ["gps-app"], ["gps", "gps-app"]):
790+
with self.subTest(stream_labels=labels):
791+
config = self._config_for(
792+
self._setup_provider(DeviceVersion.Gen2, stream_labels=labels)
793+
)
794+
self.assertTrue(config.enable_gps)
795+
796+
config = self._config_for(
797+
self._setup_provider(
798+
DeviceVersion.Gen2,
799+
stream_labels=["camera-rgb"],
800+
phantom_labels=["gps", "gps-app"],
801+
)
802+
)
803+
804+
self.assertFalse(config.enable_gps)
703805

704806
def test_user_subsample_rates_are_parsed_and_applied(self) -> None:
705807
# Real parse_subsample_rates + real get_deliver_option must translate

0 commit comments

Comments
 (0)