use autocxx::prelude::*;
use autocxx::{c_long, c_void};
use error::FFIError;
use ffi_wrapper::{
baklava_create_image_bitmap_from_path, baklava_create_image_stream_from_bitmap,
baklava_create_session_optional, HFCreateFaceFeature, HFDetectMode, HFExecuteFaceTrack,
HFFaceBasicToken, HFFaceComparison, HFFaceFeature, HFFaceFeatureWithRefExtractTo, HFGetTokens,
HFLaunchInspireFace, HFMultipleFaceData, HFReleaseFaceFeature, HFReleaseImageBitmap,
HFReleaseImageStream, HFReleaseInspireFaceSession, HFRotation, HF_ENABLE_FACE_RECOGNITION,
HSUCCEED,
};
use std::sync::{Arc, Mutex};
use std::{
ffi::CString,
mem::{self},
str::FromStr,
thread,
};
pub mod error;
mod ffi_wrapper;
const SUCCESS: i64 = HSUCCEED as i64;
const OUTPUT_MAX: f64 = 1.0;
const OUTPUT_MIN: f64 = 0.01;
const MIDDLE_SCORE: f64 = 0.6;
const STEEPNESS: f64 = 8.;
const RECOMMENDED_COSINE_THRESHOLD: f64 = 0.48;
pub struct InsightFace {
session: *mut c_void,
src_features: Vec<HFFaceFeature>,
target_feature: HFFaceFeature,
chunks: Option<usize>,
}
struct SessionHandler {
session: *mut c_void,
}
#[derive(PartialEq, Eq)]
pub enum Methodology {
Mean,
Median,
}
unsafe impl Send for InsightFace {}
unsafe impl Send for SessionHandler {}
unsafe impl Send for HFFaceFeature {}
unsafe impl Sync for InsightFace {}
impl InsightFace {
pub fn new<S: AsRef<str>>(
model: S,
chunk_size: Option<usize>,
) -> Result<Self, Box<dyn std::error::Error>> {
let model = CString::new(model.as_ref())?;
unsafe {
if HFLaunchInspireFace(model.as_ptr()).0 != SUCCESS {
return Err(FFIError::ModelLoad.into());
}
}
let session_ptr = unsafe {
let mut result = c_long(0);
let session_ptr = baklava_create_session_optional(
c_int(HF_ENABLE_FACE_RECOGNITION as i32),
HFDetectMode::HF_DETECT_MODE_ALWAYS_DETECT,
c_int(1),
c_int(-1),
c_int(-1),
&mut result,
);
if result.0 != SUCCESS {
return Err(FFIError::Session.into());
}
session_ptr
};
Ok(Self {
session: session_ptr,
src_features: vec![],
target_feature: unsafe { mem::zeroed() },
chunks: chunk_size,
})
}
pub fn prepare_images<S: AsRef<str> + std::clone::Clone + Send + Sync + Copy>(
&mut self,
sources: &[S],
) -> Result<&mut Self, Box<dyn std::error::Error>> {
self.src_features = (0..sources.len())
.map(|_| unsafe { mem::zeroed() })
.collect();
let images = Arc::new(sources.to_vec());
let send_session = Arc::new(Mutex::new(SessionHandler {
session: self.session,
}));
let chunk_size = self.chunks.unwrap_or(1);
let chunks_features = self.src_features.chunks_mut(chunk_size).collect::<Vec<_>>();
let chunks_len = chunks_features.len();
let chf = Arc::new(Mutex::new(chunks_features));
thread::scope(|s| -> Result<(), Box<dyn std::error::Error>> {
let img_handle = images.clone();
let session_incr = send_session.clone();
for idx in 0..chunks_len {
let images_clone = img_handle.clone();
let session_incr = session_incr.clone();
let chf = chf.clone();
s.spawn(move || -> Result<(), FFIError> {
let mut mutex = chf
.lock()
.map_err(|_| FFIError::IO("Unable to acquire lock"))?;
let chunk = mutex
.get_mut(idx)
.ok_or(FFIError::IO("Unable to acquire lock"))?;
for (iidx, feature) in chunk.iter_mut().enumerate() {
let mut counter = iidx;
if idx > 0 {
counter = idx * chunk_size + iidx;
}
let img_path = images_clone
.get(counter)
.map(|s| CString::new(s.as_ref()))
.ok_or(FFIError::MissingImage)?
.map_err(|_| FFIError::MissingImage)?;
InsightFace::prepare_image_for_comparison(
feature,
img_path,
session_incr.clone(),
)
.map_err(|_| FFIError::IO("Unable to prepare image"))?;
}
Ok(())
});
}
Ok(())
})?;
Ok(self)
}
pub fn prepare_target_image<S: AsRef<str>>(
&mut self,
target_img_path: S,
) -> Result<&mut Self, Box<dyn std::error::Error>> {
let send_session = Arc::new(Mutex::new(SessionHandler {
session: self.session,
}));
let img_path =
CString::from_str(target_img_path.as_ref()).map_err(|_| FFIError::Feature)?;
InsightFace::prepare_image_for_comparison(
&mut self.target_feature,
img_path,
send_session,
)?;
Ok(self)
}
fn prepare_image_for_comparison(
feature: *mut HFFaceFeature,
img_path: CString,
session_handler: Arc<Mutex<SessionHandler>>,
) -> Result<(), Box<dyn std::error::Error>> {
unsafe {
let mut multiple_face_data: HFMultipleFaceData = mem::zeroed();
if HFCreateFaceFeature(feature).0 != SUCCESS {
return Err(FFIError::Feature.into());
}
let mut result = c_long(0);
let img_ptr =
baklava_create_image_bitmap_from_path(img_path.as_ptr(), c_int(3), &mut result);
if result.0 != SUCCESS || img_ptr.is_null() {
return Err(FFIError::Bitmap("image may not be the proper size or format").into());
}
let mut result = c_long(0);
let stream_ptr = baklava_create_image_stream_from_bitmap(
img_ptr,
HFRotation::HF_CAMERA_ROTATION_0,
&mut result,
);
if result.0 != SUCCESS || stream_ptr.is_null() {
InsightFace::release_ptr(img_ptr, stream_ptr);
return Err(FFIError::Stream("Unable to create stream issue with rotation").into());
}
let mutex = session_handler
.lock()
.map_err(|_| FFIError::Comparison("Unable to acquire the session handler lock"))?;
if HFExecuteFaceTrack(mutex.session, stream_ptr, &mut multiple_face_data).0 != SUCCESS {
InsightFace::release_ptr(img_ptr, stream_ptr);
return Err(FFIError::FaceTrack("").into());
}
let tokens_slice = HFGetTokens(&mut multiple_face_data);
if tokens_slice.ptr.is_null() {
InsightFace::release_ptr(img_ptr, stream_ptr);
return Err(FFIError::FaceTrack(
"Unable to construct list of tokens due to tokens slice being null",
)
.into());
}
let tokens_ptr = tokens_slice.ptr as *mut HFFaceBasicToken;
let tokens = std::slice::from_raw_parts_mut(tokens_ptr, tokens_slice.len as usize);
let single_face = tokens.first_mut().ok_or_else(|| {
InsightFace::release_ptr(img_ptr, stream_ptr);
FFIError::FaceTrack("Unable to get the processed feature")
})?;
let res =
HFFaceFeatureWithRefExtractTo(mutex.session, stream_ptr, single_face, feature);
if res.0 != SUCCESS {
InsightFace::release_ptr(img_ptr, stream_ptr);
return Err(
FFIError::FaceTrack("Unable to extract feature from stream_ptr").into(),
);
}
InsightFace::release_ptr(img_ptr, stream_ptr);
}
Ok(())
}
pub fn compare_images(
&self,
methodology: Methodology,
) -> Result<(f32, f64), Box<dyn std::error::Error>> {
let mut cosine_result = Vec::new();
for feature in self.src_features.iter() {
let mut res: f32 = 0.0;
unsafe {
let op_res = HFFaceComparison(feature, &self.target_feature, &mut res);
if op_res.0 != SUCCESS {
return Err(FFIError::Comparison("Comparison fail").into());
}
}
cosine_result.push(res);
}
if cosine_result.len() == 2 && methodology == Methodology::Median {
return Err(FFIError::Comparison(
"Sample size is too small. You should consider to use the mean methodology instead",
)
.into());
}
let cosine = match methodology {
Methodology::Mean => {
cosine_result.into_iter().fold(0., |acc, x| acc + x)
/ self.src_features.len() as f32
}
Methodology::Median => {
cosine_result.sort_unstable_by(|a, b| a.total_cmp(b));
let mid = cosine_result.len() / 2;
match cosine_result.len() % 2 == 0 {
true => {
let low = cosine_result
.get(mid - 1)
.ok_or(FFIError::Comparison("Unable to get the low mid"))?;
let high = cosine_result
.get(mid + 1)
.ok_or(FFIError::Comparison("Unable to get the high mid"))?;
(*low + *high) / 2.
}
false => cosine_result
.get(mid)
.copied()
.ok_or(FFIError::Comparison("Unable to get the median"))?,
}
}
};
Ok((cosine, Self::compute_percentage(cosine)))
}
pub fn is_similar(cosine: f32, threshold: Option<f64>) -> bool {
cosine as f64 >= threshold.unwrap_or(RECOMMENDED_COSINE_THRESHOLD)
}
fn compute_percentage(cosine: f32) -> f64 {
let bias = -f64::ln((OUTPUT_MAX - MIDDLE_SCORE) / (MIDDLE_SCORE - OUTPUT_MIN));
let output_scale = OUTPUT_MAX - OUTPUT_MIN;
let shifted_input = STEEPNESS * (cosine as f64 - RECOMMENDED_COSINE_THRESHOLD);
let sigmoid = 1. / (1. + f64::exp(-shifted_input - bias));
sigmoid * output_scale + OUTPUT_MIN
}
fn release_ptr(img_ptr: *mut c_void, stream_ptr: *mut c_void) {
unsafe {
HFReleaseImageBitmap(img_ptr);
HFReleaseImageStream(stream_ptr);
}
}
}
impl Drop for InsightFace {
fn drop(&mut self) {
unsafe {
HFReleaseInspireFaceSession(self.session);
for feature in self.src_features.iter_mut() {
HFReleaseFaceFeature(feature);
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{InsightFace, Methodology};
use reqwest::blocking::Client;
use std::sync::{Arc, LazyLock, Mutex};
static INSIGHT_FACE_CLIENT: LazyLock<Arc<Mutex<InsightFace>>> = LazyLock::new(|| {
download_megatron_source();
Arc::new(Mutex::new(InsightFace::new("./Megatron", None).unwrap()))
});
fn download_megatron_source() {
let client = Client::new();
let response = client
.get("https://github.com/HyperInspire/InspireFace/releases/download/v1.x/Megatron")
.send()
.unwrap();
let bytes = response.bytes().unwrap();
std::fs::write("Megatron", bytes).unwrap();
}
#[test]
fn expect_to_compare_image() {
let mut model = INSIGHT_FACE_CLIENT.lock().unwrap();
let prep_image_set_1 = model
.prepare_images(&["./face1_test.png", "./face2_test.png"])
.unwrap()
.prepare_target_image("./face1_test.png");
assert!(prep_image_set_1.is_ok());
let prep_image_set_1 = prep_image_set_1.unwrap();
let (cos, percentage) = prep_image_set_1.compare_images(Methodology::Mean).unwrap();
assert!(cos > 0.6);
assert!(percentage > 0.6);
}
#[test]
fn expect_to_compare_image_with_median_methodology() {
let mut model = INSIGHT_FACE_CLIENT.lock().unwrap();
let prep_image_set_1 = model
.prepare_images(&["./face1_test.png", "./face2_test.png", "./face1_test.png"])
.unwrap()
.prepare_target_image("./face1_test.png");
assert!(prep_image_set_1.is_ok());
let prep_image_set_1 = prep_image_set_1.unwrap();
let (cos, percentage) = prep_image_set_1
.compare_images(Methodology::Median)
.unwrap();
assert!(cos > 0.6);
assert!(percentage > 0.6);
}
#[test]
fn expect_median_methodology_to_fail() {
let mut model = INSIGHT_FACE_CLIENT.lock().unwrap();
let prep_image_set_1 = model
.prepare_images(&["./face1_test.png", "./face2_test.png"])
.unwrap()
.prepare_target_image("./face1_test.png");
assert!(prep_image_set_1.is_ok());
let prep_image_set_1 = prep_image_set_1.unwrap();
let res = prep_image_set_1.compare_images(Methodology::Median);
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
"Unable to compare image due to: Sample size is too small. You should consider to use the mean methodology instead"
);
}
}