Files
dora-image-view/dora_image_view/main.py
T
cristhian 5a0f13d4db add camera_info input for the pinhole model
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.
2026-09-15 05:09:07 -03:00

112 lines
3.5 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.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()