Calling PeekingDuck in Python

Using PeekingDuck’s Pipeline

As an alternative to running PeekingDuck using the command-line interface (CLI), users can also import PeekingDuck as a Python module and run it in a Python script. This demo corresponds to the Record Video File with FPS Section of the Duck Confit tutorial.

In addition, we will demonstrate basic debugging techniques which users can employ when troubleshooting PeekingDuck projects.

Setting Up

Create a PeekingDuck project using:

Terminal Session

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

Then, download the cat and computer video to the pkd_project folder and create a Python script demo_debug.py in the same folder.

You should have the following directory structure at this point:

pkd_project/ ⠀
├── cat_and_computer.mp4
├── demo_debug.py
├── pipeline_config.yml
└── src/

Creating a Custom Node for Debugging

Run the following to create a dabble node for debugging:

Terminal Session

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

The command will create the debug.py and debug.yml files in your project directory as shown:

pkd_project/ ⠀
├── cat_and_computer.mp4
├── demo_debug.py
├── pipeline_config.yml
└── src/ ⠀
    └── custom_nodes/ ⠀
        ├── configs/ ⠀
        │   └── dabble/ ⠀
        │       └── debug.yml
        └── dabble/ ⠀
            └── debug.py

Change the content of debug.yml to:

1input: ["all"]
2output: ["none"]

Line 1: The data type all allows the node to receive all outputs from the previous nodes as its input. Please see the Glossary for a list of available data types.

Change the content of debug.py to:

Show/Hide Code

 1from typing import Any, Dict
 2
 3import numpy as np
 4
 5from peekingduck.pipeline.nodes.abstract_node import AbstractNode
 6
 7
 8class Node(AbstractNode):
 9    def __init__(self, config: Dict[str, Any] = None, **kwargs: Any) -> None:
10        super().__init__(config, node_path=__name__, **kwargs)
11        self.frame = 0
12
13    def run(self, inputs: Dict[str, Any]) -> Dict[str, Any]:  # type: ignore
14        if "cat" in inputs["bbox_labels"]:
15            print(
16                f"{self.frame} {inputs['bbox_scores'][np.where(inputs['bbox_labels'] == 'cat')]}"
17            )
18        self.frame += 1
19        return {}

Lines 14 - 17: Print out the frame number and the confidence scores of bounding boxes which are detected as “cat”.

Line 18: Increment the frame number each time run() is called.

Creating the Python Script

Copy over the following code to demo_debug.py:

Show/Hide Code

 1from pathlib import Path
 2
 3from peekingduck.pipeline.nodes.dabble import fps
 4from peekingduck.pipeline.nodes.draw import bbox, legend
 5from peekingduck.pipeline.nodes.input import visual
 6from peekingduck.pipeline.nodes.model import yolo
 7from peekingduck.pipeline.nodes.output import media_writer, screen
 8from peekingduck.runner import Runner
 9from src.custom_nodes.dabble import debug
10
11
12def main():
13    debug_node = debug.Node(pkd_base_dir=Path.cwd() / "src" / "custom_nodes")
14
15    visual_node = visual.Node(source=str(Path.cwd() / "cat_and_computer.mp4"))
16    yolo_node = yolo.Node(detect=["cup", "cat", "laptop", "keyboard", "mouse"])
17    bbox_node = bbox.Node(show_labels=True)
18
19    fps_node = fps.Node()
20    legend_node = legend.Node(show=["fps"])
21    screen_node = screen.Node()
22
23    media_writer_node = media_writer.Node(output_dir=str(Path.cwd() / "results"))
24
25    runner = Runner(
26        nodes=[
27            visual_node,
28            yolo_node,
29            debug_node,
30            bbox_node,
31            fps_node,
32            legend_node,
33            screen_node,
34            media_writer_node,
35        ]
36    )
37    runner.run()
38
39
40if __name__ == "__main__":
41    main()

Lines 9, 13: Import and initialize the debug custom node. Pass in the path/to/project_dir/src/custom_nodes via pkd_base_dir for the configuration YAML file of the custom node to be loaded properly.

Lines 15 - 23: Create the PeekingDuck nodes necessary to replicate the demo shown in the Record Video File with FPS tutorial. To change the node configuration, you can pass the new values to the Node() constructor as keyword arguments.

Lines 25 - 37: Initialize the PeekingDuck Runner from runner.py with the list of nodes passed in via the nodes argument.

Note

A PeekingDuck node can be created in Python code by passing a dictionary of config keyword - config value pairs to the Node() constructor.

Running the Python Script

Run the demo_debug.py script using:

Terminal Session

[~user/pkd_project] > python demo_debug.py

You should see the following output in your terminal:

 12022-02-24 16:33:06 peekingduck.pipeline.nodes.input.visual  INFO:  Config for node input.visual is updated to: 'source': ~user/pkd_project/cat_and_computer.mp4
 22022-02-24 16:33:06 peekingduck.pipeline.nodes.input.visual  INFO:  Video/Image size: 720 by 480
 32022-02-24 16:33:06 peekingduck.pipeline.nodes.input.visual  INFO:  Filepath used: ~user/pkd_project/cat_and_computer.mp4
 42022-02-24 16:33:06 peekingduck.pipeline.nodes.model.yolo  INFO:  Config for node model.yolo is updated to: 'detect': [41, 15, 63, 66, 64]
 52022-02-24 16:33:06 peekingduck.pipeline.nodes.model.yolov4.yolo_files.detector  INFO:  Yolo model loaded with following configs:
 6    Model type: v4tiny,
 7    Input resolution: 416,
 8    IDs being detected: [41, 15, 63, 66, 64]
 9    Max Detections per class: 50,
10    Max Total Detections: 50,
11    IOU threshold: 0.5,
12    Score threshold: 0.2
132022-02-24 16:33:07 peekingduck.pipeline.nodes.draw.bbox  INFO:  Config for node draw.bbox is updated to: 'show_labels': True
142022-02-24 16:33:07 peekingduck.pipeline.nodes.dabble.fps  INFO:  Moving average of FPS will be logged every: 100 frames
152022-02-24 16:33:07 peekingduck.pipeline.nodes.output.media_writer  INFO:  Config for node output.media_writer is updated to: 'output_dir': ~user/pkd_project/results
162022-02-24 16:33:07 peekingduck.pipeline.nodes.output.media_writer  INFO:  Output directory used is: ~user/pkd_project/results
170 [0.90861976]
181 [0.9082737]
192 [0.90818006]
203 [0.8888804]
214 [0.8877487]
225 [0.9071386]
236 [0.870267]
24
25[Truncated]

Lines 17 - 23: The debugging output showing the frame number and the confidence score of bounding boxes predicted as “cat”.

Integrating with Your Workflow

The modular design of PeekingDuck allows users to pick and choose the nodes they want to use. Users are also able to use PeekingDuck nodes with external packages when designing their pipeline.

In this demo, we will show how users can construct a custom PeekingDuck pipeline using:

The notebook corresponding to this tutorial, calling_peekingduck_in_python.ipynb, can be found in the notebooks folder of the PeekingDuck repository and is also available as a Colab notebook.

Running Locally

Install Prerequisites

Show/Hide Instructions for Linux/Mac (Intel)/Windows

pip install easyocr
pip uninstall -y opencv-python-headless opencv-contrib-python
pip install "tensorflow<2.8.0,>=2.3.0" opencv-contrib-python==4.5.4.60 matplotlib oidv6 lap==0.4.0
pip install colorama==0.4.4

Note

The uninstallation step is necessary to ensure that the proper version of OpenCV is installed.

You may receive an error message about the incompatibility between awscli and colorama==0.4.4. awscli is conservative about pinning versions to maintain backward compatibility. The code presented in this tutorial has been tested to work and we have chosen to prioritize PeekingDuck’s dependency requirements.

Show/Hide Instructions for Mac (Apple Silicon)

conda create -n pkd_notebook python=3.8
conda activate pkd_notebook

conda install jupyterlab matplotlib click colorama opencv openblas pyyaml \
requests scipy shapely tqdm pillow scikit-image python-bidi pandas awscli progressbar2
pip install easyocr oidv6 lap
pip uninstall opencv-contrib-python opencv-python-headless

# Pick one:
# for macOS Big Sur
conda install -c apple tensorflow-deps=2.6.0
pip install tensorflow-estimator==2.6.0 tensorflow-macos==2.6.0
pip install tensorflow-metal==0.2.0
# for macOS Monterey
conda install -c apple tensorflow-deps
pip install tensorflow-macos tensorflow-metal

pip install torch torchvision
pip install 'peekingduck==1.2.0' --no-dependencies

Note

We install the problematic packages easyocr and oidv6 first and then uninstall the pip-related OpenCV packages which were installed as dependencies. Mac (Apple silicon) needs conda’s OpenCV.

There will be a warning that easyocr needs some version of Pillow which can be ignored.


Download Demo Data

We are using Open Images Dataset V6 as the dataset for this demo. We recommend using the third-party oidv6 PyPI package to download the images necessary for this demo.

Run the following command after installing the prerequisites:

Terminal Session

[~user] > mkdir pkd_project
[~user] > cd pkd_project
[~user/pkd_project] > oidv6 downloader en --dataset data/oidv6 --type_data train --classes car --limit 10 --yes

Copy calling_peekingduck_in_python.ipynb to the pkd_project folder and you should have the following directory structure at this point:

pkd_project/ ⠀
├── calling_peekingduck_in_python.ipynb
└── data/ ⠀
    └── oidv6/ ⠀
        ├── boxes/ ⠀
        ├── metadata/ ⠀
        └── train/ ⠀
            └── car/

Import the Modules

Show/Hide Code

 1import os
 2from pathlib import Path
 3
 4import cv2
 5import easyocr
 6import matplotlib.pyplot as plt
 7import numpy as np
 8import tensorflow as tf
 9from peekingduck.pipeline.nodes.draw import bbox
10from peekingduck.pipeline.nodes.model import yolo_license_plate
11
12%matplotlib inline

Lines 9 - 10: You can also do:

from peekingduck.pipeline.nodes.draw import bbox as pkd_bbox
from peekingduck.pipeline.nodes.model import yolo_license_plate as pkd_yolo_license_plate

bbox_node = pkd_bbox.Node()
yolo_license_plate_node = pkd_yolo_license_plate.Node()

to avoid potential name conflicts.

Initialize PeekingDuck Nodes

Show/Hide Code

1yolo_lp_node = yolo_license_plate.Node()
2
3bbox_node = bbox.Node(show_labels=True)

Lines 3: To change the node configuration, you can pass the new values to the Node() constructor as keyword arguments.

Refer to the API Documentation for the configurable settings for each node.

Create a Dataset Loader

Show/Hide Code

1data_dir = Path.cwd().resolve() / "data" / "oidv6" / "train"
2dataset = tf.keras.preprocessing.image_dataset_from_directory(
3    data_dir, batch_size=1, shuffle=False
4)

Lines 2 - 4: We create the data loader using tf.keras.preprocessing.image_dataset_from_directory(); you can also create your own data loader class.

Create a License Plate Parser Class

Show/Hide Code

 1class LPReader:
 2    def __init__(self, use_gpu):
 3        self.reader = easyocr.Reader(["en"], gpu=use_gpu)
 4
 5    def read(self, image):
 6        """Reads text from the image and joins multiple strings to a
 7        single string.
 8        """
 9        return " ".join(self.reader.readtext(image, detail=0))
10
11reader = LPReader(False)

We create the license plate parser class in a Python class using easyocr to demonstrate how users can integrate the PeekingDuck pipeline with external packages.

Alternatively, users can create a custom node for parsing license plates and run the pipeline through the CLI instead. Refer to the custom nodes tutorial for more information.

The Inference Loop

Show/Hide Code

 1def get_best_license_plate(frame, bboxes, bbox_scores, width, height):
 2    """Returns the image region enclosed by the bounding box with the highest
 3    confidence score.
 4    """
 5    best_idx = np.argmax(bbox_scores)
 6    best_bbox = bboxes[best_idx].astype(np.float32).reshape((-1, 2))
 7    best_bbox[:, 0] *= width
 8    best_bbox[:, 1] *= height
 9    best_bbox = np.round(best_bbox).astype(int)
10
11    return frame[slice(*best_bbox[:, 1]), slice(*best_bbox[:, 0])]
12
13num_col = 3
14# For visualization, we plot 3 columns, 1) the original image, 2) image with
15# bounding box, and 3) the detected license plate region with license plate
16# number prediction shown as the plot title
17fig, ax = plt.subplots(
18    len(dataset), num_col, figsize=(num_col * 3, len(dataset) * 3)
19)
20for i, (element, path) in enumerate(zip(dataset, dataset.file_paths)):
21    image_orig = cv2.imread(path)
22    image_orig = cv2.cvtColor(image_orig, cv2.COLOR_BGR2RGB)
23    height, width = image_orig.shape[:2]
24
25    image = element[0].numpy().astype("uint8")[0].copy()
26
27    yolo_lp_input = {"img": image}
28    yolo_lp_output = yolo_lp_node.run(yolo_lp_input)
29
30    bbox_input = {
31        "img": image,
32        "bboxes": yolo_lp_output["bboxes"],
33        "bbox_labels": yolo_lp_output["bbox_labels"],
34    }
35    _ = bbox_node.run(bbox_input)
36
37    ax[i][0].imshow(image_orig)
38    ax[i][1].imshow(image)
39    # If there are any license plates detected, try to predict the license
40    # plate number
41    if len(yolo_lp_output["bboxes"]) > 0:
42        lp_image = get_best_license_plate(
43            image_orig, yolo_lp_output["bboxes"],
44            yolo_lp_output["bbox_scores"],
45            width,
46            height,
47        )
48        lp_pred = reader.read(lp_image)
49        ax[i][2].imshow(lp_image)
50        ax[i][2].title.set_text(f"Pred: {lp_pred}")

Lines 1 - 11: We define a utility function for retrieving the image region of the license plate with the highest confidence score to improve code clarity. For more information on how to convert between bounding box and image coordinates, please refer to the Bounding Box vs Image Coordinates tutorial.

Lines 27 - 35: By carefully constructing the input for each of the nodes, we can perform the inference loop within a custom workflow.

Lines 37 - 38: We plot the data using matplotlib for debugging and visualization purposes.

Lines 41 - 48: We integrate the inference loop with external packages such as the license plate parser we have created earlier using easyocr.