diff --git a/bounding-box/src/lib.rs b/bounding-box/src/lib.rs index f56802b..825464c 100644 --- a/bounding-box/src/lib.rs +++ b/bounding-box/src/lib.rs @@ -163,6 +163,21 @@ impl AxisAlignedBoundingBox { } } + 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 where T: core::ops::AddAssign, @@ -270,15 +285,17 @@ impl AxisAlignedBoundingBox { }) } - // pub fn as_(&self) -> Option> - // where - // T2: Num + simba::scalar::SubsetOf, - // { - // Some(Aabb { - // point: Point::from(self.point.coords.as_()), - // size: self.size.as_(), - // }) - // } + pub fn as_(&self) -> Aabb + where + T2: Num, + T: num::cast::AsPrimitive, + { + Aabb { + point: Point::from(self.point.coords.map(|x| x.as_())), + size: self.size.map(|x| x.as_()), + } + } + pub fn measure(&self) -> T where T: core::ops::MulAssign, diff --git a/src/gui/app.rs b/src/gui/app.rs index 642800c..5a9d6d6 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -10,6 +10,7 @@ use std::path::PathBuf; use std::sync::Arc; use crate::gui::bridge::FaceDetectionBridge; +use ::image::{DynamicImage, ImageFormat, RgbImage}; #[derive(Debug, Clone)] pub enum Message { @@ -43,6 +44,7 @@ pub enum Message { ImageLoaded(Option>>), SecondImageLoaded(Option>>), ProcessedImageUpdated(Option>), + FaceRoisLoaded(Vec, Vec), } #[derive(Debug, Clone, PartialEq)] @@ -112,6 +114,10 @@ pub struct FaceDetectorApp { current_image_handle: Option, processed_image_handle: Option, second_image_handle: Option, + + // Face ROI handles for comparison display + image1_face_roi_handles: Vec, + image2_face_roi_handles: Vec, } impl Default for FaceDetectorApp { @@ -132,6 +138,8 @@ impl Default for FaceDetectorApp { current_image_handle: None, processed_image_handle: None, second_image_handle: None, + image1_face_roi_handles: Vec::new(), + image2_face_roi_handles: Vec::new(), } } } @@ -315,6 +323,8 @@ impl FaceDetectorApp { self.detection_result = None; self.comparison_result = 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(); Task::none() } @@ -358,6 +368,8 @@ impl FaceDetectorApp { ComparisonResult::Success { best_similarity, processing_time, + image1_face_rois, + image2_face_rois, .. } => { let interpretation = if *best_similarity > 0.8 { @@ -374,6 +386,16 @@ impl FaceDetectorApp { "Comparison complete! Similarity: {:.3} - {} (Processing time: {:.2}s)", 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) => { self.status_message = format!("Comparison failed: {}", error); @@ -384,6 +406,12 @@ impl FaceDetectorApp { 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) => { self.progress = progress; Task::none() @@ -794,7 +822,7 @@ impl FaceDetectorApp { ) }; - column![ + let mut result_column = column![ text("Comparison Results").size(18), text(format!("First image faces: {}", image1_faces)), text(format!("Second image faces: {}", image2_faces)), @@ -804,7 +832,89 @@ impl FaceDetectorApp { }), text(format!("Processing time: {:.2}s", processing_time)), ] - .spacing(5) + .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) + } 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![ text("Comparison Results").size(18), @@ -820,10 +930,12 @@ impl FaceDetectorApp { ] }; - column![file_section, comparison_image_section, controls, results] - .spacing(20) - .padding(20) - .into() + scrollable( + column![file_section, comparison_image_section, controls, results] + .spacing(20) + .padding(20), + ) + .into() } fn settings_view(&self) -> Element<'_, Message> { @@ -885,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>) -> Vec { + 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 { iced::application( "Face Detector", diff --git a/src/gui/bridge.rs b/src/gui/bridge.rs index 5b28770..6a7eb32 100644 --- a/src/gui/bridge.rs +++ b/src/gui/bridge.rs @@ -326,14 +326,22 @@ where .change_context(errors::Error) .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_::().scale_uniform(1.30).as_::()) + .inspect(|bbox| tracing::info!("Padded bbox: {:?}", bbox)) + .collect_vec(); + for bbox in &bboxes { tracing::info!("Detected face: {:?}", bbox); use bounding_box::draw::*; array.draw(bbox, color::palette::css::GREEN_YELLOW.to_rgba8(), 1); } + use itertools::Itertools; let face_rois = array .view() - .multi_roi(&output.bbox) + .multi_roi(&bboxes) .change_context(Error)? .into_iter() .map(|roi| { @@ -388,7 +396,7 @@ where .collect::>>(); Ok(DetectionOutput { - bbox: output.bbox, + bbox: bboxes, rois: face_rois, embeddings, })