| @@ -3,7 +3,8 @@ nodes: | |||
| build: pip install ../../node-hub/opencv-video-capture | |||
| path: opencv-video-capture | |||
| inputs: | |||
| tick: plot/tick | |||
| tick: dora/timer/millis/16 | |||
| stop: plot/end | |||
| outputs: | |||
| - image | |||
| - text | |||
| @@ -17,7 +18,6 @@ nodes: | |||
| path: opencv-plot | |||
| inputs: | |||
| image: camera/image | |||
| tick: dora/timer/millis/16 # this node display a window, so it's better to deflect the timer, so when the window is closed, the ticks are not sent anymore in the graph | |||
| text: camera/text | |||
| outputs: | |||
| - tick | |||
| - end | |||
| @@ -3,7 +3,8 @@ nodes: | |||
| build: pip install ../../node-hub/opencv-video-capture | |||
| path: opencv-video-capture | |||
| inputs: | |||
| tick: plot/tick | |||
| tick: dora/timer/millis/16 | |||
| stop: plot/end | |||
| outputs: | |||
| - image | |||
| env: | |||
| @@ -16,6 +17,5 @@ nodes: | |||
| path: dynamic | |||
| inputs: | |||
| image: camera/image | |||
| tick: dora/timer/millis/16 # this node display a window, so it's better to deflect the timer, so when the window is closed, the ticks are not sent anymore in the graph | |||
| outputs: | |||
| - tick | |||
| - end | |||
| @@ -3,7 +3,8 @@ nodes: | |||
| build: pip install ../../node-hub/opencv-video-capture | |||
| path: opencv-video-capture | |||
| inputs: | |||
| tick: plot/tick | |||
| tick: dora/timer/millis/16 | |||
| stop: plot/end | |||
| outputs: | |||
| - image | |||
| env: | |||
| @@ -21,7 +22,7 @@ nodes: | |||
| outputs: | |||
| - bbox | |||
| env: | |||
| MODEL: yolov5n.pt | |||
| MODEL: yolov8n.pt | |||
| - id: plot | |||
| build: pip install ../../node-hub/opencv-plot | |||
| @@ -31,8 +32,5 @@ nodes: | |||
| source: camera/image | |||
| queue_size: 1 | |||
| bbox: object-detection/bbox | |||
| tick: | |||
| source: dora/timer/millis/16 # this node display a window, so it's better to deflect the timer, so when the window is closed, the ticks are not sent anymore in the graph | |||
| queue_size: 1 | |||
| outputs: | |||
| - tick | |||
| - end | |||
| @@ -13,12 +13,8 @@ This node is used to plot a text and a list of bbox on a base image (ideal for o | |||
| # bbox: Arrow array of bbox | |||
| # text: Arrow array of size 1 containing the text to be plotted | |||
| tick: | |||
| source: dora/timer/millis/16 # this node display a window, so it's better to deflect the timer, so when the window is closed, the ticks are not sent anymore in the graph | |||
| queue_size: 1 | |||
| outputs: | |||
| - tick | |||
| - end | |||
| env: | |||
| PLOT_WIDTH: 640 # optional, default is image input width | |||
| @@ -26,13 +22,11 @@ This node is used to plot a text and a list of bbox on a base image (ideal for o | |||
| ``` | |||
| # Inputs | |||
| - | |||
| - `tick`: empty Arrow array to trigger the capture | |||
| - `image`: Arrow array containing the base image | |||
| ```python | |||
| image = { | |||
| image: { | |||
| "width": np.uint32, | |||
| "height": np.uint32, | |||
| "channels": np.uint8, | |||
| @@ -50,29 +44,33 @@ decoded_image = { | |||
| ``` | |||
| - `bbox`: an arrow array containing the bounding boxes, confidence scores, and class names of the detected objects | |||
| ```Python | |||
| bbox: { | |||
| "bbox": np.array, # flattened array of bounding boxes | |||
| "conf": np.array, # flat array of confidence scores | |||
| "names": np.array, # flat array of class names | |||
| } | |||
| encoded_bbox = pa.array([bbox]) | |||
| decoded_bbox = { | |||
| "bbox": encoded_bbox[0]["bbox"].values.to_numpy().reshape(-1, 3), | |||
| "conf": encoded_bbox[0]["conf"].values.to_numpy(), | |||
| "names": encoded_bbox[0]["names"].values.to_numpy(zero_copy_only=False), | |||
| } | |||
| ``` | |||
| - `text`: Arrow array containing the text to be plotted | |||
| ```python | |||
| text = { | |||
| "text": str, | |||
| "font_scale": np.float32, | |||
| "color": (np.uint8, np.uint8, np.uint8), | |||
| "thickness": np.uint32, | |||
| "position": (np.uint32, np.uint32) | |||
| } | |||
| text: str | |||
| encoded_text = pa.array([text]) | |||
| decoded_text = { | |||
| "text": encoded_text[0]["text"].as_py(), | |||
| "font_scale": np.float32(encoded_text[0]["font_scale"].as_py()), | |||
| "color": (np.uint8(encoded_text[0]["color"].as_py()[0]), | |||
| np.uint8(encoded_text[0]["color"].as_py()[1]), | |||
| np.uint8(encoded_text[0]["color"].as_py()[2])), | |||
| "thickness": np.uint32(encoded_text[0]["thickness"].as_py()), | |||
| "position": (np.uint32(encoded_text[0]["position"].as_py()[0]), | |||
| np.uint32(encoded_text[0]["position"].as_py()[1])) | |||
| } | |||
| decoded_text = encoded_text[0].as_py() | |||
| ``` | |||
| ## License | |||
| @@ -7,6 +7,8 @@ import pyarrow as pa | |||
| from dora import Node | |||
| RUNNER_CI = True if os.getenv("CI", False) == "true" else False | |||
| class Plot: | |||
| frame: np.array = np.array([]) | |||
| @@ -23,7 +25,7 @@ class Plot: | |||
| height: np.uint32 = None | |||
| def plot_frame(plot, ci_enabled): | |||
| def plot_frame(plot): | |||
| for bbox in zip(plot.bboxes["bbox"], plot.bboxes["conf"], plot.bboxes["names"]): | |||
| [ | |||
| [min_x, min_y, max_x, max_y], | |||
| @@ -63,15 +65,10 @@ def plot_frame(plot, ci_enabled): | |||
| if plot.width is not None and plot.height is not None: | |||
| plot.frame = cv2.resize(plot.frame, (plot.width, plot.height)) | |||
| if not ci_enabled: | |||
| if not RUNNER_CI: | |||
| if len(plot.frame.shape) >= 3: | |||
| cv2.imshow("Dora Node: opencv-plot", plot.frame) | |||
| if cv2.waitKey(1) & 0xFF == ord('q'): | |||
| return True | |||
| return False | |||
| def main(): | |||
| # Handle dynamic nodes, ask for the name of the node in the dataflow, and the same values as the ENV variables. | |||
| @@ -96,11 +93,6 @@ def main(): | |||
| if isinstance(plot_height, str) and plot_height.isnumeric(): | |||
| plot_height = int(plot_height) | |||
| # check if the code is running in a CI environment (e.g. GitHub Actions) (parse to bool) | |||
| ci_enabled = os.getenv("CI", False) | |||
| if ci_enabled == "true": | |||
| ci_enabled = True | |||
| node = Node(args.name) # provide the name to connect to the dataflow if dynamic node | |||
| plot = Plot() | |||
| @@ -115,17 +107,7 @@ def main(): | |||
| if event_type == "INPUT": | |||
| event_id = event["id"] | |||
| if event_id == "tick": | |||
| if ci_enabled: | |||
| break | |||
| node.send_output( | |||
| "tick", | |||
| pa.array([]), | |||
| event["metadata"] | |||
| ) | |||
| elif event_id == "image": | |||
| if event_id == "image": | |||
| arrow_image = event["value"][0] | |||
| image = { | |||
| "width": np.uint32(arrow_image["width"].as_py()), | |||
| @@ -136,29 +118,45 @@ def main(): | |||
| plot.frame = np.reshape(image["data"], (image["height"], image["width"], image["channels"])) | |||
| if plot_frame(plot, ci_enabled): | |||
| break | |||
| plot_frame(plot) | |||
| if not RUNNER_CI: | |||
| if cv2.waitKey(1) & 0xFF == ord("q"): | |||
| break | |||
| else: | |||
| break # break the loop for CI | |||
| elif event_id == "bbox": | |||
| arrow_bbox = event["value"][0] | |||
| plot.bboxes = { | |||
| "bbox": arrow_bbox["bbox"].values.to_numpy().reshape(-1, 4), | |||
| "conf": arrow_bbox["conf"].values.to_numpy(), | |||
| "names": arrow_bbox["names"].values.to_pylist(), | |||
| "names": arrow_bbox["names"].values.to_numpy(zero_copy_only=False), | |||
| } | |||
| if plot_frame(plot, ci_enabled): | |||
| break | |||
| plot_frame(plot) | |||
| if not RUNNER_CI: | |||
| if cv2.waitKey(1) & 0xFF == ord("q"): | |||
| break | |||
| else: | |||
| break # break the loop for CI | |||
| elif event_id == "text": | |||
| plot.text = event["value"][0].as_py() | |||
| if plot_frame(plot, ci_enabled): | |||
| break | |||
| plot_frame(plot) | |||
| if not RUNNER_CI: | |||
| if cv2.waitKey(1) & 0xFF == ord("q"): | |||
| break | |||
| else: | |||
| break # break the loop for CI | |||
| elif event_type == "ERROR": | |||
| raise Exception(event["error"]) | |||
| node.send_output( | |||
| "end", | |||
| pa.array([0], type=pa.uint8()) | |||
| ) | |||
| if __name__ == "__main__": | |||
| main() | |||
| @@ -10,7 +10,7 @@ This node is used to capture video from a camera using OpenCV. | |||
| path: opencv-video-capture | |||
| inputs: | |||
| tick: dora/timer/millis/16 # try to capture at 60fps | |||
| # stop: some stop signal from another node | |||
| outputs: | |||
| - image: # the captured image | |||
| @@ -31,7 +31,7 @@ This node is used to capture video from a camera using OpenCV. | |||
| ```Python | |||
| image = { | |||
| image: { | |||
| "width": np.uint32, | |||
| "height": np.uint32, | |||
| "channels": np.uint8, | |||
| @@ -58,7 +58,7 @@ def main(): | |||
| frame = np.zeros((480, 640, 3), dtype=np.uint8) | |||
| cv2.putText( | |||
| frame, | |||
| f'Error: Could not read frame from camera at path {video_capture_path}.', | |||
| f'Error: no frame for camera at path {video_capture_path}.', | |||
| (int(30), int(30)), | |||
| cv2.FONT_HERSHEY_SIMPLEX, | |||
| 0.50, | |||
| @@ -84,6 +84,10 @@ def main(): | |||
| event["metadata"] | |||
| ) | |||
| if event_id == "stop": | |||
| video_capture.release() | |||
| break | |||
| elif event_type == "ERROR": | |||
| raise Exception(event["error"]) | |||
| @@ -22,7 +22,7 @@ This node is used to detect objects in images using YOLOv8. | |||
| - `image`: Arrow array containing the base image | |||
| ```python | |||
| image = { | |||
| image: { | |||
| "width": np.uint32, | |||
| "height": np.uint32, | |||
| "channels": np.uint8, | |||
| @@ -46,7 +46,7 @@ decoded_image = { | |||
| ```Python | |||
| bbox = { | |||
| bbox: { | |||
| "bbox": np.array, # flattened array of bounding boxes | |||
| "conf": np.array, # flat array of confidence scores | |||
| "names": np.array, # flat array of class names | |||
| @@ -57,7 +57,7 @@ encoded_bbox = pa.array([bbox]) | |||
| decoded_bbox = { | |||
| "bbox": encoded_bbox[0]["bbox"].values.to_numpy().reshape(-1, 3), | |||
| "conf": encoded_bbox[0]["conf"].values.to_numpy(), | |||
| "names": encoded_bbox[0]["names"].values.to_pylist(), | |||
| "names": encoded_bbox[0]["names"].values.to_numpy(zero_copy_only=False), | |||
| } | |||
| ``` | |||
| @@ -41,7 +41,7 @@ def main(): | |||
| "data": arrow_image["data"].values.to_numpy().astype(np.uint8) | |||
| } | |||
| frame = image["data"].reshape((image["height"], image["width"], 3)) | |||
| frame = image["data"].reshape((image["height"], image["width"], image["channels"])) | |||
| frame = frame[:, :, ::-1] # OpenCV image (BGR to RGB) | |||
| results = model(frame, verbose=False) # includes NMS | |||