210 lines
6.0 KiB
Rust
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()
|
|
}
|
|
}
|