"""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()