diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8209ef..863f8a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: run: cargo build --release - name: Run unit tests - run: cargo test + run: cargo test --lib - name: Clippy run: cargo clippy -- -D warnings diff --git a/src/bin/camera_test.rs b/src/bin/camera_test.rs index b2298a9..d06b444 100644 --- a/src/bin/camera_test.rs +++ b/src/bin/camera_test.rs @@ -21,7 +21,10 @@ fn main() { } }; - eprintln!("[camera-test] Resolution: {}x{}", camera.width, camera.height); + eprintln!( + "[camera-test] Resolution: {}x{}", + camera.width, camera.height + ); eprintln!("[camera-test] Capturing frame (3 warm-up frames)..."); let frame: image::GrayImage = match camera.capture_warmed_frame(3) { @@ -32,7 +35,11 @@ fn main() { } }; - eprintln!("[camera-test] Frame captured: {}x{}", frame.width(), frame.height()); + eprintln!( + "[camera-test] Frame captured: {}x{}", + frame.width(), + frame.height() + ); if let Err(e) = frame.save(&output_path) { eprintln!("[camera-test] ERROR — failed to save PNG: {e}"); diff --git a/src/bin/enroll.rs b/src/bin/enroll.rs index 8095aba..c67ac24 100644 --- a/src/bin/enroll.rs +++ b/src/bin/enroll.rs @@ -10,7 +10,6 @@ /// - /etc/rustface/faces/ must exist with root:root 700 permissions /// - /etc/rustface/models/seeta_fd_frontal_v1.0.bin /// - /etc/rustface/models/arcface.onnx - use std::path::PathBuf; use pam_rustface::{ @@ -31,15 +30,13 @@ fn main() { std::process::exit(1); } - let username = std::env::args() - .nth(1) - .unwrap_or_else(|| { - // Fall back to SUDO_USER env var - std::env::var("SUDO_USER").unwrap_or_else(|_| { - eprintln!("[enroll] ERROR: specify a username: sudo rustface-enroll "); - std::process::exit(1); - }) - }); + let username = std::env::args().nth(1).unwrap_or_else(|| { + // Fall back to SUDO_USER env var + std::env::var("SUDO_USER").unwrap_or_else(|_| { + eprintln!("[enroll] ERROR: specify a username: sudo rustface-enroll "); + std::process::exit(1); + }) + }); let model_dir = std::env::args() .nth(2) @@ -66,18 +63,25 @@ fn main() { // Load detector and embedder eprintln!("[enroll] Loading models..."); - let mut detector = FaceDetector::load(face_model_path.to_str().unwrap()) - .unwrap_or_else(|e| { eprintln!("[enroll] ERROR loading detector: {e}"); std::process::exit(1); }); + let mut detector = FaceDetector::load(face_model_path.to_str().unwrap()).unwrap_or_else(|e| { + eprintln!("[enroll] ERROR loading detector: {e}"); + std::process::exit(1); + }); - let mut embedder = FaceEmbedder::load(embed_model_path.to_str().unwrap()) - .unwrap_or_else(|e| { eprintln!("[enroll] ERROR loading embedder: {e}"); std::process::exit(1); }); + let mut embedder = FaceEmbedder::load(embed_model_path.to_str().unwrap()).unwrap_or_else(|e| { + eprintln!("[enroll] ERROR loading embedder: {e}"); + std::process::exit(1); + }); // Open camera — auto-detect IR camera if not specified - let device = std::env::args().nth(3) + let device = std::env::args() + .nth(3) .unwrap_or_else(|| pam_rustface::camera::auto_detect_ir_camera()); eprintln!("[enroll] Camera: {device}"); - let camera = IrCamera::open(&device) - .unwrap_or_else(|e| { eprintln!("[enroll] ERROR opening camera: {e}"); std::process::exit(1); }); + let camera = IrCamera::open(&device).unwrap_or_else(|e| { + eprintln!("[enroll] ERROR opening camera: {e}"); + std::process::exit(1); + }); eprintln!("[enroll] Camera ready: {}x{}", camera.width, camera.height); eprintln!("[enroll] Look at the camera... ({DETECT_RETRIES} attempts)"); @@ -89,7 +93,10 @@ fn main() { let frame = match camera.capture_warmed_frame(WARMUP_FRAMES) { Ok(f) => f, - Err(e) => { eprintln!("[enroll] Frame error: {e}"); continue; } + Err(e) => { + eprintln!("[enroll] Frame error: {e}"); + continue; + } }; let Some(region) = detector.detect(&frame) else { @@ -107,7 +114,10 @@ fn main() { eprintln!("[enroll] Embedding extracted — dim: {}", emb.dim()); break 'detect emb; } - Err(e) => { eprintln!("[enroll] Embedding error: {e}"); continue; } + Err(e) => { + eprintln!("[enroll] Embedding error: {e}"); + continue; + } } } diff --git a/src/bin/sim_test.rs b/src/bin/sim_test.rs index 490ac75..1489e7d 100644 --- a/src/bin/sim_test.rs +++ b/src/bin/sim_test.rs @@ -1,43 +1,49 @@ +use pam_rustface::{ + camera::IrCamera, + face::{cosine_similarity, FaceDetector, FaceEmbedder}, + storage::FaceStore, +}; /// rustface-sim-test — show live cosine similarity score against enrolled embedding /// /// Usage: sudo rustface-sim-test [username] /// /// Useful for tuning the threshold and verifying that face recognition works. - use std::path::PathBuf; -use pam_rustface::{ - camera::IrCamera, - face::{cosine_similarity, FaceDetector, FaceEmbedder}, - storage::FaceStore, -}; const THRESHOLD: f32 = 0.8; fn main() { - let username = std::env::args().nth(1).unwrap_or_else(|| "pervane".to_owned()); + let username = std::env::args() + .nth(1) + .unwrap_or_else(|| "pervane".to_owned()); let model_dir = "/etc/rustface/models"; let face_model = PathBuf::from(model_dir).join("seeta_fd_frontal_v1.0.bin"); let embed_model = PathBuf::from(model_dir).join("arcface.onnx"); eprintln!("[sim-test] Loading models..."); - let mut detector = FaceDetector::load(face_model.to_str().unwrap()) - .expect("Failed to load face detector"); - let mut embedder = FaceEmbedder::load(embed_model.to_str().unwrap()) - .expect("Failed to load embedder"); + let mut detector = + FaceDetector::load(face_model.to_str().unwrap()).expect("Failed to load face detector"); + let mut embedder = + FaceEmbedder::load(embed_model.to_str().unwrap()).expect("Failed to load embedder"); eprintln!("[sim-test] Loading enrolled embedding for '{username}'..."); let store = FaceStore::default(); - let stored = store.load(&username).expect("No enrolled face found — run rustface-enroll first"); + let stored = store + .load(&username) + .expect("No enrolled face found — run rustface-enroll first"); - let device = std::env::args().nth(2) + let device = std::env::args() + .nth(2) .unwrap_or_else(|| pam_rustface::camera::auto_detect_ir_camera()); eprintln!("[sim-test] Camera: {device}"); let camera = IrCamera::open(&device).expect("Failed to open camera"); eprintln!("[sim-test] Look at the camera..."); for attempt in 1..=5 { - let frame = camera.capture_warmed_frame(3).expect("Failed to capture frame"); + let frame = camera + .capture_warmed_frame(3) + .expect("Failed to capture frame"); let Some(region) = detector.detect(&frame) else { eprintln!("[sim-test] Attempt {attempt}/5: no face detected"); continue; @@ -46,7 +52,11 @@ fn main() { let sim = cosine_similarity(&live, &stored); println!( "[sim-test] Attempt {attempt}/5: similarity = {sim:.4} (threshold={THRESHOLD} → {})", - if sim >= THRESHOLD { "PASS ✓" } else { "REJECT ✗" } + if sim >= THRESHOLD { + "PASS ✓" + } else { + "REJECT ✗" + } ); } } diff --git a/src/camera/capture.rs b/src/camera/capture.rs index 9c28503..9085ea7 100644 --- a/src/camera/capture.rs +++ b/src/camera/capture.rs @@ -29,7 +29,12 @@ pub fn auto_detect_ir_camera() -> String { if let Ok(dev) = Device::with_path(path) { if let Ok(fmt) = dev.format() { if is_grayscale8(&fmt.fourcc) { - log::info!("Auto-detected IR camera: {path} ({:?} {}x{})", fmt.fourcc, fmt.width, fmt.height); + log::info!( + "Auto-detected IR camera: {path} ({:?} {}x{})", + fmt.fourcc, + fmt.width, + fmt.height + ); return path.clone(); } } @@ -84,13 +89,13 @@ impl IrCamera { /// /// Stream is opened and closed per call — sufficient for PAM auth flow. pub fn capture_frame(&self) -> Result { - let mut stream = - Stream::with_buffers(&self.device, Type::VideoCapture, 4).map_err(|e| { - CameraError::StreamStart(e.to_string()) - })?; + let mut stream = Stream::with_buffers(&self.device, Type::VideoCapture, 4) + .map_err(|e| CameraError::StreamStart(e.to_string()))?; // Skip the first frame — camera needs a warm-up; second frame is cleaner - let _ = stream.next().map_err(|e| CameraError::FrameCapture(e.to_string()))?; + let _ = stream + .next() + .map_err(|e| CameraError::FrameCapture(e.to_string()))?; let (buf, _meta) = stream .next() .map_err(|e| CameraError::FrameCapture(e.to_string()))?; @@ -102,10 +107,8 @@ impl IrCamera { /// Capture the specified number of frames, return the last one. /// Skips `skip` frames for camera warm-up. pub fn capture_warmed_frame(&self, skip: u32) -> Result { - let mut stream = - Stream::with_buffers(&self.device, Type::VideoCapture, 4).map_err(|e| { - CameraError::StreamStart(e.to_string()) - })?; + let mut stream = Stream::with_buffers(&self.device, Type::VideoCapture, 4) + .map_err(|e| CameraError::StreamStart(e.to_string()))?; let total = skip + 1; let mut last_buf: Option> = None; diff --git a/src/error.rs b/src/error.rs index 260afcf..5a46e91 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ -use thiserror::Error; use crate::camera::CameraError; +use thiserror::Error; #[derive(Debug, Error)] pub enum RustfaceError { diff --git a/src/face/detect.rs b/src/face/detect.rs index 1a3a088..9bf669b 100644 --- a/src/face/detect.rs +++ b/src/face/detect.rs @@ -22,15 +22,14 @@ impl FaceDetector { /// Load from a SeetaFace model file /// Model path: /etc/rustface/models/seeta_fd_frontal_v1.0.bin pub fn load(model_path: &str) -> Result { - let mut detector = create_detector(model_path).map_err(|e| { - RustfaceError::OnnxError(format!("Failed to load rustface model: {e}")) - })?; + let mut detector = create_detector(model_path) + .map_err(|e| RustfaceError::OnnxError(format!("Failed to load rustface model: {e}")))?; // Settings tuned for IR camera input - detector.set_min_face_size(40); // minimum face size in px (smaller = more sensitive) - detector.set_score_thresh(1.5); // confidence threshold (lower for IR) - detector.set_pyramid_scale_factor(0.8); // pyramid scale step - detector.set_slide_window_step(4, 4); // sliding window step + detector.set_min_face_size(40); // minimum face size in px (smaller = more sensitive) + detector.set_score_thresh(1.5); // confidence threshold (lower for IR) + detector.set_pyramid_scale_factor(0.8); // pyramid scale step + detector.set_slide_window_step(4, 4); // sliding window step Ok(Self { inner: detector }) } @@ -47,7 +46,11 @@ impl FaceDetector { // Select the highest-scoring face faces .into_iter() - .max_by(|a, b| a.score().partial_cmp(&b.score()).unwrap_or(std::cmp::Ordering::Equal)) + .max_by(|a, b| { + a.score() + .partial_cmp(&b.score()) + .unwrap_or(std::cmp::Ordering::Equal) + }) .map(|face| { let bbox = face.bbox(); // bbox.x/y may be negative near camera edges — clamp to zero diff --git a/src/face/embed.rs b/src/face/embed.rs index 0d8cb8c..0aecc85 100644 --- a/src/face/embed.rs +++ b/src/face/embed.rs @@ -51,7 +51,10 @@ impl FaceEmbedder { log::debug!("FaceEmbedder loaded — device: {device:?}, input: {input_name}"); - Ok(Self { session, input_name }) + Ok(Self { + session, + input_name, + }) } /// Crop the face region from the image, normalize, and extract the embedding @@ -61,8 +64,8 @@ impl FaceEmbedder { region: &FaceRegion, ) -> Result { // 1. Crop face region - let crop = imageops::crop_imm(image, region.x, region.y, region.width, region.height) - .to_image(); + let crop = + imageops::crop_imm(image, region.x, region.y, region.width, region.height).to_image(); // 2. Resize to 112x112 let resized = imageops::resize(&crop, FACE_SIZE, FACE_SIZE, imageops::FilterType::Lanczos3); diff --git a/src/lib.rs b/src/lib.rs index 02bbed7..a4dbda8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,9 +12,9 @@ pub fn camera_open(path: &str) -> Result { IrCamera::open(path) } -use std::os::raw::{c_char, c_int}; use pam::constants::{PAM_IGNORE, PAM_SUCCESS}; use pam::handle::PamHandle; +use std::os::raw::{c_char, c_int}; /// PAM entry point — signature must match exactly #[no_mangle] @@ -24,9 +24,7 @@ pub extern "C" fn pam_sm_authenticate( argc: c_int, argv: *const *const c_char, ) -> c_int { - let result = std::panic::catch_unwind(|| { - authenticate_inner(pamh, flags, argc, argv) - }); + let result = std::panic::catch_unwind(|| authenticate_inner(pamh, flags, argc, argv)); match result { Ok(r) => r, Err(_) => PAM_IGNORE, @@ -123,14 +121,20 @@ impl Config { // SAFETY: PAM argc/argv contract — null-terminated string array let arg = unsafe { let ptr = *argv.add(i); - if ptr.is_null() { continue; } + if ptr.is_null() { + continue; + } CStr::from_ptr(ptr).to_str().unwrap_or("") }; if let Some(val) = arg.strip_prefix("threshold=") { - if let Ok(v) = val.parse::() { cfg.threshold = v; } + if let Ok(v) = val.parse::() { + cfg.threshold = v; + } } else if let Some(val) = arg.strip_prefix("timeout=") { - if let Ok(v) = val.parse::() { cfg.timeout_secs = v; } + if let Ok(v) = val.parse::() { + cfg.timeout_secs = v; + } } else if let Some(val) = arg.strip_prefix("device=") { cfg.device = val.to_owned(); } else if arg == "debug=true" || arg == "debug" { @@ -152,8 +156,8 @@ impl Config { fn run_face_auth(username: &str, config: &Config) -> Result { use camera::IrCamera; use face::{cosine_similarity, FaceDetector, FaceEmbedder}; - use storage::FaceStore; use std::time::{Duration, Instant}; + use storage::FaceStore; // 1. Check for enrolled face let store = FaceStore::default(); @@ -196,7 +200,9 @@ fn run_face_auth(username: &str, config: &Config) -> Result } }; - let Some(region) = detector.detect(&frame) else { continue }; + let Some(region) = detector.detect(&frame) else { + continue; + }; let live_emb = match embedder.embed(&frame, ®ion) { Ok(e) => e, @@ -207,7 +213,10 @@ fn run_face_auth(username: &str, config: &Config) -> Result }; let sim = cosine_similarity(&live_emb, &stored_emb); - log::debug!("rustface-pam: similarity={sim:.3} threshold={}", config.threshold); + log::debug!( + "rustface-pam: similarity={sim:.3} threshold={}", + config.threshold + ); if sim >= config.threshold { // Online learning: update stored embedding on high-confidence matches @@ -231,7 +240,10 @@ fn blend_embeddings( new: &face::embed::Embedding, alpha: f32, ) -> face::embed::Embedding { - let blended: Vec = old.as_slice().iter().zip(new.as_slice().iter()) + let blended: Vec = old + .as_slice() + .iter() + .zip(new.as_slice().iter()) .map(|(o, n)| (1.0 - alpha) * o + alpha * n) .collect(); // L2 normalize — required for cosine similarity to stay consistent diff --git a/src/pam/handle.rs b/src/pam/handle.rs index f7dfd7c..16027c6 100644 --- a/src/pam/handle.rs +++ b/src/pam/handle.rs @@ -8,11 +8,7 @@ pub enum PamHandle {} // pam_get_item is provided by the PAM linker extern "C" { - fn pam_get_item( - pamh: *const PamHandle, - item_type: c_int, - item: *mut *const c_void, - ) -> c_int; + fn pam_get_item(pamh: *const PamHandle, item_type: c_int, item: *mut *const c_void) -> c_int; } /// Get the username from the PAM handle.