use opencv::{
prelude::NetTrait,
prelude::NetTraitConst,
prelude::MatTraitConst,
core::VectorToVec,
core::Scalar,
core::Size,
core::Mat,
core::Vector,
core::Rect,
core::CV_32F,
dnn::read_net_from_onnx,
dnn::blob_from_image,
dnn::nms_boxes,
dnn::Net,
Error
};
use crate::BBox;
#[cfg(feature = "letterbox")]
use opencv::{
core::CV_8UC3,
core::BORDER_CONSTANT,
core::copy_make_border,
imgproc::resize,
imgproc::INTER_LINEAR,
};
use super::model::ModelTrait;
use super::utils::BACKEND_TARGET_VALID;
const YOLO_BLOB_MEAN: (f64, f64, f64, f64) = (0.0, 0.0, 0.0, 0.0);
pub struct ModelYOLOv5OpenCV {
net: Net,
input_size: Size,
blob_mean: Scalar,
blob_scale: f64,
blob_name: &'static str,
out_layers: Vector<String>,
filter_classes: Vec<usize>,
#[cfg(feature = "letterbox")]
letterbox_resized: Mat,
#[cfg(feature = "letterbox")]
letterbox_padded: Mat,
}
impl ModelYOLOv5OpenCV {
pub fn new_from_onnx_file(
weight_file_path: &str,
net_size: (i32, i32),
backend_id: i32,
target_id: i32,
filter_classes: Vec<usize>,
) -> Result<Self, Error> {
if BACKEND_TARGET_VALID.get(&backend_id).and_then(|map| map.get(&target_id)).is_none() {
return Err(Error::new(400, format!(
"Combination of BACKEND '{}' and TARGET '{}' is not valid",
backend_id, target_id
)));
};
let neural_net = read_net_from_onnx(weight_file_path)?;
Self::new_from_dnn(neural_net, net_size, backend_id, target_id, filter_classes)
}
pub fn new_from_dnn(
mut neural_net: Net,
net_size: (i32, i32),
backend_id: i32,
target_id: i32,
filter_classes: Vec<usize>,
) -> Result<Self, Error> {
neural_net.set_preferable_backend(backend_id)?;
neural_net.set_preferable_target(target_id)?;
let out_layers = neural_net.get_unconnected_out_layers_names()?;
#[cfg(feature = "letterbox")]
let letterbox_padded = Mat::new_rows_cols_with_default(
net_size.1, net_size.0, CV_8UC3,
Scalar::new(114.0, 114.0, 114.0, 0.0)
)?;
Ok(Self {
net: neural_net,
input_size: Size::new(net_size.0, net_size.1),
blob_mean: Scalar::new(YOLO_BLOB_MEAN.0, YOLO_BLOB_MEAN.1, YOLO_BLOB_MEAN.2, YOLO_BLOB_MEAN.3),
blob_scale: 1.0 / 255.0,
blob_name: "",
out_layers,
filter_classes,
#[cfg(feature = "letterbox")]
letterbox_resized: Mat::default(),
#[cfg(feature = "letterbox")]
letterbox_padded,
})
}
pub fn forward(&mut self, image: &Mat, conf_threshold: f32, nms_threshold: f32) -> Result<(Vec<Rect>, Vec<usize>, Vec<f32>), Error> {
let image_width = image.cols();
let image_height = image.rows();
#[cfg(feature = "letterbox")]
let (blobimg, scale, pad_left, pad_top) = {
let scale = f32::min(
self.input_size.width as f32 / image_width as f32,
self.input_size.height as f32 / image_height as f32
);
let new_width = (image_width as f32 * scale).round() as i32;
let new_height = (image_height as f32 * scale).round() as i32;
let pad_left = (self.input_size.width - new_width) / 2;
let pad_top = (self.input_size.height - new_height) / 2;
let blob = if image_width != self.input_size.width || image_height != self.input_size.height {
resize(&image, &mut self.letterbox_resized, Size::new(new_width, new_height), 0.0, 0.0, INTER_LINEAR)?;
let pad_right = self.input_size.width - new_width - pad_left;
let pad_bottom = self.input_size.height - new_height - pad_top;
copy_make_border(
&self.letterbox_resized,
&mut self.letterbox_padded,
pad_top,
pad_bottom,
pad_left,
pad_right,
BORDER_CONSTANT,
Scalar::new(114.0, 114.0, 114.0, 0.0)
)?;
blob_from_image(&self.letterbox_padded, self.blob_scale, Size::new(0, 0), self.blob_mean, true, false, CV_32F)?
} else {
blob_from_image(&image, self.blob_scale, self.input_size, self.blob_mean, true, false, CV_32F)?
};
(blob, scale, pad_left, pad_top)
};
#[cfg(not(feature = "letterbox"))]
let (blobimg, scale_x, scale_y) = {
let scale_x = image_width as f32 / self.input_size.width as f32;
let scale_y = image_height as f32 / self.input_size.height as f32;
let blob = blob_from_image(&image, self.blob_scale, self.input_size, self.blob_mean, true, false, CV_32F)?;
(blob, scale_x, scale_y)
};
let mut detections = Vector::<Mat>::new();
self.net.set_input(&blobimg, self.blob_name, 1.0, self.blob_mean)?;
self.net.forward(&mut detections, &self.out_layers)?;
let mut bboxes = Vector::<Rect>::new();
let mut confidences = Vector::<f32>::new();
let mut class_ids = Vec::new();
for layer in detections {
let mat_size = layer.mat_size();
let num_predictions = mat_size[1];
let num_features = mat_size[2];
for i in 0..num_predictions {
let objectness = *layer.at_3d::<f32>(0, i, 4)?;
if objectness < conf_threshold {
continue;
}
let mut max_class_index = 0usize;
let mut max_class_score = 0.0f32;
for j in 5..num_features {
let score = *layer.at_3d::<f32>(0, i, j)?;
if score > max_class_score {
max_class_score = score;
max_class_index = (j - 5) as usize;
}
}
let confidence = objectness * max_class_score;
if confidence >= conf_threshold {
if !self.filter_classes.is_empty() && !self.filter_classes.contains(&max_class_index) {
continue;
}
let mut cx = *layer.at_3d::<f32>(0, i, 0)?;
let mut cy = *layer.at_3d::<f32>(0, i, 1)?;
let mut w = *layer.at_3d::<f32>(0, i, 2)?;
let mut h = *layer.at_3d::<f32>(0, i, 3)?;
if cx < 2.0 && cy < 2.0 && w < 2.0 && h < 2.0 {
cx *= self.input_size.width as f32;
cy *= self.input_size.height as f32;
w *= self.input_size.width as f32;
h *= self.input_size.height as f32;
}
#[cfg(feature = "letterbox")]
let (x_center, y_center, width, height) = {
(
(cx - pad_left as f32) / scale,
(cy - pad_top as f32) / scale,
w / scale,
h / scale,
)
};
#[cfg(not(feature = "letterbox"))]
let (x_center, y_center, width, height) = {
(
cx * scale_x,
cy * scale_y,
w * scale_x,
h * scale_y,
)
};
let bbox_cv = Rect::new(
(x_center - width / 2.0).round() as i32,
(y_center - height / 2.0).round() as i32,
width.round() as i32,
height.round() as i32
);
bboxes.push(bbox_cv);
confidences.push(confidence);
class_ids.push(max_class_index);
}
}
}
let mut indices = Vector::<i32>::new();
nms_boxes(&bboxes, &confidences, conf_threshold, nms_threshold, &mut indices, 1.0, 0)?;
let mut nms_bboxes = vec![];
let mut nms_classes_ids = vec![];
let mut nms_confidences = vec![];
let indices_vec = indices.to_vec();
let mut bboxes = bboxes.to_vec();
nms_bboxes.extend(bboxes.drain(..)
.enumerate()
.filter_map(|(idx, item)| if indices_vec.contains(&(idx as i32)) { Some(item) } else { None }));
nms_classes_ids.extend(class_ids.drain(..)
.enumerate()
.filter_map(|(idx, item)| if indices_vec.contains(&(idx as i32)) { Some(item) } else { None }));
nms_confidences.extend(confidences.to_vec().drain(..)
.enumerate()
.filter_map(|(idx, item)| if indices_vec.contains(&(idx as i32)) { Some(item) } else { None }));
Ok((nms_bboxes, nms_classes_ids, nms_confidences))
}
pub fn forward_bbox(&mut self, image: &Mat, conf_threshold: f32, nms_threshold: f32) -> Result<(Vec<BBox>, Vec<usize>, Vec<f32>), Error> {
let (rects, class_ids, confidences) = self.forward(image, conf_threshold, nms_threshold)?;
let bboxes = rects.into_iter().map(|r| r.into()).collect();
Ok((bboxes, class_ids, confidences))
}
}
impl ModelTrait for ModelYOLOv5OpenCV {
fn forward(&mut self, image: &Mat, conf_threshold: f32, nms_threshold: f32) -> Result<(Vec<Rect>, Vec<usize>, Vec<f32>), Error> {
self.forward(image, conf_threshold, nms_threshold)
}
}
impl crate::ObjectDetector for ModelYOLOv5OpenCV {
type Input = Mat;
type Error = Error;
fn detect(
&mut self,
input: &Self::Input,
conf_threshold: f32,
nms_threshold: f32,
) -> Result<(Vec<BBox>, Vec<usize>, Vec<f32>), Self::Error> {
self.forward_bbox(input, conf_threshold, nms_threshold)
}
}