| @@ -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. | |||
| @@ -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." | |||
| @@ -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() | |||
| @@ -0,0 +1,21 @@ | |||
| [tool.poetry] | |||
| name = "dora-pyrealsense" | |||
| version = "0.0.1" | |||
| authors = ["Haixuan Xavier Tao <tao.xavier@outlook.com>"] | |||
| 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" | |||
| @@ -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() | |||