validate / validate (push) Successful in 59m14s
Every setting is an environment variable, which reads fine for one and badly for six: the README's table implied the only way to set them was to export them around `dora run`. A node takes an `env:` map in the dataflow, applied to both the build and the run, which keeps configuration with the wiring rather than in whatever shell happened to start it. The README now shows a populated node. A node's `env:` wins over the environment the dataflow was started in, and a variable it does not set still passes through. That is why the block in dataflow.yml is present but commented out: CI passes CAPTURE_FRAMES in through the shell, and pinning it there would silently override the runner.
164 lines
5.8 KiB
Markdown
164 lines
5.8 KiB
Markdown
# dora-webcam
|
|
|
|
Dora node that captures frames from a webcam and publishes them as
|
|
`std/media/v1/Image`. A source for a vision pipeline: it takes a tick and produces
|
|
frames.
|
|
|
|
Based on [`opencv-video-capture`](https://github.com/dora-rs/dora-hub/tree/main/node-hub/opencv-video-capture)
|
|
from dora-hub, brought up to the dora 1.0 node contract, narrowed to Linux camera
|
|
selection, and changed to publish the typed Arrow image struct rather than a flat
|
|
buffer plus metadata.
|
|
|
|
## Usage
|
|
|
|
Add to your dataflow:
|
|
|
|
```yaml
|
|
nodes:
|
|
- id: camera
|
|
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
|
|
tag: v0.1.0
|
|
build: uv pip install .
|
|
path: dora-webcam
|
|
inputs:
|
|
tick: dora/timer/millis/33
|
|
outputs:
|
|
- image
|
|
```
|
|
|
|
Run the dataflow with `--uv` (`dora run dataflow.yml --uv`): the build command is
|
|
`uv pip install .`, so the node gets its own managed environment and never installs into
|
|
an ambient one.
|
|
|
|
### Runnable dataflow
|
|
|
|
[`dataflow.yml`](dataflow.yml) is the camera on its own, and is what CI runs:
|
|
|
|
```bash
|
|
dora run dataflow.yml --uv
|
|
```
|
|
|
|
Nothing consumes the frames, so to actually see them set `PREVIEW`:
|
|
|
|
```bash
|
|
PREVIEW=true dora run dataflow.yml --uv
|
|
```
|
|
|
|
A window opens showing the frames as they are published. Press `q` to stop. That is
|
|
enough to check a camera, an encoding or a flip without wiring a second node — which is
|
|
why there is one dataflow here rather than one per consumer.
|
|
|
|
For a real pipeline, wire `image` into any node that takes `std/media/v1/Image`, from
|
|
your own dataflow. This repository names no consumer: it is a source, and depending on
|
|
something downstream of it only to demonstrate it would be a cycle.
|
|
|
|
## Output
|
|
|
|
| Output | Payload | Metadata |
|
|
|---|---|---|
|
|
| `image` | `std/media/v1/Image` struct | none — the struct describes itself |
|
|
|
|
The struct carries `width: UInt32`, `height: UInt32`, `encoding: Utf8` and
|
|
`data: LargeBinary`, so a receiver needs nothing from the message metadata. For a
|
|
compressed encoding, `width` and `height` are the image's dimensions, not the length of
|
|
`data`.
|
|
|
|
## Inputs
|
|
|
|
| Input | Description |
|
|
|---|---|
|
|
| `tick` | Publishes one frame. Wire to `dora/timer/millis/N`; the node stops when it closes. |
|
|
|
|
## Configuring it
|
|
|
|
Every setting below is an environment variable, which is convenient for one and
|
|
awkward for several. A dataflow can carry them instead, per node, which keeps the
|
|
configuration with the wiring it belongs to and out of whatever shell happens to start
|
|
it:
|
|
|
|
```yaml
|
|
nodes:
|
|
- id: camera
|
|
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
|
|
tag: v0.1.0
|
|
build: uv pip install .
|
|
path: dora-webcam
|
|
env:
|
|
CAMERA_ID: usb-Acme_Webcam
|
|
IMAGE_WIDTH: 1280
|
|
IMAGE_HEIGHT: 720
|
|
ENCODING: jpeg
|
|
JPEG_QUALITY: 85
|
|
FLIP: HORIZONTAL
|
|
inputs:
|
|
tick: dora/timer/millis/33
|
|
outputs:
|
|
- image
|
|
```
|
|
|
|
Values may be strings, numbers or booleans, and dora applies them to both the build and
|
|
the run.
|
|
|
|
**A node's `env:` wins over the environment the dataflow was started in.** A variable
|
|
the node does not set still passes through, so `PREVIEW=true dora run ...` works
|
|
alongside the block above — but a variable pinned there cannot be overridden from the
|
|
shell, which is worth remembering before pinning something a test needs to vary.
|
|
|
|
## Environment variables
|
|
|
|
| Variable | Default | Meaning |
|
|
|---|---|---|
|
|
| `CAMERA_ID` | unset | Substring of a `/dev/v4l/by-id/` entry. Wins over `CAPTURE_PATH`. |
|
|
| `CAPTURE_PATH` | auto | Device path (`/dev/video1`) or index. A numeric string is an index. |
|
|
| `IMAGE_WIDTH` | native | Requested capture width; frames are also resized to it. |
|
|
| `IMAGE_HEIGHT` | native | Requested capture height; frames are also resized to it. |
|
|
| `ENCODING` | `bgr8` | One of `bgr8`, `rgb8`, `yuv420`, `jpeg`/`jpg`/`jpe`, `png`, `bmp`, `webp`. |
|
|
| `JPEG_QUALITY` | `95` | Quality 0-100, used only by the JPEG encodings. |
|
|
| `FLIP` | unset | `VERTICAL`, `HORIZONTAL` or `BOTH`. |
|
|
| `PREVIEW` | unset | `true` opens a window of the published frames; `q` stops the node. |
|
|
| `CAPTURE_FRAMES` | `0` | Stop after this many frames. 0 runs until the tick closes. |
|
|
|
|
Resizing only happens when both `IMAGE_WIDTH` and `IMAGE_HEIGHT` are set.
|
|
|
|
With neither `CAMERA_ID` nor `CAPTURE_PATH` set, the node opens the first `/dev/video*`
|
|
that actually delivers a frame. Merely opening is not enough to go on: a UVC camera
|
|
exposes a second node for its metadata stream — `/dev/video1` beside `/dev/video0` on a
|
|
one-camera machine — which opens happily and never yields an image, so the lowest device
|
|
number is wrong about as often as it is right.
|
|
|
|
`CAMERA_ID` is worth preferring on a robot anyway: a by-id name is stable across reboots
|
|
and across which USB port the camera is in, and neither an index nor discovery order is.
|
|
|
|
`CI=true` makes a failed read publish a labelled black frame instead of raising, which
|
|
is what lets the dataflow run on a machine with no camera. It is not a configuration
|
|
knob.
|
|
|
|
## Build
|
|
|
|
```bash
|
|
uv sync
|
|
```
|
|
|
|
Every dependency ships prebuilt wheels for linux-x86_64, linux-aarch64 and macos-arm64,
|
|
so the node installs with no system packages and no compiler.
|
|
|
|
**Requirements:** dora 1.0+ (verified with `dora-cli 1.0.1`), Python 3.11+, `uv`, and a
|
|
V4L2 camera. `PREVIEW` also needs a display. On a bare Linux image `opencv-python` needs
|
|
the libGL shared libraries (`libgl1`, `libglib2.0-0`), which it links at import time.
|
|
|
|
## Test
|
|
|
|
```bash
|
|
uv run pytest
|
|
```
|
|
|
|
Unit tests over device resolution, encoding and the image struct; they need neither a
|
|
camera nor a dataflow. The headless `dataflow.yml` run covers the wiring and the event
|
|
loop.
|
|
|
|
## Camera selection on other platforms
|
|
|
|
Selecting by `CAMERA_ID` reads `/dev/v4l/by-id/`, so it is Linux only. Upstream also
|
|
enumerates cameras through macOS `system_profiler` and Windows PowerShell; that is not
|
|
carried here. `CAPTURE_PATH` works everywhere.
|