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:
2026-04-02 15:04:52 +02:00
parent ac5c71c786
commit 8c478836d8
19 changed files with 2188 additions and 212 deletions

View File

@@ -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 {