Custom Nodes

This tutorial will show you how to create your own custom nodes to run with PeekingDuck. Perhaps you’d like to take a snapshot of a video frame, and post it to your API endpoint; or perhaps you have a model trained on a custom dataset, and would like to use PeekingDuck’s input, draw, and output nodes. PeekingDuck is designed to be very flexible — you can create your own nodes and use them with ours.

Let’s start by creating a new PeekingDuck project:

Terminal Session

[~user] > mkdir custom_project
[~user] > cd custom_project
[~user/custom_project] > peekingduck init

This creates the following custom_project folder structure:

custom_project/ ⠀
├── pipeline_config.yml
└── src/ ⠀
    └── custom_nodes/ ⠀
        └── configs/

The sub-folders src, custom_nodes, and configs are empty: they serve as placeholders for contents to be added.

Recipe 1: Object Detection Score

When the YOLO object detection model detects an object in the image, it assigns a bounding box and a score to it. This score is the “confidence score” which reflects how likely the box contains an object and how accurate is the bounding box. It is a decimal number that ranges from 0.0 to 1.0 (or 100%). This number is internal and not readily viewable.

We will create a custom node to retrieve this score and display it on screen. This tutorial will use the cat_and_computer.mp4 video from the earlier object detection tutorial. Copy it into the custom_project folder.

Use the following command to create a custom node: peekingduck create-node
It will prompt you to answer several questions. Press <Enter> to accept the default custom_nodes folder name, then key in draw for node type and score for node name. Finally, press <Enter> to answer Y when asked to proceed.

The entire interaction is shown here, the answers you type in are shown in green text:

Terminal Session

[~user/custom_project] > peekingduck create-node
Creating new custom node…
Enter node directory relative to ~user/custom_project [src/custom_nodes]:
Select node type (input, augment, model, draw, dabble, output): draw
Enter node name [my_custom_node]: score

Node directory: ~user/custom_project/src/custom_nodes
Node type: draw
Node name: score

Creating the following files:
Config file: ~user/custom_project/src/custom_nodes/configs/draw/score.yml
Script file: ~user/custom_project/src/custom_nodes/draw/score.py
Proceed? [Y/n]:
Created node!

The custom_project folder structure should look like this:

custom_project/ ⠀
├── cat_and_computer.mp4
├── pipeline_config.yml
└── src/ ⠀
    └── custom_nodes/ ⠀
        ├── configs/ ⠀
        │   └── draw/ ⠀
        │       └── score.yml
        └── draw/ ⠀
            └── score.py

custom_project now contains three files that we need to modify to implement our custom node function.

  1. src/custom_nodes/configs/draw/score.yml:

    score.yml initial content:

     1# Mandatory configs
     2# Receive bounding boxes and their respective labels as input. Replace with
     3# other data types as required. List of built-in data types for PeekingDuck can
     4# be found at https://peekingduck.readthedocs.io/en/stable/glossary.html.
     5input: ["bboxes", "bbox_labels"]
     6# Example:
     7# Output `obj_attrs` for visualization with `draw.tag` node and `custom_key` for
     8# use with other custom nodes. Replace as required.
     9output: ["obj_attrs", "custom_key"]
    10
    11# Optional configs depending on node
    12threshold: 0.5 # example
    

    The first file score.yml defines the properties of the custom node.
    Lines 5 and 9 show the mandatory configs input and output.

    input specifies the data types the node would consume, to be read from the pipeline.
    output specifies the data types the node would produce, to be put into the pipeline.

    To display the bounding box confidence score, our node requires three pieces of input data: the bounding box, the score to display, and the image to draw on. These are defined as the data types bboxes, bbox_scores, and img respectively in the API docs.

    Our custom node only displays the score on screen and does not produce any outputs for the pipeline, so the output is “none”.

    There are also no optional configs, so lines 11 - 12 can be removed.

    score.yml updated content:

    1# Mandatory configs
    2input: ["img", "bboxes", "bbox_scores"]
    3output: ["none"]
    4
    5# No optional configs
    

    Note

    Comments in yaml files start with #
    It is possible for a node to have input: ["none"]

  2. src/custom_nodes/draw/score.py:

    The second file score.py contains the boilerplate code for creating a custom node. Update the code to implement the desired behavior for the node.

    Show/Hide Code for score.py

     1"""
     2Custom node to show object detection scores
     3"""
     4
     5from typing import Any, Dict, List, Tuple
     6import cv2
     7from peekingduck.pipeline.nodes.abstract_node import AbstractNode
     8
     9YELLOW = (0, 255, 255)        # in BGR format, per opencv's convention
    10
    11
    12def map_bbox_to_image_coords(
    13   bbox: List[float], image_size: Tuple[int, int]
    14) -> List[int]:
    15   """This is a helper function to map bounding box coords (relative) to
    16   image coords (absolute).
    17   Bounding box coords ranges from 0 to 1
    18   where (0, 0) = image top-left, (1, 1) = image bottom-right.
    19
    20   Args:
    21      bbox (List[float]): List of 4 floats x1, y1, x2, y2
    22      image_size (Tuple[int, int]): Width, Height of image
    23
    24   Returns:
    25      List[int]: x1, y1, x2, y2 in integer image coords
    26   """
    27   width, height = image_size[0], image_size[1]
    28   x1, y1, x2, y2 = bbox
    29   x1 *= width
    30   x2 *= width
    31   y1 *= height
    32   y2 *= height
    33   return int(x1), int(y1), int(x2), int(y2)
    34
    35
    36class Node(AbstractNode):
    37   """This is a template class of how to write a node for PeekingDuck,
    38      using AbstractNode as the parent class.
    39      This node draws scores on objects detected.
    40
    41   Args:
    42      config (:obj:`Dict[str, Any]` | :obj:`None`): Node configuration.
    43   """
    44
    45   def __init__(self, config: Dict[str, Any] = None, **kwargs: Any) -> None:
    46      """Node initializer
    47
    48      Since we do not require any special setup, it only calls the __init__
    49      method of its parent class.
    50      """
    51      super().__init__(config, node_path=__name__, **kwargs)
    52
    53   def run(self, inputs: Dict[str, Any]) -> Dict[str, Any]:  # type: ignore
    54      """This method implements the display score function.
    55      As PeekingDuck iterates through the CV pipeline, this 'run' method
    56      is called at each iteration.
    57
    58      Args:
    59            inputs (dict): Dictionary with keys "img", "bboxes", "bbox_scores"
    60
    61      Returns:
    62            outputs (dict): Empty dictionary
    63      """
    64
    65      # extract pipeline inputs and compute image size in (width, height)
    66      img = inputs["img"]
    67      bboxes = inputs["bboxes"]
    68      scores = inputs["bbox_scores"]
    69      img_size = (img.shape[1], img.shape[0])  # width, height
    70
    71      for i, bbox in enumerate(bboxes):
    72         # for each bounding box:
    73         #   - compute (x1, y1) top-left, (x2, y2) bottom-right coordinates
    74         #   - convert score into a two decimal place numeric string
    75         #   - draw score string onto image using opencv's putText()
    76         #     (see opencv's API docs for more info)
    77         x1, y1, x2, y2 = map_bbox_to_image_coords(bbox, img_size)
    78         score = scores[i]
    79         score_str = f"{score:0.2f}"
    80         cv2.putText(
    81            img=img,
    82            text=score_str,
    83            org=(x1, y2),
    84            fontFace=cv2.FONT_HERSHEY_SIMPLEX,
    85            fontScale=1.0,
    86            color=YELLOW,
    87            thickness=3,
    88         )
    89
    90      return {}               # node has no outputs
    

    The updated node code defines a helper function map_bbox_to_image_coords to map the bounding box coordinates to the image coordinates, as explained in this section.

    The run method implements the main logic which processes every bounding box to compute its on-screen coordinates and to draw the bounding box confidence score at its left-bottom position.

  3. pipeline_config.yml:

    pipeline_config.yml initial content:

    1nodes:
    2- input.visual:
    3    source: https://storage.googleapis.com/peekingduck/videos/wave.mp4
    4- model.posenet
    5- draw.poses
    6- output.screen
    

    This file implements the pipeline. Modify the default pipeline to the one shown below:

    pipeline_config.yml updated content:

    1nodes:
    2- input.visual:
    3    source: cat_and_computer.mp4
    4- model.yolo:
    5    detect: ["cup", "cat", "laptop", "keyboard", "mouse"]
    6- draw.bbox:
    7    show_labels: True
    8- custom_nodes.draw.score
    9- output.screen
    

    Line 8 adds our custom node into the pipeline where it will be run by PeekingDuck during each pipeline iteration.

Execute peekingduck run to see your custom node in action.

Custom node screenshot - show object detection scores

Custom Node Showing Object Detection Scores

Note

Royalty free video of cat and computer from: https://www.youtube.com/watch?v=-C1TEGZavko

Recipe 2: Keypoints, Count Hand Waves

This tutorial will create a custom node to analyze the skeletal keypoints of the person from the wave.mp4 video in the pose estimation tutorial and to count the number of times the person waves his hand.

The PoseNet pose estimation model outputs seventeen keypoints for the person corresponding to the different body parts as documented here. Each keypoint is a pair of (x, y) coordinates, where x and y are real numbers ranging from 0.0 to 1.0 (using relative coordinates).

Starting with a newly initialized PeekingDuck folder, call peekingduck create-node to create a new dabble.wave custom node as shown below:

Terminal Session

[~user] > mkdir wave_project
[~user] > cd wave_project
[~user/wave_project] > peekingduck init
Welcome to PeekingDuck!
2022-02-11 18:17:31 peekingduck.cli INFO: Creating custom nodes folder in ~user/wave_project/src/custom_nodes
[~user/wave_project] > peekingduck create-node
Creating new custom node…
Enter node directory relative to ~user/wave_project [src/custom_nodes]:
Select node type (input, augment, model, draw, dabble, output): dabble
Enter node name [my_custom_node]: wave

Node directory: ~user/wave_project/src/custom_nodes
Node type: dabble
Node name: wave

Creating the following files:
Config file: ~user/wave_project/src/custom_nodes/configs/dabble/wave.yml
Script file: ~user/wave_project/src/custom_nodes/dabble/wave.py
Proceed? [Y/n]:
Created node!

Also, copy wave.mp4 into the above folder. You should end up with the following folder structure:

wave_project/ ⠀
├── pipeline_config.yml
├── src/ ⠀
│   └── custom_nodes/ ⠀
│       ├── configs/ ⠀
│       │   └── dabble/ ⠀
│       │       └── wave.yml
│       └── dabble/ ⠀
│           └── wave.py
└── wave.mp4

To implement this tutorial, the three files wave.yml, wave.py and pipeline_config.yml are to be edited as follows:

  1. src/custom_nodes/configs/dabble/wave.yml:

    1# Dabble node has both input and output
    2input: ["img", "bboxes", "bbox_scores", "keypoints", "keypoint_scores"]
    3output: ["none"]
    4
    5# No optional configs
    

    We will implement this tutorial using a custom dabble node, which will take the inputs img, bboxes, bbox_scores, keypoints, and keypoint_scores from the pipeline. The node has no output.

  2. src/custom_nodes/dabble/wave.py:

    The dabble.wave code structure is similar to the draw.score code structure in the other custom node tutorial.

    Show/Hide Code for wave.py

      1"""
      2Custom node to show keypoints and count the number of times the person's hand is waved
      3"""
      4
      5from typing import Any, Dict, List, Tuple
      6import cv2
      7from peekingduck.pipeline.nodes.abstract_node import AbstractNode
      8
      9# setup global constants
     10FONT = cv2.FONT_HERSHEY_SIMPLEX
     11WHITE = (255, 255, 255)       # opencv loads file in BGR format
     12YELLOW = (0, 255, 255)
     13THRESHOLD = 0.6               # ignore keypoints below this threshold
     14KP_RIGHT_SHOULDER = 6         # PoseNet's skeletal keypoints
     15KP_RIGHT_WRIST = 10
     16
     17
     18def map_bbox_to_image_coords(
     19   bbox: List[float], image_size: Tuple[int, int]
     20) -> List[int]:
     21   """First helper function to convert relative bounding box coordinates to
     22   absolute image coordinates.
     23   Bounding box coords ranges from 0 to 1
     24   where (0, 0) = image top-left, (1, 1) = image bottom-right.
     25
     26   Args:
     27      bbox (List[float]): List of 4 floats x1, y1, x2, y2
     28      image_size (Tuple[int, int]): Width, Height of image
     29
     30   Returns:
     31      List[int]: x1, y1, x2, y2 in integer image coords
     32   """
     33   width, height = image_size[0], image_size[1]
     34   x1, y1, x2, y2 = bbox
     35   x1 *= width
     36   x2 *= width
     37   y1 *= height
     38   y2 *= height
     39   return int(x1), int(y1), int(x2), int(y2)
     40
     41
     42def map_keypoint_to_image_coords(
     43   keypoint: List[float], image_size: Tuple[int, int]
     44) -> List[int]:
     45   """Second helper function to convert relative keypoint coordinates to
     46   absolute image coordinates.
     47   Keypoint coords ranges from 0 to 1
     48   where (0, 0) = image top-left, (1, 1) = image bottom-right.
     49
     50   Args:
     51      bbox (List[float]): List of 2 floats x, y (relative)
     52      image_size (Tuple[int, int]): Width, Height of image
     53
     54   Returns:
     55      List[int]: x, y in integer image coords
     56   """
     57   width, height = image_size[0], image_size[1]
     58   x, y = keypoint
     59   x *= width
     60   y *= height
     61   return int(x), int(y)
     62
     63
     64def draw_text(img, x, y, text_str: str, color_code):
     65   """Helper function to call opencv's drawing function,
     66   to improve code readability in node's run() method.
     67   """
     68   cv2.putText(
     69      img=img,
     70      text=text_str,
     71      org=(x, y),
     72      fontFace=cv2.FONT_HERSHEY_SIMPLEX,
     73      fontScale=0.4,
     74      color=color_code,
     75      thickness=2,
     76   )
     77
     78
     79class Node(AbstractNode):
     80   """Custom node to display keypoints and count number of hand waves
     81
     82   Args:
     83      config (:obj:`Dict[str, Any]` | :obj:`None`): Node configuration.
     84   """
     85
     86   def __init__(self, config: Dict[str, Any] = None, **kwargs: Any) -> None:
     87      super().__init__(config, node_path=__name__, **kwargs)
     88      # setup object working variables
     89      self.right_wrist = None
     90      self.direction = None
     91      self.num_direction_changes = 0
     92      self.num_waves = 0
     93
     94   def run(self, inputs: Dict[str, Any]) -> Dict[str, Any]:  # type: ignore
     95      """This node draws keypoints and count hand waves.
     96
     97      Args:
     98            inputs (dict): Dictionary with keys
     99               "img", "bboxes", "bbox_scores", "keypoints", "keypoint_scores".
    100
    101      Returns:
    102            outputs (dict): Empty dictionary.
    103      """
    104
    105      # get required inputs from pipeline
    106      img = inputs["img"]
    107      bboxes = inputs["bboxes"]
    108      bbox_scores = inputs["bbox_scores"]
    109      keypoints = inputs["keypoints"]
    110      keypoint_scores = inputs["keypoint_scores"]
    111
    112      img_size = (img.shape[1], img.shape[0])  # image width, height
    113
    114      # get bounding box confidence score and draw it at the
    115      # left-bottom (x1, y2) corner of the bounding box (offset by 30 pixels)
    116      the_bbox = bboxes[0]             # image only has one person
    117      the_bbox_score = bbox_scores[0]  # only one set of scores
    118
    119      x1, y1, x2, y2 = map_bbox_to_image_coords(the_bbox, img_size)
    120      score_str = f"BBox {the_bbox_score:0.2f}"
    121      cv2.putText(
    122         img=img,
    123         text=score_str,
    124         org=(x1, y2 - 30),            # offset by 30 pixels
    125         fontFace=cv2.FONT_HERSHEY_SIMPLEX,
    126         fontScale=1.0,
    127         color=WHITE,
    128         thickness=3,
    129      )
    130
    131      # hand wave detection using a simple heuristic of tracking the
    132      # right wrist movement
    133      the_keypoints = keypoints[0]              # image only has one person
    134      the_keypoint_scores = keypoint_scores[0]  # only one set of scores
    135      right_wrist = None
    136      right_shoulder = None
    137
    138      for i, keypoints in enumerate(the_keypoints):
    139         keypoint_score = the_keypoint_scores[i]
    140
    141         if keypoint_score >= THRESHOLD:
    142            x, y = map_keypoint_to_image_coords(keypoints.tolist(), img_size)
    143            x_y_str = f"({x}, {y})"
    144
    145            if i == KP_RIGHT_SHOULDER:
    146               right_shoulder = keypoints
    147               the_color = YELLOW
    148            elif i == KP_RIGHT_WRIST:
    149               right_wrist = keypoints
    150               the_color = YELLOW
    151            else:                   # generic keypoint
    152               the_color = WHITE
    153
    154            draw_text(img, x, y, x_y_str, the_color)
    155
    156      if right_wrist is not None and right_shoulder is not None:
    157         # only count number of hand waves after we have gotten the
    158         # skeletal poses for the right wrist and right shoulder
    159         if self.right_wrist is None:
    160            self.right_wrist = right_wrist            # first wrist data point
    161         else:
    162            # wait for wrist to be above shoulder to count hand wave
    163            if right_wrist[1] > right_shoulder[1]:
    164               pass
    165            else:
    166               if right_wrist[0] < self.right_wrist[0]:
    167                  direction = "left"
    168               else:
    169                  direction = "right"
    170
    171               if self.direction is None:
    172                  self.direction = direction          # first direction data point
    173               else:
    174                  # check if hand changes direction
    175                  if direction != self.direction:
    176                     self.num_direction_changes += 1
    177                  # every two hand direction changes == one wave
    178                  if self.num_direction_changes >= 2:
    179                     self.num_waves += 1
    180                     self.num_direction_changes = 0   # reset direction count
    181
    182               self.right_wrist = right_wrist         # save last position
    183               self.direction = direction
    184
    185         wave_str = f"#waves = {self.num_waves}"
    186         draw_text(img, 20, 30, wave_str, YELLOW)
    187
    188      return {}
    

    This (long) piece of code implements our custom dabble node. It defines three helper functions to convert relative to absolute coordinates and to draw text on-screen. The number of hand waves is displayed at the top-left corner of the screen.

    A simple heuristic is used to count the number of times the person waves his hand. It tracks the direction in which the right wrist is moving and notes when the wrist changes direction. Upon encountering two direction changes, e.g., left -> right -> left, one wave is counted.

    The heuristic also waits until the right wrist has been lifted above the right shoulder before it starts tracking hand direction and counting waves.

  3. pipeline_config.yml:

     1nodes:
     2- input.visual:
     3    source: wave.mp4
     4- model.yolo
     5- model.posenet
     6- dabble.fps
     7- custom_nodes.dabble.wave
     8- draw.poses
     9- draw.legend:
    10    show: ["fps"]
    11- output.screen
    

    We modify pipeline_config.yml to run both the object detection and pose estimation models to obtain the required inputs for our custom dabble node.

Execute peekingduck run to see your custom node in action.

Custom node screenshot - count hand waves

Custom Node Counting Hand Waves

Note

Royalty free video of man waving from: https://www.youtube.com/watch?v=IKj_z2hgYUM

Recipe 3: Debugging

When working with PeekingDuck’s pipeline, you may sometimes wonder what is available in the data pool, or whether a particular data object has been correctly computed. This tutorial will show you how to use a custom node to help with troubleshooting and debugging PeekingDuck’s pipeline.

Continuing from the above tutorial, create a new dabble.debug custom node:

Terminal Session

[~user/wave_project] > peekingduck create-node
Creating new custom node…
Enter node directory relative to ~user/wave_project [src/custom_nodes]:
Select node type (input, augment, model, draw, dabble, output): dabble
Enter node name [my_custom_node]: debug

Node directory: ~user/wave_project/src/custom_nodes
Node type: dabble
Node name: debug

Creating the following files:
Config file: ~user/wave_project/src/custom_nodes/configs/dabble/debug.yml
Script file: ~user/wave_project/src/custom_nodes/dabble/debug.py
Proceed? [Y/n]:
Created node!

The updated folder structure is:

wave_project/ ⠀
├── pipeline_config.yml
├── src ⠀
│   └── custom_nodes ⠀
│       ├── configs ⠀
│       │   └── dabble ⠀
│       │       ├── debug.yml
│       │       └── wave.yml
│       └── dabble ⠀
│           ├── debug.py
│           └── wave.py
└── wave.mp4

Make the following three changes:

  1. Define debug.yml to receive “all” inputs from the pipeline, as follows:

    1# Mandatory configs
    2input: ["all"]
    3output: ["none"]
    4
    5# No optional configs
    
  2. Update debug.py as shown below:

    Show/Hide Code for debug.py

     1"""
     2A custom node for debugging
     3"""
     4
     5from typing import Any, Dict
     6
     7from peekingduck.pipeline.nodes.abstract_node import AbstractNode
     8
     9
    10class Node(AbstractNode):
    11   """This is a simple example of creating a custom node to help with debugging.
    12
    13   Args:
    14      config (:obj:`Dict[str, Any]` | :obj:`None`): Node configuration.
    15   """
    16
    17   def __init__(self, config: Dict[str, Any] = None, **kwargs: Any) -> None:
    18      super().__init__(config, node_path=__name__, **kwargs)
    19
    20   def run(self, inputs: Dict[str, Any]) -> Dict[str, Any]:  # type: ignore
    21      """A simple debugging custom node
    22
    23      Args:
    24            inputs (dict): "all", to view everything in data pool
    25
    26      Returns:
    27            outputs (dict): "none"
    28      """
    29
    30      self.logger.info("-- debug --")
    31      # show what is available in PeekingDuck's data pool
    32      self.logger.info(f"input.keys={list(inputs.keys())}")
    33      # debug specific data: bboxes
    34      bboxes = inputs["bboxes"]
    35      bbox_labels = inputs["bbox_labels"]
    36      bbox_scores = inputs["bbox_scores"]
    37      self.logger.info(f"num bboxes={len(bboxes)}")
    38      for i, bbox in enumerate(bboxes):
    39            label, score = bbox_labels[i], bbox_scores[i]
    40            self.logger.info(f"bbox {i}:")
    41            self.logger.info(f"  label={label}, score={score:0.2f}")
    42            self.logger.info(f"  coords={bbox}")
    43
    44      return {}  # no outputs
    

    The custom node code shows how to see what is available in PeekingDuck’s pipeline data pool by printing the input dictionary keys. It also demonstrates how to debug a specific data object, such as bboxes, by printing relevant information for each item within the data.

  3. Update pipeline_config.yml:

     1nodes:
     2- input.visual:
     3    source: wave.mp4
     4- model.yolo
     5- model.posenet
     6- dabble.fps
     7- custom_nodes.dabble.wave
     8- custom_nodes.dabble.debug
     9- draw.poses
    10- draw.legend:
    11    show: ["fps"]
    12- output.screen
    

Now, do a peekingduck run and you should see a sample debug output like the one below:

Terminal Session

[~user/wave_project] > peekingduck run
2022-03-02 18:42:51 peekingduck.declarative_loader INFO: Successfully loaded pipeline_config file.
2022-03-02 18:42:51 peekingduck.declarative_loader INFO: Initializing input.visual node...
2022-03-02 18:42:51 peekingduck.declarative_loader INFO: Config for node input.visual is updated to: ‘source’: wave.mp4
2022-03-02 18:42:51 peekingduck.pipeline.nodes.input.visual INFO: Video/Image size: 710 by 540
2022-03-02 18:42:51 peekingduck.pipeline.nodes.input.visual INFO: Filepath used: wave.mp4
2022-03-02 18:42:51 peekingduck.declarative_loader INFO: Initializing model.yolo node...
[ ... many lines of output deleted here ... ]
2022-03-02 18:42:53 peekingduck.declarative_loader INFO: Initializing custom_nodes.dabble.debug node...
2022-03-02 18:42:53 peekingduck.declarative_loader INFO: Initializing draw.poses node...
2022-03-02 18:42:53 peekingduck.declarative_loader INFO: Initializing draw.legend node...
2022-03-02 18:42:53 peekingduck.declarative_loader INFO: Initializing output.screen node...
2022-03-02 18:42:55 custom_nodes.dabble.debug INFO: – debug –
2022-03-02 18:42:55 custom_nodes.dabble.debug INFO: input.keys=[‘img’, ‘pipeline_end’, ‘filename’, ‘saved_video_fps’, ‘bboxes’, ‘bbox_labels’, ‘bbox_scores’, ‘keypoints’, ‘keypoint_scores’, ‘keypoint_conns’, ‘hand_direction’, ‘num_waves’, ‘fps’]
2022-03-02 18:42:55 custom_nodes.dabble.debug INFO: num bboxes=1
2022-03-02 18:42:55 custom_nodes.dabble.debug INFO: bbox 0:
2022-03-02 18:42:55 custom_nodes.dabble.debug INFO:   label=Person, score=0.91
2022-03-02 18:42:55 custom_nodes.dabble.debug INFO:   coords=[0.40047657 0.21553655 0.85199741 1.02150181]

Other Recipes to Create Custom Nodes

This section describes two faster ways to create custom nodes for users who are familiar with PeekingDuck.

CLI Recipe

You can skip the step-by-step prompts from peekingduck create-node by specifying all the options on the command line, for instance:

Terminal Session

[~user/wave_project] > peekingduck create-node --node_subdir src/custom_nodes --node_type dabble --node_name wave

The above is the equivalent of the tutorial Recipe 1: Object Detection Score custom node creation. For more information, see peekingduck create-node --help.

Pipeline Recipe

PeekingDuck can also create custom nodes by parsing your pipeline configuration file. Starting with the basic folder structure from peekingduck init:

wave_project/ ⠀
├── pipeline_config.yml
├── src ⠀
│   └── custom_nodes ⠀
│       └── configs ⠀
└── wave.mp4

and the following modified pipeline_config.yml file:

 1nodes:
 2- input.visual:
 3    source: wave.mp4
 4- model.yolo
 5- model.posenet
 6- dabble.fps
 7- custom_nodes.dabble.wave
 8- custom_nodes.dabble.debug
 9- draw.poses
10- draw.legend:
11    show: ["fps"]
12- output.screen

You can tell PeekingDuck to parse your pipeline file with peekingduck create-node --config_path pipeline_config.yml:

Terminal Session

[~user/wave_project] > peekingduck create-node --config_path pipeline_config.yml
2022-03-14 11:21:21 peekingduck.cli INFO: Creating custom nodes declared in ~user/wave_project/pipeline_config.yml.
2022-03-14 11:21:21 peekingduck.declarative_loader INFO: Successfully loaded pipeline file.
2022-03-14 11:21:21 peekingduck.cli INFO: Creating files for custom_nodes.dabble.wave:
Config file: ~user/wave_project/src/custom_nodes/configs/dabble/wave.yml
Script file: ~user/wave_project/src/custom_nodes/dabble/wave.py
2022-03-14 11:21:21 peekingduck.cli INFO: Creating files for custom_nodes.dabble.debug:
Config file: ~user/wave_project/src/custom_nodes/configs/dabble/debug.yml
Script file: ~user/wave_project/src/custom_nodes/dabble/debug.py

PeekingDuck will read pipeline_config.yml and create the two specified custom nodes custom_nodes.dabble.wave and custom_nodes.dabble.debug. Your folder structure will now look like this:

wave_project/ ⠀
├── pipeline_config.yml
├── src ⠀
│   └── custom_nodes ⠀
│       ├── configs ⠀
│       │   └── dabble ⠀
│       │       ├── debug.yml
│       │       └── wave.yml
│       └── dabble ⠀
│           ├── debug.py
│           └── wave.py
└── wave.mp4

From here, you can proceed to edit the custom node configs and source files.