rename node to dora-image-view
The repository, the Python package, the entrypoint and the manifest name all now agree. `plot` was carried over from the upstream opencv-plot node and no longer described anything here, so the module, the class and the environment variables follow the node: dora_plot/ -> dora_image_view/ dora_plot/plot.py -> dora_image_view/view.py Plot -> View PLOT_WIDTH/HEIGHT -> VIEW_WIDTH/HEIGHT --plot-width/-height -> --view-width/-height Breaking for anyone wiring `path: dora-plot` or setting PLOT_WIDTH; nothing is tagged yet.
This commit is contained in:
@@ -23,14 +23,21 @@ CI=true SOURCE_FRAMES=30 dora run dataflow.yml --uv # the same, headless
|
||||
|
||||
## Architecture
|
||||
|
||||
- **`dora_plot/main.py`** — Argument parsing and the dora event loop, nothing else. The env var wins over the CLI flag for `PLOT_WIDTH`/`PLOT_HEIGHT`, matching the upstream node. `Node(args.name)` rather than `Node()` so the node also runs standalone as a dynamic node.
|
||||
- **`dora_plot/decode.py`** — `to_bgr()`: every supported encoding to a writable BGR array. Two behaviours are deliberate, not bugs: `bgr8` **copies** (the payload may be a read-only view on shared memory, and the frame is annotated in place), and `yuv420` converts with `COLOR_YUV420p2RGB` rather than `...2BGR` — the upstream node converts to RGB and then displays it as BGR, and that channel swap is part of the behaviour callers already see.
|
||||
- **`dora_plot/plot.py`** — `Plot` (the overlay state and the window) and `Bboxes`. `headless` (from `CI=true`) skips only the window: every input is still decoded, which is what makes the node safe in a pipeline test.
|
||||
- **`dora_plot/bbox.py`** — Parses the `bbox` struct into xyxy corners. `xywh` is centre-based (x, y are the box centre). `_flat()` unwraps the single-element list that `pa.array([{...}])` produces around each field, and reads a field stored flat as-is.
|
||||
- **`dora_image_view/main.py`** — Argument parsing and the dora event loop, nothing else. The env var wins over the CLI flag for `VIEW_WIDTH`/`VIEW_HEIGHT`. The upstream node spells these `PLOT_WIDTH`/`PLOT_HEIGHT`; they were renamed with the node, so a dataflow copied from upstream needs its env keys changed. `Node(args.name)` rather than `Node()` so the node also runs standalone as a dynamic node.
|
||||
- **`dora_image_view/decode.py`** — `to_bgr()`: every supported encoding to a writable BGR array. Two behaviours are deliberate, not bugs: `bgr8` **copies** (the payload may be a read-only view on shared memory, and the frame is annotated in place), and `yuv420` converts with `COLOR_YUV420p2RGB` rather than `...2BGR` — the upstream node converts to RGB and then displays it as BGR, and that channel swap is part of the behaviour callers already see.
|
||||
- **`dora_image_view/view.py`** — `View` (the overlay state and the window) and `Bboxes`. `headless` (from `CI=true`) skips only the window: every input is still decoded, which is what makes the node safe in a pipeline test.
|
||||
- **`dora_image_view/bbox.py`** — Parses the `bbox` struct into xyxy corners. `xywh` is centre-based (x, y are the box centre). `_flat()` unwraps the single-element list that `pa.array([{...}])` produces around each field, and reads a field stored flat as-is.
|
||||
- **`examples/source.py`** — The synthetic source node: an animated frame, the box around the square in it, and a caption. It exists so the example dataflow exercises all three inputs without a camera or a detector, and so CI can run the dataflow rather than only validate it. `SOURCE_FRAMES` bounds the run, which is what lets CI wait for a clean exit instead of killing it on a timeout.
|
||||
- **`dora-node.yml`** — Node manifest (`apiVersion: 1`). `dora validate` injects these types into any dataflow that reaches the node by `path:`; a `git:`-sourced node does **not** pick it up, which is why the example builds from the checkout. `image` and `text` carry type URNs naming the element type because each payload is a raw buffer described by its metadata, not an Arrow struct. `bbox` is deliberately **untyped**: senders disagree on whether its numeric fields are 32- or 64-bit, and the node reads either.
|
||||
- **`dataflow.yml`** — The example and the CI gate, with `strict_types: true`. `source` carries `output_types:` because it has no manifest of its own; `bbox` is annotated on neither side, matching the manifest.
|
||||
- **`.gitea/workflows/validate.yml`** — CI: validates and runs the dataflow headless, and runs the unit tests. Needs no camera and no display.
|
||||
- **`examples/dora-node.yml`** — Manifest for the source node, and not optional. `dora validate` matches a manifest to a `path:` node by entrypoint; with no manifest beside `examples/source.py`, the root `dora-node.yml` is tried against it, does not match its entrypoint, and warns — which `strict_types: true` turns into a failure. Declaring the source's types here rather than as `output_types:` in `dataflow.yml` is also what makes them *checked*: an inline annotation shadows the manifest, so the manifest would be injected but never compared.
|
||||
- **`dataflow.yml`** — The example and the CI gate, with `strict_types: true`. Both nodes are `path:` nodes with a manifest, so both contracts are injected and compared against each other; `bbox` is annotated on neither side, matching the manifests.
|
||||
- **`.gitea/workflows/validate.yml`** — CI: lints, runs the unit tests, validates the dataflow and runs it headless. Needs no camera and no display. One job on purpose — the gates themselves take about a second, and are dwarfed by preparing the runner.
|
||||
|
||||
## Why CI installs libgl1
|
||||
|
||||
`opencv-python` links against libGL at import time, so `import cv2` fails on the bare runner image and CI installs the shared libraries first. At roughly sixteen seconds it is the most expensive step in the job, several times everything else combined.
|
||||
|
||||
`opencv-python-headless` needs none of them, but it also has no `imshow`, which is this node's whole purpose. Removing the step therefore means making the GUI wheel an optional extra — headless by default, `pip install .[gui]` for a real window — and that is a change to what a consumer installs, not just to CI. It has not been done.
|
||||
|
||||
## A caveat on typed optional inputs
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# dora-plot
|
||||
# dora-image-view
|
||||
|
||||
Dora node that overlays bounding boxes, labels and text on an image input and displays
|
||||
it in an OpenCV window. A display sink for debugging a vision pipeline: it produces no
|
||||
@@ -13,11 +13,11 @@ Add to your dataflow:
|
||||
|
||||
```yaml
|
||||
nodes:
|
||||
- id: plot
|
||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-plot.git
|
||||
- id: view
|
||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-image-view.git
|
||||
tag: v0.1.0
|
||||
build: pip install .
|
||||
path: dora-plot
|
||||
path: dora-image-view
|
||||
inputs:
|
||||
image: camera/image
|
||||
bbox: detector/bbox
|
||||
@@ -61,11 +61,14 @@ CI=true SOURCE_FRAMES=30 dora run dataflow.yml --uv
|
||||
|
||||
[`dora-node.yml`](dora-node.yml) declares the node's inputs with type URNs. No source
|
||||
package is shared between node repositories, so it is the only contract a consumer has,
|
||||
and it is what makes a wiring mistake fail at build time instead of at runtime:
|
||||
and it is what makes a wiring mistake fail at build time instead of at runtime. The
|
||||
example source has a manifest of its own, so `dora validate` checks the two against
|
||||
each other:
|
||||
|
||||
```
|
||||
$ dora validate dataflow.yml
|
||||
node "plot": injected 2 contract type(s) from .../dora-node.yml
|
||||
node "source": injected 2 contract type(s) from .../examples/dora-node.yml
|
||||
node "view": injected 2 contract type(s) from .../dora-node.yml
|
||||
All type annotations OK.
|
||||
```
|
||||
|
||||
@@ -81,8 +84,8 @@ of that state:
|
||||
|
||||
- An **`image`** event decodes the frame and immediately redraws: it draws every stored
|
||||
bbox (green rectangle plus `label, conf` text), draws the stored text at the top left,
|
||||
resizes to `PLOT_WIDTH`x`PLOT_HEIGHT` if both are set, and shows it in a window titled
|
||||
`Dora Node: dora-plot`. Press `q` in the window to quit.
|
||||
resizes to `VIEW_WIDTH`x`VIEW_HEIGHT` if both are set, and shows it in a window titled
|
||||
`Dora Node: dora-image-view`. Press `q` in the window to quit.
|
||||
- A **`bbox`** or **`text`** event only updates stored state; it is drawn on the next
|
||||
`image` redraw.
|
||||
|
||||
@@ -124,11 +127,11 @@ None — it is a display sink.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `PLOT_WIDTH` | image width | Resize the displayed frame to this width, in pixels. |
|
||||
| `PLOT_HEIGHT` | image height | Resize the displayed frame to this height, in pixels. |
|
||||
| `VIEW_WIDTH` | image width | Resize the displayed frame to this width, in pixels. |
|
||||
| `VIEW_HEIGHT` | image height | Resize the displayed frame to this height, in pixels. |
|
||||
|
||||
Resizing only happens when both are set. Both are also available as `--plot-width` and
|
||||
`--plot-height`; the environment variable wins when both are given.
|
||||
Resizing only happens when both are set. Both are also available as `--view-width` and
|
||||
`--view-height`; the environment variable wins when both are given.
|
||||
|
||||
`CI=true` suppresses the display window; it is not a configuration knob.
|
||||
|
||||
@@ -143,7 +146,9 @@ so the node installs with no system packages and no compiler on any of them —
|
||||
AVIF support, which comes from `pillow-avif-plugin` rather than OpenCV.
|
||||
|
||||
**Requirements:** dora 1.0+ (verified with `dora-cli 1.0.1`), Python 3.11+, `uv`, and a
|
||||
display to show the window on (or `CI=true` to run without one).
|
||||
display to show the window on (or `CI=true` to run without one). On a bare Linux image
|
||||
`opencv-python` also needs the libGL shared libraries (`libgl1`, `libglib2.0-0`), which
|
||||
it links at import time.
|
||||
|
||||
## Test
|
||||
|
||||
|
||||
+8
-8
@@ -5,14 +5,14 @@
|
||||
# dataflow against it without running the node, so CI needs neither OpenCV nor
|
||||
# a display.
|
||||
apiVersion: 1
|
||||
name: dora-plot
|
||||
name: dora-image-view
|
||||
namespace: liria
|
||||
description: Overlays bounding boxes, labels, and text on an image input and displays it in an OpenCV window
|
||||
categories: [visualization, debug]
|
||||
keywords: [opencv, plot, bbox, bounding-box, image, visualization, object-detection]
|
||||
keywords: [opencv, view, bbox, bounding-box, image, visualization, object-detection]
|
||||
|
||||
runtime: python
|
||||
entrypoint: dora-plot
|
||||
entrypoint: dora-image-view
|
||||
# `dora build --uv` / `dora run --uv` rewrites this to `uv pip install .` and
|
||||
# gives the node its own managed venv under `.dora/python-envs/`, so the
|
||||
# dependencies it sees at runtime are the ones installed here.
|
||||
@@ -45,19 +45,19 @@ inputs:
|
||||
outputs: {}
|
||||
|
||||
env:
|
||||
PLOT_WIDTH:
|
||||
VIEW_WIDTH:
|
||||
type: int
|
||||
description: Resize the displayed frame to this width (pixels). Defaults to the image input width.
|
||||
PLOT_HEIGHT:
|
||||
VIEW_HEIGHT:
|
||||
type: int
|
||||
description: Resize the displayed frame to this height (pixels). Defaults to the image input height.
|
||||
|
||||
example: |
|
||||
- id: plot
|
||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-plot.git
|
||||
- id: view
|
||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-image-view.git
|
||||
tag: v0.1.0
|
||||
build: pip install .
|
||||
path: dora-plot
|
||||
path: dora-image-view
|
||||
inputs:
|
||||
image: camera/image
|
||||
bbox: detector/bbox
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import numpy as np
|
||||
|
||||
from dora_plot.plot import Bboxes
|
||||
from dora_image_view.view import Bboxes
|
||||
|
||||
|
||||
def parse(value, bbox_format: str) -> Bboxes:
|
||||
@@ -9,17 +9,17 @@ import os
|
||||
|
||||
from dora import Node
|
||||
|
||||
from dora_plot import bbox, decode
|
||||
from dora_plot.plot import Plot
|
||||
from dora_image_view import bbox, decode
|
||||
from dora_image_view.view import View
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Connect to the dataflow and display every frame that arrives."""
|
||||
args = _parse_args()
|
||||
|
||||
plot = Plot(
|
||||
width=_size("PLOT_WIDTH", args.plot_width),
|
||||
height=_size("PLOT_HEIGHT", args.plot_height),
|
||||
view = View(
|
||||
width=_size("VIEW_WIDTH", args.view_width),
|
||||
height=_size("VIEW_HEIGHT", args.view_height),
|
||||
headless=os.getenv("CI") == "true",
|
||||
)
|
||||
|
||||
@@ -43,12 +43,12 @@ def main() -> None:
|
||||
metadata.get("width"),
|
||||
metadata.get("height"),
|
||||
)
|
||||
if plot.show(frame):
|
||||
if view.show(frame):
|
||||
break
|
||||
elif event_id == "bbox":
|
||||
plot.bboxes = bbox.parse(event["value"], metadata.get("format", "xyxy").lower())
|
||||
view.bboxes = bbox.parse(event["value"], metadata.get("format", "xyxy").lower())
|
||||
elif event_id == "text":
|
||||
plot.text = event["value"][0].as_py()
|
||||
view.text = event["value"][0].as_py()
|
||||
else:
|
||||
print(f"ignoring unexpected input `{event_id}`")
|
||||
|
||||
@@ -59,18 +59,18 @@ def _parse_args() -> argparse.Namespace:
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
default="dora-plot",
|
||||
default="dora-image-view",
|
||||
help="The name of the node in the dataflow, used when running as a dynamic node.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plot-width",
|
||||
"--view-width",
|
||||
type=int,
|
||||
help="Resize the displayed frame to this width. Overridden by PLOT_WIDTH.",
|
||||
help="Resize the displayed frame to this width. Overridden by VIEW_WIDTH.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plot-height",
|
||||
"--view-height",
|
||||
type=int,
|
||||
help="Resize the displayed frame to this height. Overridden by PLOT_HEIGHT.",
|
||||
help="Resize the displayed frame to this height. Overridden by VIEW_HEIGHT.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
WINDOW = "Dora Node: dora-plot"
|
||||
WINDOW = "Dora Node: dora-image-view"
|
||||
GREEN = (0, 255, 0)
|
||||
WHITE = (255, 255, 255)
|
||||
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
||||
@@ -22,7 +22,7 @@ class Bboxes:
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plot:
|
||||
class View:
|
||||
"""Overlay state, plus the window it is displayed in.
|
||||
|
||||
`headless` suppresses the window; the node still decodes every input.
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
"""Synthetic source node: this repository's stand-in for a camera and a detector.
|
||||
|
||||
Emits an animated frame, the box around the square moving in it, and a caption --
|
||||
the three inputs dora-plot draws -- so the example dataflow needs no camera, no
|
||||
the three inputs dora-image-view draws -- so the example dataflow needs no camera, no
|
||||
detector and no network.
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@ FRAMES = int(os.getenv("SOURCE_FRAMES", "0"))
|
||||
|
||||
BOX = 96
|
||||
SPEED = 6
|
||||
CAPTION = "dora-plot synthetic source"
|
||||
CAPTION = "dora-image-view synthetic source"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
[project]
|
||||
name = "dora-plot"
|
||||
name = "dora-image-view"
|
||||
version = "0.1.0"
|
||||
description = "Dora node that overlays bounding boxes and text on an image and displays it in an OpenCV window"
|
||||
readme = "README.md"
|
||||
@@ -21,7 +21,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
dora-plot = "dora_plot.main:main"
|
||||
dora-image-view = "dora_image_view.main:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest >= 8.1", "ruff >= 0.9"]
|
||||
@@ -31,7 +31,7 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["dora_plot"]
|
||||
packages = ["dora_image_view"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = ["D", "I", "N", "RET", "UP"]
|
||||
|
||||
@@ -7,8 +7,8 @@ import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from dora_plot import bbox, decode
|
||||
from dora_plot.plot import Bboxes, Plot
|
||||
from dora_image_view import bbox, decode
|
||||
from dora_image_view.view import Bboxes, View
|
||||
|
||||
WIDTH, HEIGHT = 12, 8
|
||||
|
||||
@@ -123,13 +123,13 @@ def test_xywh_is_converted_to_corners():
|
||||
|
||||
|
||||
def test_headless_show_draws_without_a_window():
|
||||
plot = Plot(headless=True, text="hello")
|
||||
plot.bboxes = Bboxes(
|
||||
view = View(headless=True, text="hello")
|
||||
view.bboxes = Bboxes(
|
||||
corners=np.array([[1.0, 1.0, 5.0, 5.0]]),
|
||||
conf=np.array([0.9]),
|
||||
labels=np.array(["cat"]),
|
||||
)
|
||||
frame = np.zeros((32, 32, 3), dtype=np.uint8)
|
||||
|
||||
assert plot.show(frame) is False
|
||||
assert view.show(frame) is False
|
||||
assert frame.any(), "the overlays should have marked the frame"
|
||||
Reference in New Issue
Block a user