Compare commits

..

2 Commits

Author SHA1 Message Date
uttarayan21
3eec262076 feat(bounding-box): add scale_uniform method for consistent scaling
Some checks failed
build / checks-matrix (push) Successful in 19m22s
build / codecov (push) Failing after 19m26s
docs / docs (push) Failing after 28m51s
build / checks-build (push) Has been cancelled
feat(gui): display face ROIs in comparison results

refactor(bridge): pad detected face bounding boxes uniformly
2025-08-22 19:01:34 +05:30
uttarayan21
c758fd8d41 feat(gui): add face ROIs to comparison results and update image size 2025-08-22 18:26:29 +05:30
3 changed files with 259 additions and 63 deletions

View File

@@ -163,6 +163,21 @@ impl<T: Num, const D: usize> AxisAlignedBoundingBox<T, D> {
} }
} }
pub fn scale_uniform(self, scalar: T) -> Self
where
T: core::ops::MulAssign,
T: core::ops::DivAssign,
T: core::ops::SubAssign,
{
let two = T::one() + T::one();
let new_size = self.size * scalar;
let new_point = self.point.coords - (new_size - self.size) / two;
Self {
point: Point::from(new_point),
size: new_size,
}
}
pub fn contains_bbox(&self, other: &Self) -> bool pub fn contains_bbox(&self, other: &Self) -> bool
where where
T: core::ops::AddAssign, T: core::ops::AddAssign,
@@ -270,15 +285,17 @@ impl<T: Num, const D: usize> AxisAlignedBoundingBox<T, D> {
}) })
} }
// pub fn as_<T2>(&self) -> Option<Aabb<T2, D>> pub fn as_<T2>(&self) -> Aabb<T2, D>
// where where
// T2: Num + simba::scalar::SubsetOf<T>, T2: Num,
// { T: num::cast::AsPrimitive<T2>,
// Some(Aabb { {
// point: Point::from(self.point.coords.as_()), Aabb {
// size: self.size.as_(), point: Point::from(self.point.coords.map(|x| x.as_())),
// }) size: self.size.map(|x| x.as_()),
// } }
}
pub fn measure(&self) -> T pub fn measure(&self) -> T
where where
T: core::ops::MulAssign, T: core::ops::MulAssign,

View File

@@ -10,6 +10,7 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use crate::gui::bridge::FaceDetectionBridge; use crate::gui::bridge::FaceDetectionBridge;
use ::image::{DynamicImage, ImageFormat, RgbImage};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
@@ -43,6 +44,7 @@ pub enum Message {
ImageLoaded(Option<Arc<Vec<u8>>>), ImageLoaded(Option<Arc<Vec<u8>>>),
SecondImageLoaded(Option<Arc<Vec<u8>>>), SecondImageLoaded(Option<Arc<Vec<u8>>>),
ProcessedImageUpdated(Option<Vec<u8>>), ProcessedImageUpdated(Option<Vec<u8>>),
FaceRoisLoaded(Vec<image::Handle>, Vec<image::Handle>),
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -76,6 +78,8 @@ pub enum ComparisonResult {
Success { Success {
image1_faces: usize, image1_faces: usize,
image2_faces: usize, image2_faces: usize,
image1_face_rois: Vec<ndarray::Array3<u8>>,
image2_face_rois: Vec<ndarray::Array3<u8>>,
best_similarity: f32, best_similarity: f32,
processing_time: f64, processing_time: f64,
}, },
@@ -110,6 +114,10 @@ pub struct FaceDetectorApp {
current_image_handle: Option<image::Handle>, current_image_handle: Option<image::Handle>,
processed_image_handle: Option<image::Handle>, processed_image_handle: Option<image::Handle>,
second_image_handle: Option<image::Handle>, second_image_handle: Option<image::Handle>,
// Face ROI handles for comparison display
image1_face_roi_handles: Vec<image::Handle>,
image2_face_roi_handles: Vec<image::Handle>,
} }
impl Default for FaceDetectorApp { impl Default for FaceDetectorApp {
@@ -130,6 +138,8 @@ impl Default for FaceDetectorApp {
current_image_handle: None, current_image_handle: None,
processed_image_handle: None, processed_image_handle: None,
second_image_handle: None, second_image_handle: None,
image1_face_roi_handles: Vec::new(),
image2_face_roi_handles: Vec::new(),
} }
} }
} }
@@ -313,6 +323,8 @@ impl FaceDetectorApp {
self.detection_result = None; self.detection_result = None;
self.comparison_result = None; self.comparison_result = None;
self.processed_image_handle = None; self.processed_image_handle = None;
self.image1_face_roi_handles.clear();
self.image2_face_roi_handles.clear();
self.status_message = "Results cleared".to_string(); self.status_message = "Results cleared".to_string();
Task::none() Task::none()
} }
@@ -356,6 +368,8 @@ impl FaceDetectorApp {
ComparisonResult::Success { ComparisonResult::Success {
best_similarity, best_similarity,
processing_time, processing_time,
image1_face_rois,
image2_face_rois,
.. ..
} => { } => {
let interpretation = if *best_similarity > 0.8 { let interpretation = if *best_similarity > 0.8 {
@@ -372,6 +386,16 @@ impl FaceDetectorApp {
"Comparison complete! Similarity: {:.3} - {} (Processing time: {:.2}s)", "Comparison complete! Similarity: {:.3} - {} (Processing time: {:.2}s)",
best_similarity, interpretation, processing_time best_similarity, interpretation, processing_time
); );
// Convert face ROIs to image handles
let image1_handles = convert_face_rois_to_handles(image1_face_rois.clone());
let image2_handles = convert_face_rois_to_handles(image2_face_rois.clone());
self.comparison_result = Some(result);
return Task::perform(
async move { (image1_handles, image2_handles) },
|(h1, h2)| Message::FaceRoisLoaded(h1, h2),
);
} }
ComparisonResult::Error(error) => { ComparisonResult::Error(error) => {
self.status_message = format!("Comparison failed: {}", error); self.status_message = format!("Comparison failed: {}", error);
@@ -382,6 +406,12 @@ impl FaceDetectorApp {
Task::none() Task::none()
} }
Message::FaceRoisLoaded(image1_handles, image2_handles) => {
self.image1_face_roi_handles = image1_handles;
self.image2_face_roi_handles = image2_handles;
Task::none()
}
Message::ProgressUpdate(progress) => { Message::ProgressUpdate(progress) => {
self.progress = progress; self.progress = progress;
Task::none() Task::none()
@@ -765,6 +795,8 @@ impl FaceDetectorApp {
ComparisonResult::Success { ComparisonResult::Success {
image1_faces, image1_faces,
image2_faces, image2_faces,
image1_face_rois: _,
image2_face_rois: _,
best_similarity, best_similarity,
processing_time, processing_time,
} => { } => {
@@ -790,7 +822,7 @@ impl FaceDetectorApp {
) )
}; };
column![ let mut result_column = column![
text("Comparison Results").size(18), text("Comparison Results").size(18),
text(format!("First image faces: {}", image1_faces)), text(format!("First image faces: {}", image1_faces)),
text(format!("Second image faces: {}", image2_faces)), text(format!("Second image faces: {}", image2_faces)),
@@ -800,7 +832,89 @@ impl FaceDetectorApp {
}), }),
text(format!("Processing time: {:.2}s", processing_time)), text(format!("Processing time: {:.2}s", processing_time)),
] ]
.spacing(5);
// Add face ROI displays if available
if !self.image1_face_roi_handles.is_empty()
|| !self.image2_face_roi_handles.is_empty()
{
result_column = result_column.push(text("Detected Faces").size(16));
// Create face ROI rows
let image1_faces_row = if !self.image1_face_roi_handles.is_empty() {
let faces: Element<'_, Message> = self
.image1_face_roi_handles
.iter()
.enumerate()
.fold(row![].spacing(5), |row, (i, handle)| {
row.push(
column![
text(format!("Face {}", i + 1)).size(12),
container(
image(handle.clone())
.width(80)
.height(80)
.content_fit(iced::ContentFit::Cover)
)
.style(container::bordered_box)
.padding(2),
]
.spacing(2)
.align_x(Alignment::Center),
)
})
.into();
column![
text("First Image Faces:").size(14),
scrollable(faces).direction(scrollable::Direction::Horizontal(
scrollable::Scrollbar::new()
)),
]
.spacing(5) .spacing(5)
} else {
column![text("First Image Faces: None detected").size(14)]
};
let image2_faces_row = if !self.image2_face_roi_handles.is_empty() {
let faces: Element<'_, Message> = self
.image2_face_roi_handles
.iter()
.enumerate()
.fold(row![].spacing(5), |row, (i, handle)| {
row.push(
column![
text(format!("Face {}", i + 1)).size(12),
container(
image(handle.clone())
.width(80)
.height(80)
.content_fit(iced::ContentFit::Cover)
)
.style(container::bordered_box)
.padding(2),
]
.spacing(2)
.align_x(Alignment::Center),
)
})
.into();
column![
text("Second Image Faces:").size(14),
scrollable(faces).direction(scrollable::Direction::Horizontal(
scrollable::Scrollbar::new()
)),
]
.spacing(5)
} else {
column![text("Second Image Faces: None detected").size(14)]
};
result_column = result_column.push(image1_faces_row).push(image2_faces_row);
}
result_column
} }
ComparisonResult::Error(error) => column![ ComparisonResult::Error(error) => column![
text("Comparison Results").size(18), text("Comparison Results").size(18),
@@ -816,9 +930,11 @@ impl FaceDetectorApp {
] ]
}; };
scrollable(
column![file_section, comparison_image_section, controls, results] column![file_section, comparison_image_section, controls, results]
.spacing(20) .spacing(20)
.padding(20) .padding(20),
)
.into() .into()
} }
@@ -881,6 +997,31 @@ impl std::fmt::Display for ExecutorType {
} }
} }
// Helper function to convert face ROIs to image handles
fn convert_face_rois_to_handles(face_rois: Vec<ndarray::Array3<u8>>) -> Vec<image::Handle> {
face_rois
.into_iter()
.filter_map(|roi| {
// Convert ndarray to image::RgbImage
let (height, width, _) = roi.dim();
let (raw_data, _offset) = roi.into_raw_vec_and_offset();
if let Some(img) = RgbImage::from_raw(width as u32, height as u32, raw_data) {
// Convert to PNG bytes
let mut buffer = Vec::new();
let mut cursor = std::io::Cursor::new(&mut buffer);
if DynamicImage::ImageRgb8(img)
.write_to(&mut cursor, ImageFormat::Png)
.is_ok()
{
return Some(image::Handle::from_bytes(buffer));
}
}
None
})
.collect()
}
pub fn run() -> iced::Result { pub fn run() -> iced::Result {
iced::application( iced::application(
"Face Detector", "Face Detector",

View File

@@ -2,7 +2,7 @@ use std::path::PathBuf;
use crate::errors; use crate::errors;
use crate::facedet::{FaceDetectionConfig, FaceDetector, retinaface}; use crate::facedet::{FaceDetectionConfig, FaceDetector, retinaface};
use crate::faceembed::{FaceNetEmbedder, facenet}; use crate::faceembed::facenet;
use crate::gui::app::{ComparisonResult, DetectionResult, ExecutorType}; use crate::gui::app::{ComparisonResult, DetectionResult, ExecutorType};
use bounding_box::Aabb2; use bounding_box::Aabb2;
use bounding_box::roi::MultiRoi as _; use bounding_box::roi::MultiRoi as _;
@@ -70,11 +70,19 @@ impl FaceDetectionBridge {
) )
.await .await
{ {
Ok((image1_faces, image2_faces, best_similarity)) => { Ok((
image1_faces,
image2_faces,
image1_face_rois,
image2_face_rois,
best_similarity,
)) => {
let processing_time = start_time.elapsed().as_secs_f64(); let processing_time = start_time.elapsed().as_secs_f64();
ComparisonResult::Success { ComparisonResult::Success {
image1_faces, image1_faces,
image2_faces, image2_faces,
image1_face_rois,
image2_face_rois,
best_similarity, best_similarity,
processing_time, processing_time,
} }
@@ -180,9 +188,13 @@ impl FaceDetectionBridge {
threshold: f32, threshold: f32,
nms_threshold: f32, nms_threshold: f32,
executor_type: ExecutorType, executor_type: ExecutorType,
) -> Result<(usize, usize, f32), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<
(usize, usize, Vec<Array3<u8>>, Vec<Array3<u8>>, f32),
Box<dyn std::error::Error + Send + Sync>,
> {
// Create detector and embedder, detect faces and generate embeddings // Create detector and embedder, detect faces and generate embeddings
let (faces1, faces2, best_similarity) = match executor_type { let (image1_faces, image2_faces, image1_rois, image2_rois, best_similarity) =
match executor_type {
ExecutorType::MnnCpu | ExecutorType::MnnMetal | ExecutorType::MnnCoreML => { ExecutorType::MnnCpu | ExecutorType::MnnMetal | ExecutorType::MnnCoreML => {
let forward_type = match executor_type { let forward_type = match executor_type {
ExecutorType::MnnCpu => mnn::ForwardType::CPU, ExecutorType::MnnCpu => mnn::ForwardType::CPU,
@@ -191,7 +203,8 @@ impl FaceDetectionBridge {
_ => unreachable!(), _ => unreachable!(),
}; };
let mut detector = retinaface::mnn::FaceDetection::builder(RETINAFACE_MODEL_MNN) let mut detector =
retinaface::mnn::FaceDetection::builder(RETINAFACE_MODEL_MNN)
.map_err(|e| format!("Failed to create MNN detector: {}", e))? .map_err(|e| format!("Failed to create MNN detector: {}", e))?
.with_forward_type(forward_type.clone()) .with_forward_type(forward_type.clone())
.build() .build()
@@ -220,13 +233,30 @@ impl FaceDetectionBridge {
2, 2,
)?; )?;
let image1_rois = img_1.rois;
let image2_rois = img_2.rois;
let image1_bbox_len = img_1.bbox.len();
let image2_bbox_len = img_2.bbox.len();
let best_similarity = compare_faces(&img_1.embeddings, &img_2.embeddings)?; let best_similarity = compare_faces(&img_1.embeddings, &img_2.embeddings)?;
(img_1, img_2, best_similarity)
(
image1_bbox_len,
image2_bbox_len,
image1_rois,
image2_rois,
best_similarity,
)
} }
ExecutorType::OnnxCpu => unimplemented!(), ExecutorType::OnnxCpu => unimplemented!(),
}; };
Ok((faces1.bbox.len(), faces2.bbox.len(), best_similarity)) Ok((
image1_faces,
image2_faces,
image1_rois,
image2_rois,
best_similarity,
))
} }
} }
@@ -296,19 +326,27 @@ where
.change_context(errors::Error) .change_context(errors::Error)
.attach_printable("Failed to detect faces")?; .attach_printable("Failed to detect faces")?;
for bbox in &output.bbox { let bboxes = output
.bbox
.iter()
.inspect(|bbox| tracing::info!("Raw bbox: {:?}", bbox))
.map(|bbox| bbox.as_::<f32>().scale_uniform(1.30).as_::<usize>())
.inspect(|bbox| tracing::info!("Padded bbox: {:?}", bbox))
.collect_vec();
for bbox in &bboxes {
tracing::info!("Detected face: {:?}", bbox); tracing::info!("Detected face: {:?}", bbox);
use bounding_box::draw::*; use bounding_box::draw::*;
array.draw(bbox, color::palette::css::GREEN_YELLOW.to_rgba8(), 1); array.draw(bbox, color::palette::css::GREEN_YELLOW.to_rgba8(), 1);
} }
use itertools::Itertools;
let face_rois = array let face_rois = array
.view() .view()
.multi_roi(&output.bbox) .multi_roi(&bboxes)
.change_context(Error)? .change_context(Error)?
.into_iter() .into_iter()
.map(|roi| { .map(|roi| {
roi.as_standard_layout() roi.as_standard_layout()
.fast_resize(320, 320, &ResizeOptions::default()) .fast_resize(224, 224, &ResizeOptions::default())
.change_context(Error) .change_context(Error)
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
@@ -322,7 +360,7 @@ where
let og_size = chunk.len(); let og_size = chunk.len();
if chunk.len() < chunk_size { if chunk.len() < chunk_size {
tracing::warn!("Chunk size is less than 8, padding with zeros"); tracing::warn!("Chunk size is less than 8, padding with zeros");
let zeros = Array3::zeros((320, 320, 3)); let zeros = Array3::zeros((224, 224, 3));
let chunk: Vec<_> = chunk let chunk: Vec<_> = chunk
.iter() .iter()
.map(|arr| arr.reborrow()) .map(|arr| arr.reborrow())
@@ -358,7 +396,7 @@ where
.collect::<Vec<Array1<f32>>>(); .collect::<Vec<Array1<f32>>>();
Ok(DetectionOutput { Ok(DetectionOutput {
bbox: output.bbox, bbox: bboxes,
rois: face_rois, rois: face_rois,
embeddings, embeddings,
}) })