Files
face-detector/src/faceembed/facenet.rs
uttarayan21 bfa389b497
Some checks failed
build / checks-matrix (push) Successful in 19m23s
build / codecov (push) Failing after 19m18s
docs / docs (push) Failing after 28m50s
build / checks-build (push) Has been cancelled
feat(compare): add face comparison functionality with cosine similarity
2025-08-21 17:34:07 +05:30

210 lines
6.0 KiB
Rust

pub mod mnn;
pub mod ort;
use crate::errors::*;
use error_stack::ResultExt;
use ndarray::{Array1, Array2, ArrayView3, ArrayView4};
use ndarray_math::{CosineSimilarity, EuclideanDistance};
/// Configuration for face embedding processing
#[derive(Debug, Clone, PartialEq)]
pub struct FaceEmbeddingConfig {
/// Input image width expected by the model
pub input_width: usize,
/// Input image height expected by the model
pub input_height: usize,
/// Whether to normalize embeddings to unit vectors
pub normalize: bool,
}
impl FaceEmbeddingConfig {
pub fn with_input_size(mut self, width: usize, height: usize) -> Self {
self.input_width = width;
self.input_height = height;
self
}
pub fn with_normalization(mut self, normalize: bool) -> Self {
self.normalize = normalize;
self
}
}
impl Default for FaceEmbeddingConfig {
fn default() -> Self {
Self {
input_width: 320,
input_height: 320,
normalize: false,
}
}
}
/// Represents a face embedding vector
#[derive(Debug, Clone, PartialEq)]
pub struct FaceEmbedding {
/// The embedding vector
pub vector: Array1<f32>,
/// Optional confidence score for the embedding quality
pub confidence: Option<f32>,
}
impl FaceEmbedding {
pub fn new(vector: Array1<f32>) -> Self {
Self {
vector,
confidence: None,
}
}
pub fn with_confidence(mut self, confidence: f32) -> Self {
self.confidence = Some(confidence);
self
}
/// Calculate cosine similarity with another embedding
pub fn cosine_similarity(&self, other: &FaceEmbedding) -> f32 {
self.vector.cosine_similarity(&other.vector).unwrap_or(0.0)
}
/// Calculate Euclidean distance with another embedding
pub fn euclidean_distance(&self, other: &FaceEmbedding) -> f32 {
self.vector
.euclidean_distance(other.vector.view())
.unwrap_or(f32::INFINITY)
}
/// Normalize the embedding vector to unit length
pub fn normalize(&mut self) {
let norm = self.vector.mapv(|x| x * x).sum().sqrt();
if norm > 0.0 {
self.vector.mapv_inplace(|x| x / norm);
}
}
/// Get the dimensionality of the embedding
pub fn dimension(&self) -> usize {
self.vector.len()
}
}
/// Raw model outputs that can be converted to embeddings
pub trait IntoEmbeddings {
fn into_embeddings(self, config: &FaceEmbeddingConfig) -> Result<Vec<FaceEmbedding>>;
}
impl IntoEmbeddings for Array2<f32> {
fn into_embeddings(self, config: &FaceEmbeddingConfig) -> Result<Vec<FaceEmbedding>> {
let mut embeddings = Vec::new();
for row in self.rows() {
let mut vector = row.to_owned();
if config.normalize {
let norm = vector.mapv(|x| x * x).sum().sqrt();
if norm > 0.0 {
vector.mapv_inplace(|x| x / norm);
}
}
embeddings.push(FaceEmbedding::new(vector));
}
Ok(embeddings)
}
}
/// Common trait for face embedding backends
pub trait FaceNetEmbedder {
/// Generate embeddings for a batch of face images
fn run_model(&mut self, faces: ArrayView4<u8>) -> Result<Array2<f32>>;
/// Generate embeddings with full pipeline including postprocessing
fn generate_embeddings(
&mut self,
faces: ArrayView4<u8>,
config: FaceEmbeddingConfig,
) -> Result<Vec<FaceEmbedding>> {
let raw_output = self
.run_model(faces)
.change_context(Error)
.attach_printable("Failed to generate embeddings")?;
raw_output
.into_embeddings(&config)
.attach_printable("Failed to process embeddings")
}
/// Generate a single embedding from a single face image
fn generate_embedding(
&mut self,
face: ArrayView3<u8>,
config: FaceEmbeddingConfig,
) -> Result<FaceEmbedding> {
// Add batch dimension
let face_batch = face.insert_axis(ndarray::Axis(0));
let embeddings = self.generate_embeddings(face_batch.view(), config)?;
embeddings
.into_iter()
.next()
.ok_or(Error)
.attach_printable("No embedding generated for input face")
}
}
/// Utility functions for embedding processing
pub mod utils {
use super::*;
/// Compute pairwise cosine similarities between two sets of embeddings
pub fn pairwise_cosine_similarities(
embeddings1: &[FaceEmbedding],
embeddings2: &[FaceEmbedding],
) -> Array2<f32> {
let n1 = embeddings1.len();
let n2 = embeddings2.len();
let mut similarities = Array2::zeros((n1, n2));
for (i, emb1) in embeddings1.iter().enumerate() {
for (j, emb2) in embeddings2.iter().enumerate() {
similarities[(i, j)] = emb1.cosine_similarity(emb2);
}
}
similarities
}
/// Find the best matching embedding from a gallery for each query
pub fn find_best_matches(
queries: &[FaceEmbedding],
gallery: &[FaceEmbedding],
) -> Vec<(usize, f32)> {
let similarities = pairwise_cosine_similarities(queries, gallery);
let mut best_matches = Vec::new();
for i in 0..queries.len() {
let row = similarities.row(i);
let (best_idx, best_score) = row
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap();
best_matches.push((best_idx, *best_score));
}
best_matches
}
/// Filter embeddings by minimum quality threshold
pub fn filter_by_confidence(
embeddings: Vec<FaceEmbedding>,
min_confidence: f32,
) -> Vec<FaceEmbedding> {
embeddings
.into_iter()
.filter(|emb| emb.confidence.map_or(true, |conf| conf >= min_confidence))
.collect()
}
}