Files
dora-image-view/dora_image_view/main.py
T
cristhian 0efeacd538 add a depth map panel
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.
2026-09-15 05:06:50 -03:00

104 lines
3.0 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` and
`text` 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
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"]
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 == "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.get("encoding", "bgr8").lower(),
metadata.get("width"),
metadata.get("height"),
)
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()