//! Authentication Module //! //! Handles the authentication flow: capture frames, detect faces, extract embeddings, //! and match against stored templates. use linux_hello_common::{Config, FaceTemplate, Result, TemplateStore}; use tracing::{debug, info, warn}; use crate::camera::PixelFormat; use crate::detection::detect_face_simple; use crate::embedding::{EmbeddingExtractor, PlaceholderEmbeddingExtractor}; use crate::matching::{average_embeddings, match_template}; use image::GrayImage; /// Authentication service #[derive(Clone)] pub struct AuthService { config: Config, template_store_path: std::path::PathBuf, embedding_extractor: PlaceholderEmbeddingExtractor, } impl AuthService { /// Create a new authentication service pub fn new(config: Config) -> Self { let template_store_path = TemplateStore::default_path(); let embedding_extractor = PlaceholderEmbeddingExtractor::new(PlaceholderEmbeddingExtractor::default_dimension()); Self { config, template_store_path, embedding_extractor, } } /// Initialize the authentication service pub fn initialize(&self) -> Result<()> { let template_store = TemplateStore::new(&self.template_store_path); template_store.initialize()?; Ok(()) } fn template_store(&self) -> TemplateStore { TemplateStore::new(&self.template_store_path) } /// Authenticate a user pub async fn authenticate(&self, user: &str) -> Result { info!("Authenticating user: {}", user); let template_store = self.template_store(); // Check if user is enrolled if !template_store.is_enrolled(user) { warn!("User {} is not enrolled", user); return Err(linux_hello_common::Error::UserNotEnrolled(user.to_string())); } // Load templates let templates = template_store.load_all(user)?; if templates.is_empty() { return Err(linux_hello_common::Error::UserNotEnrolled(user.to_string())); } // Capture and process frames let embedding = self.capture_and_extract_embedding().await?; // Match against templates let result = match_template( &embedding, &templates, self.config.embedding.distance_threshold, ); debug!( "Match result: matched={}, similarity={:.3}, threshold={:.3}", result.matched, result.best_similarity, result.distance_threshold ); Ok(result.matched) } /// Enroll a user pub async fn enroll(&self, user: &str, label: &str, frame_count: u32) -> Result<()> { info!("Enrolling user: {} with label: {}", user, label); // Capture multiple frames let mut embeddings = Vec::new(); for i in 0..frame_count { debug!("Capturing frame {}/{}", i + 1, frame_count); match self.capture_and_extract_embedding().await { Ok(emb) => { embeddings.push(emb); } Err(e) => { warn!("Failed to capture frame {}: {}", i + 1, e); // Continue with other frames } } } if embeddings.is_empty() { return Err(linux_hello_common::Error::Detection( "Failed to capture any valid frames".to_string(), )); } // Average embeddings let averaged_embedding = average_embeddings(&embeddings)?; // Create template let template = FaceTemplate { user: user.to_string(), label: label.to_string(), embedding: averaged_embedding, enrolled_at: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(), frame_count: embeddings.len() as u32, }; // Store template let template_store = self.template_store(); template_store.store(&template)?; info!("User {} enrolled successfully with {} frames", user, template.frame_count); Ok(()) } /// Capture a frame and extract embedding async fn capture_and_extract_embedding(&self) -> Result> { // Open camera #[cfg(target_os = "linux")] use crate::camera::{enumerate_cameras, Camera}; #[cfg(target_os = "linux")] let device_path = if self.config.camera.device == "auto" { let cameras = enumerate_cameras()?; let ir_cam = cameras.iter().find(|c| c.is_ir); let cam = ir_cam.or(cameras.first()); match cam { Some(c) => c.device_path.clone(), None => return Err(linux_hello_common::Error::NoCameraFound), } } else { self.config.camera.device.clone() }; #[cfg(not(target_os = "linux"))] let device_path = "mock_cam_0".to_string(); let mut camera = Camera::open(&device_path)?; // Capture frame let frame = camera.capture_frame()?; debug!("Captured frame: {}x{}, format: {:?}", frame.width, frame.height, frame.format); // Convert frame to grayscale image let gray_image = match frame.format { PixelFormat::Grey => { GrayImage::from_raw(frame.width, frame.height, frame.data) .ok_or_else(|| linux_hello_common::Error::Detection( "Failed to create grayscale image".to_string(), ))? } PixelFormat::Yuyv => { // Simple YUYV to grayscale conversion (take Y channel) let mut gray_data = Vec::with_capacity((frame.width * frame.height) as usize); for chunk in frame.data.chunks_exact(2) { gray_data.push(chunk[0]); // Y component } GrayImage::from_raw(frame.width, frame.height, gray_data) .ok_or_else(|| linux_hello_common::Error::Detection( "Failed to create grayscale from YUYV".to_string(), ))? } PixelFormat::Mjpeg => { // Decode MJPEG (JPEG) to image, then convert to grayscale image::load_from_memory(&frame.data) .map_err(|e| linux_hello_common::Error::Detection( format!("Failed to decode MJPEG: {}", e) ))? .to_luma8() } _ => { return Err(linux_hello_common::Error::Detection(format!( "Unsupported pixel format: {:?}", frame.format ))); } }; // Detect face let face_detection = detect_face_simple(gray_image.as_raw(), frame.width, frame.height) .ok_or(linux_hello_common::Error::NoFaceDetected)?; // Extract face region let (x, y, w, h) = face_detection.to_pixels(frame.width, frame.height); let face_image = extract_face_region(&gray_image, x, y, w, h)?; // Extract embedding let embedding = self.embedding_extractor.extract(&face_image)?; Ok(embedding) } } /// Extract a face region from an image fn extract_face_region( image: &GrayImage, x: u32, y: u32, w: u32, h: u32, ) -> Result { let (img_width, img_height) = image.dimensions(); // Clamp coordinates to image bounds let x = x.min(img_width); let y = y.min(img_height); let w = w.min(img_width - x); let h = h.min(img_height - y); if w == 0 || h == 0 { return Err(linux_hello_common::Error::Detection( "Invalid face region".to_string(), )); } let mut face_data = Vec::with_capacity((w * h) as usize); for row in y..(y + h) { for col in x..(x + w) { let pixel = image.get_pixel(col, row); face_data.push(pixel[0]); } } GrayImage::from_raw(w, h, face_data) .ok_or_else(|| linux_hello_common::Error::Detection( "Failed to create face image".to_string(), )) }