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