From 44ff08c7b29c9fd2756a735d370f5f25a34635f3 Mon Sep 17 00:00:00 2001 From: joaner Date: Wed, 1 Jul 2026 15:32:22 +0800 Subject: [PATCH 1/2] feat(image): add ROS compressedDepth rendering with depth colormap defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support 16UC1/32FC1 PNG decode, correct 16-bit PNG scanline unfilter, turbo colormap with ROS image_view range (200–10000 mm), and topic presets. Bump version to 1.6.0. --- package-lock.json | 4 +- package.json | 2 +- scripts/gen-e2e-fixtures.mjs | 1 + scripts/gen-test-mcap-compressed-depth.mjs | 42 +++++ src/features/panels/Image/ImagePanel.tsx | 3 +- .../panels/Image/ImagePanelSettings.tsx | 24 ++- .../panels/Image/core/ImageRender.worker.ts | 124 ++++++++----- .../Image/core/compressedDepthDecoder.test.ts | 56 ++++++ .../Image/core/compressedDepthDecoder.ts | 131 +++++++++++++ .../Image/core/depthColorDefaults.test.ts | 89 +++++++++ .../panels/Image/core/depthColorDefaults.ts | 127 +++++++++++++ .../Image/core/depthColorization.test.ts | 84 +++++++++ .../Image/core/grayscale16PngDecoder.test.ts | 104 +++++++++++ .../Image/core/grayscale16PngDecoder.ts | 135 ++++++++++++++ .../panels/Image/core/imageColorMode.test.ts | 32 ++++ .../panels/Image/core/imageColorMode.ts | 14 +- .../panels/Image/core/imageTypes.test.ts | 71 ++++++- src/features/panels/Image/core/imageTypes.ts | 131 ++++++++++++- .../Image/core/png16ScanlineUnfilter.test.ts | 30 +++ .../Image/core/png16ScanlineUnfilter.ts | 175 ++++++++++++++++++ .../panels/Image/core/rawDecoders.test.ts | 17 ++ src/features/panels/Image/core/rawDecoders.ts | 10 +- .../image/compressed-depth-16uc1.bin | Bin 0 -> 126023 bytes .../image/compressed-depth-16uc1.format.txt | 1 + .../image/compressed-depth-32fc1.bin | Bin 0 -> 82 bytes tests/fixturePaths.ts | 2 + tests/image-compressed-depth.spec.ts | 29 +++ 27 files changed, 1368 insertions(+), 70 deletions(-) create mode 100644 scripts/gen-test-mcap-compressed-depth.mjs create mode 100644 src/features/panels/Image/core/compressedDepthDecoder.test.ts create mode 100644 src/features/panels/Image/core/compressedDepthDecoder.ts create mode 100644 src/features/panels/Image/core/depthColorDefaults.test.ts create mode 100644 src/features/panels/Image/core/depthColorDefaults.ts create mode 100644 src/features/panels/Image/core/depthColorization.test.ts create mode 100644 src/features/panels/Image/core/grayscale16PngDecoder.test.ts create mode 100644 src/features/panels/Image/core/grayscale16PngDecoder.ts create mode 100644 src/features/panels/Image/core/imageColorMode.test.ts create mode 100644 src/features/panels/Image/core/png16ScanlineUnfilter.test.ts create mode 100644 src/features/panels/Image/core/png16ScanlineUnfilter.ts create mode 100644 test-fixtures/image/compressed-depth-16uc1.bin create mode 100644 test-fixtures/image/compressed-depth-16uc1.format.txt create mode 100644 test-fixtures/image/compressed-depth-32fc1.bin create mode 100644 tests/image-compressed-depth.spec.ts diff --git a/package-lock.json b/package-lock.json index 13401d6..c7a84b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ioai/rosview", - "version": "1.5.4", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ioai/rosview", - "version": "1.5.4", + "version": "1.6.0", "license": "MIT", "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/package.json b/package.json index a9852f8..680639b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ioai/rosview", - "version": "1.5.4", + "version": "1.6.0", "description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA", "keywords": [ "ros", diff --git a/scripts/gen-e2e-fixtures.mjs b/scripts/gen-e2e-fixtures.mjs index 309ecde..e8ada66 100644 --- a/scripts/gen-e2e-fixtures.mjs +++ b/scripts/gen-e2e-fixtures.mjs @@ -29,6 +29,7 @@ runNode('gen-test-mcap.mjs'); runNode('gen-test-mcap-pose.mjs'); runNode('gen-test-mcap-3cam.mjs'); runNode('gen-test-mcap-h264.mjs'); +runNode('gen-test-mcap-compressed-depth.mjs'); runPython('gen-test-hdf5.py'); runNode('gen-test-bvh.mjs'); diff --git a/scripts/gen-test-mcap-compressed-depth.mjs b/scripts/gen-test-mcap-compressed-depth.mjs new file mode 100644 index 0000000..e000c4b --- /dev/null +++ b/scripts/gen-test-mcap-compressed-depth.mjs @@ -0,0 +1,42 @@ +/** + * Minimal MCAP with 16UC1 compressedDepth CompressedImage messages for E2E. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + createIndexedMcapWriter, + encodeCompressedImageCdr, + readFixture, + registerCompressedImageChannel, + writeExample, +} from './mcap-fixture-utils.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const payload = readFixture('image/compressed-depth-16uc1.bin'); +const format = fs.readFileSync( + path.join(__dirname, '../test-fixtures/image/compressed-depth-16uc1.format.txt'), + 'utf8', +).trim(); + +const { writer, writable } = await createIndexedMcapWriter(); + +const channelId = await registerCompressedImageChannel( + '/camera/realsense2_camera/aligned_depth_to_color/image_raw/compressed_depth', + writer, +); + +const frames = [1_000_000_000n, 2_000_000_000n, 3_000_000_000n]; +for (const [idx, ts] of frames.entries()) { + const stamp = { sec: Number(ts / 1_000_000_000n), nsec: Number(ts % 1_000_000_000n) }; + await writer.addMessage({ + channelId, + sequence: idx + 1, + logTime: ts, + publishTime: ts, + data: encodeCompressedImageCdr(stamp, format, payload), + }); +} + +await writer.end(); +writeExample('test_compressed_depth.mcap', writable.getBuffer()); diff --git a/src/features/panels/Image/ImagePanel.tsx b/src/features/panels/Image/ImagePanel.tsx index b0be675..2af9d4e 100644 --- a/src/features/panels/Image/ImagePanel.tsx +++ b/src/features/panels/Image/ImagePanel.tsx @@ -16,6 +16,7 @@ import { } from './core/imageTypes'; import { repairH264Seek } from './core/h264SeekRepair'; import { isH264MessageEvent, toWorkerFrame } from './core/messageFrameAdapter'; +import { applyDepthTopicPreset } from './core/depthColorDefaults'; import type { ImageConfig } from './defaults'; import { TopicQuickPicker } from '../framework/TopicQuickPicker'; import ImageRenderWorkerClass from './core/ImageRender.worker.ts?worker&inline'; @@ -306,7 +307,7 @@ export const ImagePanel: React.FC = (props) => {
setConfig((prev) => ({ ...prev, topic: nextTopic }))} + onChange={(nextTopic) => setConfig((prev) => applyDepthTopicPreset(nextTopic, prev))} typeIncludes={[...IMAGE_PANEL_TOPIC_INCLUDES]} placeholder={formatMessage({ id: 'panels.framework.topicPicker.imagePlaceholder' })} className="min-w-0 flex-1" diff --git a/src/features/panels/Image/ImagePanelSettings.tsx b/src/features/panels/Image/ImagePanelSettings.tsx index d9026e1..782acfd 100644 --- a/src/features/panels/Image/ImagePanelSettings.tsx +++ b/src/features/panels/Image/ImagePanelSettings.tsx @@ -13,7 +13,8 @@ import { } from '../framework/settings'; import { messageBus } from '@/core/pipeline/messageBus'; import { useTopicSeq } from '@/core/pipeline/useMessageBus'; -import { isRawImageMessage, isRawImageTopicSchema, IMAGE_PANEL_TOPIC_INCLUDES } from './core/imageTypes'; +import { isRawImageMessage, isRawImageTopicSchema, isCompressedImageMessage, depthEncodingFromFormat, IMAGE_PANEL_TOPIC_INCLUDES } from './core/imageTypes'; +import { applyDepthTopicPreset, defaultDepthMaxValue, defaultDepthMinValue } from './core/depthColorDefaults'; import type { ImageConfig } from './defaults'; const DEPTH_ENCODINGS = new Set(['mono16', '16uc1', '32fc1']); @@ -44,6 +45,9 @@ function useLastFrameEncoding(topic: string): string | null { if (isRawImageMessage(msg)) { return msg.encoding.trim().toLowerCase(); } + if (isCompressedImageMessage(msg)) { + return depthEncodingFromFormat(msg.format); + } return null; } @@ -99,6 +103,18 @@ export function ImagePanelSettings({ encodingForDepthSliders != null && DEPTH_ENCODINGS.has(encodingForDepthSliders) ? depthSliderBounds(encodingForDepthSliders) : { min: 0, max: 65535 }; + const sliderDefaultMax = + encodingForDepthSliders != null + ? defaultDepthMaxValue(encodingForDepthSliders, config.topic) + : sliderBounds.max; + const sliderDefaultMin = + encodingForDepthSliders != null + ? defaultDepthMinValue(encodingForDepthSliders, config.topic) + : sliderBounds.min; + + const applyTopicChange = (topic: string) => { + setConfig(applyDepthTopicPreset(topic, config)); + }; return (
@@ -109,7 +125,7 @@ export function ImagePanelSettings({ > setConfig({ ...config, topic })} + onChange={applyTopicChange} topics={topics} typeIncludes={[...IMAGE_PANEL_TOPIC_INCLUDES]} placeholder={formatMessage({ id: 'panels.image.settings.field.topic.placeholder' })} @@ -295,7 +311,7 @@ export function ImagePanelSettings({ )} > setConfig({ ...config, minValue })} min={sliderBounds.min} max={sliderBounds.max} @@ -310,7 +326,7 @@ export function ImagePanelSettings({ )} > setConfig({ ...config, maxValue })} min={sliderBounds.min} max={sliderBounds.max} diff --git a/src/features/panels/Image/core/ImageRender.worker.ts b/src/features/panels/Image/core/ImageRender.worker.ts index 8166478..cf3525b 100644 --- a/src/features/panels/Image/core/ImageRender.worker.ts +++ b/src/features/panels/Image/core/ImageRender.worker.ts @@ -1,11 +1,13 @@ /// import type { Time } from '@/core/types/ros'; +import { decodeCompressedDepth } from './compressedDepthDecoder'; import { decodeRawImage } from './rawDecoders'; import { getH264ChunkType } from './h264'; import { withTimeout } from './asyncTimeout'; import { getCompressedKind, + isCompressedDepthFormat, normalizeCompressedMime, type ImageSurfaceStatus, } from './imageTypes'; @@ -396,11 +398,27 @@ class ImageRenderWorkerRuntime { this.#emitStatus({ phase: 'decoding', receiveTime: frame.receiveTime }); try { if (frame.kind === 'compressed') { - const kind = getCompressedKind(frame.format); const bytes = ensureOwnedBytes(frame.data); if (bytes.byteLength === 0) { throw new Error(`Compressed image payload is empty: ${frame.format}`); } + + // ROS compressedDepth: PNG → 16UC1/32FC1, then same colormap path as RawImage. + if (isCompressedDepthFormat(frame.format)) { + const decoded = await decodeCompressedDepth(bytes, frame.format); + this.#renderRawFrame({ + receiveTime: frame.receiveTime, + encoding: decoded.encoding, + width: decoded.width, + height: decoded.height, + step: decoded.step, + isBigEndian: decoded.isBigEndian, + data: ensureOwnedBytes(decoded.data), + }); + return; + } + + const kind = getCompressedKind(frame.format); const sortKey = timeToKey(frame.receiveTime); if (kind === 'h264') { @@ -471,52 +489,14 @@ class ImageRenderWorkerRuntime { // Raw frame const bytes = ensureOwnedBytes(frame.data); - const pixelBytes = frame.width * frame.height * 4; - let rgba = this.#rawRgba; - if (!rgba || rgba.length !== pixelBytes) { - rgba = new Uint8ClampedArray(pixelBytes); - this.#rawRgba = rgba; - } - if (!this.#rawImageData || this.#rawImageData.width !== frame.width || this.#rawImageData.height !== frame.height) { - this.#rawImageData = new ImageData(rgba, frame.width, frame.height); - } - - const step = frame.step ?? (frame.width * bytesPerPixel(frame.encoding)); - const isBigEndian = frame.isBigEndian ?? false; - - decodeRawImage( - { - encoding: frame.encoding, - width: frame.width, - height: frame.height, - step, - is_bigendian: isBigEndian, - data: bytes, - }, - rgba, - this.#rawDecodeOptions, - ); - - // Store source bytes so rawDecodeOptions changes can re-decode immediately. - this.#disposeCachedBitmap(); - this.#cachedFrame = { - kind: 'raw', - width: frame.width, - height: frame.height, - encoding: frame.encoding, - step, - isBigEndian, - data: bytes, + this.#renderRawFrame({ receiveTime: frame.receiveTime, - }; - - this.#drawRawImageData(frame.width, frame.height); - this.#emitStatus({ - phase: 'ready', + encoding: frame.encoding, width: frame.width, height: frame.height, - encoding: frame.encoding, - receiveTime: frame.receiveTime, + step: frame.step ?? (frame.width * bytesPerPixel(frame.encoding)), + isBigEndian: frame.isBigEndian ?? false, + data: bytes, }); } catch (error) { this.#haltUntilReset = true; @@ -527,6 +507,60 @@ class ImageRenderWorkerRuntime { } } + #renderRawFrame(frame: { + receiveTime: Time; + encoding: string; + width: number; + height: number; + step: number; + isBigEndian: boolean; + data: Uint8Array; + }): void { + const pixelBytes = frame.width * frame.height * 4; + let rgba = this.#rawRgba; + if (!rgba || rgba.length !== pixelBytes) { + rgba = new Uint8ClampedArray(pixelBytes); + this.#rawRgba = rgba; + } + if (!this.#rawImageData || this.#rawImageData.width !== frame.width || this.#rawImageData.height !== frame.height) { + this.#rawImageData = new ImageData(rgba, frame.width, frame.height); + } + + decodeRawImage( + { + encoding: frame.encoding, + width: frame.width, + height: frame.height, + step: frame.step, + is_bigendian: frame.isBigEndian, + data: frame.data, + }, + rgba, + this.#rawDecodeOptions, + ); + + this.#disposeCachedBitmap(); + this.#cachedFrame = { + kind: 'raw', + width: frame.width, + height: frame.height, + encoding: frame.encoding, + step: frame.step, + isBigEndian: frame.isBigEndian, + data: frame.data, + receiveTime: frame.receiveTime, + }; + + this.#drawRawImageData(frame.width, frame.height); + this.#emitStatus({ + phase: 'ready', + width: frame.width, + height: frame.height, + encoding: frame.encoding, + receiveTime: frame.receiveTime, + }); + } + /** Re-decode the last raw frame with current rawDecodeOptions and redraw. */ #redrawRawCached(): void { const cached = this.#cachedFrame; @@ -611,7 +645,7 @@ class ImageRenderWorkerRuntime { data: Uint8Array, format: string, ): Promise { - const mime = normalizeCompressedMime(format); + const mime = normalizeCompressedMime(format, data); if (typeof ImageDecoder !== 'undefined') { let supported = this.#imageDecoderMimeSupported.get(mime); if (supported === undefined) { diff --git a/src/features/panels/Image/core/compressedDepthDecoder.test.ts b/src/features/panels/Image/core/compressedDepthDecoder.test.ts new file mode 100644 index 0000000..cb0c457 --- /dev/null +++ b/src/features/panels/Image/core/compressedDepthDecoder.test.ts @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { + decodeCompressedDepth, + findPngOffset, + parseCompressedDepthHeader, +} from './compressedDepthDecoder'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURE_16UC1 = path.join(__dirname, '../../../../../test-fixtures/image/compressed-depth-16uc1.bin'); +const FIXTURE_32FC1 = path.join(__dirname, '../../../../../test-fixtures/image/compressed-depth-32fc1.bin'); +const FIXTURE_FORMAT_16UC1 = '16UC1; compressedDepth'; + +describe('compressedDepthDecoder', () => { + it('finds the PNG signature after the ROS header', () => { + const fixture = fs.readFileSync(FIXTURE_16UC1); + expect(findPngOffset(fixture)).toBe(12); + }); + + it('decodes a RealSense 16UC1 compressed depth fixture', async () => { + const fixture = fs.readFileSync(FIXTURE_16UC1); + const decoded = await decodeCompressedDepth(fixture, FIXTURE_FORMAT_16UC1); + + expect(decoded.encoding).toBe('16uc1'); + expect(decoded.width).toBe(640); + expect(decoded.height).toBe(480); + expect(decoded.step).toBe(640 * 2); + expect(decoded.isBigEndian).toBe(false); + + const view = new DataView(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength); + expect(view.getUint16(0, true)).toBe(3524); + expect(view.getUint16(2, true)).toBe(3565); + }); + + it('dequantizes 32FC1 compressed depth PNG payloads', async () => { + const payload = fs.readFileSync(FIXTURE_32FC1); + const header = parseCompressedDepthHeader(payload, findPngOffset(payload)); + expect(header.depthParam).toEqual([10, 5]); + + const decoded = await decodeCompressedDepth(payload, '32FC1; compressedDepth'); + expect(decoded.encoding).toBe('32fc1'); + expect(decoded.width).toBe(2); + expect(decoded.height).toBe(1); + + const view = new DataView(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength); + expect(view.getFloat32(0, true)).toBeCloseTo(10 / (100 - 5), 5); + expect(view.getFloat32(4, true)).toBeCloseTo(10 / (200 - 5), 5); + }); + + it('rejects unsupported RVL compressed depth formats', async () => { + const fixture = fs.readFileSync(FIXTURE_16UC1); + await expect(decodeCompressedDepth(fixture, '16UC1; compressedDepth rvl')).rejects.toThrow(/RVL/); + }); +}); diff --git a/src/features/panels/Image/core/compressedDepthDecoder.ts b/src/features/panels/Image/core/compressedDepthDecoder.ts new file mode 100644 index 0000000..50f6352 --- /dev/null +++ b/src/features/panels/Image/core/compressedDepthDecoder.ts @@ -0,0 +1,131 @@ +/** + * Decode `sensor_msgs/CompressedImage` payloads with `compressedDepth` transport. + * + * Layout: optional 12-byte ROS header (quant params) + PNG (16-bit grayscale). + * Supported encodings: 16UC1 (direct mm samples) and 32FC1 (PNG uint16 dequantized to float). + */ + +import { decodeGrayscale16Png, findPngSignatureOffset } from './grayscale16PngDecoder'; +import { + depthEncodingFromFormat, + isCompressedDepthFormat, + parseCompressedImageFormat, + type DepthImageEncoding, +} from './imageTypes'; + +/** Bytes before PNG in standard ROS compressedDepth messages. */ +const ROS_COMPRESSED_DEPTH_HEADER_SIZE = 12; + +export interface CompressedDepthHeader { + compressionFormat: number; + depthParam: [number, number]; +} + +/** Raw image bytes ready for the same path as `sensor_msgs/Image`. */ +export interface DecodedCompressedDepth { + encoding: DepthImageEncoding; + width: number; + height: number; + step: number; + isBigEndian: false; + data: Uint8Array; +} + +export function findPngOffset(data: Uint8Array): number { + return findPngSignatureOffset(data); +} + +export function parseCompressedDepthHeader(data: Uint8Array, pngOffset: number): CompressedDepthHeader { + if (pngOffset < 4) { + return { compressionFormat: 0, depthParam: [0, 0] }; + } + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return { + compressionFormat: view.getInt32(0, true), + depthParam: [view.getFloat32(4, true), view.getFloat32(8, true)], + }; +} + +async function decodePngPayload(data: Uint8Array): Promise<{ width: number; height: number; data: Uint8Array }> { + const pngOffset = findPngOffset(data); + if (pngOffset < 0) { + throw new Error('Compressed depth payload does not contain a PNG signature'); + } + const decoded = await decodeGrayscale16Png(data.subarray(pngOffset)); + return { + width: decoded.width, + height: decoded.height, + data: decoded.data, + }; +} + +async function decode16uc1(data: Uint8Array): Promise { + const { width, height, data: pixelBytes } = await decodePngPayload(data); + return { + encoding: '16uc1', + width, + height, + step: width * 2, + isBigEndian: false, + data: pixelBytes, + }; +} + +async function decode32fc1(data: Uint8Array, header: CompressedDepthHeader): Promise { + const { width, height, data: pixelBytes } = await decodePngPayload(data); + const [depthQuantA, depthQuantB] = header.depthParam; + const out = new Uint8Array(width * height * 4); + const outView = new DataView(out.buffer, out.byteOffset, out.byteLength); + const sampleView = new DataView(pixelBytes.buffer, pixelBytes.byteOffset, pixelBytes.byteLength); + + for (let i = 0; i < width * height; i++) { + const raw = sampleView.getUint16(i * 2, true); + let depth = 0; + if (raw !== 0) { + depth = depthQuantA / (raw - depthQuantB); + if (!Number.isFinite(depth)) { + depth = 0; + } + } + outView.setFloat32(i * 4, depth, true); + } + + return { + encoding: '32fc1', + width, + height, + step: width * 4, + isBigEndian: false, + data: out, + }; +} + +export async function decodeCompressedDepth(data: Uint8Array, format: string): Promise { + if (!isCompressedDepthFormat(format)) { + throw new Error(`Not a compressed depth format: ${format}`); + } + + const parsed = parseCompressedImageFormat(format); + if (parsed.depthCodec === 'rvl') { + throw new Error('RVL compressed depth is not supported yet'); + } + + const encoding = depthEncodingFromFormat(format); + if (!encoding) { + throw new Error(`Unsupported compressed depth encoding in format: ${format}`); + } + + const pngOffset = findPngOffset(data); + if (pngOffset < 0) { + throw new Error('Compressed depth payload is missing PNG data'); + } + if (pngOffset < ROS_COMPRESSED_DEPTH_HEADER_SIZE && data.byteLength < ROS_COMPRESSED_DEPTH_HEADER_SIZE) { + throw new Error('Compressed depth payload is too small'); + } + + const header = parseCompressedDepthHeader(data, pngOffset); + if (encoding === '16uc1') { + return decode16uc1(data); + } + return decode32fc1(data, header); +} diff --git a/src/features/panels/Image/core/depthColorDefaults.test.ts b/src/features/panels/Image/core/depthColorDefaults.test.ts new file mode 100644 index 0000000..ccb051b --- /dev/null +++ b/src/features/panels/Image/core/depthColorDefaults.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest'; +import { + DEFAULT_DEPTH_16UC1_COLOR_MAX, + DEFAULT_DEPTH_16UC1_COLOR_MIN, + applyDepthTopicPreset, + defaultDepthMinValue, + getDepthColormapDefaults, + isDepthTopicName, + resolveDepthColorRange, +} from './depthColorDefaults'; +import { defaultImageConfig } from '../defaults'; + +describe('getDepthColormapDefaults', () => { + it('returns depth topic preset', () => { + expect(getDepthColormapDefaults('/camera/depth/image_raw/compressed')).toEqual({ + colorMode: 'colormap', + colorMap: 'turbo', + minValue: DEFAULT_DEPTH_16UC1_COLOR_MIN, + maxValue: DEFAULT_DEPTH_16UC1_COLOR_MAX, + }); + }); + + it('returns wrist preset for non-depth wrist topics', () => { + expect(getDepthColormapDefaults('/robot/wrist_cam/image')).toMatchObject({ + minValue: 0, + maxValue: 1000, + }); + }); + + it('returns null for unrelated topics', () => { + expect(getDepthColormapDefaults('/camera/rgb/image_raw')).toBeNull(); + }); +}); + +describe('resolveDepthColorRange', () => { + it('uses ROS-style defaults for 16uc1 when unset', () => { + expect(resolveDepthColorRange('16uc1', {})).toEqual({ + minValue: DEFAULT_DEPTH_16UC1_COLOR_MIN, + maxValue: DEFAULT_DEPTH_16UC1_COLOR_MAX, + }); + }); + + it('respects explicit user overrides', () => { + expect(resolveDepthColorRange('16uc1', { minValue: 0, maxValue: 5000 })).toEqual({ + minValue: 0, + maxValue: 5000, + }); + }); + + it('uses topic preset when topic is provided', () => { + expect(resolveDepthColorRange('mono16', {}, '/depth/compressed')).toEqual({ + minValue: DEFAULT_DEPTH_16UC1_COLOR_MIN, + maxValue: DEFAULT_DEPTH_16UC1_COLOR_MAX, + }); + }); +}); + +describe('isDepthTopicName', () => { + it('matches common depth topic patterns', () => { + expect(isDepthTopicName('/camera/aligned_depth_to_color/image_raw')).toBe(true); + expect(isDepthTopicName('/rgb/image_raw')).toBe(false); + }); +}); + +describe('defaultDepthMinValue', () => { + it('returns 200 for depth encodings without an explicit topic preset', () => { + expect(defaultDepthMinValue('16UC1')).toBe(DEFAULT_DEPTH_16UC1_COLOR_MIN); + }); +}); + +describe('applyDepthTopicPreset', () => { + it('merges depth colormap preset when topic matches', () => { + const next = applyDepthTopicPreset('/camera/depth/compressed', { + ...defaultImageConfig(), + topic: '', + colorMode: 'rgb', + }); + expect(next.topic).toBe('/camera/depth/compressed'); + expect(next.colorMode).toBe('colormap'); + expect(next.minValue).toBe(DEFAULT_DEPTH_16UC1_COLOR_MIN); + expect(next.maxValue).toBe(DEFAULT_DEPTH_16UC1_COLOR_MAX); + }); + + it('only updates topic for non-depth sources', () => { + const base = { ...defaultImageConfig(), colorMode: 'rgb' as const, minValue: 42 }; + const next = applyDepthTopicPreset('/camera/rgb/image_raw', base); + expect(next).toEqual({ ...base, topic: '/camera/rgb/image_raw' }); + }); +}); diff --git a/src/features/panels/Image/core/depthColorDefaults.ts b/src/features/panels/Image/core/depthColorDefaults.ts new file mode 100644 index 0000000..250e568 --- /dev/null +++ b/src/features/panels/Image/core/depthColorDefaults.ts @@ -0,0 +1,127 @@ +/** + * Default colormap range and topic presets for depth image rendering. + * + * 16UC1 compressed depth is typically millimeters. Defaults follow ROS + * image_view / RViz conventions: min 200 mm filters near-field noise and + * invalid zeros; max 10000 mm (10 m) spans the full turbo colormap for + * indoor sensors. + */ + +import type { ImageConfig } from '../defaults'; +import type { RawImageDecodeOptions } from './imageColorMode'; + +/** Lower bound (mm) for 16UC1 turbo colormap when unset. */ +export const DEFAULT_DEPTH_16UC1_COLOR_MIN = 200; + +/** Upper bound (mm) for 16UC1 turbo colormap when unset. */ +export const DEFAULT_DEPTH_16UC1_COLOR_MAX = 10000; + +/** Default max for 32FC1 float depth (meters). */ +export const DEFAULT_DEPTH_32FC1_MAX = 1; + +const DEPTH_TOPIC_RE = /depth|aligned_depth|compressed_depth/i; +const WRIST_TOPIC_RE = /wrist|hand|left|right|gripper|eef|end_effector/; + +export function normalizedDepthEncoding(encoding: string): string { + return encoding.trim().toLowerCase(); +} + +export function is16BitDepthEncoding(encoding: string): boolean { + const lower = normalizedDepthEncoding(encoding); + return lower === '16uc1' || lower === 'mono16'; +} + +export function is32FloatDepthEncoding(encoding: string): boolean { + return normalizedDepthEncoding(encoding) === '32fc1'; +} + +export function isDepthScalarEncoding(encoding: string): boolean { + return is16BitDepthEncoding(encoding) || is32FloatDepthEncoding(encoding); +} + +export function isDepthTopicName(topic: string): boolean { + return DEPTH_TOPIC_RE.test(topic); +} + +export interface DepthColormapPreset { + colorMode: 'colormap'; + colorMap: 'turbo'; + minValue: number; + maxValue: number; +} + +/** + * Colormap preset inferred from topic name when the user picks a source. + * Returns null for generic RGB or unknown topics. + */ +export function getDepthColormapDefaults(topic: string): DepthColormapPreset | null { + const tp = topic.trim().toLowerCase(); + if (!tp) { + return null; + } + + const isDepthTopic = isDepthTopicName(tp); + const isWrist = !isDepthTopic && WRIST_TOPIC_RE.test(tp); + const base = { colorMode: 'colormap' as const, colorMap: 'turbo' as const }; + + if (isDepthTopic) { + return { ...base, minValue: DEFAULT_DEPTH_16UC1_COLOR_MIN, maxValue: DEFAULT_DEPTH_16UC1_COLOR_MAX }; + } + if (isWrist) { + return { ...base, minValue: 0, maxValue: 1000 }; + } + return null; +} + +export function defaultDepthMinValue(encoding: string, topic?: string): number { + const topicPreset = topic ? getDepthColormapDefaults(topic) : null; + if (topicPreset) { + return topicPreset.minValue; + } + if (is16BitDepthEncoding(encoding)) { + return DEFAULT_DEPTH_16UC1_COLOR_MIN; + } + return 0; +} + +export function defaultDepthMaxValue(encoding: string, topic?: string): number { + const topicPreset = topic ? getDepthColormapDefaults(topic) : null; + if (topicPreset) { + return topicPreset.maxValue; + } + if (is32FloatDepthEncoding(encoding)) { + return DEFAULT_DEPTH_32FC1_MAX; + } + if (is16BitDepthEncoding(encoding)) { + return DEFAULT_DEPTH_16UC1_COLOR_MAX; + } + return 65535; +} + +/** Resolve effective min/max for colormap decoding, honoring explicit panel settings. */ +export function resolveDepthColorRange( + encoding: string, + options?: Partial, + topic?: string, +): { minValue: number; maxValue: number } { + return { + minValue: options?.minValue ?? defaultDepthMinValue(encoding, topic), + maxValue: options?.maxValue ?? defaultDepthMaxValue(encoding, topic), + }; +} + +/** Apply topic-based depth colormap preset when the user selects a new source. */ +export function applyDepthTopicPreset(topic: string, config: ImageConfig): ImageConfig { + const preset = getDepthColormapDefaults(topic); + if (!preset) { + return { ...config, topic }; + } + return { + ...config, + topic, + colorMode: preset.colorMode, + colorMap: preset.colorMap, + minValue: preset.minValue, + maxValue: preset.maxValue, + }; +} diff --git a/src/features/panels/Image/core/depthColorization.test.ts b/src/features/panels/Image/core/depthColorization.test.ts new file mode 100644 index 0000000..4ead011 --- /dev/null +++ b/src/features/panels/Image/core/depthColorization.test.ts @@ -0,0 +1,84 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { decodeCompressedDepth } from './compressedDepthDecoder'; +import { decodeRawImage } from './rawDecoders'; +import { getColorConverter, DEFAULT_RAW_IMAGE_DECODE_OPTIONS } from './imageColorMode'; +import { + DEFAULT_DEPTH_16UC1_COLOR_MAX, + DEFAULT_DEPTH_16UC1_COLOR_MIN, +} from './depthColorDefaults'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURE_16UC1 = path.join(__dirname, '../../../../../test-fixtures/image/compressed-depth-16uc1.bin'); + +function isDarkBlue(r: number, g: number, b: number): boolean { + return b > r && b > g && b > 40; +} + +describe('compressed depth colorization', () => { + it('maps near-zero depths to dark blue and keeps valid depths distinguishable', async () => { + const fixture = fs.readFileSync(FIXTURE_16UC1); + const decoded = await decodeCompressedDepth(fixture, '16UC1; compressedDepth'); + const view = new DataView(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength); + const { width, height } = decoded; + + const rgba = new Uint8ClampedArray(width * height * 4); + decodeRawImage( + { + encoding: '16uc1', + width, + height, + step: width * 2, + is_bigendian: false, + data: decoded.data, + }, + rgba, + { colorMode: 'colormap', colorMap: 'turbo' }, + ); + + let zeroCount = 0; + let zeroDarkBlue = 0; + let minDepth = Infinity; + let maxDepth = 0; + let minIndex = -1; + let maxIndex = -1; + for (let i = 0; i < width * height; i++) { + const value = view.getUint16(i * 2, true); + const o = i * 4; + if (value === 0) { + zeroCount++; + if (isDarkBlue(rgba[o]!, rgba[o + 1]!, rgba[o + 2]!)) zeroDarkBlue++; + } + if (value >= DEFAULT_DEPTH_16UC1_COLOR_MIN && value <= DEFAULT_DEPTH_16UC1_COLOR_MAX) { + if (value < minDepth) { + minDepth = value; + minIndex = i; + } + if (value > maxDepth) { + maxDepth = value; + maxIndex = i; + } + } + } + + expect(zeroCount).toBeGreaterThan(0); + expect(zeroDarkBlue / zeroCount).toBeGreaterThan(0.9); + expect(minIndex).toBeGreaterThanOrEqual(0); + expect(maxIndex).toBeGreaterThanOrEqual(0); + expect(maxDepth - minDepth).toBeGreaterThan(500); + + const convert = getColorConverter( + { ...DEFAULT_RAW_IMAGE_DECODE_OPTIONS, colorMode: 'colormap', colorMap: 'turbo' }, + DEFAULT_DEPTH_16UC1_COLOR_MIN, + DEFAULT_DEPTH_16UC1_COLOR_MAX, + ); + const nearPx = { r: 0, g: 0, b: 0, a: 0 }; + const farPx = { r: 0, g: 0, b: 0, a: 0 }; + convert(nearPx, minDepth); + convert(farPx, maxDepth); + expect(Math.abs(nearPx.g - farPx.g)).toBeGreaterThan(0.05); + expect(Math.abs(nearPx.b - farPx.b)).toBeGreaterThan(0.05); + }); +}); diff --git a/src/features/panels/Image/core/grayscale16PngDecoder.test.ts b/src/features/panels/Image/core/grayscale16PngDecoder.test.ts new file mode 100644 index 0000000..807ed5e --- /dev/null +++ b/src/features/panels/Image/core/grayscale16PngDecoder.test.ts @@ -0,0 +1,104 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { decodeGrayscale16Png } from './grayscale16PngDecoder'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURE_16UC1 = path.join(__dirname, '../../../../../test-fixtures/image/compressed-depth-16uc1.bin'); + +describe('decodeGrayscale16Png', () => { + it('decodes 16-bit grayscale PNG bytes from a compressed depth fixture', async () => { + const payload = fs.readFileSync(FIXTURE_16UC1); + const pngOffset = payload.indexOf(Buffer.from([0x89, 0x50, 0x4e, 0x47])); + expect(pngOffset).toBeGreaterThanOrEqual(0); + + const decoded = await decodeGrayscale16Png(payload.subarray(pngOffset)); + expect(decoded.width).toBe(640); + expect(decoded.height).toBe(480); + expect(decoded.data.byteLength).toBe(640 * 480 * 2); + + const view = new DataView(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength); + expect(view.getUint16(0, true)).toBeGreaterThan(0); + expect(view.getUint16(2, true)).toBeGreaterThan(0); + }); + + it('preserves 16-bit sample values through unfilter and byte-order swap', async () => { + const width = 2; + const height = 1; + // Filter None; PNG stores 16-bit grayscale big-endian: 1000, 2000 mm. + const filtered = new Uint8Array([0, 0x03, 0xe8, 0x07, 0xd0]); + + const idat = await (async () => { + if (typeof CompressionStream === 'undefined') { + return null; + } + const stream = new Blob([filtered.buffer]).stream().pipeThrough(new CompressionStream('deflate')); + return new Uint8Array(await new Response(stream).arrayBuffer()); + })(); + + if (!idat) { + return; + } + + const ihdr = new Uint8Array([ + 0x00, 0x00, 0x00, 0x02, // width + 0x00, 0x00, 0x00, 0x01, // height + 0x10, // bit depth 16 + 0x00, // grayscale + 0x00, + 0x00, + 0x00, + ]); + const ihdrChunk = wrapChunk('IHDR', ihdr); + const idatChunk = wrapChunk('IDAT', idat); + const iendChunk = wrapChunk('IEND', new Uint8Array(0)); + const png = concatBytes([ + new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + ihdrChunk, + idatChunk, + iendChunk, + ]); + + const decoded = await decodeGrayscale16Png(png); + const view = new DataView(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength); + expect(view.getUint16(0, true)).toBe(1000); + expect(view.getUint16(2, true)).toBe(2000); + }); +}); + +function crc32(data: Uint8Array): number { + let c = 0xffffffff; + for (let i = 0; i < data.length; i++) { + c ^= data[i]!; + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + } + return (c ^ 0xffffffff) >>> 0; +} + +function wrapChunk(type: string, data: Uint8Array): Uint8Array { + const out = new Uint8Array(12 + data.length); + const view = new DataView(out.buffer); + view.setUint32(0, data.length, false); + out[4] = type.charCodeAt(0)!; + out[5] = type.charCodeAt(1)!; + out[6] = type.charCodeAt(2)!; + out[7] = type.charCodeAt(3)!; + out.set(data, 8); + const crcInput = out.subarray(4, 8 + data.length); + view.setUint32(8 + data.length, crc32(crcInput), false); + return out; +} + +function concatBytes(parts: Uint8Array[]): Uint8Array { + const total = parts.reduce((sum, part) => sum + part.length, 0); + const out = new Uint8Array(total); + let offset = 0; + for (const part of parts) { + out.set(part, offset); + offset += part.length; + } + return out; +} diff --git a/src/features/panels/Image/core/grayscale16PngDecoder.ts b/src/features/panels/Image/core/grayscale16PngDecoder.ts new file mode 100644 index 0000000..f26919a --- /dev/null +++ b/src/features/panels/Image/core/grayscale16PngDecoder.ts @@ -0,0 +1,135 @@ +/** + * Minimal 16-bit grayscale PNG decoder for ROS `compressedDepth` payloads. + * Uses the platform `DecompressionStream` API (no extra dependencies). + */ + +import { unfilter16BitGrayscaleScanlines } from './png16ScanlineUnfilter'; + +/** Standard PNG file signature. */ +export const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +export interface Grayscale16PngImage { + width: number; + height: number; + /** Little-endian 16-bit samples, `width * height * 2` bytes. */ + data: Uint8Array; +} + +/** Locate the first PNG signature inside a buffer (e.g. after a ROS depth header). */ +export function findPngSignatureOffset(data: Uint8Array): number { + const limit = Math.max(0, data.byteLength - PNG_SIGNATURE.byteLength); + for (let offset = 0; offset <= limit; offset++) { + let matched = true; + for (let i = 0; i < PNG_SIGNATURE.byteLength; i++) { + if (data[offset + i] !== PNG_SIGNATURE[i]) { + matched = false; + break; + } + } + if (matched) { + return offset; + } + } + return -1; +} + +function readU32Be(view: DataView, offset: number): number { + return view.getUint32(offset, false); +} + +function concatIdatChunks(png: Uint8Array): Uint8Array { + const view = new DataView(png.buffer, png.byteOffset, png.byteLength); + const chunks: Uint8Array[] = []; + let offset = PNG_SIGNATURE.byteLength; + + while (offset + 8 <= png.byteLength) { + const length = readU32Be(view, offset); + const type = + String.fromCharCode(png[offset + 4]!, png[offset + 5]!, png[offset + 6]!, png[offset + 7]!); + const dataStart = offset + 8; + const dataEnd = dataStart + length; + if (dataEnd + 4 > png.byteLength) { + throw new Error('PNG chunk exceeds buffer bounds'); + } + if (type === 'IDAT') { + chunks.push(png.subarray(dataStart, dataEnd)); + } + if (type === 'IEND') { + break; + } + offset = dataEnd + 4; + } + + if (chunks.length === 0) { + throw new Error('PNG is missing IDAT chunks'); + } + + const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + const out = new Uint8Array(total); + let writeOffset = 0; + for (const chunk of chunks) { + out.set(chunk, writeOffset); + writeOffset += chunk.byteLength; + } + return out; +} + +async function inflateZlib(zlibData: Uint8Array): Promise { + if (typeof DecompressionStream === 'undefined') { + throw new Error('DecompressionStream is not supported in this environment'); + } + const owned = new Uint8Array(zlibData.byteLength); + owned.set(zlibData); + const stream = new Blob([owned.buffer]).stream().pipeThrough(new DecompressionStream('deflate')); + return new Uint8Array(await new Response(stream).arrayBuffer()); +} + +function parseIhdr(png: Uint8Array): { width: number; height: number; bitDepth: number; colorType: number } { + if (png.byteLength < PNG_SIGNATURE.byteLength + 8 + 13) { + throw new Error('PNG buffer is too small'); + } + for (let i = 0; i < PNG_SIGNATURE.byteLength; i++) { + if (png[i] !== PNG_SIGNATURE[i]) { + throw new Error('Invalid PNG signature'); + } + } + + const view = new DataView(png.buffer, png.byteOffset, png.byteLength); + const firstLength = readU32Be(view, PNG_SIGNATURE.byteLength); + const firstType = String.fromCharCode( + png[PNG_SIGNATURE.byteLength + 4]!, + png[PNG_SIGNATURE.byteLength + 5]!, + png[PNG_SIGNATURE.byteLength + 6]!, + png[PNG_SIGNATURE.byteLength + 7]!, + ); + if (firstType !== 'IHDR' || firstLength !== 13) { + throw new Error('PNG is missing IHDR chunk'); + } + + const ihdrOffset = PNG_SIGNATURE.byteLength + 8; + return { + width: readU32Be(view, ihdrOffset), + height: readU32Be(view, ihdrOffset + 4), + bitDepth: png[ihdrOffset + 8]!, + colorType: png[ihdrOffset + 9]!, + }; +} + +/** Decode a 16-bit grayscale PNG into little-endian depth sample bytes. */ +export async function decodeGrayscale16Png(png: Uint8Array): Promise { + const { width, height, bitDepth, colorType } = parseIhdr(png); + if (width <= 0 || height <= 0) { + throw new Error(`Invalid PNG dimensions: ${width}x${height}`); + } + if (bitDepth !== 16 || colorType !== 0) { + throw new Error( + `Compressed depth PNG must be 16-bit grayscale (got depth=${bitDepth}, colorType=${colorType})`, + ); + } + + const idat = concatIdatChunks(png); + const filtered = await inflateZlib(idat); + const data = unfilter16BitGrayscaleScanlines(filtered, width, height); + + return { width, height, data }; +} diff --git a/src/features/panels/Image/core/imageColorMode.test.ts b/src/features/panels/Image/core/imageColorMode.test.ts new file mode 100644 index 0000000..880fada --- /dev/null +++ b/src/features/panels/Image/core/imageColorMode.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { DEFAULT_RAW_IMAGE_DECODE_OPTIONS, getColorConverter } from './imageColorMode'; + +describe('getColorConverter turbo', () => { + it('does not collapse mid-range values to pure green', () => { + const convert = getColorConverter( + { ...DEFAULT_RAW_IMAGE_DECODE_OPTIONS, colorMode: 'colormap', colorMap: 'turbo' }, + 0, + 10000, + ); + const px = { r: 0, g: 0, b: 0, a: 0 }; + convert(px, 3537); + const g = Math.round(px.g * 255); + const r = Math.round(px.r * 255); + const b = Math.round(px.b * 255); + expect(g).toBeLessThan(255); + expect(r + g + b).toBeGreaterThan(80); + expect(b).toBeGreaterThan(0); + }); + + it('maps zero to dark blue at the colormap minimum', () => { + const convert = getColorConverter( + { ...DEFAULT_RAW_IMAGE_DECODE_OPTIONS, colorMode: 'colormap', colorMap: 'turbo' }, + 0, + 10000, + ); + const px = { r: 0, g: 0, b: 0, a: 0 }; + convert(px, 0); + expect(Math.round(px.b * 255)).toBeGreaterThan(Math.round(px.g * 255)); + expect(Math.round(px.r * 255)).toBeLessThan(80); + }); +}); diff --git a/src/features/panels/Image/core/imageColorMode.ts b/src/features/panels/Image/core/imageColorMode.ts index 9c20954..951df7d 100644 --- a/src/features/panels/Image/core/imageColorMode.ts +++ b/src/features/panels/Image/core/imageColorMode.ts @@ -1,6 +1,5 @@ /** - * Depth / mono16 colorization (Foxglove-style), without THREE.js dependency. - * Ported from studio/packages/studio-base/src/panels/ThreeDeeRender/renderables/colorMode.ts + * Scalar depth colorization (turbo / rainbow / gradient) for mono16 and float depth. */ export interface ColorRGBA { @@ -121,11 +120,12 @@ function turboLinear(output: ColorRGBA, pct: number): void { const v4r = 1 * kRedVec4[0] + x * kRedVec4[1] + x2 * kRedVec4[2] + x3 * kRedVec4[3]; const v4g = 1 * kGreenVec4[0] + x * kGreenVec4[1] + x2 * kGreenVec4[2] + x3 * kGreenVec4[3]; const v4b = 1 * kBlueVec4[0] + x * kBlueVec4[1] + x2 * kBlueVec4[2] + x3 * kBlueVec4[3]; - const v2x = x2; - const v2y = x3; - const dot2r = v2x * kRedVec2[0] + v2y * kRedVec2[1]; - const dot2g = v2x * kGreenVec2[0] + v2y * kGreenVec2[1]; - const dot2b = v2x * kBlueVec2[0] + v2y * kBlueVec2[1]; + // GLSL: vec2 v2 = v4.zw * v4.z → (x^4, x^5) + const x4 = x2 * x2; + const x5 = x3 * x2; + const dot2r = x4 * kRedVec2[0] + x5 * kRedVec2[1]; + const dot2g = x4 * kGreenVec2[0] + x5 * kGreenVec2[1]; + const dot2b = x4 * kBlueVec2[0] + x5 * kBlueVec2[1]; output.r = clamp(v4r + dot2r, 0, 1); output.g = clamp(v4g + dot2g, 0, 1); output.b = clamp(v4b + dot2b, 0, 1); diff --git a/src/features/panels/Image/core/imageTypes.test.ts b/src/features/panels/Image/core/imageTypes.test.ts index 005fe34..5da4ce5 100644 --- a/src/features/panels/Image/core/imageTypes.test.ts +++ b/src/features/panels/Image/core/imageTypes.test.ts @@ -1,12 +1,16 @@ import { describe, expect, it } from 'vitest'; import { + depthEncodingFromFormat, getCompressedKind, + isCompressedDepthFormat, isCompressedVideoMessage, isH264CompressedFrameMessage, isImagePanelTopicSchema, isRawImageTopicSchema, normalizeCompressedMime, + parseCompressedImageFormat, prepareImageWorkerBytes, + sniffCompressedMime, snapshotBytes, } from './imageTypes'; @@ -114,11 +118,74 @@ describe('isImagePanelTopicSchema', () => { }); }); +describe('parseCompressedImageFormat', () => { + it('parses jpeg color transport strings', () => { + expect(parseCompressedImageFormat('rgb8; jpeg compressed bgr8')).toEqual({ + rawEncoding: 'rgb8', + transport: 'jpeg compressed bgr8', + depthCodec: undefined, + bitmapKind: 'jpeg', + }); + }); + + it('parses compressed depth transport strings', () => { + expect(parseCompressedImageFormat('16UC1; compressedDepth')).toEqual({ + rawEncoding: '16uc1', + transport: 'compressedDepth', + depthCodec: 'png', + bitmapKind: null, + }); + expect(parseCompressedImageFormat('32FC1; compressedDepth')).toMatchObject({ + rawEncoding: '32fc1', + depthCodec: 'png', + }); + expect(parseCompressedImageFormat('16UC1; compressedDepth rvl')).toMatchObject({ + rawEncoding: '16uc1', + depthCodec: 'rvl', + }); + }); +}); + +describe('isCompressedDepthFormat', () => { + it('recognizes ROS compressed depth formats', () => { + expect(isCompressedDepthFormat('16UC1; compressedDepth')).toBe(true); + expect(isCompressedDepthFormat('32FC1; compressedDepth')).toBe(true); + expect(isCompressedDepthFormat('rgb8; jpeg compressed bgr8')).toBe(false); + }); +}); + +describe('depthEncodingFromFormat', () => { + it('returns normalized depth encodings for compressed depth formats', () => { + expect(depthEncodingFromFormat('16UC1; compressedDepth')).toBe('16uc1'); + expect(depthEncodingFromFormat('32FC1; compressedDepth')).toBe('32fc1'); + expect(depthEncodingFromFormat('rgb8; jpeg compressed bgr8')).toBeNull(); + }); +}); + +describe('sniffCompressedMime', () => { + it('detects jpeg and png magic bytes', () => { + expect(sniffCompressedMime(new Uint8Array([0xff, 0xd8, 0xff, 0xe0]))).toBe('image/jpeg'); + expect( + sniffCompressedMime(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])), + ).toBe('image/png'); + }); +}); + describe('normalizeCompressedMime', () => { it('falls back to JPEG for raw-style format tokens on CompressedImage', () => { expect(getCompressedKind('bgr8')).toBeNull(); - expect(normalizeCompressedMime('bgr8')).toBe('image/jpeg'); - expect(normalizeCompressedMime('rgb8')).toBe('image/jpeg'); + expect(normalizeCompressedMime('bgr8', new Uint8Array([0xff, 0xd8, 0xff, 0xe0]))).toBe('image/jpeg'); + expect(normalizeCompressedMime('rgb8', new Uint8Array([0xff, 0xd8, 0xff, 0xe0]))).toBe('image/jpeg'); + }); + + it('does not treat compressed depth formats as JPEG', () => { + expect(() => normalizeCompressedMime('16UC1; compressedDepth')).toThrow(/Compressed depth/); + }); + + it('sniffs PNG when raw encoding token is unknown but payload is PNG', () => { + expect( + normalizeCompressedMime('16UC1', new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])), + ).toBe('image/png'); }); it('still recognizes explicit codec hints in compound format strings', () => { diff --git a/src/features/panels/Image/core/imageTypes.ts b/src/features/panels/Image/core/imageTypes.ts index efe389d..3b29880 100644 --- a/src/features/panels/Image/core/imageTypes.ts +++ b/src/features/panels/Image/core/imageTypes.ts @@ -123,8 +123,89 @@ export type CompressedImageKind = | 'h264' | null; +export type CompressedDepthCodec = 'png' | 'rvl'; + +export type DepthImageEncoding = '16uc1' | '32fc1'; + +export interface ParsedCompressedImageFormat { + rawEncoding?: string; + transport?: string; + depthCodec?: CompressedDepthCodec; + bitmapKind: CompressedImageKind; +} + const COMPRESSED_KIND_RE = /\b(jpeg|jpg|png|webp|gif|avif|bmp|h264)\b/i; +const RAW_ENCODING_TOKENS = new Set([ + '16uc1', + '32fc1', + 'mono16', + 'mono8', + '8uc1', + 'rgb8', + 'bgr8', + 'rgba8', + 'bgra8', + '8uc3', +]); + +function normalizeFormatToken(token: string): string { + return token.trim().toLowerCase(); +} + +function depthEncodingFromRawToken(token: string): DepthImageEncoding | null { + const lower = normalizeFormatToken(token); + if (lower === '16uc1' || lower === 'mono16') { + return '16uc1'; + } + if (lower === '32fc1') { + return '32fc1'; + } + return null; +} + +function parseCompressedDepthTransport(transport: string): CompressedDepthCodec | undefined { + const lower = transport.trim().toLowerCase(); + if (!lower.includes('compresseddepth')) { + return undefined; + } + if (/\brvl\b/.test(lower)) { + return 'rvl'; + } + return 'png'; +} + +/** + * Structured parse of `sensor_msgs/CompressedImage.format`. + * Handles `rgb8; jpeg compressed bgr8`, `16UC1; compressedDepth`, etc. + */ +export function parseCompressedImageFormat(format: string): ParsedCompressedImageFormat { + const trimmed = format.trim(); + const parts = trimmed.split(';').map((part) => part.trim()).filter(Boolean); + const rawEncoding = parts[0] ? normalizeFormatToken(parts[0]) : undefined; + const transport = parts.length > 1 ? parts.slice(1).join(';').trim() : undefined; + const depthCodec = transport ? parseCompressedDepthTransport(transport) : undefined; + + return { + rawEncoding, + transport, + depthCodec, + bitmapKind: getCompressedKind(trimmed), + }; +} + +export function isCompressedDepthFormat(format: string): boolean { + const parsed = parseCompressedImageFormat(format); + return parsed.depthCodec != null && depthEncodingFromRawToken(parsed.rawEncoding ?? '') != null; +} + +export function depthEncodingFromFormat(format: string): DepthImageEncoding | null { + if (!isCompressedDepthFormat(format)) { + return null; + } + return depthEncodingFromRawToken(parseCompressedImageFormat(format).rawEncoding ?? ''); +} + /** * Classify `sensor_msgs/CompressedImage.format` for decode routing. * Handles `rgb8; jpeg compressed bgr8`, `jpeg`, `image/png`, `h264`, etc. @@ -151,7 +232,44 @@ export function getCompressedKind(format: string): CompressedImageKind { return null; } -export function normalizeCompressedMime(format: string): string { +export function sniffCompressedMime(data: Uint8Array): string | null { + if (data.byteLength >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) { + return 'image/jpeg'; + } + if ( + data.byteLength >= 8 && + data[0] === 0x89 && + data[1] === 0x50 && + data[2] === 0x4e && + data[3] === 0x47 && + data[4] === 0x0d && + data[5] === 0x0a && + data[6] === 0x1a && + data[7] === 0x0a + ) { + return 'image/png'; + } + if ( + data.byteLength >= 12 && + data[0] === 0x52 && + data[1] === 0x49 && + data[2] === 0x46 && + data[3] === 0x46 && + data[8] === 0x57 && + data[9] === 0x45 && + data[10] === 0x42 && + data[11] === 0x50 + ) { + return 'image/webp'; + } + return null; +} + +export function normalizeCompressedMime(format: string, data?: Uint8Array): string { + if (isCompressedDepthFormat(format)) { + throw new Error(`Compressed depth format must not use bitmap MIME routing: ${format}`); + } + const kind = getCompressedKind(format); if (kind === 'jpeg') { return 'image/jpeg'; @@ -167,7 +285,7 @@ export function normalizeCompressedMime(format: string): string { ?.toLowerCase(); if (!firstToken) { - return 'image/jpeg'; + return data ? sniffCompressedMime(data) ?? 'image/jpeg' : 'image/jpeg'; } if (firstToken.startsWith('image/')) { return firstToken; @@ -175,8 +293,15 @@ export function normalizeCompressedMime(format: string): string { if (firstToken === 'jpg') { return 'image/jpeg'; } + if (RAW_ENCODING_TOKENS.has(firstToken)) { + const sniffed = data ? sniffCompressedMime(data) : null; + if (sniffed) { + return sniffed; + } + throw new Error(`Unsupported compressed image format token: ${format}`); + } // Unknown tokens (e.g. `bgr8` mislabeled on CompressedImage): many bags still carry JPEG bytes. - return 'image/jpeg'; + return data ? sniffCompressedMime(data) ?? 'image/jpeg' : 'image/jpeg'; } /** diff --git a/src/features/panels/Image/core/png16ScanlineUnfilter.test.ts b/src/features/panels/Image/core/png16ScanlineUnfilter.test.ts new file mode 100644 index 0000000..e939949 --- /dev/null +++ b/src/features/panels/Image/core/png16ScanlineUnfilter.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { unfilter16BitGrayscaleScanlines } from './png16ScanlineUnfilter'; + +describe('unfilter16BitGrayscaleScanlines', () => { + it('reconstructs Sub-filtered rows using 2-byte pixel stride', () => { + const width = 2; + const height = 1; + // Sub: first pixel stored raw (1000), second pixel as deltas from pixel0 high byte. + const inflated = new Uint8Array([1, 0x03, 0xe8, 0x04, 0xe8]); + + const out = unfilter16BitGrayscaleScanlines(inflated, width, height); + const view = new DataView(out.buffer, out.byteOffset, out.byteLength); + expect(view.getUint16(0, true)).toBe(1000); + expect(view.getUint16(2, true)).toBe(2000); + }); + + it('byte-swaps to little-endian after reconstruction', () => { + const inflated = new Uint8Array([ + 0, // None + 0x03, + 0xe8, + 0x07, + 0xd0, + ]); + + const out = unfilter16BitGrayscaleScanlines(inflated, 2, 1); + expect(out[0]).toBe(0xe8); + expect(out[1]).toBe(0x03); + }); +}); diff --git a/src/features/panels/Image/core/png16ScanlineUnfilter.ts b/src/features/panels/Image/core/png16ScanlineUnfilter.ts new file mode 100644 index 0000000..c714dfe --- /dev/null +++ b/src/features/panels/Image/core/png16ScanlineUnfilter.ts @@ -0,0 +1,175 @@ +/** + * PNG scanline reconstruction for 16-bit grayscale (color type 0). + * + * ROS `compressedDepth` PNGs use 2 bytes per pixel. Sub/Avg/Paeth filters must + * reference the previous *pixel* (2 bytes), not the previous byte — otherwise + * decoded depth values show horizontal striping. + */ + +const GRAYSCALE_16_BPP = 2; + +function paethPredictor(a: number, b: number, c: number): number { + const p = a + b - c; + const pa = Math.abs(p - a); + const pb = Math.abs(p - b); + const pc = Math.abs(p - c); + if (pa <= pb && pa <= pc) return a; + if (pb <= pc) return b; + return c; +} + +function unfilterNone(currentLine: Uint8Array, newLine: Uint8Array, bytesPerLine: number): void { + for (let i = 0; i < bytesPerLine; i++) { + newLine[i] = currentLine[i]!; + } +} + +function unfilterSub( + currentLine: Uint8Array, + newLine: Uint8Array, + bytesPerLine: number, + bytesPerPixel: number, +): void { + let i = 0; + for (; i < bytesPerPixel; i++) { + newLine[i] = currentLine[i]!; + } + for (; i < bytesPerLine; i++) { + newLine[i] = (currentLine[i]! + newLine[i - bytesPerPixel]!) & 0xff; + } +} + +function unfilterUp( + currentLine: Uint8Array, + newLine: Uint8Array, + prevLine: Uint8Array, + bytesPerLine: number, +): void { + if (prevLine.length === 0) { + for (let i = 0; i < bytesPerLine; i++) { + newLine[i] = currentLine[i]!; + } + return; + } + for (let i = 0; i < bytesPerLine; i++) { + newLine[i] = (currentLine[i]! + prevLine[i]!) & 0xff; + } +} + +function unfilterAverage( + currentLine: Uint8Array, + newLine: Uint8Array, + prevLine: Uint8Array, + bytesPerLine: number, + bytesPerPixel: number, +): void { + let i = 0; + if (prevLine.length === 0) { + for (; i < bytesPerPixel; i++) { + newLine[i] = currentLine[i]!; + } + for (; i < bytesPerLine; i++) { + newLine[i] = (currentLine[i]! + (newLine[i - bytesPerPixel]! >> 1)) & 0xff; + } + return; + } + for (; i < bytesPerPixel; i++) { + newLine[i] = (currentLine[i]! + (prevLine[i]! >> 1)) & 0xff; + } + for (; i < bytesPerLine; i++) { + newLine[i] = + (currentLine[i]! + ((newLine[i - bytesPerPixel]! + prevLine[i]!) >> 1)) & 0xff; + } +} + +function unfilterPaeth( + currentLine: Uint8Array, + newLine: Uint8Array, + prevLine: Uint8Array, + bytesPerLine: number, + bytesPerPixel: number, +): void { + let i = 0; + if (prevLine.length === 0) { + for (; i < bytesPerPixel; i++) { + newLine[i] = currentLine[i]!; + } + for (; i < bytesPerLine; i++) { + newLine[i] = (currentLine[i]! + newLine[i - bytesPerPixel]!) & 0xff; + } + return; + } + for (; i < bytesPerPixel; i++) { + newLine[i] = (currentLine[i]! + prevLine[i]!) & 0xff; + } + for (; i < bytesPerLine; i++) { + newLine[i] = + (currentLine[i]! + + paethPredictor( + newLine[i - bytesPerPixel]!, + prevLine[i]!, + prevLine[i - bytesPerPixel]!, + )) & + 0xff; + } +} + +/** + * Reconstruct raw scanlines from inflated PNG image data (one filter byte per row). + * Returns little-endian 16-bit sample bytes suitable for `decodeRawImage`. + */ +export function unfilter16BitGrayscaleScanlines( + inflated: Uint8Array, + width: number, + height: number, +): Uint8Array { + const bytesPerLine = width * GRAYSCALE_16_BPP; + const expectedLen = height * (1 + bytesPerLine); + if (inflated.byteLength !== expectedLen) { + throw new Error( + `PNG inflated length ${inflated.byteLength} !== expected ${expectedLen}`, + ); + } + + const out = new Uint8Array(height * bytesPerLine); + const empty = new Uint8Array(0); + let prevLine = empty; + let offset = 0; + + for (let row = 0; row < height; row++) { + const filterType = inflated[offset++]!; + const currentLine = inflated.subarray(offset, offset + bytesPerLine); + offset += bytesPerLine; + const newLine = out.subarray(row * bytesPerLine, (row + 1) * bytesPerLine); + + switch (filterType) { + case 0: + unfilterNone(currentLine, newLine, bytesPerLine); + break; + case 1: + unfilterSub(currentLine, newLine, bytesPerLine, GRAYSCALE_16_BPP); + break; + case 2: + unfilterUp(currentLine, newLine, prevLine, bytesPerLine); + break; + case 3: + unfilterAverage(currentLine, newLine, prevLine, bytesPerLine, GRAYSCALE_16_BPP); + break; + case 4: + unfilterPaeth(currentLine, newLine, prevLine, bytesPerLine, GRAYSCALE_16_BPP); + break; + default: + throw new Error(`Unsupported PNG filter type: ${filterType}`); + } + prevLine = newLine; + } + + // PNG stores 16-bit samples big-endian; RawImage decode expects little-endian. + for (let i = 0; i < out.length; i += 2) { + const t = out[i]!; + out[i] = out[i + 1]!; + out[i + 1] = t; + } + + return out; +} diff --git a/src/features/panels/Image/core/rawDecoders.test.ts b/src/features/panels/Image/core/rawDecoders.test.ts index 160232a..56ef29d 100644 --- a/src/features/panels/Image/core/rawDecoders.test.ts +++ b/src/features/panels/Image/core/rawDecoders.test.ts @@ -32,6 +32,23 @@ describe('decodeRawImage', () => { expect(Array.from(output)).toEqual([0, 0, 0, 255, 255, 255, 255, 255]); }); + it('decodes 16uc1 using default depth colormap range when maxValue is omitted', () => { + const output = new Uint8ClampedArray(4); + decodeRawImage( + { + encoding: '16UC1', + width: 1, + height: 1, + data: new Uint8Array([0x00, 0x00]), + }, + output, + { colorMode: 'colormap', colorMap: 'turbo' }, + ); + + expect(output[2]).toBeGreaterThan(output[1]); + expect(output[3]).toBe(255); + }); + it('decodes 16uc1 using grayscale normalization', () => { const output = new Uint8ClampedArray(4); decodeRawImage( diff --git a/src/features/panels/Image/core/rawDecoders.ts b/src/features/panels/Image/core/rawDecoders.ts index a46453c..e68dbbd 100644 --- a/src/features/panels/Image/core/rawDecoders.ts +++ b/src/features/panels/Image/core/rawDecoders.ts @@ -4,6 +4,7 @@ import { type ColorRGBA, type RawImageDecodeOptions, } from './imageColorMode'; +import { resolveDepthColorRange } from './depthColorDefaults'; export interface RawImageLike { encoding: string; @@ -194,8 +195,7 @@ function decodeFloat1c( throw new Error(`Float image row step (${step}) must be at least 4*width (${width * 4})`); } - const minV = colorOpts.minValue ?? 0; - const maxV = colorOpts.maxValue ?? 1; + const { minValue: minV, maxValue: maxV } = resolveDepthColorRange('32fc1', colorOpts); let converter; try { converter = getColorConverter(colorOpts, minV, maxV); @@ -259,13 +259,13 @@ function decodeMono16( isBigEndian: boolean, output: Uint8ClampedArray, colorOpts: RawImageDecodeOptions, + encoding: string, ): void { if (step < width * 2) { throw new Error(`Mono16 image row step (${step}) must be at least 2*width (${width * 2})`); } - const minValue = colorOpts.minValue ?? 0; - const maxValue = colorOpts.maxValue ?? 65535; + const { minValue, maxValue } = resolveDepthColorRange(encoding, colorOpts); let converter; try { converter = getColorConverter(colorOpts, minValue, maxValue); @@ -406,7 +406,7 @@ export function decodeRawImage( return; case 'mono16': case '16uc1': - decodeMono16(data, width, height, step, isBigEndian, output, colorOpts); + decodeMono16(data, width, height, step, isBigEndian, output, colorOpts, encoding); return; case '32fc1': decodeFloat1c(data, width, height, step, isBigEndian, output, colorOpts); diff --git a/test-fixtures/image/compressed-depth-16uc1.bin b/test-fixtures/image/compressed-depth-16uc1.bin new file mode 100644 index 0000000000000000000000000000000000000000..317bd14dfbc3bcedde809c7b46867cba76e367d0 GIT binary patch literal 126023 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

ZxSKL9XCsxt-=Ku`vx#& zjfFefIQzQC9iIsQ6QNvFIk^lH$5A7_m@E8-5!E`yxkci2OUV}X4I9!#fB#nny8-03mZp^&%XHT{ zo_>B1ARRS8nVh@o4EAV>_x%s+4IVTB_79k*#sW~pq8CPw3cV& zT%bxZ?YjlS|9;`X>>e-*?~oWUL`Jx%=)u|D_(J_MK~rm;<* zG55+5TY)(7B0%r(!8md(C?@Jx+mnI56nP}{7eiS-zd0@efU>h(Rg$1wVFB&M#JpuD z|7A$y>Sn2&R49!SZo?T0n(#UY|LxgUAmRRhvw~!>Co41r7NAzw^T{MJmV<(qBFB!# zK{Qmi#h13#9uh^Mw)P>eK1bQzX#*nF+H9V3%@slKN-YP!1Qw;^Co_&DZbVHfHCOn{ z&C!?^2xY%cI5qfq^e5}xrt=oVuUl0G#@7r${;y0^2^Y`Ky)`@W<|qny(Ut2J&n>Z( zUu}vQHCFVj>vH4G#1*(XPOxgk$iWPmi+9f*qrD?;VeYEo7(+S_hz7-QJVuXhjgk~6 zexYU~2v8PayOfuI%#`YBMcj4kZ2h05ItsqIurf80-)*EtG?^cSd+l1|z^dtvZ7%(d zzzi?qf3MXz{lwB{9hCotmF)D`0#=^2TJe}LeLs{rZp8T|Az0oa-EJfCa9H;zYg3$2 z{Z`E*%L;{C?*_nDdiGTph`@1faq$S{by${lE{jqO2Ma^@*@&_4h3FISBr|*O?UI1* zy7B&^qhXgngt1ilK>N7A zGyO0Ess+|?fH;*B(CAQX^Kbyg1eIQ^Q0-Dd=t3KvFZbG15IWjVSrJM4^U$_x&H)DlFGqUU?je z^CuiRcKfrA)0N)*Kb9(4uS0tbAWQ8>6J9>>#nyRA)Y%%~uI8tR#Lf@bI0d@9U$9gv z_8QpD)=>Rp|7OHt(9Xt8#&m&^`8^oE?J356BqLZw-32yTcTz@>&-K)JZXmbnszB{% zPKD}iOZQ-^PNCwmOR7)4ycd3qQaeOeUJgdy7lCI5tJvf-v)hynM%3^|8f$sHE$&lK z9T0MHq2p!UkN@*ofS$@EBO-|+?WWk9F-6s;M2eA!cse+=%Y#GFy^`lRND5 z(H=D(LlJ_$+kq+V*z8rz(<_>h(HKBncL1nQ5Wo>#D(94Hh1Qxua}ly55zGII$X)ohK3L z#!L8{I%c)XWPZRgi^&`6(WX9P{yx8q6RIbuB%3?ly-rl^oX@&7L@psOleupF8Ndu z_4zzo`P_a(tW^0t?+nijPb=}V^Ip?zr{lbA)MGmC?lp4ts%QQfh1+=O^Wf*CWFtW( zn;H)-Qr&m8ot4Iw5eefm_QItnf+6AWm;NgV_iY|4qbKXSJ`Nhy&~gR^L)1pe-0EZ< z-m1aL(50TGi(w?eQ>lc3I-lW&A|62{8iUvJ-)i`#P9gRDXiT!IydJ`Z`| z0?G*X6rHI6?y|Jy!V34d#kF)><01M3k<-0sR9%@iQly5)t~%spv=cx)8RqlF?qPBW z!de_LS)@HYH%RuCS?dXB>ZVm<6H;#YiX%DMA|T!+i&0C*sLSql@=Z5xj+)yk-{M>U zF4}2Dv+a_;Kcm#y)NBau@uc z^-qimDtq90%S^p(-A5v728vLNRVRh25{%KwBtTjPFFcw?W+r9r>Ea@H0$y*(kUNBQ z)lx{rBv&%0peSjOO*P=!eco)}FaR?m+mu&99S-Rfg(>R^0@*3+6cTtoXQjsb8e3}a zDXv1q4%%ATYOU*>!I~c)v$Ru=;Ij zR0+J1b=hoTgRI#}gaqRrWZ>G0_e}MEV&I?5&*v_FaM0cq@2x|75ST#)W%t$ddCQC! z<30nZ=qxBSFA<9u6;(ot(6Mn{Mh7BPtw=T3y|lsqIQ5>YK^FED@Wrr(V$}VCE}>N3 zY>fNEU+=@IP(?z=eZ+dWkcS#dI^@?R#mC~g^C=CbR<_fBlh5e?&$|8g1v}Q~!hT%p zXlsAk6xWOtt3k7$B=D*Zkw?iPx(#vCun_Pq)n1W#RZE`IagZCgK1twSj0lA|xK^jVC=@k{2z5-Ybzr$r>d8S*UqaJ@U>@-~u zE#Ira#wDnPW+mwy;K$9&8es z(oQ3(PohOm&tvarxj(%T+V}qNI+^M-u$fm3UCcI3Gg41X`C&vfMLOpzFWZf=kp?}C zY{)405yoNW8>>k4tZ77!MutB9YsEbD>2^-FW%dWH0BdJk=M;u|v#{GF4wIGD|CY1% z(6mumc7cYO;^%*?EX3oP@BW16l;^mqb!MptFEPs2=6%2e!{{}@$$@`99n7pjXKNt8 zD=a?toOFVhmKR)Nrn>1boy!lYKiwOaS=afsV#M%b z8$vG@R9e+v;Q2NS=#KLyo4&egq1HG5M5|r(9?l3>s2ZeCpjZKq2XlT5uW3;WHv} zGkq+7Udw&5ar#&%su%4%%z^hZ4;w4DXW?(jyK? zkBl2*uW@gyiA-Gfygen+w|=pHA0_m_uxen*s&1&q!zI5f|NIA{DVHFu?3b4*)z#L8 zAc4%Y3ZQy=(4CP3fBP+Pf6@cn+~Ksk z^#)S3*1<`KF249^!e!c-NS;ntOx!Vc8zn(+_qMA25~ND6x(jsDl|AK!$1`zgNc3WC<+Jr4woeW38h=lW=j+RGy+ zweDu+bOB`@5(*pY74Ja`JABQN%n*ftyQsM+HgvKxC_1R*aG-lMs^T53m4_6JWDP@{)rEPXX�+QJz3-&b&-?E|+PXG)!1Ijl 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

E%}Um{J4gSH>H0CgUEFW#;$NfP#|Fm|n;#4P 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 ^`YulKbdBVqkurQ-IK9zOlU)z|ORh&?MfOmbm|P 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 literal 0 HcmV?d00001 diff --git a/test-fixtures/image/compressed-depth-16uc1.format.txt b/test-fixtures/image/compressed-depth-16uc1.format.txt new file mode 100644 index 0000000..44202cd --- /dev/null +++ b/test-fixtures/image/compressed-depth-16uc1.format.txt @@ -0,0 +1 @@ +16UC1; compressedDepth diff --git a/test-fixtures/image/compressed-depth-32fc1.bin b/test-fixtures/image/compressed-depth-32fc1.bin new file mode 100644 index 0000000000000000000000000000000000000000..81cbe26f57c776c96e82c833d0ba114236391ee3 GIT binary patch literal 82 zcmZQz009L@28IO=odJICyj)UTKmlG)50@Yy%>=}Z0ziVH@#Y^~{ aXu-(9_>)mrLihO_kOEIvKbLh*2~7YQnh<#a literal 0 HcmV?d00001 diff --git a/tests/fixturePaths.ts b/tests/fixturePaths.ts index abe28e1..188cd9b 100644 --- a/tests/fixturePaths.ts +++ b/tests/fixturePaths.ts @@ -10,6 +10,7 @@ export const MCAP_BASIC = path.join(EXAMPLES_DIR, 'test_5s.mcap'); export const MCAP_POSE = path.join(EXAMPLES_DIR, 'test_pose.mcap'); export const MCAP_3CAM = path.join(EXAMPLES_DIR, 'test_3cam.mcap'); export const MCAP_H264 = path.join(EXAMPLES_DIR, 'test_h264.mcap'); +export const MCAP_COMPRESSED_DEPTH = path.join(EXAMPLES_DIR, 'test_compressed_depth.mcap'); export const HDF5_MINIMAL = path.join(EXAMPLES_DIR, 'test_minimal.hdf5'); export const BVH_MINIMAL = path.join(EXAMPLES_DIR, 'test_minimal.bvh'); @@ -17,6 +18,7 @@ export const MCAP_BASIC_URL = '/examples/test_5s.mcap'; export const MCAP_POSE_URL = '/examples/test_pose.mcap'; export const MCAP_3CAM_URL = '/examples/test_3cam.mcap'; export const MCAP_H264_URL = '/examples/test_h264.mcap'; +export const MCAP_COMPRESSED_DEPTH_URL = '/examples/test_compressed_depth.mcap'; export const HDF5_MINIMAL_URL = '/examples/test_minimal.hdf5'; export const BVH_MINIMAL_URL = '/examples/test_minimal.bvh'; diff --git a/tests/image-compressed-depth.spec.ts b/tests/image-compressed-depth.spec.ts new file mode 100644 index 0000000..a52dc58 --- /dev/null +++ b/tests/image-compressed-depth.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; +import { MCAP_COMPRESSED_DEPTH, MCAP_COMPRESSED_DEPTH_URL, requireFixture } from './fixturePaths'; +import { attachBrowserDiagnostics, openFixtureByUrl } from './helpers/rosview'; + +test.describe.configure({ timeout: 120_000 }); + +test.beforeAll(() => { + requireFixture(MCAP_COMPRESSED_DEPTH); +}); + +test('16UC1 compressedDepth CompressedImage decodes without error', async ({ page }) => { + const diagnostics = attachBrowserDiagnostics(page); + await openFixtureByUrl(page, MCAP_COMPRESSED_DEPTH_URL, { diagnostics }); + + const play = page.getByRole('button', { name: 'Play playback' }); + if (await play.isVisible().catch(() => false)) { + await play.click(); + } + + await expect(page.getByTestId('image-panel')).toBeVisible({ timeout: 60_000 }); + await expect(page.getByText(/Failed to decode frame at index 0|Image decode failed/i)).toHaveCount(0); + + const imageStatus = page.getByTestId('image-panel-status'); + await expect(imageStatus).toBeVisible({ timeout: 90_000 }); + await expect(imageStatus).toHaveText(/640x480.*16uc1/i); + + expect(diagnostics.pageErrors.filter((entry) => /decode frame at index 0|Image decode failed/i.test(entry))).toEqual([]); + expect(diagnostics.consoleErrors.filter((entry) => /decode frame at index 0|Image decode failed/i.test(entry))).toEqual([]); +}); From b3bdfc0b405353505fdf5096f930553641aa8b7c Mon Sep 17 00:00:00 2001 From: joaner Date: Wed, 1 Jul 2026 15:35:04 +0800 Subject: [PATCH 2/2] fix(image): satisfy eslint in compressed depth decoder modules --- .../Image/core/depthColorization.test.ts | 2 +- .../Image/core/grayscale16PngDecoder.test.ts | 4 ++- .../Image/core/grayscale16PngDecoder.ts | 14 +++++----- .../Image/core/png16ScanlineUnfilter.ts | 26 +++++++++---------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/features/panels/Image/core/depthColorization.test.ts b/src/features/panels/Image/core/depthColorization.test.ts index 4ead011..e54c006 100644 --- a/src/features/panels/Image/core/depthColorization.test.ts +++ b/src/features/panels/Image/core/depthColorization.test.ts @@ -49,7 +49,7 @@ describe('compressed depth colorization', () => { const o = i * 4; if (value === 0) { zeroCount++; - if (isDarkBlue(rgba[o]!, rgba[o + 1]!, rgba[o + 2]!)) zeroDarkBlue++; + if (isDarkBlue(rgba[o], rgba[o + 1], rgba[o + 2])) zeroDarkBlue++; } if (value >= DEFAULT_DEPTH_16UC1_COLOR_MIN && value <= DEFAULT_DEPTH_16UC1_COLOR_MAX) { if (value < minDepth) { diff --git a/src/features/panels/Image/core/grayscale16PngDecoder.test.ts b/src/features/panels/Image/core/grayscale16PngDecoder.test.ts index 807ed5e..cefab68 100644 --- a/src/features/panels/Image/core/grayscale16PngDecoder.test.ts +++ b/src/features/panels/Image/core/grayscale16PngDecoder.test.ts @@ -61,6 +61,8 @@ describe('decodeGrayscale16Png', () => { ]); const decoded = await decodeGrayscale16Png(png); + expect(decoded.width).toBe(width); + expect(decoded.height).toBe(height); const view = new DataView(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength); expect(view.getUint16(0, true)).toBe(1000); expect(view.getUint16(2, true)).toBe(2000); @@ -70,7 +72,7 @@ describe('decodeGrayscale16Png', () => { function crc32(data: Uint8Array): number { let c = 0xffffffff; for (let i = 0; i < data.length; i++) { - c ^= data[i]!; + c ^= data[i]; for (let k = 0; k < 8; k++) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } diff --git a/src/features/panels/Image/core/grayscale16PngDecoder.ts b/src/features/panels/Image/core/grayscale16PngDecoder.ts index f26919a..44c08d4 100644 --- a/src/features/panels/Image/core/grayscale16PngDecoder.ts +++ b/src/features/panels/Image/core/grayscale16PngDecoder.ts @@ -45,7 +45,7 @@ function concatIdatChunks(png: Uint8Array): Uint8Array { while (offset + 8 <= png.byteLength) { const length = readU32Be(view, offset); const type = - String.fromCharCode(png[offset + 4]!, png[offset + 5]!, png[offset + 6]!, png[offset + 7]!); + String.fromCharCode(png[offset + 4], png[offset + 5], png[offset + 6], png[offset + 7]); const dataStart = offset + 8; const dataEnd = dataStart + length; if (dataEnd + 4 > png.byteLength) { @@ -97,10 +97,10 @@ function parseIhdr(png: Uint8Array): { width: number; height: number; bitDepth: const view = new DataView(png.buffer, png.byteOffset, png.byteLength); const firstLength = readU32Be(view, PNG_SIGNATURE.byteLength); const firstType = String.fromCharCode( - png[PNG_SIGNATURE.byteLength + 4]!, - png[PNG_SIGNATURE.byteLength + 5]!, - png[PNG_SIGNATURE.byteLength + 6]!, - png[PNG_SIGNATURE.byteLength + 7]!, + png[PNG_SIGNATURE.byteLength + 4], + png[PNG_SIGNATURE.byteLength + 5], + png[PNG_SIGNATURE.byteLength + 6], + png[PNG_SIGNATURE.byteLength + 7], ); if (firstType !== 'IHDR' || firstLength !== 13) { throw new Error('PNG is missing IHDR chunk'); @@ -110,8 +110,8 @@ function parseIhdr(png: Uint8Array): { width: number; height: number; bitDepth: return { width: readU32Be(view, ihdrOffset), height: readU32Be(view, ihdrOffset + 4), - bitDepth: png[ihdrOffset + 8]!, - colorType: png[ihdrOffset + 9]!, + bitDepth: png[ihdrOffset + 8], + colorType: png[ihdrOffset + 9], }; } diff --git a/src/features/panels/Image/core/png16ScanlineUnfilter.ts b/src/features/panels/Image/core/png16ScanlineUnfilter.ts index c714dfe..9fb28f6 100644 --- a/src/features/panels/Image/core/png16ScanlineUnfilter.ts +++ b/src/features/panels/Image/core/png16ScanlineUnfilter.ts @@ -35,7 +35,7 @@ function unfilterSub( newLine[i] = currentLine[i]!; } for (; i < bytesPerLine; i++) { - newLine[i] = (currentLine[i]! + newLine[i - bytesPerPixel]!) & 0xff; + newLine[i] = (currentLine[i] + newLine[i - bytesPerPixel]) & 0xff; } } @@ -52,7 +52,7 @@ function unfilterUp( return; } for (let i = 0; i < bytesPerLine; i++) { - newLine[i] = (currentLine[i]! + prevLine[i]!) & 0xff; + newLine[i] = (currentLine[i] + prevLine[i]) & 0xff; } } @@ -69,16 +69,16 @@ function unfilterAverage( newLine[i] = currentLine[i]!; } for (; i < bytesPerLine; i++) { - newLine[i] = (currentLine[i]! + (newLine[i - bytesPerPixel]! >> 1)) & 0xff; + newLine[i] = (currentLine[i] + (newLine[i - bytesPerPixel] >> 1)) & 0xff; } return; } for (; i < bytesPerPixel; i++) { - newLine[i] = (currentLine[i]! + (prevLine[i]! >> 1)) & 0xff; + newLine[i] = (currentLine[i] + (prevLine[i] >> 1)) & 0xff; } for (; i < bytesPerLine; i++) { newLine[i] = - (currentLine[i]! + ((newLine[i - bytesPerPixel]! + prevLine[i]!) >> 1)) & 0xff; + (currentLine[i] + ((newLine[i - bytesPerPixel] + prevLine[i]) >> 1)) & 0xff; } } @@ -95,20 +95,20 @@ function unfilterPaeth( newLine[i] = currentLine[i]!; } for (; i < bytesPerLine; i++) { - newLine[i] = (currentLine[i]! + newLine[i - bytesPerPixel]!) & 0xff; + newLine[i] = (currentLine[i] + newLine[i - bytesPerPixel]) & 0xff; } return; } for (; i < bytesPerPixel; i++) { - newLine[i] = (currentLine[i]! + prevLine[i]!) & 0xff; + newLine[i] = (currentLine[i] + prevLine[i]) & 0xff; } for (; i < bytesPerLine; i++) { newLine[i] = - (currentLine[i]! + + (currentLine[i] + paethPredictor( - newLine[i - bytesPerPixel]!, - prevLine[i]!, - prevLine[i - bytesPerPixel]!, + newLine[i - bytesPerPixel], + prevLine[i], + prevLine[i - bytesPerPixel], )) & 0xff; } @@ -137,7 +137,7 @@ export function unfilter16BitGrayscaleScanlines( let offset = 0; for (let row = 0; row < height; row++) { - const filterType = inflated[offset++]!; + const filterType = inflated[offset++]; const currentLine = inflated.subarray(offset, offset + bytesPerLine); offset += bytesPerLine; const newLine = out.subarray(row * bytesPerLine, (row + 1) * bytesPerLine); @@ -166,7 +166,7 @@ export function unfilter16BitGrayscaleScanlines( // PNG stores 16-bit samples big-endian; RawImage decode expects little-endian. for (let i = 0; i < out.length; i += 2) { - const t = out[i]!; + const t = out[i]; out[i] = out[i + 1]!; out[i + 1] = t; }