Commit Graph
26 Commits
Author SHA1 Message Date
cristhian 344a527097 record that the webcam example is verified, and pin v0.1.1
The example carried a caveat saying it validated but could not be run, because
a `git:` node could not be fetched. The organization is public now, so it can:
dora clones dora-webcam at the pinned tag, builds both nodes, and runs them
against a real camera to a clean exit.

The pin moves to v0.1.1, whose documentation shows the https URL. v0.1.0 points
at an ssh:// URL that dora cannot authenticate, so anyone copying the wiring out
of that tag would have hit the failure this example exists to demonstrate the
absence of.

What a `git:` node does require is still recorded, since it is not obvious:
anonymous read, which on Gitea means a public organization and not merely a
public repository.
2026-09-15 06:50:43 -03:00
cristhian c178afd58e add depth and camera intrinsics, and adopt the typed Arrow image (#4)
validate / validate (push) Successful in 59m16s
Adds a depth panel tiled beside the image, the camera_info input holding the
pinhole model, and std/media/v1/Image as the payload for both image and depth.
Builds with uv, lints markdown, keeps the workflow where act looks for it, and
carries a worked example wiring dora-webcam in.
2026-09-15 09:36:16 +00:00
cristhian 7eb00000ea make the node standalone and rename it to dora-image-view (#3)
validate / validate (push) Successful in 59m14s
The node was named opencv-plot everywhere, and the example dataflow could not
run without a webcam and two nodes fetched over the network.

Renames the node to dora-image-view, replaces the external example with a
synthetic source in this checkout, types that source inline rather than
shipping a second manifest, and merges the two CI jobs into one.
2026-09-15 09:33:11 +00:00
cristhian f7171de2af fetch git nodes over https
validate / validate (pull_request) Successful in 59m10s
ssh:// cannot authenticate under dora-cli 1.0.1: it fetches through libgit2
with no credential callback, so the only transport that can work is anonymous
https. The documented URLs move accordingly, and the repository is now
dora-image-view rather than dora-plot.

The note about what a `git:` node needs is corrected while here. The cause is
not that a repository is private but that the fetch is anonymous: on Gitea a
repository inside a private organization is internal and answers an anonymous
clone with 401 even when the repository itself is marked public. The
organization has to be public too.
2026-09-15 06:29:37 -03:00
cristhian 15e0b9155a add a worked example wiring dora-webcam into this node
The only dataflow here drove the viewer from a test fixture, so nothing showed
how to consume a real producer. dataflow_webcam.yml wires dora-webcam v0.1.0
into it: the camera publishes std/media/v1/Image and this node draws it, with
depth, camera_info, text and bbox simply unwired, which is what every input
being optional buys.

The dependency points one way only. A consumer naming its producer is an arrow;
dora-webcam names no consumer, so there is no cycle.

Two things are deliberate. The camera's output type is repeated in the dataflow
because a `git:` node's manifest is not read at validate time, only at build, so
without it the two contracts are never compared -- a deliberate mismatch is
caught with the annotation and passes silently without it. And the file carries
no strict_types: a camera publishes an image and nothing else, and strict
validation warns about every typed input a dataflow leaves unwired.

Interoperability was verified directly, feeding dora-webcam's packed payload
into this node's decoder across bgr8, rgb8, jpeg, png and yuv420: the struct
types are identical and the lossless encodings round-trip exactly.

The dataflow validates but was not run: with dora-cli 1.0.1 a `git:` node
pointing at a private repository does not build over either transport, which
the file and the README record.
2026-09-15 06:26:10 -03:00
cristhian 363bf006be lint markdown recursively
`scan *.md` only matched the repository root, so a file added under any
subdirectory would have stopped being checked without the CI step noticing.
Scanning recursively follows the files instead of the layout, and
--respect-gitignore keeps it off .venv and .dora.
2026-09-15 06:20:14 -03:00
cristhian c9715a34d5 move the workflow to .github/workflows
act looks in .github/workflows by default, and running it locally before a
commit is one of this repository's rules; keeping the workflow where act cannot
see it meant every invocation needed -W. Gitea Actions reads both locations, so
only one of them may exist or a push fires the run twice.
2026-09-15 06:14:00 -03:00
cristhian c1dba8998d build with uv rather than pip
The build command was `pip install .`, which is what the dora docs use because
`--uv` rewrites it. Spelling it `uv pip install .` means the dataflow never
installs through pip at all, whether or not `--uv` was passed. The cost is that
a node then needs a virtualenv to install into, so dataflows must be run with
`--uv`; AGENTS.md says so.

Note that the PR #3 branch still carries `pip install .`; landing this branch
on top of it is what corrects that.
2026-09-15 06:09:33 -03:00
cristhian 093715e78f add markdown linting and cut AGENTS.md back to rules
AGENTS.md was a per-module tour of the code, which restated what the code says
and had already drifted: it described the example as emitting three inputs when
it emits five. It is now the conventions an agent cannot infer -- dora version,
uv, ruff, numpy docstrings, the platform requirement, act, and what not to do --
and nothing else. The gotchas it carried are explained at their point of use in
the code instead.

pymarkdown lints the markdown, configured in pyproject.toml and run in CI
beside ruff. It caught a fenced block with no language and an over-long table
row. ruff's pydocstyle convention moves to numpy.

GEMINI.md is removed.
2026-09-15 06:03:40 -03:00
cristhian 7d9ebe8b38 move the synthetic source into tests
It is a test fixture, so it lives in tests/. It was only ever an example by
placement: CI runs it as the end-to-end gate, SOURCE_FRAMES exists so CI can
bound it, and it publishes depth and camera_info because those inputs are typed
and strict validation fails on a typed input the dataflow does not wire -- none
of which is pedagogy. The worked example of wiring this node is the README's
usage block, against a real camera.

It stays a dora node rather than a pytest case because what it exercises is the
wiring and the event loop, which only a running dataflow covers. pytest does
not collect it: the filename does not start with `test_`.

dataflow.yml stays at the repository root rather than moving beside it. A
node's working directory is the directory of the dataflow file, and both nodes
build with `pip install .`, which needs pyproject.toml in that directory.
2026-09-15 05:31:47 -03:00
cristhian 2b70d07868 docs: correct the example's description and say what it is for
The README still described the source as emitting "the three inputs this node
draws". It emits five outputs -- the depth map and the intrinsics were added
without the prose following.

Also records that the example is the CI gate as well as the demo, and that the
second role is what has been shaping it: it publishes depth and camera_info
because those inputs are typed and strict validation fails on a typed input the
dataflow does not wire. Worth knowing before adding the next output.
2026-09-15 05:30:03 -03:00
cristhian 8a7bda5867 use std/media/v1/Image for image and depth
Both inputs now declare dora's Arrow struct {width, height, encoding, data}
rather than a flat buffer plus loose metadata. The payload describes itself, so
nothing about its shape has to be agreed out of band, and the frame travels as
one Arrow value instead of a buffer whose meaning lives in a sidecar dict.

Depth uses the same type. dora's registry has no depth type, and a depth map is
an image whose pixels are distances -- the reading ROS takes as well -- so it
arrives with a single-channel encoding: 32FC1/float32 or 16UC1/mono16/uint16.
`units` stays in message metadata because the struct has no field for it.

decode.IMAGE_TYPE is the single definition of the struct; the example source
and the tests build payloads from it rather than respelling the schema.

The older shapes are still decoded -- a flat UInt8 buffer with
width/height/encoding for image, a flat Float32 buffer with shape/units for
depth -- so a producer's payload can move before its manifest does. That is
only half a migration path: type compatibility is directional, so a producer
still declaring std/core/v1/UInt8 fails validate against an
std/media/v1/Image input regardless of what it sends, and has to update its
manifest too.

Verified: 41 tests, validate clean with 4 contracts injected, the dataflow runs
to a clean exit, and a composite rendered through the struct path is identical
to the one rendered before the migration.
2026-09-15 05:27:40 -03:00
cristhian a6fa3ce32b accept std/media/v1/Image payloads
dora's typed image is an Arrow struct of width, height, encoding and data --
it carries inside the payload what this node has been reading out of message
metadata. The decoder now takes either shape: a struct describes itself and
its metadata is ignored, a flat UInt8 buffer is described by width/height/
encoding beside it, and everything after unpacking is common.

The declared contract stays std/core/v1/UInt8, deliberately. Type
compatibility is directional -- Image satisfies a Bytes input, but a
std/core/v1/UInt8 output wired into an std/media/v1/Image input is a validate
error -- and dora-zed declares image_bgr as std/core/v1/UInt8. Flipping this
manifest now would break the camera. Accepting both shapes first is what lets
the producers move before the contract does.

output_framing is not part of this. It selects the wire format, raw or
arrow-ipc, and is not required for images: a struct payload was verified to
arrive identically under both framings, and validate does not ask for
arrow-ipc when an output is annotated std/media/v1/Image. It earns its keep
when the wire must be self-describing, such as crossing languages or
recording.
2026-09-15 05:25:18 -03:00
cristhian 92abe8a446 refactor: type the example source inline instead of shipping a manifest
A dora-node.yml describes exactly one node and is the unit `dora hub publish`
operates on, which expects it beside pyproject.toml. This repository ships one
node, so it should carry one manifest. examples/dora-node.yml described a test
fixture that nobody installs and that has no package of its own -- a manifest
for something that is not a node.

The source's outputs are now annotated with `output_types:` in the dataflow,
which is the documented way to type an output without a manifest: "only
annotated outputs are type-checked". It runs as `path: python` + `args:`
because `dora validate` matches a manifest to a `path:` node by entrypoint, so
a path node would be tried against the root manifest, fail to match
`dora-image-view`, and warn -- fatal under strict_types. Through the
interpreter there is nothing to match.

The type gate is unchanged in strength: the viewer's contract is still
injected and compared against the source's annotations, and a deliberate
Float32 -> String swap still fails validate.

The cost is that the fixture no longer gets a managed venv of its own and runs
against the project environment. That applies to the fixture only; the shipped
node still uses path: + build:, which is what a consumer exercises.

This supersedes the approach in "fix strict-type validation by giving the
example source a manifest" and the doc commit that cited the
manifest-next-to-a-path-node rule as justification.
2026-09-15 05:21:36 -03:00
cristhian 1c7a9c816d docs: cite the manifest-next-to-a-path-node rule
The comment on examples/dora-node.yml read as an apology for a workaround.
It is the documented mechanism: a manifest describes exactly one node, and one
sitting next to a plain `path:` node is picked up automatically, which is how a
node gets contract validation without being published.
2026-09-15 05:17:33 -03:00
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
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
cristhian 407395ab88 refactor CI into a single job and cache the dora CLI
validate / validate (pull_request) Successful in 59m51s
The two jobs each paid for a checkout, an apt-get update and an OpenCV shared
library install -- about twenty seconds apiece -- to run gates that together
take about a second. Merging them pays that once. The dora CLI is a static
binary pinned to DORA_VERSION, so it caches exactly.
2026-09-15 03:22:27 -03:00
cristhian b9d9843fa1 fix strict-type validation by giving the example source a manifest
`dora validate` matches a node manifest to a `path:` node by entrypoint. With
no manifest beside examples/source.py the root dora-node.yml was tried against
it, did not match its entrypoint, and warned -- which `strict_types: true`
turns into a non-zero exit, failing CI.

Declaring the source's output types in the new manifest 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 against the viewer's.

Verified with dora-cli 1.0.1: validate passes with both contracts injected, and
`CI=true SOURCE_FRAMES=30 dora run dataflow.yml --uv` runs both nodes to a
clean exit.
2026-09-15 03:22:27 -03:00
cristhian 00dc4c7b71 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.
2026-09-15 03:22:27 -03:00
cristhianandClaude Opus 5 22bd1e9e89 add an in-repo synthetic source so the example needs no network
validate / dataflow (pull_request) Failing after 59m9s
validate / test (pull_request) Successful in 59m8s
The example dataflow pulled opencv-video-capture and pyarrow-sender from
dora-hub, needed a webcam, and left `bbox` unwired because every bbox source
is a detector. examples/source.py emits an animated frame, the box around the
square in it, and a caption, so the dataflow exercises all three inputs with
nothing outside this checkout.

SOURCE_FRAMES bounds the run, which lets CI run the dataflow headless rather
than only validate it. Drops the dora-zed and dora-yolo wirings from the
README and trims AGENTS.md to what the code and README do not already say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WLVZCFFcCH1EpTXGfS9aD
2026-09-14 16:09:01 -05:00
cristhianandClaude Opus 5 e2debe0581 refactor: rename the node from opencv-plot to dora-plot
The repository is dora-plot, but the package, the entrypoint, the manifest
name and the window title were all opencv-plot. Consumers wire the node by
`path:`, so the split name was visible in every dataflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WLVZCFFcCH1EpTXGfS9aD
2026-09-14 16:08:48 -05:00
cristhian d5d3026b92 Merge pull request 'add opencv-plot node for dora dataflows' (#2) from add-opencv-plot-node into main
validate / validate (push) Successful in 58m50s
validate / test (push) Successful in 1h0m5s
Reviewed-on: liria/dora-plot#2
2026-09-11 10:34:34 +00:00
cristhianandClaude Opus 5 702812c35f fix duplicate CI runs by scoping the push trigger to main
validate / validate (pull_request) Successful in 58m50s
validate / test (pull_request) Successful in 59m57s
A pull request ran the workflow twice on the same commit, once for the
branch push and once for the PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPUZks9z3Pi5oxw9GQ9znt
2026-09-11 06:47:00 -03:00
cristhianandClaude Opus 5 cdc17d2787 fix cv2 import in CI by installing the OpenCV shared libraries
The opencv-python wheel links against libGL at import time, so pytest
failed at collection with ImportError: libGL.so.1 on the runner image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPUZks9z3Pi5oxw9GQ9znt
2026-09-11 06:46:56 -03:00
cristhianandClaude Opus 5 7c8eec9e81 add pyarrow as an explicit dependency
The bbox input path and the tests import pyarrow directly; it was
present only because dora-rs pulls it in transitively.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPUZks9z3Pi5oxw9GQ9znt
2026-09-11 06:46:52 -03:00