use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; #[derive(Debug, Clone)] pub enum RekognitionError { ConfigError(String), AwsError(String), InvalidImage(String), FaceNotFound(String), CollectionNotFound(String), CollectionAlreadyExists(String), QuotaExceeded, ServiceUnavailable, Unauthorized, } impl std::fmt::Display for RekognitionError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::ConfigError(s) => write!(f, "Config error: {s}"), Self::AwsError(s) => write!(f, "AWS error: {s}"), Self::InvalidImage(s) => write!(f, "Invalid image: {s}"), Self::FaceNotFound(s) => write!(f, "Face not found: {s}"), Self::CollectionNotFound(s) => write!(f, "Collection not found: {s}"), Self::CollectionAlreadyExists(s) => write!(f, "Collection already exists: {s}"), Self::QuotaExceeded => write!(f, "Quota exceeded"), Self::ServiceUnavailable => write!(f, "Service unavailable"), Self::Unauthorized => write!(f, "Unauthorized"), } } } impl std::error::Error for RekognitionError {} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RekognitionConfig { pub region: String, pub access_key_id: Option, pub secret_access_key: Option, pub collection_id: Option, pub max_faces: u32, pub quality_filter: QualityFilter, pub face_match_threshold: f32, pub use_face_liveness: bool, pub enable_age_range: bool, pub enable_emotions: bool, pub enable_smile_detection: bool, pub enable_eyeglasses_detection: bool, pub enable_sunglasses_detection: bool, pub enable_gender_detection: bool, pub enable_beard_detection: bool, pub enable_mustache_detection: bool, pub enable_eyes_open_detection: bool, pub enable_mouth_open_detection: bool, pub enable_face_occluded_detection: bool, } impl Default for RekognitionConfig { fn default() -> Self { Self { region: "us-east-1".to_string(), access_key_id: None, secret_access_key: None, collection_id: None, max_faces: 100, quality_filter: QualityFilter::Auto, face_match_threshold: 80.0, use_face_liveness: false, enable_age_range: true, enable_emotions: true, enable_smile_detection: true, enable_eyeglasses_detection: true, enable_sunglasses_detection: true, enable_gender_detection: true, enable_beard_detection: true, enable_mustache_detection: true, enable_eyes_open_detection: true, enable_mouth_open_detection: true, enable_face_occluded_detection: true, } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum QualityFilter { None, Auto, Low, Medium, High, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RekognitionFace { pub face_id: Option, pub bounding_box: BoundingBox, pub confidence: f32, pub image_id: Option, pub external_image_id: Option, pub landmarks: Vec, pub pose: Pose, pub quality: FaceQuality, pub age_range: Option, pub smile: Option, pub eyeglasses: Option, pub sunglasses: Option, pub gender: Option, pub beard: Option, pub mustache: Option, pub eyes_open: Option, pub mouth_open: Option, pub emotions: Vec, pub face_occluded: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BoundingBox { pub width: f32, pub height: f32, pub left: f32, pub top: f32, } impl BoundingBox { pub fn to_absolute(&self, image_width: u32, image_height: u32) -> AbsoluteBoundingBox { AbsoluteBoundingBox { x: (self.left * image_width as f32) as u32, y: (self.top * image_height as f32) as u32, width: (self.width * image_width as f32) as u32, height: (self.height * image_height as f32) as u32, } } pub fn area(&self) -> f32 { self.width * self.height } pub fn center(&self) -> (f32, f32) { (self.left + self.width / 2.0, self.top + self.height / 2.0) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AbsoluteBoundingBox { pub x: u32, pub y: u32, pub width: u32, pub height: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Landmark { pub landmark_type: LandmarkType, pub x: f32, pub y: f32, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum LandmarkType { EyeLeft, EyeRight, MouthLeft, MouthRight, Nose, LeftEyeBrowLeft, LeftEyeBrowRight, LeftEyeBrowUp, RightEyeBrowLeft, RightEyeBrowRight, RightEyeBrowUp, LeftEyeLeft, LeftEyeRight, LeftEyeUp, LeftEyeDown, RightEyeLeft, RightEyeRight, RightEyeUp, RightEyeDown, NoseLeft, NoseRight, MouthUp, MouthDown, LeftPupil, RightPupil, UpperJawlineLeft, MidJawlineLeft, ChinBottom, MidJawlineRight, UpperJawlineRight, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Pose { pub roll: f32, pub yaw: f32, pub pitch: f32, } impl Pose { pub fn is_frontal(&self, threshold: f32) -> bool { self.yaw.abs() < threshold && self.pitch.abs() < threshold && self.roll.abs() < threshold } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FaceQuality { pub brightness: f32, pub sharpness: f32, } impl FaceQuality { pub fn is_acceptable(&self, min_brightness: f32, min_sharpness: f32) -> bool { self.brightness >= min_brightness && self.sharpness >= min_sharpness } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgeRange { pub low: u32, pub high: u32, } impl AgeRange { pub fn midpoint(&self) -> u32 { (self.low + self.high) / 2 } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Smile { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Eyeglasses { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Sunglasses { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Gender { pub value: GenderValue, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum GenderValue { Male, Female, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Beard { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Mustache { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EyesOpen { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MouthOpen { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FaceOccluded { pub value: bool, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Emotion { pub emotion_type: EmotionType, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum EmotionType { Happy, Sad, Angry, Confused, Disgusted, Surprised, Calm, Fear, Unknown, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FaceCollection { pub collection_id: String, pub collection_arn: Option, pub face_count: u64, pub face_model_version: String, pub creation_timestamp: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexedFace { pub face_id: String, pub bounding_box: BoundingBox, pub image_id: String, pub external_image_id: Option, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FaceMatch { pub similarity: f32, pub face: RekognitionFace, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompareFacesMatch { pub similarity: f32, pub face: RekognitionFace, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DetectFacesRequest { pub image_bytes: Vec, pub attributes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum FaceAttribute { Default, All, AgeRange, Beard, Emotions, Eyeglasses, EyesOpen, Gender, MouthOpen, Mustache, Smile, Sunglasses, FaceOccluded, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DetectFacesResponse { pub faces: Vec, pub orientation_correction: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexFacesRequest { pub collection_id: String, pub image_bytes: Vec, pub external_image_id: Option, pub max_faces: Option, pub quality_filter: Option, pub detection_attributes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexFacesResponse { pub face_records: Vec, pub orientation_correction: Option, pub face_model_version: String, pub unindexed_faces: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FaceRecord { pub face: IndexedFace, pub face_detail: RekognitionFace, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UnindexedFace { pub reasons: Vec, pub face_detail: RekognitionFace, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum UnindexedFaceReason { ExceedMaxFaces, ExtremePose, LowBrightness, LowSharpness, LowConfidence, SmallBoundingBox, LowFaceQuality, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchFacesRequest { pub collection_id: String, pub face_id: String, pub max_faces: Option, pub face_match_threshold: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchFacesResponse { pub searched_face_id: String, pub face_matches: Vec, pub face_model_version: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchFacesByImageRequest { pub collection_id: String, pub image_bytes: Vec, pub max_faces: Option, pub face_match_threshold: Option, pub quality_filter: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchFacesByImageResponse { pub searched_face_bounding_box: BoundingBox, pub searched_face_confidence: f32, pub face_matches: Vec, pub face_model_version: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompareFacesRequest { pub source_image_bytes: Vec, pub target_image_bytes: Vec, pub similarity_threshold: Option, pub quality_filter: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompareFacesResponse { pub source_image_face: Option, pub face_matches: Vec, pub unmatched_faces: Vec, pub source_image_orientation_correction: Option, pub target_image_orientation_correction: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeleteFacesRequest { pub collection_id: String, pub face_ids: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeleteFacesResponse { pub deleted_faces: Vec, pub undeleted_faces: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UnsuccessfulFaceDeletion { pub face_id: String, pub reasons: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LivenessSessionRequest { pub settings: Option, pub client_request_token: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LivenessSettings { pub output_config: Option, pub audit_images_limit: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LivenessOutputConfig { pub s3_bucket: String, pub s3_key_prefix: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LivenessSessionResponse { pub session_id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LivenessSession { pub session_id: String, pub status: LivenessSessionStatus, pub settings: Option, pub created_at: chrono::DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetFaceLivenessSessionResultsResponse { pub session_id: String, pub status: LivenessSessionStatus, pub confidence: Option, pub reference_image: Option, pub audit_images: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum LivenessSessionStatus { Created, InProgress, Succeeded, Failed, Expired, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuditImage { pub bytes: Option>, pub s3_object: Option, pub bounding_box: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct S3Object { pub bucket: String, pub name: String, pub version: Option, } pub struct RekognitionService { config: Arc>, collections: Arc>>, indexed_faces: Arc>>>, face_details: Arc>>, } impl Default for RekognitionService { fn default() -> Self { Self::new() } } impl RekognitionService { pub fn new() -> Self { Self { config: Arc::new(RwLock::new(RekognitionConfig::default())), collections: Arc::new(RwLock::new(HashMap::new())), indexed_faces: Arc::new(RwLock::new(HashMap::new())), face_details: Arc::new(RwLock::new(HashMap::new())), } } pub fn with_config(config: RekognitionConfig) -> Self { Self { config: Arc::new(RwLock::new(config)), collections: Arc::new(RwLock::new(HashMap::new())), indexed_faces: Arc::new(RwLock::new(HashMap::new())), face_details: Arc::new(RwLock::new(HashMap::new())), } } pub async fn detect_faces(&self, request: DetectFacesRequest) -> Result { if request.image_bytes.is_empty() { return Err(RekognitionError::InvalidImage("Image bytes cannot be empty".to_string())); } let config = self.config.read().await; let include_all = request.attributes.contains(&FaceAttribute::All); let mut faces = Vec::new(); let simulated_face_count = 1; for _ in 0..simulated_face_count { let face = self.create_simulated_face(&config, include_all); faces.push(face); } Ok(DetectFacesResponse { faces, orientation_correction: Some("ROTATE_0".to_string()), }) } fn create_simulated_face(&self, config: &RekognitionConfig, include_all: bool) -> RekognitionFace { let landmarks = vec![ Landmark { landmark_type: LandmarkType::EyeLeft, x: 0.35, y: 0.35 }, Landmark { landmark_type: LandmarkType::EyeRight, x: 0.65, y: 0.35 }, Landmark { landmark_type: LandmarkType::Nose, x: 0.5, y: 0.55 }, Landmark { landmark_type: LandmarkType::MouthLeft, x: 0.38, y: 0.75 }, Landmark { landmark_type: LandmarkType::MouthRight, x: 0.62, y: 0.75 }, ]; let age_range = if include_all || config.enable_age_range { Some(AgeRange { low: 25, high: 35 }) } else { None }; let smile = if include_all || config.enable_smile_detection { Some(Smile { value: true, confidence: 95.5 }) } else { None }; let eyeglasses = if include_all || config.enable_eyeglasses_detection { Some(Eyeglasses { value: false, confidence: 98.2 }) } else { None }; let sunglasses = if include_all || config.enable_sunglasses_detection { Some(Sunglasses { value: false, confidence: 99.1 }) } else { None }; let gender = if include_all || config.enable_gender_detection { Some(Gender { value: GenderValue::Male, confidence: 97.8, }) } else { None }; let beard = if include_all || config.enable_beard_detection { Some(Beard { value: false, confidence: 96.3 }) } else { None }; let mustache = if include_all || config.enable_mustache_detection { Some(Mustache { value: false, confidence: 97.1 }) } else { None }; let eyes_open = if include_all || config.enable_eyes_open_detection { Some(EyesOpen { value: true, confidence: 99.5 }) } else { None }; let mouth_open = if include_all || config.enable_mouth_open_detection { Some(MouthOpen { value: false, confidence: 98.7 }) } else { None }; let emotions = if include_all || config.enable_emotions { vec![ Emotion { emotion_type: EmotionType::Happy, confidence: 85.2 }, Emotion { emotion_type: EmotionType::Calm, confidence: 12.5 }, Emotion { emotion_type: EmotionType::Surprised, confidence: 2.3 }, ] } else { Vec::new() }; let face_occluded = if include_all || config.enable_face_occluded_detection { Some(FaceOccluded { value: false, confidence: 98.9 }) } else { None }; RekognitionFace { face_id: Some(Uuid::new_v4().to_string()), bounding_box: BoundingBox { width: 0.25, height: 0.35, left: 0.35, top: 0.15, }, confidence: 99.8, image_id: Some(Uuid::new_v4().to_string()), external_image_id: None, landmarks, pose: Pose { roll: 2.5, yaw: -5.0, pitch: 3.2, }, quality: FaceQuality { brightness: 85.5, sharpness: 92.3, }, age_range, smile, eyeglasses, sunglasses, gender, beard, mustache, eyes_open, mouth_open, emotions, face_occluded, } } pub async fn create_collection(&self, collection_id: &str) -> Result { let mut collections = self.collections.write().await; if collections.contains_key(collection_id) { return Err(RekognitionError::CollectionAlreadyExists(collection_id.to_string())); } let collection = FaceCollection { collection_id: collection_id.to_string(), collection_arn: Some(format!("arn:aws:rekognition:us-east-1:123456789012:collection/{collection_id}")), face_count: 0, face_model_version: "6.0".to_string(), creation_timestamp: Some(Utc::now()), }; collections.insert(collection_id.to_string(), collection.clone()); let mut indexed_faces = self.indexed_faces.write().await; indexed_faces.insert(collection_id.to_string(), Vec::new()); Ok(collection) } pub async fn delete_collection(&self, collection_id: &str) -> Result<(), RekognitionError> { let mut collections = self.collections.write().await; if collections.remove(collection_id).is_none() { return Err(RekognitionError::CollectionNotFound(collection_id.to_string())); } let mut indexed_faces = self.indexed_faces.write().await; indexed_faces.remove(collection_id); Ok(()) } pub async fn list_collections(&self) -> Vec { let collections = self.collections.read().await; collections.values().cloned().collect() } pub async fn describe_collection(&self, collection_id: &str) -> Result { let collections = self.collections.read().await; collections .get(collection_id) .cloned() .ok_or_else(|| RekognitionError::CollectionNotFound(collection_id.to_string())) } pub async fn index_faces(&self, request: IndexFacesRequest) -> Result { if request.image_bytes.is_empty() { return Err(RekognitionError::InvalidImage("Image bytes cannot be empty".to_string())); } let mut collections = self.collections.write().await; let collection = collections .get_mut(&request.collection_id) .ok_or_else(|| RekognitionError::CollectionNotFound(request.collection_id.clone()))?; let config = self.config.read().await; let max_faces = request.max_faces.unwrap_or(config.max_faces); let mut face_records = Vec::new(); let simulated_face_count = 1.min(max_faces as usize); for _ in 0..simulated_face_count { let face_id = Uuid::new_v4().to_string(); let image_id = Uuid::new_v4().to_string(); let indexed_face = IndexedFace { face_id: face_id.clone(), bounding_box: BoundingBox { width: 0.25, height: 0.35, left: 0.35, top: 0.15, }, image_id: image_id.clone(), external_image_id: request.external_image_id.clone(), confidence: 99.8, }; let face_detail = self.create_simulated_face(&config, true); face_records.push(FaceRecord { face: indexed_face.clone(), face_detail: face_detail.clone(), }); let mut indexed_faces = self.indexed_faces.write().await; if let Some(faces) = indexed_faces.get_mut(&request.collection_id) { faces.push(indexed_face); } let mut face_details = self.face_details.write().await; face_details.insert(face_id, face_detail); } collection.face_count += face_records.len() as u64; Ok(IndexFacesResponse { face_records, orientation_correction: Some("ROTATE_0".to_string()), face_model_version: "6.0".to_string(), unindexed_faces: Vec::new(), }) } pub async fn search_faces(&self, request: SearchFacesRequest) -> Result { let indexed_faces = self.indexed_faces.read().await; let collection_faces = indexed_faces .get(&request.collection_id) .ok_or_else(|| RekognitionError::CollectionNotFound(request.collection_id.clone()))?; if !collection_faces.iter().any(|f| f.face_id == request.face_id) { return Err(RekognitionError::FaceNotFound(request.face_id.clone())); } let config = self.config.read().await; let threshold = request.face_match_threshold.unwrap_or(config.face_match_threshold); let max_faces = request.max_faces.unwrap_or(10); let mut face_matches = Vec::new(); for face in collection_faces.iter().filter(|f| f.face_id != request.face_id) { let similarity = 85.0 + (rand_float() * 15.0); if similarity >= threshold { let face_details = self.face_details.read().await; if let Some(detail) = face_details.get(&face.face_id) { face_matches.push(FaceMatch { similarity, face: detail.clone(), }); } } } face_matches.sort_by(|a, b| b.similarity.partial_cmp(&a.similarity).unwrap_or(std::cmp::Ordering::Equal)); face_matches.truncate(max_faces as usize); Ok(SearchFacesResponse { searched_face_id: request.face_id, face_matches, face_model_version: "6.0".to_string(), }) } pub async fn search_faces_by_image(&self, request: SearchFacesByImageRequest) -> Result { if request.image_bytes.is_empty() { return Err(RekognitionError::InvalidImage("Image bytes cannot be empty".to_string())); } let indexed_faces = self.indexed_faces.read().await; let collection_faces = indexed_faces .get(&request.collection_id) .ok_or_else(|| RekognitionError::CollectionNotFound(request.collection_id.clone()))?; let config = self.config.read().await; let threshold = request.face_match_threshold.unwrap_or(config.face_match_threshold); let max_faces = request.max_faces.unwrap_or(10); let searched_face_bounding_box = BoundingBox { width: 0.25, height: 0.35, left: 0.35, top: 0.15, }; let mut face_matches = Vec::new(); for face in collection_faces.iter() { let similarity = 80.0 + (rand_float() * 20.0); if similarity >= threshold { let face_details = self.face_details.read().await; if let Some(detail) = face_details.get(&face.face_id) { face_matches.push(FaceMatch { similarity, face: detail.clone(), }); } } } face_matches.sort_by(|a, b| b.similarity.partial_cmp(&a.similarity).unwrap_or(std::cmp::Ordering::Equal)); face_matches.truncate(max_faces as usize); Ok(SearchFacesByImageResponse { searched_face_bounding_box, searched_face_confidence: 99.5, face_matches, face_model_version: "6.0".to_string(), }) } } fn rand_float() -> f32 { use std::time::{SystemTime, UNIX_EPOCH}; let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .subsec_nanos(); (nanos % 1000) as f32 / 1000.0 }