Captures frames from a V4L2 camera and publishes them as std/media/v1/Image: an Arrow struct of width, height, encoding and data, so a receiver needs nothing from the message metadata. For a compressed encoding the recorded dimensions are the image's, not the length of the payload. Based on opencv-video-capture from dora-hub, with three changes. The output is the typed image struct rather than a flat UInt8 buffer plus metadata. Camera selection by id reads /dev/v4l/by-id/ only; the upstream macOS system_profiler and Windows PowerShell enumeration is not carried. CAPTURE_FRAMES bounds a run so a test ends on its own, in place of upstream's ten-second CI cutoff -- a timeout would have made the CI step exit non-zero. CI=true keeps a failed capture from raising, publishing a labelled black frame instead, which is what lets dataflow.yml run on a machine with no camera. Two dataflows, run one at a time, as dora has no cross-dataflow wiring: dataflow.yml is the camera alone and the CI gate; dataflow_view.yml wires it into dora-image-view and is not run by CI, since it fetches that node over the network and needs a tag to exist there.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# 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: pip install .
|
||||
path: dora-webcam
|
||||
inputs:
|
||||
tick: dora/timer/millis/33
|
||||
outputs:
|
||||
- image
|
||||
```
|
||||
|
||||
Run the dataflow with `--uv` (`dora run dataflow.yml --uv`) so the node gets its own
|
||||
managed environment.
|
||||
|
||||
### Runnable dataflows
|
||||
|
||||
Two dataflows live here, run one at a time — dora has no cross-dataflow wiring, so each
|
||||
declares the camera itself:
|
||||
|
||||
Run either with `dora run <file> --uv`:
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| [`dataflow.yml`](dataflow.yml) | The camera alone |
|
||||
| [`dataflow_view.yml`](dataflow_view.yml) | The camera into `dora-image-view` |
|
||||
|
||||
`dataflow_view.yml` is the end-to-end check of the pair: it wires this node's `image`
|
||||
output into the viewer's `image` input, so a window opens showing the webcam. It fetches
|
||||
the viewer over the network, which is why CI validates and runs `dataflow.yml` only.
|
||||
|
||||
## 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. |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `CAMERA_ID` | unset | Substring of a `/dev/v4l/by-id/` entry. Wins over `CAPTURE_PATH`. |
|
||||
| `CAPTURE_PATH` | `0` | 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`. |
|
||||
| `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.
|
||||
|
||||
`CAMERA_ID` is worth preferring on a robot: a by-id name is stable across reboots and
|
||||
across which USB port the camera is in, and an index is not.
|
||||
|
||||
`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. On a bare Linux image `opencv-python` also 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.
|
||||
Reference in New Issue
Block a user