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