feat(msgs): typed CompressedImage sensor msg - #2814
Merged
Merged
Conversation
…ges on any transport adds sensor_msgs.CompressedImage wrapping the existing dimos_lcm binding (jpeg/png, ts+frame_id preserved, rr.EncodedImage viz) and a CodecTransport that compresses Image->CompressedImage over any inner transport (lcm, zenoh, shm, webrtc). benchmark test compares raw image vs compressed on lcm+zenoh: 19.5x wire reduction, raw 720p over lcm drops most frames while comressed delivers all
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #2814 +/- ##
==========================================
+ Coverage 75.34% 75.36% +0.01%
==========================================
Files 1149 1153 +4
Lines 110476 110948 +472
Branches 10007 10627 +620
==========================================
+ Hits 83234 83611 +377
- Misses 24378 24465 +87
- Partials 2864 2872 +8
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 4 files with indirect coverage changes 🚀 New features to boost your workflow:
|
… compressed image transport go2 basic with CodecTransport on color_image (jpeg q80 on the wire istead of 2.76MB raw frames). readme gets a short compressed-images section with the publish path and how to run the raw-vs-compressed benchmark
the ubuntu-arm runner doesnt have libturbojpeg installed, same optional-dep treatment as test_cli.py / test_all_blueprints.py
…issing root cause of the arm failures: the tests job only apt-installed portaudio, x64 runners had libturbojpeg by accident from the runner image. install it explicitly and turn the silent skip into a CI assert (same treatment as the webrtc aiortc guard from #2048), skip stays for local no-syslib installs
collect rows across the lcm/zenoh params in a module fixture and print a single table (wire/median/max/fps/drops) like tool_benchmark does, plus a note on what the latency number actually measures
installing libturbojpeg on the arm runner unblocked _jpeg_case, which then hit the CI LFS download cap at collection time. treat unavailable data like unavailable turbojpeg (case returns None)
runs a real blueprint in replay mode with BenchSink consumer modules (per-frame arrival log + optional synthetic detector load) and a host sampler (cpu/rss/loopback rate). used for the raw-vs-codec report on unitree-go2; tool_ prefix keeps it out of pytest and the module registry
'codec' already means the memory2 storage codecs (JpegCodec/Lz4Codec) and the generic name promised flexibility the class doesnt have — its hardwired Image<->CompressedImage. new name matches the msg type and the existing JpegLcmTransport/WebRTCVideoTransport naming style
the import-time CI assert broke the self-hosted ros job which collects these files but deselects them by marker. new form: skip locally when the native lib is missing, run (and fail loudly) in CI — same intent, respects deselection like the aiortc guard test does
Team direction is option 5 (connections output CompressedImage directly); the transport wrapper was rejected as public API. Keep it inside tool_replay_bench.py so raw-vs-codec benchmark cells stay reproducible, move its tests alongside, drop the unitree-go2-compressed-image blueprint.
CompressedCodec is bench-only now: drop format/max_width/decode params and the double-wrap guard the bench never uses; rename its tests to test_replay_bench.py to match. Turbojpeg guard moves to conftest as a skipif_no_turbojpeg marker (skip locally, run-and-fail-loudly in CI).
spomichter
marked this pull request as ready for review
July 11, 2026 20:00
spomichter
requested review from
Dreamsorcerer,
leshy,
mustafab0 and
paul-nechifor
as code owners
July 11, 2026 20:00
Contributor
Greptile SummaryThis PR adds a typed compressed-image message and benchmark support. The main changes are:
Confidence Score: 4/5ROS bridge conversion and alpha-bearing PNG decoding can return invalid message data.
dimos/msgs/sensor_msgs/CompressedImage.py; dimos/protocol/pubsub/benchmark/tool_replay_bench.py Important Files Changed
Reviews (1): Last reviewed commit: "Merge branch 'main' into feat/compressed..." | Re-trigger Greptile |
1 task
leshy
reviewed
Jul 13, 2026
leshy
reviewed
Jul 13, 2026
spomichter
commented
Jul 28, 2026
paul-nechifor
previously approved these changes
Jul 29, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Jul 29, 2026
spomichter
enabled auto-merge
July 30, 2026 08:38
leshy
previously approved these changes
Jul 30, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Jul 30, 2026
paul-nechifor
enabled auto-merge
July 31, 2026 17:16
spomichter
disabled auto-merge
August 3, 2026 05:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
dimos has no first-class compressed image type. jpeg support is bolted onto individual transports (
JpegLcmTransport,JpegShmTransport, nothing for zenoh/webrtc),JpegShmTransportloses timestamps on decode, and every new transport needs its own jpeg variant. meanwhile raw images are big — 720p rgb is 2.76MB per frame, ~40MB/s at camera rate. thats fine for shared memory on one host but it kills lcm the moment you leave it: one dropped udp fragment loses the whole frame.CompressedImageis a first-class msg (jpeg/png bytes + timestamp + frame_id, wraps thedimos_lcmbinding that already existed):depth/16-bit images can't go through jpeg — use
format="png"(lossless, covers GRAY16). float DEPTH raises on both.Benchmark report: raw Image vs CompressedImage
(numbers produced with the bench harness in this PR; the codec pin is the bench mechanism, the wins are the type's)
full grid run on this dev box (x86_64, 32 cpus), 6 runs x 10 frames per cell, sequential round-trips. what "latency" measures: one
broadcast()call until the subscriber callback fires, on localhost — so it includes the jpeg encode+decode cpu for the codec path but near-zero wire cost. thats why codec shows higher latency here: raw is two memcpys, codec pays ~5ms encode + ~5ms decode of real cpu. localhost is raw's best case — on a real 1Gbps link the raw frame pays ~22ms of transmit time alone vs <1ms for the jpeg, and over webrtc/wan its not close. what you buy for those ms: 8-40x less wire and the difference between frames arriving or not (see drops).jpeg codec cpu cost (median of 20, ms)
round-trip latency, median ms (LOST = zero frames ever arrived)
round-trip latency, p95 ms
dropped frames (of 60)
lcm raw 480p/720p lost every single frame on this box (untuned kernel udp buffers —
CI=1skips the lcm autoconf sysctl tuning, so this is what a fresh install sees). note the 1080p codec drops too: 315KB @ q75 is already past what untuned lcm handles reliably — thats what thequality/max_widthknobs are for (q50 = 69KB, zero drops).jpeg quality sweep (lcm, 720p)
sustained 14Hz 720p camera feed for 8s (the go2 workload)
this is the headline: publish a go2-style camera feed at 14Hz over lcm and the raw pipeline delivers 0.8 fps. the codec pipeline delivers all 112 frames.
throughput capacity, fps (1 / median latency)
codec capacity is cpu-bound (~1/encode+decode time) and the same on every transport — ~100fps at 720p, 7x a 14Hz camera. pshm raw shows why shm stays the on-host raw escape hatch: 1765fps at 720p, zero encode cost.
end-to-end replay benchmark (full nav stack, jetson-class 4-core profile: raw delivers 6.5 of 14 fps, compressed delivers all of it, with LESS total cpu) is in #2831 with charts.
notes: webrtc isnt in the grid because its loopback provider is an in-memory dict (stack overhead only, no real SCTP — thats P1 of the webrtc backend spec) and the cloudflare path is cred-gated. the vlm/agent blueprints (spatial, temporal-memory, agentic) are the biggest indirect winners — they re-encode jpeg per VLM call today, with option 5 they get wire bytes for free.
follow-ups (not this PR): option 5 migration per #2831 (
GO2Connection->Out[CompressedImage]+ consumers, needs a replay-dataset compat answer since recorded datasets store rawImage), migratingJpegLcmTransport/JpegShmTransportonto CompressedImage, webrtcImageDecimationemitting this type.Breaking Changes
None
How to Test
14 tests incl. real lcm+zenoh round-trips (skipped where native libturbojpeg is missing — and ci.yml now installs it so CI cant skip silently). benchmark alone, prints one grid at the end:
e2e replay bench:
python -m dimos.protocol.pubsub.benchmark.tool_replay_bench --blueprint unitree-go2-basic --mode codec --duration 30 --out /tmp/bench(re-ran post-trim: 473 frames / 33s = 14.3fps, full delivery). full suite + mypy run clean locally.Contributor License Agreement