use std::{path::Path, sync::Arc};
use crate::pp_log::{PpLog, pp_error, pp_info};
use ffmpeg_next as ffmpeg;
use ndarray::{Array4, Axis, s};
use ort::{inputs, session::Session, value::TensorRef};
use thiserror::Error as ThisError;
use crate::{
buffer::MediaBuffer,
control::ControlMsg,
element::{Element, ElementType, Sink, element_pp_log},
error::Result,
};
#[derive(Debug, Clone, Copy)]
pub struct Detection {
pub class_id: usize,
pub score: f32,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[rustfmt::skip]
pub const COCO_CLASS_LABELS: [&str; 80] = [
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
"fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant",
"bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",
"sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle",
"wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli",
"carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet",
"tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator",
"book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush",
];
#[derive(Debug, ThisError)]
pub enum OrtDetectorError {
#[error("onnxruntime error: {0}")]
Ort(#[from] ort::Error),
#[error(
"OrtDetector only accepts RGB24 Video frames, got {0:?}; \
link it straight after a Scaler configured with Pixel::RGB24"
)]
UnsupportedFormat(ffmpeg::format::Pixel),
#[error(
"OrtDetector only accepts decoded Video frames, got a {0}; \
link it straight after a Scaler"
)]
UnsupportedBuffer(&'static str),
}
pub struct OrtDetector<F> {
pp_log: PpLog,
name: Arc<str>,
session: Session,
conf_threshold: f32,
iou_threshold: f32,
on_detections: F,
}
impl<F> OrtDetector<F>
where
F: FnMut(&ffmpeg::frame::Video, &[Detection]) -> Result<()> + Send + 'static,
{
pub fn new(
name: impl Into<String>,
model_path: impl AsRef<Path>,
conf_threshold: f32,
iou_threshold: f32,
on_detections: F,
) -> Result<Self> {
let model_path_display = model_path.as_ref().display().to_string();
let session = Session::builder()
.map_err(OrtDetectorError::from)?
.commit_from_file(model_path)
.map_err(OrtDetectorError::from)?;
let name: Arc<str> = name.into().into();
let pp_log = element_pp_log(ElementType::OrtDetector, &name, None);
pp_info!(
pp_log: &pp_log,
"model loaded: path={model_path_display}, conf_threshold={conf_threshold}, iou_threshold={iou_threshold}"
);
Ok(Self {
name,
pp_log,
session,
conf_threshold,
iou_threshold,
on_detections,
})
}
fn detect(&mut self, frame: &ffmpeg::frame::Video) -> Result<Vec<Detection>> {
let width = frame.width() as usize;
let height = frame.height() as usize;
let stride = frame.stride(0);
let data = frame.data(0);
let mut input = Array4::<f32>::zeros((1, 3, height, width));
for y in 0..height {
let row = &data[y * stride..y * stride + width * 3];
for x in 0..width {
let pixel = &row[x * 3..x * 3 + 3];
input[[0, 0, y, x]] = pixel[0] as f32 / 255.0;
input[[0, 1, y, x]] = pixel[1] as f32 / 255.0;
input[[0, 2, y, x]] = pixel[2] as f32 / 255.0;
}
}
let outputs = self
.session
.run(inputs![
TensorRef::from_array_view(&input).map_err(OrtDetectorError::from)?
])
.map_err(OrtDetectorError::from)?;
let output = outputs[0]
.try_extract_array::<f32>()
.map_err(OrtDetectorError::from)?
.t()
.into_owned();
let output = output.slice(s![.., .., 0]);
let mut candidates = Vec::new();
for row in output.axis_iter(Axis(0)) {
let (class_id, score) = row
.iter()
.skip(4)
.enumerate()
.map(|(index, value)| (index, *value))
.reduce(|best, next| if next.1 > best.1 { next } else { best })
.expect("model output has at least one class column");
if score < self.conf_threshold {
continue;
}
let (cx, cy, w, h) = (row[0usize], row[1usize], row[2usize], row[3usize]);
candidates.push(Detection {
class_id,
score,
x: cx - w / 2.0,
y: cy - h / 2.0,
width: w,
height: h,
});
}
Ok(non_max_suppression(candidates, self.iou_threshold))
}
}
fn iou(a: &Detection, b: &Detection) -> f32 {
let (ax2, ay2) = (a.x + a.width, a.y + a.height);
let (bx2, by2) = (b.x + b.width, b.y + b.height);
let overlap_w = (ax2.min(bx2) - a.x.max(b.x)).max(0.0);
let overlap_h = (ay2.min(by2) - a.y.max(b.y)).max(0.0);
let intersection = overlap_w * overlap_h;
let union = a.width * a.height + b.width * b.height - intersection;
if union <= 0.0 {
0.0
} else {
intersection / union
}
}
fn non_max_suppression(mut candidates: Vec<Detection>, iou_threshold: f32) -> Vec<Detection> {
candidates.sort_by(|a, b| b.score.total_cmp(&a.score));
let mut kept: Vec<Detection> = Vec::with_capacity(candidates.len());
'candidates: for candidate in candidates {
for already_kept in &kept {
if already_kept.class_id == candidate.class_id
&& iou(already_kept, &candidate) > iou_threshold
{
continue 'candidates;
}
}
kept.push(candidate);
}
kept
}
impl<F> Element for OrtDetector<F>
where
F: FnMut(&ffmpeg::frame::Video, &[Detection]) -> Result<()> + Send + 'static,
{
fn name(&self) -> Arc<str> {
self.name.clone()
}
fn element_type(&self) -> ElementType {
ElementType::OrtDetector
}
fn pp_log(&self) -> &PpLog {
&self.pp_log
}
fn pp_log_mut(&mut self) -> &mut PpLog {
&mut self.pp_log
}
}
impl<F> Sink for OrtDetector<F>
where
F: FnMut(&ffmpeg::frame::Video, &[Detection]) -> Result<()> + Send + 'static,
{
fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
match buf {
MediaBuffer::Video(frame) => {
if frame.format() != ffmpeg::format::Pixel::RGB24 {
pp_error!(self, "unsupported pixel format: {:?}", frame.format());
return Err(OrtDetectorError::UnsupportedFormat(frame.format()).into());
}
let detections = self
.detect(&frame)
.inspect_err(|error| pp_error!(self, "detect failed: {error}"))?;
(self.on_detections)(&frame, &detections)
}
MediaBuffer::Eos => Ok(()),
MediaBuffer::Packet(_) => {
pp_error!(self, "unsupported buffer: Packet");
Err(OrtDetectorError::UnsupportedBuffer("Packet").into())
}
MediaBuffer::Audio(_) => {
pp_error!(self, "unsupported buffer: Audio");
Err(OrtDetectorError::UnsupportedBuffer("Audio").into())
}
}
}
fn control(&mut self, _msg: ControlMsg) -> Result<()> {
Ok(())
}
}