248 lines
8.1 KiB
Rust
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(),
|
|
))
|
|
}
|