Skip to content

Commit d284936

Browse files
committed
fix(layout): auto-layout image panels for foxglove protobuf MCAP
Extend isRosImageSchema to recognize foxglove.CompressedImage and related protobuf image types. Use buildImageRows for multi-camera color-only datasets (e.g. NuScenes) while keeping planColorDepthCameraRows for depth/color robot bags.
1 parent 8ac00ef commit d284936

5 files changed

Lines changed: 99 additions & 16 deletions

File tree

src/features/layout/autoLayout/applyDefaultRosDockLayout.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,35 @@ describe('buildDefaultRosFoxgloveLayoutData', () => {
202202
expect(rawSnapshots).toHaveLength(1);
203203
expect((rawSnapshots[0]?.config as { topic?: string }).topic).toBe('/observations/joint_states');
204204
});
205+
206+
it('lays out six NuScenes protobuf CompressedImage streams without RawMessages fallback', () => {
207+
const topics: TopicInfo[] = [
208+
{ name: '/CAM_FRONT/image_rect_compressed', type: 'foxglove.CompressedImage' },
209+
{ name: '/CAM_FRONT_LEFT/image_rect_compressed', type: 'foxglove.CompressedImage' },
210+
{ name: '/CAM_FRONT_RIGHT/image_rect_compressed', type: 'foxglove.CompressedImage' },
211+
{ name: '/CAM_BACK/image_rect_compressed', type: 'foxglove.CompressedImage' },
212+
{ name: '/CAM_BACK_LEFT/image_rect_compressed', type: 'foxglove.CompressedImage' },
213+
{ name: '/CAM_BACK_RIGHT/image_rect_compressed', type: 'foxglove.CompressedImage' },
214+
{ name: '/imu', type: 'IMU' },
215+
];
216+
217+
const data = buildDefaultRosFoxgloveLayoutData(topics);
218+
const ids = collectMosaicPanelIds(data.layout);
219+
const panelTypes = ids.map((id) => getPanelTypeFromId(id));
220+
expect(panelTypes.filter((type) => type === 'Image')).toHaveLength(6);
221+
expect(panelTypes.filter((type) => type === 'RawMessages')).toHaveLength(0);
222+
223+
const imageTopics = Object.values(data.configById)
224+
.map((config) => (config as { topic?: string }).topic)
225+
.filter((topic): topic is string => Boolean(topic))
226+
.sort();
227+
expect(imageTopics).toEqual([
228+
'/CAM_BACK/image_rect_compressed',
229+
'/CAM_BACK_LEFT/image_rect_compressed',
230+
'/CAM_BACK_RIGHT/image_rect_compressed',
231+
'/CAM_FRONT/image_rect_compressed',
232+
'/CAM_FRONT_LEFT/image_rect_compressed',
233+
'/CAM_FRONT_RIGHT/image_rect_compressed',
234+
]);
235+
});
205236
});

src/features/layout/autoLayout/applyDefaultRosDockLayout.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import {
1515
type FoxgloveLayoutData,
1616
type FoxgloveMosaicNode,
1717
} from '@/core/preferences/foxgloveLayout';
18-
import { planColorDepthCameraRows } from '@/features/layout/autoLayout/planRosImageGrid';
18+
import { planColorDepthCameraRows, isDepthImageTopicName } from '@/features/layout/autoLayout/planRosImageGrid';
19+
import { buildImageRows } from '@/features/layout/autoLayout/buildImageRows';
20+
import { selectImageTopicsForAutoLayout } from '@/features/layout/autoLayout/imageTopicSelection';
1921
import { heuristicAudioInfoTopics } from '@/features/panels/Audio/core/resolveAudioInfo';
2022
import { getPanelDefinition } from '@/features/panels/registry';
21-
import { isAudioCommonInfoSchema, isJointStateSchema, isRawAudioSchema, normalizeRosSchemaName } from '@/shared/ros/rosMessageTypes';
23+
import { isAudioCommonInfoSchema, isJointStateSchema, isRawAudioSchema, isRosImageSchema, normalizeRosSchemaName } from '@/shared/ros/rosMessageTypes';
2224
import { pickDefaultRawMessagesTopic } from '@/features/layout/autoLayout/pickDefaultRawMessagesTopic';
2325

2426
function imageTabTitle(topic: string): string {
@@ -230,24 +232,41 @@ export function buildDefaultRosFoxgloveLayoutData(
230232
topics: ReadonlyArray<TopicInfo>,
231233
options?: BuildDefaultRosLayoutOptions,
232234
): FoxgloveLayoutData {
233-
const { colorRow, depthRow } = planColorDepthCameraRows(topics);
234235
const configById: Record<string, FoxgloveConfig> = {};
235236
const hdf5Dataset = isHdf5Dataset(topics, options?.publishersByTopic);
236237

237238
const stackParts: FoxgloveMosaicNode[] = [];
238239
const usedImageTopics = new Set<string>();
239-
const colorImageIds = appendImagePanelsForRow(colorRow, configById);
240-
const depthImageIds = appendImagePanelsForRow(depthRow, configById);
241-
for (const topic of colorRow) {
242-
if (topic) usedImageTopics.add(topic);
243-
}
244-
for (const topic of depthRow) {
245-
if (topic) usedImageTopics.add(topic);
240+
241+
const pickedImageTopics = selectImageTopicsForAutoLayout(topics);
242+
const hasDepthImageStreams = topics.some(
243+
(topic) => isRosImageSchema(topic.type) && isDepthImageTopicName(topic.name),
244+
);
245+
246+
if (hasDepthImageStreams) {
247+
const { colorRow, depthRow } = planColorDepthCameraRows(topics);
248+
const colorImageIds = appendImagePanelsForRow(colorRow, configById);
249+
const depthImageIds = appendImagePanelsForRow(depthRow, configById);
250+
for (const topic of colorRow) {
251+
if (topic) usedImageTopics.add(topic);
252+
}
253+
for (const topic of depthRow) {
254+
if (topic) usedImageTopics.add(topic);
255+
}
256+
const colorMosaic = rowMosaicFromPanelIds(colorImageIds);
257+
const depthMosaic = rowMosaicFromPanelIds(depthImageIds);
258+
if (colorMosaic) stackParts.push(colorMosaic);
259+
if (depthMosaic) stackParts.push(depthMosaic);
260+
} else if (pickedImageTopics.length > 0) {
261+
for (const row of buildImageRows(pickedImageTopics)) {
262+
const imageIds = appendImagePanelsForRow(row, configById);
263+
for (const topic of row) {
264+
usedImageTopics.add(topic);
265+
}
266+
const mosaic = rowMosaicFromPanelIds(imageIds);
267+
if (mosaic) stackParts.push(mosaic);
268+
}
246269
}
247-
const colorMosaic = rowMosaicFromPanelIds(colorImageIds);
248-
const depthMosaic = rowMosaicFromPanelIds(depthImageIds);
249-
if (colorMosaic) stackParts.push(colorMosaic);
250-
if (depthMosaic) stackParts.push(depthMosaic);
251270

252271
const audioTopicCandidates = collectTopicsForPanelSchemas(topics, 'Audio')
253272
.sort((a, b) => audioTopicPriorityScore(b) - audioTopicPriorityScore(a) || a.localeCompare(b))

src/features/layout/autoLayout/imageTopicSelection.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ describe('selectImageTopicsForAutoLayout', () => {
5454
]);
5555
});
5656

57+
it('includes foxglove protobuf CompressedImage topics', () => {
58+
const topics: TopicInfo[] = [
59+
{ name: '/CAM_FRONT/image_rect_compressed', type: 'foxglove.CompressedImage' },
60+
{ name: '/CAM_BACK/image_rect_compressed', type: 'foxglove.CompressedImage' },
61+
];
62+
const picked = selectImageTopicsForAutoLayout(topics);
63+
expect(picked).toEqual([
64+
'/CAM_BACK/image_rect_compressed',
65+
'/CAM_FRONT/image_rect_compressed',
66+
]);
67+
});
68+
5769
it('keeps distinct non-overlapping streams', () => {
5870
const topics: TopicInfo[] = [
5971
{ name: '/camera/left/color/image_resized/compressed', type: 'sensor_msgs/msg/CompressedImage' },

src/shared/ros/rosMessageTypes.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ describe('rosMessageTypes', () => {
6363
expect(isRosImageSchema('sensor_msgs/msg/Image')).toBe(true);
6464
expect(isRosImageSchema('sensor_msgs/msg/CompressedImage')).toBe(true);
6565
expect(isRosImageSchema(ROS_MSG_FOXGLOVE_COMPRESSED_VIDEO)).toBe(true);
66+
expect(isRosImageSchema('foxglove.CompressedImage')).toBe(true);
6667
expect(isRosImageSchema('sensor_msgs/msg/CameraInfo')).toBe(false);
68+
expect(isRosImageSchema('foxglove.ImageAnnotations')).toBe(false);
69+
expect(isRosImageSchema('foxglove.CameraCalibration')).toBe(false);
6770
});
6871

6972
it('detects RawAudio and audio panel main schemas', () => {

src/shared/ros/rosMessageTypes.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,29 @@ export function isJointStateSchema(type: string): boolean {
3434
}
3535

3636
export function isRosImageSchema(type: string): boolean {
37-
return (
37+
if (
3838
matchesRosSchema(type, ROS_MSG_SENSOR_IMAGE) ||
3939
matchesRosSchema(type, ROS_MSG_SENSOR_COMPRESSED_IMAGE) ||
4040
matchesRosSchema(type, ROS_MSG_FOXGLOVE_COMPRESSED_VIDEO)
41-
);
41+
) {
42+
return true;
43+
}
44+
45+
const normalized = normalizeRosSchemaName(type);
46+
if (normalized.includes('compressedimage')) {
47+
return true;
48+
}
49+
if (normalized.includes('compressedvideo')) {
50+
return true;
51+
}
52+
if (
53+
/\/image$/i.test(normalized) &&
54+
!normalized.includes('camerainfo') &&
55+
!normalized.includes('annotations')
56+
) {
57+
return true;
58+
}
59+
return false;
4260
}
4361

4462
export function isPoseStampedSchema(type: string): boolean {

0 commit comments

Comments
 (0)