validate / validate (push) Successful in 59m12s
ssh:// cannot authenticate under dora-cli 1.0.1, which fetches through libgit2 with no credential callback, so a consumer wiring this node by `git:` needs the anonymous https URL.
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: https://gitea.liria.cl/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: https://gitea.liria.cl/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.
|