diff --git a/node-hub/dora-pyrealsense/README.md b/node-hub/dora-pyrealsense/README.md new file mode 100644 index 00000000..56c5bb15 --- /dev/null +++ b/node-hub/dora-pyrealsense/README.md @@ -0,0 +1,70 @@ +# Dora Node for capturing video with OpenCV + +This node is used to capture video from a camera using OpenCV. + +# YAML + +```yaml +- id: opencv-video-capture + build: pip install ../../node-hub/opencv-video-capture + path: opencv-video-capture + inputs: + tick: dora/timer/millis/16 # try to capture at 60fps + outputs: + - image: # the captured image + + env: + PATH: 0 # optional, default is 0 + + IMAGE_WIDTH: 640 # optional, default is video capture width + IMAGE_HEIGHT: 480 # optional, default is video capture height +``` + +# Inputs + +- `tick`: empty Arrow array to trigger the capture + +# Outputs + +- `image`: an arrow array containing the captured image + +```Python +## Image data +image_data: UInt8Array # Example: pa.array(img.ravel()) +metadata = { + "width": 640, + "height": 480, + "encoding": str, # bgr8, rgb8 +} + +## Example +node.send_output( + image_data, {"width": 640, "height": 480, "encoding": "bgr8"} + ) + +## Decoding +storage = event["value"] + +metadata = event["metadata"] +encoding = metadata["encoding"] +width = metadata["width"] +height = metadata["height"] + +if encoding == "bgr8": + channels = 3 + storage_type = np.uint8 + +frame = ( + storage.to_numpy() + .astype(storage_type) + .reshape((height, width, channels)) +) +``` + +## Examples + +Check example at [examples/python-dataflow](examples/python-dataflow) + +## License + +This project is licensed under Apache-2.0. Check out [NOTICE.md](../../NOTICE.md) for more information. diff --git a/node-hub/dora-pyrealsense/dora_pyrealsense/__init__.py b/node-hub/dora-pyrealsense/dora_pyrealsense/__init__.py new file mode 100644 index 00000000..ac3cbef9 --- /dev/null +++ b/node-hub/dora-pyrealsense/dora_pyrealsense/__init__.py @@ -0,0 +1,11 @@ +import os + +# Define the path to the README file relative to the package directory +readme_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "README.md") + +# Read the content of the README file +try: + with open(readme_path, "r", encoding="utf-8") as f: + __doc__ = f.read() +except FileNotFoundError: + __doc__ = "README file not found." diff --git a/node-hub/dora-pyrealsense/dora_pyrealsense/main.py b/node-hub/dora-pyrealsense/dora_pyrealsense/main.py new file mode 100644 index 00000000..d566bbf9 --- /dev/null +++ b/node-hub/dora-pyrealsense/dora_pyrealsense/main.py @@ -0,0 +1,89 @@ +import argparse +import os +import time + +import cv2 +import numpy as np +import pyarrow as pa + +from dora import Node + +RUNNER_CI = True if os.getenv("CI") == "true" else False +import pyrealsense2 as rs + +FLIP = os.getenv("FLIP", "") +DEVICE_ID = os.getenv("DEVICE_ID", "") +pipeline = rs.pipeline() +config = rs.config() +config.enable_device(DEVICE_ID) +config.enable_stream(rs.stream.color, 640, 480, rs.format.rgb8, 15) +align_to = rs.stream.color +align = rs.align(align_to) +pipeline.start(config) + + +def main(): + node = Node() + start_time = time.time() + + pa.array([]) # initialize pyarrow array + + for event in node: + + # Run this example in the CI for 10 seconds only. + if RUNNER_CI and time.time() - start_time > 10: + break + + event_type = event["type"] + + if event_type == "INPUT": + event_id = event["id"] + + if event_id == "tick": + frames = pipeline.wait_for_frames() + # 使用线程锁来确保只有一个线程能访问数据流 + # 获取各种数据流 + color_frames = frames.get_color_frame() + frame = np.asanyarray(color_frames.get_data()) + + if FLIP == "VERTICAL": + frame = cv2.flip(frame, 0) + elif FLIP == "HORIZONTAL": + frame = cv2.flip(frame, 1) + elif FLIP == "BOTH": + frame = cv2.flip(frame, -1) + + # resize the frame + if ( + image_width is not None + and image_height is not None + and ( + frame.shape[1] != image_width or frame.shape[0] != image_height + ) + ): + frame = cv2.resize(frame, (image_width, image_height)) + + metadata = event["metadata"] + metadata["encoding"] = encoding + metadata["width"] = int(frame.shape[1]) + metadata["height"] = int(frame.shape[0]) + + # Get the right encoding + if encoding == "rgb8": + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + elif encoding in ["jpeg", "jpg", "jpe", "bmp", "webp", "png"]: + ret, frame = cv2.imencode("." + encoding, frame) + if not ret: + print("Error encoding image...") + continue + + storage = pa.array(frame.ravel()) + + node.send_output("image", storage, metadata) + + elif event_type == "ERROR": + raise RuntimeError(event["error"]) + + +if __name__ == "__main__": + main() diff --git a/node-hub/dora-pyrealsense/pyproject.toml b/node-hub/dora-pyrealsense/pyproject.toml new file mode 100644 index 00000000..383f846c --- /dev/null +++ b/node-hub/dora-pyrealsense/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "dora-pyrealsense" +version = "0.0.1" +authors = ["Haixuan Xavier Tao "] +description = "Dora Node for capturing video with Pyrealsense" +readme = "README.md" + +packages = [{ include = "dora_pyrealsense" }] + +[tool.poetry.dependencies] +dora-rs = "^0.3.6" +numpy = "< 2.0.0" +opencv-python = ">= 4.1.1" +pyrealsense2 = "2.54.1.5216" + +[tool.poetry.scripts] +dora-pyrealsense = "dora_pyrealsense.main:main" + +[build-system] +requires = ["poetry-core>=1.8.0"] +build-backend = "poetry.core.masonry.api" diff --git a/node-hub/dora-pyrealsense/tests/test_dora_pyrealsense.py b/node-hub/dora-pyrealsense/tests/test_dora_pyrealsense.py new file mode 100644 index 00000000..bed0ab79 --- /dev/null +++ b/node-hub/dora-pyrealsense/tests/test_dora_pyrealsense.py @@ -0,0 +1,9 @@ +import pytest + + +def test_import_main(): + from opencv_video_capture.main import main + + # Check that everything is working, and catch dora Runtime Exception as we're not running in a dora dataflow. + with pytest.raises(RuntimeError): + main()