Both inputs now declare dora's Arrow struct {width, height, encoding, data}
rather than a flat buffer plus loose metadata. The payload describes itself, so
nothing about its shape has to be agreed out of band, and the frame travels as
one Arrow value instead of a buffer whose meaning lives in a sidecar dict.
Depth uses the same type. dora's registry has no depth type, and a depth map is
an image whose pixels are distances -- the reading ROS takes as well -- so it
arrives with a single-channel encoding: 32FC1/float32 or 16UC1/mono16/uint16.
`units` stays in message metadata because the struct has no field for it.
decode.IMAGE_TYPE is the single definition of the struct; the example source
and the tests build payloads from it rather than respelling the schema.
The older shapes are still decoded -- a flat UInt8 buffer with
width/height/encoding for image, a flat Float32 buffer with shape/units for
depth -- so a producer's payload can move before its manifest does. That is
only half a migration path: type compatibility is directional, so a producer
still declaring std/core/v1/UInt8 fails validate against an
std/media/v1/Image input regardless of what it sends, and has to update its
manifest too.
Verified: 41 tests, validate clean with 4 contracts injected, the dataflow runs
to a clean exit, and a composite rendered through the struct path is identical
to the one rendered before the migration.
dora's typed image is an Arrow struct of width, height, encoding and data --
it carries inside the payload what this node has been reading out of message
metadata. The decoder now takes either shape: a struct describes itself and
its metadata is ignored, a flat UInt8 buffer is described by width/height/
encoding beside it, and everything after unpacking is common.
The declared contract stays std/core/v1/UInt8, deliberately. Type
compatibility is directional -- Image satisfies a Bytes input, but a
std/core/v1/UInt8 output wired into an std/media/v1/Image input is a validate
error -- and dora-zed declares image_bgr as std/core/v1/UInt8. Flipping this
manifest now would break the camera. Accepting both shapes first is what lets
the producers move before the contract does.
output_framing is not part of this. It selects the wire format, raw or
arrow-ipc, and is not required for images: a struct payload was verified to
arrive identically under both framings, and validate does not ask for
arrow-ipc when an output is annotated std/media/v1/Image. It earns its keep
when the wire must be self-describing, such as crossing languages or
recording.
Nothing in a dataflow carries fx, fy, cx and cy except the camera, and every
pixel of a depth map needs them to become a 3D point. The node now accepts
them so whatever computes that next has them to hand.
They are read from two places because dora-zed publishes them twice: the
camera_info message, and the metadata of every frame. The per-frame copy is
not redundant -- camera_info goes out once per camera session, so a viewer
started after the camera would otherwise never see it, and reading frame
metadata means wiring the input is optional even for a camera that publishes
it.
A nine-element row-major K is accepted alongside the compact [fx, fy, cx, cy],
since that is how ROS spells the same model in sensor_msgs/CameraInfo.
The model is drawn under the caption rather than only stored. A depth map read
through the wrong intrinsics looks entirely plausible, and this is where that
becomes visible.
as_float32 moves to a payload module: camera_info arrives as a raw byte slice
for the same reason the depth map does, and the reinterpretation is now shared
rather than written twice.
Verified with dora-cli 1.0.1: validate passes with four contracts injected on
each side, the dataflow runs to a clean exit, and a composed frame was
rendered and inspected with the model drawn on it.
The node showed a colour image only, so debugging a depth pipeline meant
either trusting the numbers or writing a throwaway viewer. It now keeps one
panel per frame stream -- image and depth -- and tiles them side by side,
scaled to a common height. Wiring one is a plain viewer; wiring both shows
them together. bbox and text stay overlays rather than panels.
The contract follows what dora-zed already publishes: float32 [H, W] in
millimetres with 0 for missing. It is sent there as a raw byte slice, so the
payload arrives as UInt8 despite the Float32 contract and is reinterpreted
rather than converted.
Auto-ranging the colormap over the valid pixels' full extent, rather than over
a percentile of them, is deliberate and was not the first attempt: trimming
the tails collapsed on the scene the node exists for. A small object in front
of a large wall is a few percent of the pixels, so both tails landed on the
wall and the object clipped away. Rendering a frame and looking at it found
that; every test passed either way.
Verified with dora-cli 1.0.1: validate passes with both contracts injected,
the dataflow runs both nodes to a clean exit, and a composed frame was
rendered and inspected.
The repository, the Python package, the entrypoint and the manifest name all
now agree. `plot` was carried over from the upstream opencv-plot node and no
longer described anything here, so the module, the class and the environment
variables follow the node:
dora_plot/ -> dora_image_view/
dora_plot/plot.py -> dora_image_view/view.py
Plot -> View
PLOT_WIDTH/HEIGHT -> VIEW_WIDTH/HEIGHT
--plot-width/-height -> --view-width/-height
Breaking for anyone wiring `path: dora-plot` or setting PLOT_WIDTH; nothing is
tagged yet.
The repository is dora-plot, but the package, the entrypoint, the manifest
name and the window title were all opencv-plot. Consumers wire the node by
`path:`, so the split name was visible in every dataflow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WLVZCFFcCH1EpTXGfS9aD
A display sink that overlays bounding boxes and text on an image input and
shows it in an OpenCV window. Based on opencv-plot from dora-hub, brought up
to the dora 1.0 node contract and packaged with uv.
Python rather than Rust: the opencv Rust crate generates its bindings from the
OpenCV headers on the build machine, so it needs libopencv-dev and libclang-dev
per machine and per architecture and does not cross-compile. Every dependency
here ships prebuilt wheels for linux-x86_64, linux-aarch64 and macos-arm64.
Covers every encoding the upstream node did — bgr8, rgb8, jpeg/jpg/jpe/bmp/
webp/png, yuv420 and avif — plus both bbox formats, the text overlay,
PLOT_WIDTH/PLOT_HEIGHT and headless operation under CI=true.