From 87f51ec7705204e87adc9352291882234a4902d5 Mon Sep 17 00:00:00 2001 From: Horatiu Almasan Date: Thu, 8 Oct 2026 13:27:27 +0300 Subject: [PATCH] Show the tick's camera frame in the projection strip. The strip sends the tick's log time as well as its keyframe, so the image changes with the point cloud instead of repeating the keyframe. --- .../CameraProjectionFrame.svelte | 19 ++++++++++++++----- .../CameraProjectionFrame.test.ts | 5 +++-- .../CameraProjectionStrip.svelte | 3 ++- .../CameraProjectionStrip.test.ts | 6 +++--- .../getCameraFrameUrl.test.ts | 5 +++-- .../getCameraFrameUrl/getCameraFrameUrl.ts | 15 +++++++++++---- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.svelte b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.svelte index de8872acd9..6b5fe43ddf 100644 --- a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.svelte +++ b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.svelte @@ -14,17 +14,26 @@ recordingId?: string; /** MCAP channel to render. */ channelId: number; - /** Frame timestamp in nanoseconds, as a string to preserve full precision. */ - timestampNs?: string; + /** Keyframe to decode from, in nanoseconds, as a string to preserve full precision. */ + keyframeTimestampNs?: string; + /** Frame to show, in nanoseconds, as a string to preserve full precision. */ + logTimeNs?: string; /** Studio slot name shown as the caption, e.g. `front`. */ label: string; } - let { datasetId, recordingId, channelId, timestampNs, label }: Props = $props(); + let { datasetId, recordingId, channelId, keyframeTimestampNs, logTimeNs, label }: Props = + $props(); const frameUrl = $derived( - recordingId && timestampNs - ? getCameraFrameUrl({ datasetId, recordingId, channelId, timestampNs }) + recordingId && keyframeTimestampNs && logTimeNs + ? getCameraFrameUrl({ + datasetId, + recordingId, + channelId, + keyframeTimestampNs, + logTimeNs + }) : null ); diff --git a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.test.ts b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.test.ts index 82602fc758..b704833356 100644 --- a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.test.ts +++ b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionFrame/CameraProjectionFrame.test.ts @@ -12,7 +12,8 @@ const defaultProps = { datasetId: 'dataset-1', recordingId: 'recording-1', channelId: 3, - timestampNs: '2000', + keyframeTimestampNs: '1500', + logTimeNs: '2000', label: 'front' }; @@ -32,7 +33,7 @@ describe('CameraProjectionFrame', () => { expect(screen.getByRole('img', { name: 'front' })).toHaveAttribute( 'src', - `${TEST_BASE_URL}/datasets/dataset-1/recordings/recording-1/camera-frame?channel_id=3&keyframe_timestamp_ns=2000` + `${TEST_BASE_URL}/datasets/dataset-1/recordings/recording-1/camera-frame?channel_id=3&keyframe_timestamp_ns=1500&log_time_ns=2000` ); expect(screen.getByText('front')).toBeInTheDocument(); }); diff --git a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.svelte b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.svelte index b1fab2dd9d..8991225d87 100644 --- a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.svelte +++ b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.svelte @@ -53,7 +53,8 @@ {datasetId} {recordingId} channelId={summaryChannel.channel_id} - timestampNs={channel?.keyframe_log_time_ns ?? undefined} + keyframeTimestampNs={channel?.keyframe_log_time_ns ?? undefined} + logTimeNs={channel?.log_time_ns} label={summaryChannel.group_component_name} /> {/each} diff --git a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.test.ts b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.test.ts index 1b80191df6..36d8bf496a 100644 --- a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.test.ts +++ b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/CameraProjectionStrip.test.ts @@ -34,7 +34,7 @@ const tickDetails: TickDetailView = { seq_number: 0, timestamp_ns: 1000, camera_channels: { - // Camera channel with a keyframe locator: renders, seeking to the keyframe. + // Camera channel with a keyframe locator: renders, decoding from the keyframe. front: { channel_id: 1, group_component_name: 'front', @@ -78,13 +78,13 @@ describe('CameraProjectionStrip', () => { expect(screen.getByText('rear')).toBeInTheDocument(); }); - it('seeks frames by their keyframe timestamp', () => { + it('requests the frame of the tick, decoded from its keyframe', () => { tickDetailsResult.data = tickDetails; render(CameraProjectionStrip, { props: defaultProps }); expect(screen.getByRole('img', { name: 'front' })).toHaveAttribute( 'src', - `${TEST_BASE_URL}/datasets/dataset-1/recordings/recording-1/camera-frame?channel_id=1&keyframe_timestamp_ns=1500` + `${TEST_BASE_URL}/datasets/dataset-1/recordings/recording-1/camera-frame?channel_id=1&keyframe_timestamp_ns=1500&log_time_ns=2000` ); }); diff --git a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.test.ts b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.test.ts index 2b7c3db668..ede4b13971 100644 --- a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.test.ts +++ b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.test.ts @@ -21,12 +21,13 @@ describe('getCameraFrameUrl', () => { datasetId: 'dataset-1', recordingId: 'recording-1', channelId: 3, - timestampNs: '1234567890' + keyframeTimestampNs: '1234567890', + logTimeNs: '1234567990' }; it("builds the camera-frame URL from the client's base URL and route template", () => { expect(getCameraFrameUrl(defaultParams)).toBe( - `${TEST_BASE_URL}/datasets/dataset-1/recordings/recording-1/camera-frame?channel_id=3&keyframe_timestamp_ns=1234567890` + `${TEST_BASE_URL}/datasets/dataset-1/recordings/recording-1/camera-frame?channel_id=3&keyframe_timestamp_ns=1234567890&log_time_ns=1234567990` ); }); diff --git a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.ts b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.ts index 1be7c66d95..5a61bdb17f 100644 --- a/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.ts +++ b/lightly_studio_view/src/lib/components/PointCloudLabelingWorkspace/CameraProjectionStrip/getCameraFrameUrl/getCameraFrameUrl.ts @@ -4,8 +4,10 @@ interface CameraFrameUrlParams { datasetId: string; recordingId: string; channelId: number; - /** Nanosecond seek time, kept as a string to preserve full precision. */ - timestampNs: string; + /** Nanosecond log time of the keyframe to decode from, kept as a string to preserve full precision. */ + keyframeTimestampNs: string; + /** Nanosecond log time of the frame to show, kept as a string to preserve full precision. */ + logTimeNs: string; } /** @@ -18,12 +20,17 @@ export function getCameraFrameUrl({ datasetId, recordingId, channelId, - timestampNs + keyframeTimestampNs, + logTimeNs }: CameraFrameUrlParams): string { return client.buildUrl({ url: '/datasets/{dataset_id}/recordings/{recording_id}/camera-frame', baseUrl: client.getConfig().baseUrl, path: { dataset_id: datasetId, recording_id: recordingId }, - query: { channel_id: channelId, keyframe_timestamp_ns: timestampNs } + query: { + channel_id: channelId, + keyframe_timestamp_ns: keyframeTimestampNs, + log_time_ns: logTimeNs + } }); }