build with uv, and preview locally instead of a second dataflow
validate / validate (push) Successful in 59m16s
validate / validate (push) Successful in 59m16s
The build command was `pip install .`, which is what upstream and 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 the node then needs a virtualenv to install into, so it must be run with `--uv`; the README says so. The second dataflow is gone. It existed to wire this node into dora-image-view just to see the frames, which pulled another repository over the network and needed a tag there. PREVIEW=true now opens a local window of the frames as they are published, so checking a camera, an encoding or a flip needs this repository alone. It shows the frame after the flip and the resize, so what is on screen is what is sent, and it never opens under CI, where there is no display. That a source node can display its own output overlaps with what a viewer node does. The overlap is the point: the alternative was a second repository in the loop before you can tell whether a camera works.
This commit is contained in:
@@ -18,7 +18,7 @@ nodes:
|
|||||||
- id: camera
|
- id: camera
|
||||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
|
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
|
||||||
tag: v0.1.0
|
tag: v0.1.0
|
||||||
build: pip install .
|
build: uv pip install .
|
||||||
path: dora-webcam
|
path: dora-webcam
|
||||||
inputs:
|
inputs:
|
||||||
tick: dora/timer/millis/33
|
tick: dora/timer/millis/33
|
||||||
@@ -26,24 +26,30 @@ nodes:
|
|||||||
- image
|
- image
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the dataflow with `--uv` (`dora run dataflow.yml --uv`) so the node gets its own
|
Run the dataflow with `--uv` (`dora run dataflow.yml --uv`): the build command is
|
||||||
managed environment.
|
`uv pip install .`, so the node gets its own managed environment and never installs into
|
||||||
|
an ambient one.
|
||||||
|
|
||||||
### Runnable dataflows
|
### Runnable dataflow
|
||||||
|
|
||||||
Two dataflows live here, run one at a time — dora has no cross-dataflow wiring, so each
|
[`dataflow.yml`](dataflow.yml) is the camera on its own, and is what CI runs:
|
||||||
declares the camera itself:
|
|
||||||
|
|
||||||
Run either with `dora run <file> --uv`:
|
```bash
|
||||||
|
dora run dataflow.yml --uv
|
||||||
|
```
|
||||||
|
|
||||||
| File | Contents |
|
Nothing consumes the frames, so to actually see them set `PREVIEW`:
|
||||||
|---|---|
|
|
||||||
| [`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`
|
```bash
|
||||||
output into the viewer's `image` input, so a window opens showing the webcam. It fetches
|
PREVIEW=true dora run dataflow.yml --uv
|
||||||
the viewer over the network, which is why CI validates and runs `dataflow.yml` only.
|
```
|
||||||
|
|
||||||
|
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 a viewer such as `dora-image-view` from your own
|
||||||
|
dataflow.
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
@@ -73,6 +79,7 @@ compressed encoding, `width` and `height` are the image's dimensions, not the le
|
|||||||
| `ENCODING` | `bgr8` | One of `bgr8`, `rgb8`, `yuv420`, `jpeg`/`jpg`/`jpe`, `png`, `bmp`, `webp`. |
|
| `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. |
|
| `JPEG_QUALITY` | `95` | Quality 0-100, used only by the JPEG encodings. |
|
||||||
| `FLIP` | unset | `VERTICAL`, `HORIZONTAL` or `BOTH`. |
|
| `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. |
|
| `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.
|
Resizing only happens when both `IMAGE_WIDTH` and `IMAGE_HEIGHT` are set.
|
||||||
@@ -94,8 +101,8 @@ Every dependency ships prebuilt wheels for linux-x86_64, linux-aarch64 and macos
|
|||||||
so the node installs with no system packages and no compiler.
|
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
|
**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
|
V4L2 camera. `PREVIEW` also needs a display. On a bare Linux image `opencv-python` needs
|
||||||
(`libgl1`, `libglib2.0-0`), which it links at import time.
|
the libGL shared libraries (`libgl1`, `libglib2.0-0`), which it links at import time.
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -6,11 +6,12 @@ strict_types: true
|
|||||||
nodes:
|
nodes:
|
||||||
- id: camera
|
- id: camera
|
||||||
path: dora-webcam
|
path: dora-webcam
|
||||||
build: pip install .
|
build: uv pip install .
|
||||||
inputs:
|
inputs:
|
||||||
tick: dora/timer/millis/33
|
tick: dora/timer/millis/33
|
||||||
outputs:
|
outputs:
|
||||||
- image
|
- image
|
||||||
|
|
||||||
# Nothing consumes the frames here; the point is that the node opens, encodes
|
# Nothing consumes the frames: the point is that the node opens, encodes and
|
||||||
# and publishes. dataflow_view.yml is the pair that shows them.
|
# publishes. Set PREVIEW=true to watch them in a local window instead of
|
||||||
|
# wiring a viewer node.
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# The camera into the viewer: the end-to-end check of the pair.
|
|
||||||
#
|
|
||||||
# Not run by CI. It fetches dora-image-view over the network, and it needs a
|
|
||||||
# tag to exist in that repository; CI validates and runs dataflow.yml, which
|
|
||||||
# needs neither.
|
|
||||||
strict_types: true
|
|
||||||
|
|
||||||
nodes:
|
|
||||||
- id: camera
|
|
||||||
path: dora-webcam
|
|
||||||
build: pip install .
|
|
||||||
inputs:
|
|
||||||
tick: dora/timer/millis/33
|
|
||||||
outputs:
|
|
||||||
- image
|
|
||||||
|
|
||||||
- id: view
|
|
||||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-image-view.git
|
|
||||||
tag: v0.1.0
|
|
||||||
build: pip install .
|
|
||||||
path: dora-image-view
|
|
||||||
inputs:
|
|
||||||
image: camera/image
|
|
||||||
+11
-5
@@ -13,10 +13,10 @@ keywords: [opencv, camera, webcam, video, capture, image, v4l2]
|
|||||||
|
|
||||||
runtime: python
|
runtime: python
|
||||||
entrypoint: dora-webcam
|
entrypoint: dora-webcam
|
||||||
# `dora build --uv` / `dora run --uv` rewrites this to `uv pip install .` and
|
# Run the dataflow with --uv, which gives the node its own managed venv under
|
||||||
# gives the node its own managed venv under `.dora/python-envs/`, so the
|
# `.dora/python-envs/`, so what it imports at runtime is what was installed
|
||||||
# dependencies it sees at runtime are the ones installed here.
|
# here. Without --uv this needs a virtualenv to install into.
|
||||||
build: pip install .
|
build: uv pip install .
|
||||||
dora: ">=1.0"
|
dora: ">=1.0"
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
@@ -59,6 +59,12 @@ env:
|
|||||||
description: Quality 0-100, used only by the JPEG encodings.
|
description: Quality 0-100, used only by the JPEG encodings.
|
||||||
FLIP:
|
FLIP:
|
||||||
description: Flip the frame. One of VERTICAL, HORIZONTAL or BOTH. Unset means no flip.
|
description: Flip the frame. One of VERTICAL, HORIZONTAL or BOTH. Unset means no flip.
|
||||||
|
PREVIEW:
|
||||||
|
type: bool
|
||||||
|
description: >-
|
||||||
|
Open a local window showing the frames as they are published, so a camera
|
||||||
|
can be checked without wiring a viewer. Press q to stop the node. Ignored
|
||||||
|
under CI, where there is no display.
|
||||||
CAPTURE_FRAMES:
|
CAPTURE_FRAMES:
|
||||||
type: int
|
type: int
|
||||||
default: 0
|
default: 0
|
||||||
@@ -76,7 +82,7 @@ example: |
|
|||||||
- id: camera
|
- id: camera
|
||||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
|
git: ssh://git@gitea.liria.cl:5001/liria/dora-webcam.git
|
||||||
tag: v0.1.0
|
tag: v0.1.0
|
||||||
build: pip install .
|
build: uv pip install .
|
||||||
path: dora-webcam
|
path: dora-webcam
|
||||||
inputs:
|
inputs:
|
||||||
tick: dora/timer/millis/33
|
tick: dora/timer/millis/33
|
||||||
|
|||||||
+13
-3
@@ -1,8 +1,8 @@
|
|||||||
"""A dora webcam source: captures frames and publishes them as std/media/v1/Image.
|
"""A dora webcam source: captures frames and publishes them as std/media/v1/Image.
|
||||||
|
|
||||||
Each `tick` reads one frame, optionally flips and resizes it, encodes it, and
|
Each `tick` reads one frame, optionally flips and resizes it, encodes it, and
|
||||||
sends it. The node stops when the tick input closes, or after CAPTURE_FRAMES
|
sends it. The node stops when the tick input closes, after CAPTURE_FRAMES
|
||||||
frames if that is set.
|
frames if that is set, or when `q` is pressed in the preview window.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -11,7 +11,7 @@ import os
|
|||||||
import cv2
|
import cv2
|
||||||
from dora import Node
|
from dora import Node
|
||||||
|
|
||||||
from dora_webcam import camera, encode, image
|
from dora_webcam import camera, encode, image, preview
|
||||||
|
|
||||||
FLIPS = {
|
FLIPS = {
|
||||||
"VERTICAL": 0,
|
"VERTICAL": 0,
|
||||||
@@ -43,6 +43,8 @@ def main() -> None:
|
|||||||
# 0 runs until the tick closes; a positive count is what lets CI run the
|
# 0 runs until the tick closes; a positive count is what lets CI run the
|
||||||
# dataflow to completion instead of killing it on a timeout.
|
# dataflow to completion instead of killing it on a timeout.
|
||||||
limit = _int("CAPTURE_FRAMES") or 0
|
limit = _int("CAPTURE_FRAMES") or 0
|
||||||
|
# Never under CI: there is no display there, and imshow would fail.
|
||||||
|
previewing = os.getenv("PREVIEW", "").lower() in ("1", "true") and not ci
|
||||||
|
|
||||||
node = Node(args.name)
|
node = Node(args.name)
|
||||||
sent = 0
|
sent = 0
|
||||||
@@ -66,6 +68,11 @@ def main() -> None:
|
|||||||
if width and height and (frame.shape[1], frame.shape[0]) != (width, height):
|
if width and height and (frame.shape[1], frame.shape[0]) != (width, height):
|
||||||
frame = cv2.resize(frame, (width, height))
|
frame = cv2.resize(frame, (width, height))
|
||||||
|
|
||||||
|
# After the flip and the resize, so what is on screen is what is
|
||||||
|
# published rather than what came off the sensor.
|
||||||
|
if previewing and preview.show(frame):
|
||||||
|
break
|
||||||
|
|
||||||
# Taken before encoding: a compressed payload's byte count is not
|
# Taken before encoding: a compressed payload's byte count is not
|
||||||
# its pixel dimensions.
|
# its pixel dimensions.
|
||||||
frame_height, frame_width = frame.shape[:2]
|
frame_height, frame_width = frame.shape[:2]
|
||||||
@@ -87,6 +94,9 @@ def main() -> None:
|
|||||||
finally:
|
finally:
|
||||||
capture.release()
|
capture.release()
|
||||||
|
|
||||||
|
if previewing:
|
||||||
|
preview.close()
|
||||||
|
|
||||||
|
|
||||||
def _parse_args() -> argparse.Namespace:
|
def _parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""An optional local preview window.
|
||||||
|
|
||||||
|
A source node showing its own frames overlaps with what a viewer node does, and
|
||||||
|
that is the point: it means checking that a camera works needs this repository
|
||||||
|
only, not a second node wired into a dataflow. It is off unless asked for, and
|
||||||
|
never opens under CI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
WINDOW = "dora-webcam"
|
||||||
|
|
||||||
|
|
||||||
|
def show(frame: np.ndarray) -> bool:
|
||||||
|
"""Display one frame and report whether the viewer asked to quit.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
frame : np.ndarray
|
||||||
|
The captured BGR frame, before encoding, which is what a person wants
|
||||||
|
to look at.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
True when `q` was pressed, which stops the node.
|
||||||
|
|
||||||
|
"""
|
||||||
|
cv2.imshow(WINDOW, frame)
|
||||||
|
return cv2.waitKey(1) & 0xFF == ord("q")
|
||||||
|
|
||||||
|
|
||||||
|
def close() -> None:
|
||||||
|
"""Tear the window down, if one was ever opened."""
|
||||||
|
cv2.destroyAllWindows()
|
||||||
Reference in New Issue
Block a user