Files
Linux-Hello/linux-hello-daemon/src/auth.rs

248 lines
8.1 KiB
Rust

//! 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<bool> {
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<Vec<f32>> {
// 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<GrayImage> {
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(),
))
}