Peaking Duck
PeekingDuck includes some “power” nodes that are capable of processing the contents
or outputs of the other nodes and to accumulate information over time.
An example is the dabble.statistics
node which can accumulate statistical
information, such as calculating the cumulative average and maximum of particular
objects (like people or cars).
This tutorial presents advanced recipes to showcase the power features of
PeekingDuck, such as using dabble.statistics
for object counting and tracking.
Interfacing with SQL
This tutorial demonstrates how to save data to an SQLite database.
We will extend the tutorial for counting hand waves
with a new custom output
node that writes information into a local SQLite database.
Note
The above tutorial assumes
sqlite3
has been installed in your system.
If your system does not havesqlite3
, please see the SQLite Home Page for installation instructions.
First, create a new custom output.sqlite
node in the custom_project
folder:
Terminal Session
[~user/wave_project] > peekingduck create-nodeCreating new custom node…Enter node directory relative to ~user/wave_project [src/custom_nodes]: ⏎Select node type (input, augment, model, draw, dabble, output): outputEnter node name [my_custom_node]: sqliteNode directory: ~user/wave_project/src/custom_nodesNode type: outputNode name: sqliteCreating the following files:Config file: ~user/wave_project/src/custom_nodes/configs/output/sqlite.ymlScript file: ~user/wave_project/src/custom_nodes/output/sqlite.pyProceed? [Y/n]: ⏎Created node!
The updated folder structure would be:
wave_project/ ⠀ ├── pipeline_config.yml ├── src/ ⠀ │ └── custom_nodes/ ⠀ │ ├── configs/ ⠀ │ │ ├── dabble/ ⠀ │ │ │ └── wave.yml │ │ └── output/ ⠀ │ │ └── sqlite.yml │ ├── dabble/ ⠀ │ │ └── wave.py │ └── output/ ⠀ │ └── sqlite.py └── wave.mp4
Edit the following five files as described below:
src/custom_nodes/configs/output/sqlite.yml:
1# Mandatory configs 2input: ["hand_direction", "num_waves"] 3output: ["none"] 4 5# No optional configs
The new
output.sqlite
custom node will take in the hand direction and the current number of hand waves to save to the external database.src/custom_nodes/output/sqlite.py:
Show/Hide Code for sqlite.py
1""" 2Custom node to save data to external database. 3""" 4 5from typing import Any, Dict 6from datetime import datetime 7from peekingduck.pipeline.nodes.abstract_node import AbstractNode 8import sqlite3 9 10DB_FILE = "wave.db" # name of database file 11 12 13class Node(AbstractNode): 14 """Custom node to save hand direction and current wave count to database. 15 16 Args: 17 config (:obj:`Dict[str, Any]` | :obj:`None`): Node configuration. 18 """ 19 20 def __init__(self, config: Dict[str, Any] = None, **kwargs: Any) -> None: 21 super().__init__(config, node_path=__name__, **kwargs) 22 23 self.conn = None 24 try: 25 # try to establish connection to database, 26 # will create DB_FILE if it does not exist 27 self.conn = sqlite3.connect(DB_FILE) 28 self.logger.info(f"Connected to {DB_FILE}") 29 sql = """ CREATE TABLE IF NOT EXISTS wavetable ( 30 datetime text, 31 hand_direction text, 32 wave_count integer 33 ); """ 34 cur = self.conn.cursor() 35 cur.execute(sql) 36 except sqlite3.Error as e: 37 self.logger.info(f"SQL Error: {e}") 38 39 def update_db(self, hand_direction: str, num_waves: int) -> None: 40 """Helper function to save current time stamp, hand direction and 41 wave count into DB wavetable. 42 """ 43 now = datetime.now() 44 dt_str = f"{now:%Y-%m-%d %H:%M:%S}" 45 sql = """ INSERT INTO wavetable(datetime,hand_direction,wave_count) 46 values (?,?,?) """ 47 cur = self.conn.cursor() 48 cur.execute(sql, (dt_str, hand_direction, num_waves)) 49 self.conn.commit() 50 51 def run(self, inputs: Dict[str, Any]) -> Dict[str, Any]: # type: ignore 52 """Node to output hand wave data into sqlite database. 53 54 Args: 55 inputs (dict): Dictionary with keys "hand_direction", "num_waves" 56 57 Returns: 58 outputs (dict): Empty dictionary 59 """ 60 61 hand_direction = inputs["hand_direction"] 62 num_waves = inputs["num_waves"] 63 self.update_db(hand_direction, num_waves) 64 65 return {}
This tutorial uses the
sqlite3
package to interface with the database.On first run, the node initializer will create the
wave.db
database file. It will establish a connection to the database and create a table calledwavetable
if it does not exist. This table is used to store the hand direction and wave count data.A helper function
update_db
is called to update the database. It saves the current date time stamp, hand direction and wave count into thewavetable
.The node’s
run
method retrieves the required inputs from the pipeline’s data pool and callsself.update_db
to save the data.src/custom_nodes/configs/dabble/wave.yml:
1# Dabble node has both input and output 2input: ["img", "bboxes", "bbox_scores", "keypoints", "keypoint_scores"] 3output: ["hand_direction", "num_waves"] 4 5# No optional configs
To support the
output.sqlite
custom node’s input requirements, we need to modify thedabble.wave
custom node to return the current hand directionhand_direction
and the current wave countnum_waves
.src/custom_nodes/dabble/wave.py:
173 ... same as previous ... 174 return { 175 "hand_direction": self.direction if self.direction is not None else "None", 176 "num_waves": self.num_waves, 177 }
This file is the same as the
wave.py
in the counting hand waves tutorial, except for the changes in the last few lines as shown above. These changes outputs thehand_direction
andnum_waves
to the pipeline’s data pool for subsequent consumption by theoutput.sqlite
custom node.pipeline_config.yml:
11... same as previous ... 12- custom_nodes.output.sqlite
Likewise, the pipeline is the same as in the previous tutorial, except for line 12 that has been added to call the new custom node.
Run this project with peekingduck run and when completed, a new wave.db
sqlite database file would be created in the current folder.
Examine the created database as follows:
Terminal Session
[~user/wave_project] > sqlite3SQLite version 3.37.0 2021-11-27 14:13:22Enter “.help” for usage hints.Connected to a transient in-memory database.Use “.open FILENAME” to reopen on a persistent database.sqlite> .open wave.dbsqlite> .schema wavetableCREATE TABLE wavetable (datetime text,hand_direction text,wave_count integer);sqlite> select * from wavetable where wave_count > 0 limit 5;2022-02-15 19:26:16|left|12022-02-15 19:26:16|right|12022-02-15 19:26:16|left|22022-02-15 19:26:16|right|22022-02-15 19:26:16|right|2sqlite> select * from wavetable order by datetime desc limit 5;2022-02-15 19:26:44|right|722022-02-15 19:26:44|right|722022-02-15 19:26:44|right|722022-02-15 19:26:44|right|722022-02-15 19:26:43|right|70
Press CTRL-D to exit from sqlite3
.
Counting Cars
This tutorial demonstrates using the dabble.statistics
node to count the number
of cars traveling across a highway over time and the draw.legend
node to display
the relevant statistics.
Create a new PeekingDuck project, download the highway cars video and save it into the project folder.
Terminal Session
[~user] > mkdir car_project[~user] > cd car_project[~user/car_project] > peekingduck init
The car_project
folder structure:
car_project/ ⠀ ├── highway_cars.mp4 ├── pipeline_config.yml └── src ⠀ └── custom_nodes └── configs
Edit pipeline_config.yml
as follows:
1nodes: 2- input.visual: 3 source: highway_cars.mp4 4- model.yolo: 5 detect: ["car"] 6 score_threshold: 0.3 7- dabble.bbox_count 8- dabble.fps 9- dabble.statistics: 10 identity: count 11- draw.bbox 12- draw.legend: 13 show: ["fps", "count", "cum_max", "cum_min"] 14- output.screen
Run it with peekingduck run and you should see a video of cars travelling across a highway with a legend box on the bottom left showing the realtime count of the number of cars on-screen, the cumulative maximum and minimum number of cars detected since the video started. The sample screenshot below shows:
the count that there are currently 3 cars on-screen
the cumulative maximum number of cars “seen” previously was 5
the cumulative minimum number of cars was 1
Note
Royalty free video of cars on highway from: https://www.youtube.com/watch?v=8yP1gjg4b2w
Object Tracking
Object tracking is the application of CV models to automatically detect objects in a video and to assign a unique identity to each of them. These objects can be either living (e.g. person) or non-living (e.g. car). As they move around in the video, these objects are identified based on their assigned identities and tracked according to their movements.
This tutorial demonstrates using dabble.statistics
with a custom node to
track the number of people walking down a path.
Create a new PeekingDuck project, download the people walking video and save it into the project folder.
Terminal Session
[~user] > mkdir people_walking[~user] > cd people_walking[~user/people_walking] > peekingduck init
Create the following pipeline_config.yml
:
1nodes: 2- input.visual: 3 source: people_walking.mp4 4- model.yolo: 5 detect: ["person"] 6- dabble.tracking 7- dabble.statistics: 8 maximum: obj_attrs["ids"] 9- dabble.fps 10- draw.bbox 11- draw.tag: 12 show: ["ids"] 13- draw.legend: 14 show: ["fps", "cum_max", "cum_min", "cum_avg"] 15- output.screen
The above pipeline uses the YOLO model to detect people in the video and uses
the dabble.tracking
node to track the people as they walk.
Each person is assigned a tracking ID and dabble.tracking
returns a list of
tracking IDs.
dabble.statistics
is used to process these tracking IDs: since each person is
assigned a monotonically increasing integer ID, the maximum ID within the list
tells us the number of persons tracked so far.
draw.tag
shows the ID above the tracked person.
draw.legend
is used to display the various statistics: the FPS, and the
cumulative maximum, minimum and average relating to the number of persons tracked.
Do a peekingduck run and you will see the following display:
Note
Royalty free video of people walking from: https://www.youtube.com/watch?v=du74nvmRUzo
Tracking People within a Zone
Suppose we are only interested in people walking down the center of the video (imagine a carpet running down the middle). We can create a custom node to tell PeekingDuck to focus on the middle zone, by filtering away the detected bounding boxes outside the zone.
Start by creating a custom node dabble.filter_bbox
:
Terminal Session
[~user/people_walking] > peekingduck create-nodeCreating new custom node…Enter node directory relative to ~user/people_walking [src/custom_nodes]: ⏎Select node type (input, augment, model, draw, dabble, output): dabbleEnter node name [my_custom_node]: filter_bboxNode directory: ~user/people_walking/src/custom_nodesNode type: dabbleNode name: filter_bboxCreating the following files:Config file: ~user/people_walking/src/custom_nodes/configs/dabble/filter_bbox.ymlScript file: ~user/people_walking/src/custom_nodes/dabble/filter_bbox.pyProceed? [Y/n]: ⏎Created node!
The folder structure looks like this:
people_walking/ ⠀ ├── people_walking.mp4 ├── pipeline_config.yml └── src └── custom_nodes ├── configs │ └── dabble │ └── filter_bbox.yml └── dabble └── filter_bbox.py
Change pipeline_config.yml
to the following:
1nodes: 2- input.visual: 3 source: people_walking.mp4 4- model.yolo: 5 detect: ["person"] 6- dabble.bbox_to_btm_midpoint 7- dabble.zone_count: 8 resolution: [720, 480] 9 zones: [ 10 [[0.35,0], [0.65,0], [0.65,1], [0.35,1]], 11 ] 12- custom_nodes.dabble.filter_bbox: 13 zones: [ 14 [[0.35,0], [0.65,0], [0.65,1], [0.35,1]], 15 ] 16- dabble.tracking 17- dabble.statistics: 18 maximum: obj_attrs["ids"] 19- dabble.fps 20- draw.bbox 21- draw.zones 22- draw.tag: 23 show: ["ids"] 24- draw.legend: 25 show: ["fps", "cum_max", "cum_min", "cum_avg", "zone_count"] 26- output.screen
We make use of dabble.zone_count
and dabble.bbox_to_btm_midpoint
nodes to
create a zone in the middle. The zone is defined by a rectangle with the
four corners (0.35, 0.0) - (0.65, 0.0) - (0.65, 1.0) - (0.35, 1.0).
(For more info, see Zone Counting)
This zone is also defined in our custom node dabble.filter_bbox
for bounding box
filtering.
What dabble.filter_bbox
will do is to take the list of bboxes as input and
output a list of bboxes within the zone, dropping all bboxes outside it.
Then, dabble.tracking
is used to track the people walking and
dabble.statistics
is used to determine the number of people walking in the zone,
by getting the maximum of the tracked IDs.
draw.legend
has a new item zone_count which displays the number of people
walking in the zone currently.
The filter_bbox.yml
and filter_bbox.py
files are shown below:
src/custom_nodes/configs/dabble/filter_bbox.yml:
1# Mandatory configs 2input: ["bboxes"] 3output: ["bboxes"] 4 5zones: [ 6 [[0,0], [0,1], [1,1], [1,0]], 7]
Note
The
zones
default value of[[0,0], [0,1], [1,1], [1,0]]
will be overridden by those specified inpipeline_config.yml
above. See Configuration - Behind The Scenes for more details.src/custom_nodes/dabble/filter_bbox.py:
Show/Hide Code for filter_bbox.py
1""" 2Custom node to filter bboxes outside a zone 3""" 4 5from typing import Any, Dict 6import numpy as np 7from peekingduck.pipeline.nodes.abstract_node import AbstractNode 8 9 10class Node(AbstractNode): 11 """Custom node to filter bboxes outside a zone 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 """Checks bounding box x-coordinates against the zone left and right borders. 22 Retain bounding box if within, otherwise discard it. 23 24 Args: 25 inputs (dict): Dictionary with keys "bboxes" 26 27 Returns: 28 outputs (dict): Dictionary with keys "bboxes". 29 """ 30 bboxes = inputs["bboxes"] 31 zones = self.config["zones"] 32 zone = zones[0] # only work with one zone currently 33 # convert zone with 4 points to a zone bbox with (x1, y1), (x2, y2) 34 x1, y1 = zone[0] 35 x2, y2 = zone[2] 36 zone_bbox = np.asarray([x1, y1, x2, y2]) 37 38 retained_bboxes = [] 39 for bbox in bboxes: 40 # filter by left and right borders (ignore top and bottom) 41 if bbox[0] > zone_bbox[0] and bbox[2] < zone_bbox[2]: 42 retained_bboxes.append(bbox) 43 44 return {"bboxes": np.asarray(retained_bboxes)}
Do a peekingduck run and you will see the following display: