Files
dora-image-view/dora_image_view/main.py
T
cristhian a6fa3ce32b accept std/media/v1/Image payloads
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.
2026-09-15 05:25:18 -03:00

107 lines
3.3 KiB
Python

"""A dora display sink: overlays bounding boxes and text on an image input.
`image` and `depth` each fill one panel of the window and redraw. `bbox`,
`text` and `camera_info` events only update stored state, which the next redraw
draws.
"""
import argparse
import os
import numpy as np
from dora import Node
from dora_image_view import bbox, decode, depth, intrinsics
from dora_image_view.view import View
def main() -> None:
"""Connect to the dataflow and display every frame that arrives."""
args = _parse_args()
view = View(
width=_size("VIEW_WIDTH", args.view_width),
height=_size("VIEW_HEIGHT", args.view_height),
headless=os.getenv("CI") == "true",
)
# Passing the name lets the node also run standalone as a dynamic node; when
# dora spawns it, the id from the dataflow wins.
node = Node(args.name)
for event in node:
if event["type"] == "ERROR":
raise RuntimeError(event["error"])
if event["type"] != "INPUT":
continue
event_id = event["id"]
metadata = event["metadata"]
# A sender that puts intrinsics on every frame keeps them current even
# if the camera_info message itself was published before this node
# started, which is when dora-zed sends it.
view.intrinsics = intrinsics.from_metadata(metadata) or view.intrinsics
panel = _decode_panel(event_id, event["value"], metadata)
if panel is not None:
name, frame = panel
view.panels[name] = frame
if view.show():
break
elif event_id == "camera_info":
view.intrinsics = intrinsics.parse(event["value"], metadata)
elif event_id == "bbox":
view.bboxes = bbox.parse(event["value"], metadata.get("format", "xyxy").lower())
elif event_id == "text":
view.text = event["value"][0].as_py()
else:
print(f"ignoring unexpected input `{event_id}`")
def _decode_panel(event_id: str, value, metadata: dict) -> tuple[str, np.ndarray] | None:
"""Decode a frame-bearing input into the panel name and frame it fills.
Returns None for an input that carries no frame, which is what tells the
event loop to treat it as overlay state instead.
"""
if event_id == "image":
return "image", decode.to_bgr(value, metadata)
if event_id == "depth":
return "depth", depth.colorize(depth.to_millimetres(value, metadata))
return None
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="OpenCV viewer: image and depth, with bbox and text overlays.",
)
parser.add_argument(
"--name",
default="dora-image-view",
help="The name of the node in the dataflow, used when running as a dynamic node.",
)
parser.add_argument(
"--view-width",
type=int,
help="Resize the displayed frame to this width. Overridden by VIEW_WIDTH.",
)
parser.add_argument(
"--view-height",
type=int,
help="Resize the displayed frame to this height. Overridden by VIEW_HEIGHT.",
)
return parser.parse_args()
def _size(name: str, fallback: int | None) -> int | None:
value = os.getenv(name)
return int(value) if value and value.isnumeric() else fallback
if __name__ == "__main__":
main()