feat: ONNX face detection, IR camera support, and PAM authentication
Wire up ONNX RetinaFace detector and MobileFaceNet embeddings in the CLI and auth pipeline. Add IR camera detection for Windows Hello-style "Integrated I" cameras and greyscale-only format heuristic. Add histogram normalization for underexposed IR frames from low-power emitters. - Add `onnx` feature flag to CLI crate forwarding to daemon - Wire ONNX detector into `detect` command with fallback to simple detector - Fix IR camera detection for Chicony "Integrated I" naming pattern - Add `normalize_if_dark()` for underexposed IR frames in auth pipeline - Load user config from ~/.config/linux-hello/ as fallback - Update systemd service for IR emitter integration and camera access - Add system installation script and ONNX runtime installer - Update .gitignore for local dev artifacts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -294,50 +294,100 @@ async fn cmd_detect(
|
||||
let gray = img.to_luma8();
|
||||
let (width, height) = gray.dimensions();
|
||||
|
||||
// Run simple detection (placeholder)
|
||||
use linux_hello_daemon::detection::detect_face_simple;
|
||||
let detection = detect_face_simple(gray.as_raw(), width, height);
|
||||
// Try ONNX detection first, fall back to simple detection
|
||||
#[cfg(feature = "onnx")]
|
||||
let detections = {
|
||||
let model_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("models");
|
||||
let detector_path = model_dir.join("retinaface.onnx");
|
||||
|
||||
match detection {
|
||||
Some(det) => {
|
||||
if detector_path.exists() {
|
||||
info!("Using ONNX RetinaFace detector");
|
||||
use linux_hello_daemon::onnx::OnnxFaceDetector;
|
||||
match OnnxFaceDetector::load(&detector_path) {
|
||||
Ok(mut detector) => {
|
||||
detector.set_confidence_threshold(0.5);
|
||||
match detector.detect(gray.as_raw(), width, height) {
|
||||
Ok(dets) => dets,
|
||||
Err(e) => {
|
||||
warn!("ONNX detection failed: {}, falling back to simple", e);
|
||||
linux_hello_daemon::detection::detect_face_simple(
|
||||
gray.as_raw(),
|
||||
width,
|
||||
height,
|
||||
)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to load ONNX model: {}, falling back to simple", e);
|
||||
linux_hello_daemon::detection::detect_face_simple(
|
||||
gray.as_raw(),
|
||||
width,
|
||||
height,
|
||||
)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("ONNX model not found at {:?}, using simple detection", detector_path);
|
||||
linux_hello_daemon::detection::detect_face_simple(
|
||||
gray.as_raw(),
|
||||
width,
|
||||
height,
|
||||
)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "onnx"))]
|
||||
let detections: Vec<linux_hello_daemon::FaceDetection> =
|
||||
linux_hello_daemon::detection::detect_face_simple(gray.as_raw(), width, height)
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
if detections.is_empty() {
|
||||
println!("No face detected");
|
||||
|
||||
if let Some(out_path) = output {
|
||||
img.save(out_path).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
println!("Image saved to: {} (no face detected)", out_path);
|
||||
}
|
||||
} else {
|
||||
let mut rgb_img = img.to_rgb8();
|
||||
|
||||
for (i, det) in detections.iter().enumerate() {
|
||||
let (x, y, w, h) = det.to_pixels(width, height);
|
||||
println!("Face detected:");
|
||||
println!("Face {} detected:", i + 1);
|
||||
println!(" Position: ({}, {})", x, y);
|
||||
println!(" Size: {}x{}", w, h);
|
||||
if show_scores {
|
||||
println!(" Confidence: {:.2}%", det.confidence * 100.0);
|
||||
}
|
||||
|
||||
if let Some(out_path) = output {
|
||||
info!("Saving annotated image to: {}", out_path);
|
||||
// Convert to RGB for drawing
|
||||
let mut rgb_img = img.to_rgb8();
|
||||
|
||||
// Draw bounding box (red color)
|
||||
draw_bounding_box(&mut rgb_img, x, y, w, h, [255, 0, 0]);
|
||||
|
||||
rgb_img.save(out_path).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
println!("Annotated image saved to: {}", out_path);
|
||||
}
|
||||
draw_bounding_box(&mut rgb_img, x, y, w, h, [255, 0, 0]);
|
||||
}
|
||||
None => {
|
||||
println!("No face detected");
|
||||
|
||||
if let Some(out_path) = output {
|
||||
// Save original image without annotations
|
||||
img.save(out_path).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
println!("Image saved to: {} (no face detected)", out_path);
|
||||
}
|
||||
if let Some(out_path) = output {
|
||||
info!("Saving annotated image to: {}", out_path);
|
||||
rgb_img.save(out_path).map_err(|e| {
|
||||
linux_hello_common::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
println!("Annotated image saved to: {}", out_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,8 +522,12 @@ async fn cmd_enroll(config: &Config, label: &str) -> Result<()> {
|
||||
println!("Label: {}", label);
|
||||
println!("Please look at the camera...");
|
||||
|
||||
// Create auth service
|
||||
let auth_service = AuthService::new(config.clone());
|
||||
// Create auth service with custom paths if specified
|
||||
let template_path = std::env::var("LINUX_HELLO_TEMPLATES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| linux_hello_common::TemplateStore::default_path());
|
||||
|
||||
let auth_service = AuthService::with_paths(config.clone(), template_path);
|
||||
auth_service.initialize()?;
|
||||
|
||||
// Enroll with 5 frames
|
||||
@@ -492,7 +546,11 @@ async fn cmd_enroll(config: &Config, label: &str) -> Result<()> {
|
||||
async fn cmd_list(_config: &Config) -> Result<()> {
|
||||
use linux_hello_common::TemplateStore;
|
||||
|
||||
let store = TemplateStore::new(TemplateStore::default_path());
|
||||
let template_path = std::env::var("LINUX_HELLO_TEMPLATES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| TemplateStore::default_path());
|
||||
|
||||
let store = TemplateStore::new(template_path);
|
||||
|
||||
let users = store.list_users()?;
|
||||
|
||||
@@ -529,7 +587,11 @@ async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result<
|
||||
.or_else(|_| std::env::var("USERNAME"))
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
|
||||
let store = TemplateStore::new(TemplateStore::default_path());
|
||||
let template_path = std::env::var("LINUX_HELLO_TEMPLATES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| TemplateStore::default_path());
|
||||
|
||||
let store = TemplateStore::new(template_path);
|
||||
|
||||
if all {
|
||||
store.remove_all(&user)?;
|
||||
@@ -556,8 +618,12 @@ async fn cmd_test(config: &Config, verbose: bool, _debug: bool) -> Result<()> {
|
||||
println!("Testing authentication for user: {}", user);
|
||||
println!("Please look at the camera...");
|
||||
|
||||
// Create auth service
|
||||
let auth_service = AuthService::new(config.clone());
|
||||
// Create auth service with custom paths if specified
|
||||
let template_path = std::env::var("LINUX_HELLO_TEMPLATES")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| linux_hello_common::TemplateStore::default_path());
|
||||
|
||||
let auth_service = AuthService::with_paths(config.clone(), template_path);
|
||||
auth_service.initialize()?;
|
||||
|
||||
match auth_service.authenticate(&user).await {
|
||||
|
||||
Reference in New Issue
Block a user