1use std::{path::Path, sync::Arc};
2
3use crate::pp_log::{PpLog, pp_error, pp_info};
4use ffmpeg_next as ffmpeg;
5use ndarray::{Array4, Axis, s};
6use ort::{inputs, session::Session, value::TensorRef};
7use thiserror::Error as ThisError;
8
9use crate::{
10 buffer::MediaBuffer,
11 control::ControlMsg,
12 element::{Element, ElementType, Sink, element_pp_log},
13 error::Result,
14};
15
16#[derive(Debug, Clone, Copy)]
20pub struct Detection {
21 pub class_id: usize,
24 pub score: f32,
25 pub x: f32,
28 pub y: f32,
29 pub width: f32,
30 pub height: f32,
31}
32
33#[rustfmt::skip]
38pub const COCO_CLASS_LABELS: [&str; 80] = [
39 "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
40 "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant",
41 "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",
42 "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle",
43 "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli",
44 "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet",
45 "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator",
46 "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush",
47];
48
49#[derive(Debug, ThisError)]
52pub enum OrtDetectorError {
53 #[error("onnxruntime error: {0}")]
54 Ort(#[from] ort::Error),
55
56 #[error(
57 "OrtDetector only accepts RGB24 Video frames, got {0:?}; \
58 link it straight after a Scaler configured with Pixel::RGB24"
59 )]
60 UnsupportedFormat(ffmpeg::format::Pixel),
61
62 #[error(
63 "OrtDetector only accepts decoded Video frames, got a {0}; \
64 link it straight after a Scaler"
65 )]
66 UnsupportedBuffer(&'static str),
67}
68
69pub struct OrtDetector<F> {
92 pp_log: PpLog,
93 name: Arc<str>,
94 session: Session,
95 conf_threshold: f32,
96 iou_threshold: f32,
97 on_detections: F,
98}
99
100impl<F> OrtDetector<F>
101where
102 F: FnMut(&ffmpeg::frame::Video, &[Detection]) -> Result<()> + Send + 'static,
103{
104 pub fn new(
109 name: impl Into<String>,
110 model_path: impl AsRef<Path>,
111 conf_threshold: f32,
112 iou_threshold: f32,
113 on_detections: F,
114 ) -> Result<Self> {
115 let model_path_display = model_path.as_ref().display().to_string();
116 let session = Session::builder()
117 .map_err(OrtDetectorError::from)?
118 .commit_from_file(model_path)
119 .map_err(OrtDetectorError::from)?;
120 let name: Arc<str> = name.into().into();
121 let pp_log = element_pp_log(ElementType::OrtDetector, &name, None);
122 pp_info!(
123 pp_log: &pp_log,
124 "model loaded: path={model_path_display}, conf_threshold={conf_threshold}, iou_threshold={iou_threshold}"
125 );
126 Ok(Self {
127 name,
128 pp_log,
129 session,
130 conf_threshold,
131 iou_threshold,
132 on_detections,
133 })
134 }
135
136 fn detect(&mut self, frame: &ffmpeg::frame::Video) -> Result<Vec<Detection>> {
141 let width = frame.width() as usize;
142 let height = frame.height() as usize;
143 let stride = frame.stride(0);
144 let data = frame.data(0);
145
146 let mut input = Array4::<f32>::zeros((1, 3, height, width));
147 for y in 0..height {
148 let row = &data[y * stride..y * stride + width * 3];
149 for x in 0..width {
150 let pixel = &row[x * 3..x * 3 + 3];
151 input[[0, 0, y, x]] = pixel[0] as f32 / 255.0;
152 input[[0, 1, y, x]] = pixel[1] as f32 / 255.0;
153 input[[0, 2, y, x]] = pixel[2] as f32 / 255.0;
154 }
155 }
156
157 let outputs = self
158 .session
159 .run(inputs![
160 TensorRef::from_array_view(&input).map_err(OrtDetectorError::from)?
161 ])
162 .map_err(OrtDetectorError::from)?;
163 let output = outputs[0]
167 .try_extract_array::<f32>()
168 .map_err(OrtDetectorError::from)?
169 .t()
170 .into_owned();
171 let output = output.slice(s![.., .., 0]);
172
173 let mut candidates = Vec::new();
174 for row in output.axis_iter(Axis(0)) {
175 let (class_id, score) = row
176 .iter()
177 .skip(4)
179 .enumerate()
180 .map(|(index, value)| (index, *value))
181 .reduce(|best, next| if next.1 > best.1 { next } else { best })
182 .expect("model output has at least one class column");
183 if score < self.conf_threshold {
184 continue;
185 }
186 let (cx, cy, w, h) = (row[0usize], row[1usize], row[2usize], row[3usize]);
187 candidates.push(Detection {
188 class_id,
189 score,
190 x: cx - w / 2.0,
191 y: cy - h / 2.0,
192 width: w,
193 height: h,
194 });
195 }
196
197 Ok(non_max_suppression(candidates, self.iou_threshold))
198 }
199}
200
201fn iou(a: &Detection, b: &Detection) -> f32 {
202 let (ax2, ay2) = (a.x + a.width, a.y + a.height);
203 let (bx2, by2) = (b.x + b.width, b.y + b.height);
204 let overlap_w = (ax2.min(bx2) - a.x.max(b.x)).max(0.0);
205 let overlap_h = (ay2.min(by2) - a.y.max(b.y)).max(0.0);
206 let intersection = overlap_w * overlap_h;
207 let union = a.width * a.height + b.width * b.height - intersection;
208 if union <= 0.0 {
209 0.0
210 } else {
211 intersection / union
212 }
213}
214
215fn non_max_suppression(mut candidates: Vec<Detection>, iou_threshold: f32) -> Vec<Detection> {
218 candidates.sort_by(|a, b| b.score.total_cmp(&a.score));
219
220 let mut kept: Vec<Detection> = Vec::with_capacity(candidates.len());
221 'candidates: for candidate in candidates {
222 for already_kept in &kept {
223 if already_kept.class_id == candidate.class_id
224 && iou(already_kept, &candidate) > iou_threshold
225 {
226 continue 'candidates;
227 }
228 }
229 kept.push(candidate);
230 }
231 kept
232}
233
234impl<F> Element for OrtDetector<F>
235where
236 F: FnMut(&ffmpeg::frame::Video, &[Detection]) -> Result<()> + Send + 'static,
237{
238 fn name(&self) -> Arc<str> {
239 self.name.clone()
240 }
241
242 fn element_type(&self) -> ElementType {
243 ElementType::OrtDetector
244 }
245
246 fn pp_log(&self) -> &PpLog {
247 &self.pp_log
248 }
249
250 fn pp_log_mut(&mut self) -> &mut PpLog {
251 &mut self.pp_log
252 }
253}
254
255impl<F> Sink for OrtDetector<F>
256where
257 F: FnMut(&ffmpeg::frame::Video, &[Detection]) -> Result<()> + Send + 'static,
258{
259 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
260 match buf {
261 MediaBuffer::Video(frame) => {
262 if frame.format() != ffmpeg::format::Pixel::RGB24 {
263 pp_error!(self, "unsupported pixel format: {:?}", frame.format());
264 return Err(OrtDetectorError::UnsupportedFormat(frame.format()).into());
265 }
266 let detections = self
267 .detect(&frame)
268 .inspect_err(|error| pp_error!(self, "detect failed: {error}"))?;
269 (self.on_detections)(&frame, &detections)
270 }
271 MediaBuffer::Eos => Ok(()),
272 MediaBuffer::Packet(_) => {
273 pp_error!(self, "unsupported buffer: Packet");
274 Err(OrtDetectorError::UnsupportedBuffer("Packet").into())
275 }
276 MediaBuffer::Audio(_) => {
277 pp_error!(self, "unsupported buffer: Audio");
278 Err(OrtDetectorError::UnsupportedBuffer("Audio").into())
279 }
280 }
281 }
282
283 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
284 Ok(())
287 }
288}