Skip to content

Commit 45d8a93

Browse files
facontidavideclaude
andcommitted
depthz: quantized-by-default qpred codec, fpred fallback, SIMD, benchmark
Real full-precision stereo depth (e.g. ZED 32FC1) has more distinct float patterns than the 16-bit dictionary admits, so dpred always fell back to plain zstd on raw floats. Two new vendored methods fix that: - fpred (lossless): dictionary-free MED prediction on float total-order integers, used automatically when the dictionary overflows. Byte- compatible with the standalone depth_codec library. - qpred (lossy, NEW DEFAULT at 0.1 mm): uniform quantization with a configurable step, MED prediction on the code plane, zigzag + LEB128 varint residuals (level count not limited to 16 bits), zstd. Valid pixels reconstruct within +/- step/2; invalid pixels (NaN/inf/<=0) and off-grid depths decode as NaN per REP 118. The step is stored uncompressed in the payload and exposed via BlobHeader, so blobs stay fully self-describing from the header alone. New publisher parameter <base_topic>.depthz.quantization (mm, default 0.1, 0 = bit-exact lossless; ignored for 16UC1 which stays lossless). The CompressedImage format string advertises the lossiness. The qpred hot paths are SIMD-optimized with runtime AVX2 dispatch (quantize, residuals, code->float; scalar and AVX2 encode emit bit-identical blobs, and quantization is FPU-rounding-mode independent), plus a varint fast path and the same wavefront reconstruction the 16UC1 decoder uses. Codex review fixes: off-grid depths (v/step beyond ~2^31) are treated as invalid instead of silently saturating with unbounded error; the AVX2 code->float conversion multiplies in double so a blob decodes identically with or without AVX2. Adds benchmark/: a black-box perf tool driving any image_transport plugin (depthz, compressedDepth, ...) end to end over real frames extracted from MCAP bags, with error-bound verification and perf-record integration. Off by default (-DDEPTHZ_BUILD_BENCHMARK=ON). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 088de27 commit 45d8a93

13 files changed

Lines changed: 1822 additions & 60 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Currently provided are:
5757

5858
- [compressed_image_transport](https://github.com/ros-perception/image_transport_plugins/tree/rolling/compressed_image_transport)
5959

60-
- [depthz_image_transport](https://github.com/ros-perception/image_transport_plugins/tree/rolling/depthz_image_transport) - A library compressing 32FC1 and 16UC1 depth images losslessly (value dictionary + 2D prediction + zstd).
60+
- [depthz_image_transport](https://github.com/ros-perception/image_transport_plugins/tree/rolling/depthz_image_transport) - A library compressing 32FC1 and 16UC1 depth images (2D prediction + zstd). By default 32FC1 is quantized to a configurable 0.1 mm grid; a bit-exact lossless mode is available (`quantization: 0.0`), and 16UC1 is always lossless.
6161

6262
- [zstd_image_transport](https://github.com/ros-perception/image_transport_plugins/tree/rolling/zstd_image_transport) - A libraory using ZSTD to compress the pointclouds.
6363

depthz_image_transport/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,21 @@ if(BUILD_TESTING)
8080
target_link_libraries(test_depth_codec ${PROJECT_NAME})
8181
endif()
8282

83+
# Local perf-profiling tool, not part of the package's public surface or its
84+
# CI: exercises DepthzPublisher/DepthzSubscriber end to end through the
85+
# public image_transport API. Off by default; see benchmark/README.md.
86+
option(DEPTHZ_BUILD_BENCHMARK "Build the depthz_image_transport perf benchmark tool" OFF)
87+
if(DEPTHZ_BUILD_BENCHMARK)
88+
add_executable(benchmark_depthz benchmark/benchmark_depthz.cpp)
89+
target_compile_features(benchmark_depthz PRIVATE cxx_std_20)
90+
target_link_libraries(benchmark_depthz
91+
image_transport::image_transport
92+
rclcpp::rclcpp
93+
${sensor_msgs_TARGETS}
94+
)
95+
install(TARGETS benchmark_depthz
96+
RUNTIME DESTINATION lib/${PROJECT_NAME}
97+
)
98+
endif()
99+
83100
ament_package()

depthz_image_transport/README.md

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
11
# depthz_image_transport
22

3-
Lossless `image_transport` plugin for depth images (`32FC1` and `16UC1`).
3+
`image_transport` plugin for depth images (`32FC1` and `16UC1`).
44

5-
Compared to `compressedDepth` (PNG), `depthz` compresses better, encodes
6-
roughly 10× faster, and is truly lossless: `compressedDepth` quantizes
7-
32FC1 to 16 bits before the PNG stage, `depthz` reproduces the input
8-
bit-exactly (NaN included). The codec is vendored from
5+
**By default the 32FC1 transport is LOSSY**: depth is quantized to a uniform
6+
0.1 mm grid before compression, so every decoded pixel is within ±0.05 mm of
7+
the input — far below the noise floor of any real depth camera. Set the
8+
`quantization` parameter to `0.0` for bit-exact lossless mode (NaN payloads
9+
included), or to a larger step for more compression. `16UC1` input (already
10+
integer millimeters) is always compressed losslessly.
11+
12+
The codec is vendored from
913
[facontidavide/depth_image_compression](https://github.com/facontidavide/depth_image_compression),
10-
where the algorithm is documented. Blobs produced by this plugin decode
11-
cleanly with that standalone library, but this plugin only vendors its
12-
"dpred"/"dpred16" methods (a subset of the standalone library's full method
13-
list), so blobs encoded elsewhere with a different method are rejected.
14+
where the algorithm is documented. Method by input and configuration:
15+
16+
| input | `quantization` | blob method | guarantee |
17+
|---|---|---|---|
18+
| 32FC1 | `> 0` (default 0.1 mm) | `qpred` | ± step/2 per valid pixel, invalid (NaN/inf/≤0) → NaN |
19+
| 32FC1 | `0.0`, ≤ 65536 distinct values | `dpred` | bit-exact |
20+
| 32FC1 | `0.0`, > 65536 distinct values | `fpred` | bit-exact |
21+
| 16UC1 | (ignored) | `dpred16` | bit-exact |
22+
23+
Lossless `dpred`/`fpred`/`dpred16` blobs are interchangeable with the
24+
standalone library in both directions; `qpred` originates in this plugin
25+
(standalone releases predating it reject it, as this plugin rejects the
26+
standalone library's other methods). Every blob is self-describing:
27+
dimensions, pixel format and quantization step are readable from its header
28+
without decompressing anything.
29+
30+
## Performance
31+
32+
On real full-precision stereo depth (the hardest input: nearly every pixel
33+
carries a unique float bit pattern), the default quantized mode compresses
34+
substantially better than `compressedDepth` while encoding and decoding
35+
several times faster — and with a much finer, explicitly bounded
36+
quantization error than `compressedDepth`'s 16-bit inverse-depth stage.
37+
Coarser steps trade precision for ratio; the lossless mode compresses the
38+
least, since it must reproduce the sensor's mantissa noise bit-exactly.
39+
Compression is data-dependent, so measure on your own streams: the
40+
`benchmark/` directory contains a tool that runs any recorded MCAP depth
41+
topic through the real publisher/subscriber plugins and reports ratio,
42+
throughput, and error-bound verification (see `benchmark/README.md`).
1443

1544
## Usage
1645

@@ -39,7 +68,18 @@ An existing stream can be converted with
3968
bag recording: record `<base_topic>/depthz` instead of
4069
`<base_topic>/compressedDepth`.
4170

42-
Parameters: `<base_topic>.depthz.zstd_level` (1–3, default 1).
71+
## Parameters
72+
73+
| parameter | default | meaning |
74+
|---|---|---|
75+
| `<base_topic>.depthz.quantization` | `0.1` | 32FC1 quantization step in **millimeters**; decoded depth is within ± half this step. `0.0` = bit-exact lossless. Ignored for 16UC1. |
76+
| `<base_topic>.depthz.zstd_level` | `1` | zstd level of the entropy stage (1–3); higher is slower with slightly better ratio. |
77+
78+
The published `CompressedImage.format` string advertises the lossiness
79+
(e.g. `32FC1; depthz; lossy 0.100mm`), so bag consumers can tell without
80+
decoding.
4381

4482
Only `32FC1` and `16UC1` encodings are accepted; other encodings are
4583
declined with an error log (use `compressed` or `zstd` for color images).
84+
85+
See `benchmark/README.md` for the tooling that produced the numbers above.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
data/
2+
__pycache__/
3+
colcon_ws/
4+
results/
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# depthz_image_transport perf benchmark
2+
3+
Local tooling for profiling `DepthzPublisher`/`DepthzSubscriber` with `perf`,
4+
using real depth data. Not part of the package's public surface or CI --
5+
opt in with `-DDEPTHZ_BUILD_BENCHMARK=ON`.
6+
7+
`benchmark_depthz` talks to the plugin exclusively through the public
8+
`image_transport`/`rclcpp` API (`ImageTransport::advertise`,
9+
`image_transport::create_subscription(..., "depthz", ...)`), the same way
10+
any real node would. Profiling this binary profiles exactly what a deployed
11+
publisher or subscriber node spends its time on -- pluginlib loading
12+
included -- not just the vendored codec's hot loop in isolation.
13+
14+
## 1. Get real depth frames
15+
16+
`extract_frames.py` pulls raw `sensor_msgs/Image` frames off an MCAP topic
17+
into a compact `.dzbm` file (see the docstring for the format):
18+
19+
```sh
20+
python3 extract_frames.py \
21+
--mcap ~/ws_eternal/src/Harvesting/harvest_bringup/test/test_data/rosbags/van_noord_tomato_20250919_181407/20250919_181407_0.mcap \
22+
--topic /zed_wrist/zed_node/depth/depth_registered \
23+
--out data/wrist_1920x1200.dzbm
24+
```
25+
26+
Needs `pip install mcap mcap-ros2-support`. That bag has two raw 32FC1
27+
streams from the ZED stereo cameras worth benchmarking:
28+
29+
| topic | resolution | frames |
30+
|---|---|---|
31+
| `/zed_base/zed_node/depth/depth_registered` | 960x600 | 10 |
32+
| `/zed_wrist/zed_node/depth/depth_registered` | 1920x1200 | 10 |
33+
34+
Point `--mcap`/`--topic` at any other bag with a raw (uncompressed) 32FC1 or
35+
16UC1 `sensor_msgs/Image` topic -- it doesn't have to be ZED/depth-camera
36+
specific.
37+
38+
## 2. Build
39+
40+
```sh
41+
colcon build --packages-select depthz_image_transport \
42+
--cmake-args -DDEPTHZ_BUILD_BENCHMARK=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
43+
source install/setup.bash
44+
```
45+
46+
`RelWithDebInfo` keeps optimizations on (required -- see the package's own
47+
`CMakeLists.txt` comment) while still emitting frame pointers/debug info for
48+
`perf record -g` to unwind.
49+
50+
## 3. Run
51+
52+
```sh
53+
benchmark_depthz --frames data/wrist_1920x1200.dzbm --mode roundtrip --iterations 300
54+
```
55+
56+
`--mode`:
57+
- `roundtrip` (default): a real `DepthzPublisher` encodes each frame, a real
58+
`DepthzSubscriber` decodes it. End-to-end throughput and compression
59+
ratio.
60+
- `encode`: only the publisher plugin's encode path runs (the "subscriber"
61+
is a bare passthrough callback, kept alive only because
62+
`image_transport::Publisher` publishes on demand and needs to see a real
63+
subscriber count -- see its class doc).
64+
- `decode`: all frames are pre-encoded once (untimed) by a real
65+
`DepthzPublisher`, then the timed loop republishes those captured blobs
66+
directly onto the internal `<topic>/depthz` topic with a plain
67+
`rclcpp::Publisher`, bypassing the publisher plugin entirely, while a real
68+
`DepthzSubscriber` decodes them. This isolates decode cost from encode
69+
cost.
70+
71+
Other flags: `--transport <name>` (benchmark any installed image_transport
72+
plugin against the same frames, e.g. `compressedDepth`), `--iterations N`
73+
(loop the dataset N times), `--warmup N` (untimed iterations first, so
74+
`thread_local` scratch buffers reach their steady-state size before the
75+
clock starts), `--zstd-level 1-3`, `--quantization MM` (depthz quantization
76+
step in millimeters; the benchmark defaults to `0` = lossless, deliberately
77+
overriding the plugin's own lossy 0.1 mm default so the bit-exact verify
78+
stays meaningful — pass `--quantization 0.1` to measure the plugin's actual
79+
default behavior), `--no-verify` (skip verification entirely; use before
80+
profiling so the comparison doesn't show up as noise in the flamegraph),
81+
`--qos-depth N`.
82+
83+
Output reports wall time, fps, MB/s (raw), compression ratio, and (unless
84+
`--no-verify`) a verification against the source frames: bit-exact for
85+
lossless depthz, the documented ± step/2 error bound plus NaN preservation
86+
for quantized depthz, and informational-only for other transports.
87+
88+
## 4. Profile with perf
89+
90+
```sh
91+
perf stat -d -- benchmark_depthz --frames data/wrist_1920x1200.dzbm --mode encode --no-verify
92+
perf record -g --call-graph dwarf -o encode.perf.data -- \
93+
benchmark_depthz --frames data/wrist_1920x1200.dzbm --mode encode --no-verify
94+
perf report -i encode.perf.data
95+
```
96+
97+
`run_perf.sh` automates all of the above (build, extract both ZED streams,
98+
run `perf stat` + `perf record` for all 3 modes x 2 resolutions) and drops
99+
results under `results/`:
100+
101+
```sh
102+
./run_perf.sh
103+
```
104+
105+
Because the benchmark drives a `SingleThreadedExecutor` by hand (publish,
106+
then `spin_some` until the callback fires, repeat) rather than a background
107+
spin thread, encode and decode samples land on the same call stack you'd
108+
expect from the source -- `dpred_encode`/`build_value_dict`/`zstd_append`
109+
for encode, `dpred_decode`/`predict_unpack`/`ZSTD_decompressDCtx` for
110+
decode -- with no cross-thread noise to untangle.

0 commit comments

Comments
 (0)