Skip to content

Commit 85aed5f

Browse files
authored
Merge pull request #132 from pipeless-ai/sequential_execution
feat(hook): Support sequential sorted execution
2 parents 7a2e718 + 26eb8a6 commit 85aed5f

File tree

9 files changed

+109
-15
lines changed

9 files changed

+109
-15
lines changed

examples/object-tracking/init.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from norfair import Tracker
2+
3+
def init():
4+
return {
5+
"tracker": Tracker(distance_function="euclidean", distance_threshold=50)
6+
}

examples/object-tracking/process.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# make stateful
2+
3+
from norfair import Detection, draw_points
4+
import numpy as np
5+
6+
def hook(frame_data, context):
7+
tracker = context['tracker']
8+
frame = frame_data['modified']
9+
bboxes, scores, labels = frame_data['user_data'].values()
10+
norfair_detections = yolo_to_norfair(bboxes, scores)
11+
tracked_objects = tracker.update(detections=norfair_detections)
12+
draw_points(frame, drawables=tracked_objects)
13+
frame_data['modified'] = frame
14+
15+
def yolo_to_norfair(bboxes, scores):
16+
norfair_detections = []
17+
for i, bbox in enumerate(bboxes):
18+
box_corners = [[bbox[0], bbox[1]], [bbox[2], bbox[3]]]
19+
box_corners = np.array(box_corners)
20+
corners_scores = np.array([scores[i], scores[i]])
21+
norfair_detections.append(Detection(points=box_corners, scores=corners_scores))
22+
23+
return norfair_detections

examples/yolo/post-process.py

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ def hook(frame_data, _):
66
for box in bboxes:
77
x1, y1, x2, y2, score, class_number = box
88
box_label(frame, [x1, y1, x2, y2], yolo_classes[int(class_number)], score, (255, 0, 255))
9+
10+
bboxes = bboxes.tolist()
11+
# Add the predictions to the frame user_data in order to recover it frm other stages
12+
frame_data['user_data'] = {
13+
"bboxes": [bbox[:4] for bbox in bboxes],
14+
"scores": [bbox[4] for bbox in bboxes],
15+
"labels": [yolo_classes[int(bbox[5])] for bbox in bboxes]
16+
}
17+
918
frame_data['modified'] = frame
1019

1120
# Classes defined in the YOLO model to obtain the predicted class label

pipeless/Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pipeless/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pipeless-ai"
3-
version = "1.8.0"
3+
version = "1.9.0"
44
edition = "2021"
55
authors = ["Miguel A. Cabrera Minagorri"]
66
description = "An open-source computer vision framework to build and deploy applications in minutes"

pipeless/src/data.rs

+24-7
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ pub struct RgbFrame {
2929
inference_output: ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<ndarray::IxDynImpl>>,
3030
pipeline_id: uuid::Uuid,
3131
user_data: UserData,
32+
frame_number: u64,
3233
}
3334
impl RgbFrame {
3435
pub fn new(
3536
original: ndarray::Array3<u8>,
3637
width: usize, height: usize,
3738
pts: gst::ClockTime, dts: gst::ClockTime, duration: gst::ClockTime,
3839
fps: u8, input_ts: f64,
39-
pipeline_id: uuid::Uuid,
40+
pipeline_id: uuid::Uuid, frame_number: u64
4041
) -> Self {
4142
let modified = original.to_owned();
4243
RgbFrame {
@@ -49,6 +50,7 @@ impl RgbFrame {
4950
inference_output: ndarray::ArrayBase::zeros(ndarray::IxDyn(&[0])),
5051
pipeline_id,
5152
user_data: UserData::Empty,
53+
frame_number,
5254
}
5355
}
5456

@@ -62,7 +64,7 @@ impl RgbFrame {
6264
inference_input: ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<ndarray::IxDynImpl>>,
6365
inference_output: ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<ndarray::IxDynImpl>>,
6466
pipeline_id: &str,
65-
user_data: UserData,
67+
user_data: UserData, frame_number: u64,
6668
) -> Self {
6769
RgbFrame {
6870
uuid: uuid::Uuid::from_str(uuid).unwrap(),
@@ -74,7 +76,8 @@ impl RgbFrame {
7476
fps, input_ts,
7577
inference_input, inference_output,
7678
pipeline_id: uuid::Uuid::from_str(pipeline_id).unwrap(),
77-
user_data: user_data
79+
user_data: user_data,
80+
frame_number,
7881
}
7982
}
8083

@@ -128,15 +131,18 @@ impl RgbFrame {
128131
pub fn set_inference_output(&mut self, output_data: ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<ndarray::IxDynImpl>>) {
129132
self.inference_output = output_data;
130133
}
131-
pub fn get_pipeline_id(&self) -> uuid::Uuid {
132-
self.pipeline_id
134+
pub fn get_pipeline_id(&self) -> &uuid::Uuid {
135+
&self.pipeline_id
133136
}
134137
pub fn set_pipeline_id(&mut self, pipeline_id: &str) {
135138
self.pipeline_id = uuid::Uuid::from_str(pipeline_id).unwrap();
136139
}
137140
pub fn get_user_data(&self) -> &UserData {
138141
&self.user_data
139142
}
143+
pub fn get_frame_number(&self) -> &u64 {
144+
&self.frame_number
145+
}
140146
}
141147

142148
pub enum Frame {
@@ -148,12 +154,13 @@ impl Frame {
148154
width: usize, height: usize,
149155
pts: gst::ClockTime, dts: gst::ClockTime, duration: gst::ClockTime,
150156
fps: u8, input_ts: f64,
151-
pipeline_id: uuid::Uuid
157+
pipeline_id: uuid::Uuid, frame_number: u64,
152158
) -> Self {
153159
let rgb = RgbFrame::new(
154160
original, width, height,
155161
pts, dts, duration,
156-
fps, input_ts, pipeline_id
162+
fps, input_ts,
163+
pipeline_id, frame_number
157164
);
158165
Self::RgbFrame(rgb)
159166
}
@@ -188,4 +195,14 @@ impl Frame {
188195
Frame::RgbFrame(frame) => { frame.set_inference_output(output_data); },
189196
}
190197
}
198+
pub fn get_pipeline_id(&self) -> &uuid::Uuid {
199+
match self {
200+
Frame::RgbFrame(frame) => frame.get_pipeline_id(),
201+
}
202+
}
203+
pub fn get_frame_number(&self) -> &u64 {
204+
match self {
205+
Frame::RgbFrame(frame) => frame.get_frame_number(),
206+
}
207+
}
191208
}

pipeless/src/input/pipeline.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fn on_new_sample(
6161
pipeless_pipeline_id: uuid::Uuid,
6262
appsink: &gst_app::AppSink,
6363
pipeless_bus_sender: &tokio::sync::mpsc::UnboundedSender<pipeless::events::Event>,
64+
frame_number: &mut u64,
6465
) -> Result<gst::FlowSuccess, gst::FlowError> {
6566
let sample = appsink.pull_sample().map_err(|_err| {
6667
error!("Sample is None");
@@ -116,11 +117,12 @@ fn on_new_sample(
116117
gst::FlowError::Error
117118
})?;
118119

120+
*frame_number += 1;
119121
let frame = pipeless::data::Frame::new_rgb(
120122
ndframe, width, height,
121123
pts, dts, duration,
122124
fps as u8, frame_input_instant,
123-
pipeless_pipeline_id
125+
pipeless_pipeline_id, *frame_number,
124126
);
125127
// The event takes ownership of the frame
126128
pipeless::events::publish_new_frame_change_event_sync(
@@ -413,11 +415,13 @@ fn create_gst_pipeline(
413415
.new_sample(
414416
{
415417
let pipeless_bus_sender = pipeless_bus_sender.clone();
418+
let mut frame_number: u64 = 0; // Used to set the frame number
416419
move |appsink: &gst_app::AppSink| {
417420
on_new_sample(
418421
pipeless_pipeline_id,
419422
appsink,
420423
&pipeless_bus_sender,
424+
&mut frame_number,
421425
)
422426
}
423427
}).build();

pipeless/src/stages/hook.rs

+37-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use std::{sync::Arc, fmt};
1+
use std::{collections::HashMap, fmt, sync::Arc};
22
use log::error;
3-
use tokio::sync::Mutex;
3+
use tokio::sync::{Mutex, RwLock};
4+
use uuid::Uuid;
45

56
use crate as pipeless;
67

@@ -46,6 +47,9 @@ impl StatelessHook {
4647
pub struct StatefulHook {
4748
h_type: HookType,
4849
h_body: Arc<Mutex<dyn HookTrait>>,
50+
// Stateful hooks would not necesarily require to process sequentially, however, in almost all cases preserving a state in CV are related to sorted processing, such as tracking.
51+
// Maps the stream uuid to the last frame processed for the stream. Used for stateful sequential sorted processing.
52+
last_frame_map: Arc<RwLock<HashMap<Uuid, u64>>>,
4953
}
5054
impl StatefulHook {
5155
fn get_hook_type(&self) -> HookType {
@@ -54,6 +58,21 @@ impl StatefulHook {
5458
fn get_hook_body(&self) -> Arc<Mutex<dyn HookTrait>> {
5559
self.h_body.clone()
5660
}
61+
async fn last_processed_frame_number(&self, pipeline_id: &Uuid) -> u64 {
62+
let read_guard = self.last_frame_map.read().await;
63+
match read_guard.get(pipeline_id) {
64+
Some(n) => *n,
65+
None => 0,
66+
}
67+
}
68+
async fn increment_last_processed(&self, pipeline_id: &Uuid) {
69+
let mut write_guard = self.last_frame_map.write().await;
70+
let last = match write_guard.get(pipeline_id) {
71+
Some(n) => *n,
72+
None => 0,
73+
};
74+
write_guard.insert(*pipeline_id, last + 1);
75+
}
5776
}
5877

5978
/// Pipeless hooks are the minimal executable unit
@@ -85,6 +104,7 @@ impl Hook {
85104
let hook = StatefulHook {
86105
h_type: hook_type,
87106
h_body: hook_body,
107+
last_frame_map: Arc::new(RwLock::new(HashMap::new())),
88108
};
89109
Self::StatefulHook(hook)
90110
}
@@ -122,15 +142,28 @@ impl Hook {
122142
}
123143
},
124144
Hook::StatefulHook(hook) => {
145+
let frame_pipeline_id = frame.get_pipeline_id().clone();
146+
// Wait until it's this frame's turn to be processed
147+
{
148+
let prev_frame_number = frame.get_frame_number() - 1;
149+
let mut last_processed = hook.last_processed_frame_number(&frame_pipeline_id).await;
150+
while last_processed != prev_frame_number {
151+
last_processed = hook.last_processed_frame_number(&frame_pipeline_id).await;
152+
tokio::task::yield_now().await;
153+
}
154+
}
155+
125156
// NOTE: the following is sub-optimal. We can't use rayon with async code and
126157
// for stateful hooks we need to lock the mutex before running.
127158
let worker_res = tokio::task::spawn_blocking({
128159
let stage_context = stage_context.clone();
129160
let hook = hook.clone();
130-
|| async move {
161+
move || async move{
131162
let h_body = hook.get_hook_body();
132163
let locked_hook = h_body.lock().await;
133-
locked_hook.exec_hook(frame, &stage_context)
164+
let res = locked_hook.exec_hook(frame, &stage_context);
165+
hook.increment_last_processed(&frame_pipeline_id).await;
166+
res
134167
}
135168
}).await;
136169
match worker_res {

pipeless/src/stages/languages/python.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ impl IntoPy<Py<PyAny>> for RgbFrame {
4242
dict.set_item("inference_output", self.get_inference_output().to_pyarray(py)).unwrap();
4343
dict.set_item("pipeline_id", self.get_pipeline_id().to_string()).unwrap();
4444
dict.set_item("user_data", self.get_user_data()).unwrap();
45+
dict.set_item("frame_number", self.get_frame_number()).unwrap();
4546
dict.into()
4647
}
4748
}
@@ -84,12 +85,13 @@ impl<'source> FromPyObject<'source> for RgbFrame {
8485
let inference_output =inference_output_ndarray;
8586
let pipeline_id = ob.get_item("pipeline_id").unwrap().extract()?;
8687
let user_data = ob.get_item("user_data").unwrap().extract()?;
88+
let frame_number = ob.get_item("frame_number").unwrap().extract()?;
8789

8890
let frame = RgbFrame::from_values(
8991
uuid, original, modified, width, height,
9092
pts, dts, duration, fps, input_ts,
9193
inference_input, inference_output,
92-
pipeline_id, user_data
94+
pipeline_id, user_data, frame_number
9395
);
9496

9597
Ok(frame)

0 commit comments

Comments
 (0)