use iced::{ Alignment, Element, Length, Settings, Task, Theme, widget::{ Space, button, column, container, image, pick_list, progress_bar, row, scrollable, slider, text, }, }; use rfd::FileDialog; use std::path::PathBuf; use std::sync::Arc; use crate::gui::bridge::FaceDetectionBridge; use ::image::{DynamicImage, ImageFormat, RgbImage}; #[derive(Debug, Clone)] pub enum Message { // File operations OpenImageDialog, ImageSelected(Option), OpenSecondImageDialog, SecondImageSelected(Option), SaveOutputDialog, OutputPathSelected(Option), // Detection parameters ThresholdChanged(f32), NmsThresholdChanged(f32), ExecutorChanged(ExecutorType), // Actions DetectFaces, CompareFaces, ClearResults, // Results DetectionComplete(DetectionResult), ComparisonComplete(ComparisonResult), // UI state TabChanged(Tab), ProgressUpdate(f32), // Image loading ImageLoaded(Option>>), SecondImageLoaded(Option>>), ProcessedImageUpdated(Option>), FaceRoisLoaded(Vec, Vec), } #[derive(Debug, Clone, PartialEq)] pub enum Tab { Detection, Comparison, Settings, } #[derive(Debug, Clone, PartialEq)] pub enum ExecutorType { MnnCpu, #[cfg(feature = "mnn-metal")] MnnMetal, #[cfg(feature = "mnn-coreml")] MnnCoreML, OnnxCpu, #[cfg(feature = "ort-cuda")] OrtCuda, } #[derive(Debug, Clone)] pub enum DetectionResult { Success { image_path: PathBuf, faces_count: usize, processed_image: Option>, processing_time: f64, }, Error(String), } #[derive(Debug, Clone)] pub enum ComparisonResult { Success { image1_faces: usize, image2_faces: usize, image1_face_rois: Vec>, image2_face_rois: Vec>, best_similarity: f32, processing_time: f64, }, Error(String), } #[derive(Debug)] pub struct FaceDetectorApp { // Current tab current_tab: Tab, // File paths input_image: Option, second_image: Option, output_path: Option, // Detection parameters threshold: f32, nms_threshold: f32, executor_type: ExecutorType, // UI state is_processing: bool, progress: f32, status_message: String, // Results detection_result: Option, comparison_result: Option, // Image data for display 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 { fn default() -> Self { Self { current_tab: Tab::Detection, input_image: None, second_image: None, output_path: None, threshold: 0.8, nms_threshold: 0.3, #[cfg(not(any(feature = "mnn-metal", feature = "ort-cuda")))] executor_type: ExecutorType::MnnCpu, #[cfg(feature = "ort-cuda")] executor_type: ExecutorType::OrtCuda, is_processing: false, progress: 0.0, status_message: "Ready".to_string(), detection_result: None, comparison_result: None, current_image_handle: None, processed_image_handle: None, second_image_handle: None, image1_face_roi_handles: Vec::new(), image2_face_roi_handles: Vec::new(), } } } impl FaceDetectorApp { fn new() -> (Self, Task) { (Self::default(), Task::none()) } fn title(&self) -> String { "Face Detector - Rust GUI".to_string() } fn update(&mut self, message: Message) -> Task { match message { Message::TabChanged(tab) => { self.current_tab = tab; Task::none() } Message::OpenImageDialog => { self.status_message = "Opening file dialog...".to_string(); Task::perform( async { FileDialog::new() .add_filter("Images", &["jpg", "jpeg", "png", "bmp", "tiff", "webp"]) .pick_file() }, Message::ImageSelected, ) } Message::ImageSelected(path) => { if let Some(path) = path { self.input_image = Some(path.clone()); self.status_message = format!("Selected: {}", path.display()); // Load image data for display Task::perform( async move { match std::fs::read(&path) { Ok(data) => Some(Arc::new(data)), Err(_) => None, } }, Message::ImageLoaded, ) } else { self.status_message = "No file selected".to_string(); Task::none() } } Message::OpenSecondImageDialog => Task::perform( async { FileDialog::new() .add_filter("Images", &["jpg", "jpeg", "png", "bmp", "tiff", "webp"]) .pick_file() }, Message::SecondImageSelected, ), Message::SecondImageSelected(path) => { if let Some(path) = path { self.second_image = Some(path.clone()); self.status_message = format!("Second image selected: {}", path.display()); // Load second image data for display Task::perform( async move { match std::fs::read(&path) { Ok(data) => Some(Arc::new(data)), Err(_) => None, } }, Message::SecondImageLoaded, ) } else { self.status_message = "No second image selected".to_string(); Task::none() } } Message::SaveOutputDialog => Task::perform( async { FileDialog::new() .add_filter("Images", &["jpg", "jpeg", "png"]) .save_file() }, Message::OutputPathSelected, ), Message::OutputPathSelected(path) => { if let Some(path) = path { self.output_path = Some(path.clone()); self.status_message = format!("Output will be saved to: {}", path.display()); } else { self.status_message = "No output path selected".to_string(); } Task::none() } Message::ThresholdChanged(value) => { self.threshold = value; Task::none() } Message::NmsThresholdChanged(value) => { self.nms_threshold = value; Task::none() } Message::ExecutorChanged(executor_type) => { self.executor_type = executor_type; Task::none() } Message::DetectFaces => { if let Some(input_path) = &self.input_image { self.is_processing = true; self.progress = 0.0; self.status_message = "Detecting faces...".to_string(); let input_path = input_path.clone(); let output_path = self.output_path.clone(); let threshold = self.threshold; let nms_threshold = self.nms_threshold; let executor_type = self.executor_type.clone(); Task::perform( async move { FaceDetectionBridge::detect_faces( input_path, output_path, threshold, nms_threshold, executor_type, ) .await }, Message::DetectionComplete, ) } else { self.status_message = "Please select an image first".to_string(); Task::none() } } Message::CompareFaces => { if let (Some(image1), Some(image2)) = (&self.input_image, &self.second_image) { self.is_processing = true; self.progress = 0.0; self.status_message = "Comparing faces...".to_string(); let image1 = image1.clone(); let image2 = image2.clone(); let threshold = self.threshold; let nms_threshold = self.nms_threshold; let executor_type = self.executor_type.clone(); Task::perform( async move { FaceDetectionBridge::compare_faces( image1, image2, threshold, nms_threshold, executor_type, ) .await }, Message::ComparisonComplete, ) } else { self.status_message = "Please select both images for comparison".to_string(); Task::none() } } Message::ClearResults => { 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() } Message::DetectionComplete(result) => { self.is_processing = false; self.progress = 100.0; match &result { DetectionResult::Success { faces_count, processing_time, processed_image, .. } => { self.status_message = format!( "Detection complete! Found {} faces in {:.2}s", faces_count, processing_time ); // Update processed image if available if let Some(image_data) = processed_image { self.processed_image_handle = Some(image::Handle::from_bytes(image_data.clone())); } } DetectionResult::Error(error) => { self.status_message = format!("Detection failed: {}", error); } } self.detection_result = Some(result); Task::none() } Message::ComparisonComplete(result) => { self.is_processing = false; self.progress = 100.0; match &result { ComparisonResult::Success { best_similarity, processing_time, image1_face_rois, image2_face_rois, .. } => { let interpretation = if *best_similarity > 0.8 { "Very likely the same person" } else if *best_similarity > 0.6 { "Possibly the same person" } else if *best_similarity > 0.4 { "Unlikely to be the same person" } else { "Very unlikely to be the same person" }; self.status_message = format!( "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); } } self.comparison_result = Some(result); 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() } Message::ImageLoaded(data) => { if let Some(image_data) = data { self.current_image_handle = Some(image::Handle::from_bytes(image_data.as_ref().clone())); self.status_message = "Image loaded successfully".to_string(); } else { self.status_message = "Failed to load image".to_string(); } Task::none() } Message::SecondImageLoaded(data) => { if let Some(image_data) = data { self.second_image_handle = Some(image::Handle::from_bytes(image_data.as_ref().clone())); self.status_message = "Second image loaded successfully".to_string(); } else { self.status_message = "Failed to load second image".to_string(); } Task::none() } Message::ProcessedImageUpdated(data) => { if let Some(image_data) = data { self.processed_image_handle = Some(image::Handle::from_bytes(image_data)); } Task::none() } } } fn view(&self) -> Element<'_, Message> { let tabs = row![ button("Detection") .on_press(Message::TabChanged(Tab::Detection)) .style(if self.current_tab == Tab::Detection { button::primary } else { button::secondary }), button("Comparison") .on_press(Message::TabChanged(Tab::Comparison)) .style(if self.current_tab == Tab::Comparison { button::primary } else { button::secondary }), button("Settings") .on_press(Message::TabChanged(Tab::Settings)) .style(if self.current_tab == Tab::Settings { button::primary } else { button::secondary }), ] .spacing(10) .padding(10); let content = match self.current_tab { Tab::Detection => self.detection_view(), Tab::Comparison => self.comparison_view(), Tab::Settings => self.settings_view(), }; let status_bar = container( row![ text(&self.status_message), Space::with_width(Length::Fill), if self.is_processing { Element::from(progress_bar(0.0..=100.0, self.progress)) } else { Space::with_width(Length::Shrink).into() } ] .align_y(Alignment::Center) .spacing(10), ) .padding(10) .style(container::bordered_box); column![tabs, content, status_bar].into() } } impl FaceDetectorApp { fn detection_view(&self) -> Element<'_, Message> { let file_section = column![ text("Input Image").size(18), row![ button("Select Image").on_press(Message::OpenImageDialog), text( self.input_image .as_ref() .map(|p| p .file_name() .unwrap_or_default() .to_string_lossy() .to_string()) .unwrap_or_else(|| "No image selected".to_string()) ), ] .spacing(10) .align_y(Alignment::Center), row![ button("Output Path").on_press(Message::SaveOutputDialog), text( self.output_path .as_ref() .map(|p| p .file_name() .unwrap_or_default() .to_string_lossy() .to_string()) .unwrap_or_else(|| "Auto-generate".to_string()) ), ] .spacing(10) .align_y(Alignment::Center), ] .spacing(10); // Image display section let image_section = if let Some(ref handle) = self.current_image_handle { let original_image = column![ text("Original Image").size(16), container( image(handle.clone()) .width(400) .height(300) .content_fit(iced::ContentFit::ScaleDown) ) .style(container::bordered_box) .padding(5), ] .spacing(5) .align_x(Alignment::Center); let processed_section = if let Some(ref processed_handle) = self.processed_image_handle { column![ text("Detected Faces").size(16), container( image(processed_handle.clone()) .width(400) .height(300) .content_fit(iced::ContentFit::ScaleDown) ) .style(container::bordered_box) .padding(5), ] .spacing(5) .align_x(Alignment::Center) } else { column![ text("Detected Faces").size(16), container( text("Process image to see results").style(|_theme| text::Style { color: Some(iced::Color::from_rgb(0.6, 0.6, 0.6)), }) ) .width(400) .height(300) .style(container::bordered_box) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill), ] .spacing(5) .align_x(Alignment::Center) }; row![original_image, processed_section] .spacing(20) .align_y(Alignment::Start) } else { row![ container( text("Select an image to display").style(|_theme| text::Style { color: Some(iced::Color::from_rgb(0.6, 0.6, 0.6)), }) ) .width(400) .height(300) .style(container::bordered_box) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill) ] }; let controls = column![ text("Detection Parameters").size(18), row![ text("Threshold:"), slider(0.1..=1.0, self.threshold, Message::ThresholdChanged).step(0.01), text(format!("{:.2}", self.threshold)), ] .spacing(10) .align_y(Alignment::Center), row![ text("NMS Threshold:"), slider(0.1..=1.0, self.nms_threshold, Message::NmsThresholdChanged).step(0.01), text(format!("{:.2}", self.nms_threshold)), ] .spacing(10) .align_y(Alignment::Center), row![ button("Detect Faces") .on_press(Message::DetectFaces) .style(button::primary), button("Clear Results").on_press(Message::ClearResults), ] .spacing(10), ] .spacing(10); let results = if let Some(result) = &self.detection_result { match result { DetectionResult::Success { faces_count, processing_time, .. } => column![ text("Detection Results").size(18), text(format!("Faces detected: {}", faces_count)), text(format!("Processing time: {:.2}s", processing_time)), ] .spacing(5), DetectionResult::Error(error) => column![ text("Detection Results").size(18), text(format!("Error: {}", error)).style(text::danger), ] .spacing(5), } } else { column![text("No results yet").style(|_theme| text::Style { color: Some(iced::Color::from_rgb(0.6, 0.6, 0.6)), })] }; column![file_section, image_section, controls, results] .spacing(20) .padding(20) .into() } fn comparison_view(&self) -> Element<'_, Message> { let file_section = column![ text("Image Comparison").size(18), row![ button("Select First Image").on_press(Message::OpenImageDialog), text( self.input_image .as_ref() .map(|p| p .file_name() .unwrap_or_default() .to_string_lossy() .to_string()) .unwrap_or_else(|| "No image selected".to_string()) ), ] .spacing(10) .align_y(Alignment::Center), row![ button("Select Second Image").on_press(Message::OpenSecondImageDialog), text( self.second_image .as_ref() .map(|p| p .file_name() .unwrap_or_default() .to_string_lossy() .to_string()) .unwrap_or_else(|| "No image selected".to_string()) ), ] .spacing(10) .align_y(Alignment::Center), ] .spacing(10); // Image comparison display section let comparison_image_section = { let first_image = if let Some(ref handle) = self.current_image_handle { column![ text("First Image").size(16), container( image(handle.clone()) .width(350) .height(250) .content_fit(iced::ContentFit::ScaleDown) ) .style(container::bordered_box) .padding(5), ] .spacing(5) .align_x(Alignment::Center) } else { column![ text("First Image").size(16), container(text("Select first image").style(|_theme| text::Style { color: Some(iced::Color::from_rgb(0.6, 0.6, 0.6)), })) .width(350) .height(250) .style(container::bordered_box) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill), ] .spacing(5) .align_x(Alignment::Center) }; let second_image = if let Some(ref handle) = self.second_image_handle { column![ text("Second Image").size(16), container( image(handle.clone()) .width(350) .height(250) .content_fit(iced::ContentFit::ScaleDown) ) .style(container::bordered_box) .padding(5), ] .spacing(5) .align_x(Alignment::Center) } else { column![ text("Second Image").size(16), container(text("Select second image").style(|_theme| text::Style { color: Some(iced::Color::from_rgb(0.6, 0.6, 0.6)), })) .width(350) .height(250) .style(container::bordered_box) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill), ] .spacing(5) .align_x(Alignment::Center) }; row![first_image, second_image] .spacing(20) .align_y(Alignment::Start) }; let controls = column![ text("Comparison Parameters").size(18), row![ text("Threshold:"), slider(0.1..=1.0, self.threshold, Message::ThresholdChanged).step(0.01), text(format!("{:.2}", self.threshold)), ] .spacing(10) .align_y(Alignment::Center), row![ text("NMS Threshold:"), slider(0.1..=1.0, self.nms_threshold, Message::NmsThresholdChanged).step(0.01), text(format!("{:.2}", self.nms_threshold)), ] .spacing(10) .align_y(Alignment::Center), button("Compare Faces") .on_press(Message::CompareFaces) .style(button::primary), ] .spacing(10); let results = if let Some(result) = &self.comparison_result { match result { ComparisonResult::Success { image1_faces, image2_faces, image1_face_rois: _, image2_face_rois: _, best_similarity, processing_time, } => { let interpretation = if *best_similarity > 0.8 { ( "Very likely the same person", iced::Color::from_rgb(0.2, 0.8, 0.2), ) } else if *best_similarity > 0.6 { ( "Possibly the same person", iced::Color::from_rgb(0.8, 0.8, 0.2), ) } else if *best_similarity > 0.4 { ( "Unlikely to be the same person", iced::Color::from_rgb(0.8, 0.6, 0.2), ) } else { ( "Very unlikely to be the same person", iced::Color::from_rgb(0.8, 0.2, 0.2), ) }; let mut result_column = column![ text("Comparison Results").size(18), text(format!("First image faces: {}", image1_faces)), text(format!("Second image faces: {}", image2_faces)), text(format!("Best similarity: {:.3}", best_similarity)), text(interpretation.0).style(move |_theme| text::Style { color: Some(interpretation.1), }), 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) } 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), text(format!("Error: {}", error)).style(text::danger), ] .spacing(5), } } else { column![ text("No comparison results yet").style(|_theme| text::Style { color: Some(iced::Color::from_rgb(0.6, 0.6, 0.6)), }) ] }; scrollable( column![file_section, comparison_image_section, controls, results] .spacing(20) .padding(20), ) .into() } fn settings_view(&self) -> Element<'_, Message> { #[allow(unused_mut)] let mut executor_options = vec![ExecutorType::MnnCpu, ExecutorType::OnnxCpu]; #[cfg(feature = "mnn-metal")] executor_options.push(ExecutorType::MnnMetal); #[cfg(feature = "mnn-coreml")] executor_options.push(ExecutorType::MnnCoreML); #[cfg(feature = "ort-cuda")] executor_options.push(ExecutorType::OrtCuda); container( column![ text("Model Settings").size(18), row![ text("Execution Backend:"), pick_list( executor_options, Some(self.executor_type.clone()), Message::ExecutorChanged, ), ] .spacing(10) .align_y(Alignment::Center), text("Detection Thresholds").size(18), row![ text("Detection Threshold:"), slider(0.1..=1.0, self.threshold, Message::ThresholdChanged).step(0.01), text(format!("{:.2}", self.threshold)), ] .spacing(10) .align_y(Alignment::Center), row![ text("NMS Threshold:"), slider(0.1..=1.0, self.nms_threshold, Message::NmsThresholdChanged).step(0.01), text(format!("{:.2}", self.nms_threshold)), ] .spacing(10) .align_y(Alignment::Center), text("About").size(18), text("Face Detection and Embedding - Rust GUI"), text("Built with iced.rs and your face detection engine"), ] .spacing(15) .padding(20), ) .height(Length::Shrink) .into() } } impl std::fmt::Display for ExecutorType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ExecutorType::MnnCpu => write!(f, "MNN (CPU)"), #[cfg(feature = "mnn-metal")] ExecutorType::MnnMetal => write!(f, "MNN (Metal)"), #[cfg(feature = "mnn-coreml")] ExecutorType::MnnCoreML => write!(f, "MNN (CoreML)"), ExecutorType::OnnxCpu => write!(f, "ONNX (CPU)"), #[cfg(feature = "ort-cuda")] ExecutorType::OrtCuda => write!(f, "ONNX (CUDA)"), } } } // 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 { let settings = Settings { antialiasing: true, ..Default::default() }; iced::application( "Face Detector", FaceDetectorApp::update, FaceDetectorApp::view, ) .settings(settings) .run_with(FaceDetectorApp::new) }