1#![allow(dead_code)]
12
13use std::collections::HashMap;
14
15#[derive(Clone, Debug, PartialEq, Eq, Hash)]
21pub enum ActionUnit {
22 AU1, AU2, AU4, AU5, AU6, AU7, AU9, AU10, AU11, AU12, AU13, AU14, AU15, AU16, AU17, AU18, AU20, AU22, AU23, AU24, AU25, AU26, AU27, AU28, AU41, AU42, AU43, AU44, AU45, AU46, }
57
58impl ActionUnit {
59 pub fn number(&self) -> u32 {
61 match self {
62 ActionUnit::AU1 => 1,
63 ActionUnit::AU2 => 2,
64 ActionUnit::AU4 => 4,
65 ActionUnit::AU5 => 5,
66 ActionUnit::AU6 => 6,
67 ActionUnit::AU7 => 7,
68 ActionUnit::AU9 => 9,
69 ActionUnit::AU10 => 10,
70 ActionUnit::AU11 => 11,
71 ActionUnit::AU12 => 12,
72 ActionUnit::AU13 => 13,
73 ActionUnit::AU14 => 14,
74 ActionUnit::AU15 => 15,
75 ActionUnit::AU16 => 16,
76 ActionUnit::AU17 => 17,
77 ActionUnit::AU18 => 18,
78 ActionUnit::AU20 => 20,
79 ActionUnit::AU22 => 22,
80 ActionUnit::AU23 => 23,
81 ActionUnit::AU24 => 24,
82 ActionUnit::AU25 => 25,
83 ActionUnit::AU26 => 26,
84 ActionUnit::AU27 => 27,
85 ActionUnit::AU28 => 28,
86 ActionUnit::AU41 => 41,
87 ActionUnit::AU42 => 42,
88 ActionUnit::AU43 => 43,
89 ActionUnit::AU44 => 44,
90 ActionUnit::AU45 => 45,
91 ActionUnit::AU46 => 46,
92 }
93 }
94
95 pub fn name(&self) -> &'static str {
97 match self {
98 ActionUnit::AU1 => "Inner Brow Raise",
99 ActionUnit::AU2 => "Outer Brow Raise",
100 ActionUnit::AU4 => "Brow Lowerer",
101 ActionUnit::AU5 => "Upper Lid Raiser",
102 ActionUnit::AU6 => "Cheek Raiser",
103 ActionUnit::AU7 => "Lid Tightener",
104 ActionUnit::AU9 => "Nose Wrinkler",
105 ActionUnit::AU10 => "Upper Lip Raiser",
106 ActionUnit::AU11 => "Nasolabial Deepener",
107 ActionUnit::AU12 => "Lip Corner Puller",
108 ActionUnit::AU13 => "Cheek Puffer",
109 ActionUnit::AU14 => "Dimpler",
110 ActionUnit::AU15 => "Lip Corner Depressor",
111 ActionUnit::AU16 => "Lower Lip Depressor",
112 ActionUnit::AU17 => "Chin Raiser",
113 ActionUnit::AU18 => "Lip Puckerer",
114 ActionUnit::AU20 => "Lip Stretcher",
115 ActionUnit::AU22 => "Lip Funneler",
116 ActionUnit::AU23 => "Lip Tightener",
117 ActionUnit::AU24 => "Lip Pressor",
118 ActionUnit::AU25 => "Lips Part",
119 ActionUnit::AU26 => "Jaw Drop",
120 ActionUnit::AU27 => "Mouth Stretch",
121 ActionUnit::AU28 => "Lip Suck",
122 ActionUnit::AU41 => "Lid Droop",
123 ActionUnit::AU42 => "Slit",
124 ActionUnit::AU43 => "Eyes Closed",
125 ActionUnit::AU44 => "Squint",
126 ActionUnit::AU45 => "Blink",
127 ActionUnit::AU46 => "Wink",
128 }
129 }
130
131 pub fn description(&self) -> &'static str {
133 match self {
134 ActionUnit::AU1 => "Medial frontalis raises the inner portion of the brow",
135 ActionUnit::AU2 => "Lateral frontalis raises the outer portion of the brow",
136 ActionUnit::AU4 => "Corrugator and depressor supercilii lower the brows",
137 ActionUnit::AU5 => "Levator palpebrae superioris raises the upper eyelid",
138 ActionUnit::AU6 => "Orbicularis oculi (orbital) raises the cheek",
139 ActionUnit::AU7 => "Orbicularis oculi (palpebral) tightens the lower lid",
140 ActionUnit::AU9 => "Levator labii superioris alaeque nasi wrinkles the nose",
141 ActionUnit::AU10 => "Levator labii superioris raises the upper lip",
142 ActionUnit::AU11 => "Zygomaticus minor deepens the nasolabial fold",
143 ActionUnit::AU12 => "Zygomaticus major pulls the lip corners upward and outward",
144 ActionUnit::AU13 => "Levator anguli oris puffs the cheeks",
145 ActionUnit::AU14 => "Buccinator creates dimples at the lip corners",
146 ActionUnit::AU15 => "Depressor anguli oris pulls the lip corners downward",
147 ActionUnit::AU16 => "Depressor labii inferioris lowers the lower lip",
148 ActionUnit::AU17 => "Mentalis raises and wrinkles the chin",
149 ActionUnit::AU18 => "Incisivii labii pucker the lips",
150 ActionUnit::AU20 => "Risorius stretches the lip corners horizontally",
151 ActionUnit::AU22 => "Orbicularis oris creates a funnel/O-shape with the lips",
152 ActionUnit::AU23 => "Orbicularis oris narrows and tightens the lips",
153 ActionUnit::AU24 => "Orbicularis oris presses the lips together",
154 ActionUnit::AU25 => "Depressor labii or relaxed mentalis parts the lips",
155 ActionUnit::AU26 => "Internal pterygoid, digastric, etc. drop the jaw",
156 ActionUnit::AU27 => "Pterygoids, digastric open the mouth extremely wide",
157 ActionUnit::AU28 => "Orbicularis oris sucks the lips inward",
158 ActionUnit::AU41 => "Relaxation of levator palpebrae droops the upper lid",
159 ActionUnit::AU42 => "Orbicularis oculi narrows the eye opening",
160 ActionUnit::AU43 => "Relaxed levator palpebrae closes the eyes",
161 ActionUnit::AU44 => "Orbicularis oculi squints the eyes",
162 ActionUnit::AU45 => "Rapid closing and opening of the eyelids (blink)",
163 ActionUnit::AU46 => "Closing one eye (wink)",
164 }
165 }
166
167 pub fn all() -> &'static [ActionUnit] {
169 use ActionUnit::*;
170 &[
171 AU1, AU2, AU4, AU5, AU6, AU7, AU9, AU10, AU11, AU12, AU13, AU14, AU15, AU16, AU17,
172 AU18, AU20, AU22, AU23, AU24, AU25, AU26, AU27, AU28, AU41, AU42, AU43, AU44, AU45,
173 AU46,
174 ]
175 }
176
177 pub fn upper_face() -> &'static [ActionUnit] {
179 use ActionUnit::*;
180 &[AU1, AU2, AU4, AU5, AU6, AU7, AU9, AU10]
181 }
182
183 pub fn lower_face() -> &'static [ActionUnit] {
185 use ActionUnit::*;
186 &[
187 AU11, AU12, AU13, AU14, AU15, AU16, AU17, AU18, AU20, AU22, AU23, AU24, AU25, AU26,
188 AU27, AU28,
189 ]
190 }
191
192 pub fn eye_units() -> &'static [ActionUnit] {
194 use ActionUnit::*;
195 &[AU41, AU42, AU43, AU44, AU45, AU46]
196 }
197
198 pub fn from_number(n: u32) -> Option<ActionUnit> {
200 match n {
201 1 => Some(ActionUnit::AU1),
202 2 => Some(ActionUnit::AU2),
203 4 => Some(ActionUnit::AU4),
204 5 => Some(ActionUnit::AU5),
205 6 => Some(ActionUnit::AU6),
206 7 => Some(ActionUnit::AU7),
207 9 => Some(ActionUnit::AU9),
208 10 => Some(ActionUnit::AU10),
209 11 => Some(ActionUnit::AU11),
210 12 => Some(ActionUnit::AU12),
211 13 => Some(ActionUnit::AU13),
212 14 => Some(ActionUnit::AU14),
213 15 => Some(ActionUnit::AU15),
214 16 => Some(ActionUnit::AU16),
215 17 => Some(ActionUnit::AU17),
216 18 => Some(ActionUnit::AU18),
217 20 => Some(ActionUnit::AU20),
218 22 => Some(ActionUnit::AU22),
219 23 => Some(ActionUnit::AU23),
220 24 => Some(ActionUnit::AU24),
221 25 => Some(ActionUnit::AU25),
222 26 => Some(ActionUnit::AU26),
223 27 => Some(ActionUnit::AU27),
224 28 => Some(ActionUnit::AU28),
225 41 => Some(ActionUnit::AU41),
226 42 => Some(ActionUnit::AU42),
227 43 => Some(ActionUnit::AU43),
228 44 => Some(ActionUnit::AU44),
229 45 => Some(ActionUnit::AU45),
230 46 => Some(ActionUnit::AU46),
231 _ => None,
232 }
233 }
234}
235
236pub type FacsState = HashMap<ActionUnit, f32>;
242
243pub struct FacsMapper {
253 mappings: HashMap<ActionUnit, Vec<(String, f32)>>,
255}
256
257impl FacsMapper {
258 pub fn new() -> Self {
260 Self {
261 mappings: HashMap::new(),
262 }
263 }
264
265 pub fn add_mapping(&mut self, au: ActionUnit, morph: impl Into<String>, weight: f32) {
270 self.mappings
271 .entry(au)
272 .or_default()
273 .push((morph.into(), weight));
274 }
275
276 pub fn mappings_for(&self, au: &ActionUnit) -> &[(String, f32)] {
278 self.mappings.get(au).map(|v| v.as_slice()).unwrap_or(&[])
279 }
280
281 pub fn evaluate(&self, state: &FacsState) -> HashMap<String, f32> {
287 let mut result: HashMap<String, f32> = HashMap::new();
288 for (au, &intensity) in state {
289 if let Some(pairs) = self.mappings.get(au) {
290 for (morph, max_weight) in pairs {
291 let contribution = intensity * *max_weight;
292 let entry = result.entry(morph.clone()).or_insert(0.0);
293 *entry = (*entry + contribution).min(1.0);
294 }
295 }
296 }
297 result
298 }
299}
300
301impl Default for FacsMapper {
302 fn default() -> Self {
303 Self::new()
304 }
305}
306
307pub fn default_facs_mapper() -> FacsMapper {
313 let mut m = FacsMapper::new();
314
315 m.add_mapping(ActionUnit::AU1, "brow_inner_raise", 0.8);
317 m.add_mapping(ActionUnit::AU2, "brow_outer_raise", 0.8);
318 m.add_mapping(ActionUnit::AU4, "brow_lower", 0.9);
319 m.add_mapping(ActionUnit::AU4, "brow_furrow", 0.6);
320 m.add_mapping(ActionUnit::AU5, "upper_lid_raise", 0.8);
321 m.add_mapping(ActionUnit::AU6, "cheek_raise", 0.7);
322 m.add_mapping(ActionUnit::AU7, "lid_tighten", 0.7);
323
324 m.add_mapping(ActionUnit::AU9, "nose_wrinkle", 0.8);
326 m.add_mapping(ActionUnit::AU10, "upper_lip_raise", 0.7);
327
328 m.add_mapping(ActionUnit::AU11, "nasolabial_deepen", 0.7);
330 m.add_mapping(ActionUnit::AU12, "smile_mouth", 0.9);
331 m.add_mapping(ActionUnit::AU12, "lip_corner_pull", 0.7);
332 m.add_mapping(ActionUnit::AU13, "cheek_puff", 0.7);
333 m.add_mapping(ActionUnit::AU14, "dimple", 0.7);
334 m.add_mapping(ActionUnit::AU15, "lip_corner_depress", 0.8);
335 m.add_mapping(ActionUnit::AU16, "lower_lip_depress", 0.8);
336 m.add_mapping(ActionUnit::AU17, "chin_raise", 0.7);
337 m.add_mapping(ActionUnit::AU18, "lip_pucker", 0.8);
338 m.add_mapping(ActionUnit::AU20, "lip_stretch", 0.8);
339 m.add_mapping(ActionUnit::AU22, "lip_funnel", 0.8);
340 m.add_mapping(ActionUnit::AU23, "lip_tighten", 0.7);
341 m.add_mapping(ActionUnit::AU24, "lip_press", 0.7);
342 m.add_mapping(ActionUnit::AU25, "lips_part", 0.8);
343 m.add_mapping(ActionUnit::AU25, "jaw_open", 0.3);
344 m.add_mapping(ActionUnit::AU26, "jaw_drop", 0.9);
345 m.add_mapping(ActionUnit::AU27, "mouth_stretch", 0.9);
346 m.add_mapping(ActionUnit::AU27, "jaw_drop", 0.5);
347 m.add_mapping(ActionUnit::AU28, "lip_suck", 0.8);
348
349 m.add_mapping(ActionUnit::AU41, "lid_droop", 0.8);
351 m.add_mapping(ActionUnit::AU42, "eye_slit", 0.7);
352 m.add_mapping(ActionUnit::AU43, "eyes_closed", 1.0);
353 m.add_mapping(ActionUnit::AU44, "eye_squint", 0.8);
354 m.add_mapping(ActionUnit::AU45, "blink_l", 1.0);
355 m.add_mapping(ActionUnit::AU45, "blink_r", 1.0);
356 m.add_mapping(ActionUnit::AU46, "wink_l", 1.0);
357
358 m
359}
360
361pub fn emotion_to_facs(emotion: &str) -> FacsState {
370 let mut state: FacsState = HashMap::new();
371
372 match emotion.to_lowercase().as_str() {
373 "happy" => {
374 state.insert(ActionUnit::AU6, 0.8);
375 state.insert(ActionUnit::AU12, 0.9);
376 }
377 "sad" => {
378 state.insert(ActionUnit::AU1, 0.8);
379 state.insert(ActionUnit::AU4, 0.6);
380 state.insert(ActionUnit::AU15, 0.7);
381 }
382 "angry" => {
383 state.insert(ActionUnit::AU4, 0.9);
384 state.insert(ActionUnit::AU5, 0.6);
385 state.insert(ActionUnit::AU7, 0.8);
386 state.insert(ActionUnit::AU23, 0.7);
387 }
388 "surprised" => {
389 state.insert(ActionUnit::AU1, 0.8);
390 state.insert(ActionUnit::AU2, 0.8);
391 state.insert(ActionUnit::AU5, 0.9);
392 state.insert(ActionUnit::AU26, 0.7);
393 }
394 "fear" => {
395 state.insert(ActionUnit::AU1, 0.8);
396 state.insert(ActionUnit::AU2, 0.8);
397 state.insert(ActionUnit::AU4, 0.6);
398 state.insert(ActionUnit::AU5, 0.8);
399 state.insert(ActionUnit::AU20, 0.7);
400 }
401 "disgust" => {
402 state.insert(ActionUnit::AU9, 0.9);
403 state.insert(ActionUnit::AU15, 0.7);
404 state.insert(ActionUnit::AU16, 0.6);
405 }
406 _ => {}
407 }
408
409 state
410}
411
412pub struct FacsIntensity(pub f32);
421
422impl FacsIntensity {
423 pub fn from_letter(letter: char) -> Option<Self> {
433 let v = match letter.to_ascii_uppercase() {
434 'A' => 0.10,
435 'B' => 0.30,
436 'C' => 0.50,
437 'D' => 0.75,
438 'E' => 1.00,
439 _ => return None,
440 };
441 Some(FacsIntensity(v))
442 }
443
444 pub fn to_normalized(&self) -> f32 {
446 self.0.clamp(0.0, 1.0)
447 }
448
449 pub fn from_normalized(v: f32) -> Self {
451 FacsIntensity(v.clamp(0.0, 1.0))
452 }
453}
454
455pub fn parse_facs_string(s: &str) -> FacsState {
465 let mut state: FacsState = HashMap::new();
466
467 for token in s.split('+') {
468 let token = token.trim();
469 if token.is_empty() {
470 continue;
471 }
472
473 let without_prefix = if token.to_uppercase().starts_with("AU") {
475 &token[2..]
476 } else {
477 continue;
478 };
479
480 let last = without_prefix.chars().last();
482 let (num_str, intensity) = match last {
483 Some(c) if c.is_ascii_alphabetic() => {
484 let num_part = &without_prefix[..without_prefix.len() - c.len_utf8()];
485 let intensity = FacsIntensity::from_letter(c)
486 .map(|fi| fi.to_normalized())
487 .unwrap_or(1.0);
488 (num_part, intensity)
489 }
490 _ => (without_prefix, 1.0_f32),
491 };
492
493 if let Ok(n) = num_str.parse::<u32>() {
494 if let Some(au) = ActionUnit::from_number(n) {
495 state.insert(au, intensity);
496 }
497 }
498 }
499
500 state
501}
502
503#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_action_unit_number() {
513 assert_eq!(ActionUnit::AU1.number(), 1);
514 assert_eq!(ActionUnit::AU12.number(), 12);
515 assert_eq!(ActionUnit::AU45.number(), 45);
516 assert_eq!(ActionUnit::AU46.number(), 46);
517 }
518
519 #[test]
520 fn test_action_unit_name() {
521 assert_eq!(ActionUnit::AU1.name(), "Inner Brow Raise");
522 assert_eq!(ActionUnit::AU12.name(), "Lip Corner Puller");
523 assert_eq!(ActionUnit::AU45.name(), "Blink");
524 assert_eq!(ActionUnit::AU26.name(), "Jaw Drop");
525 }
526
527 #[test]
528 fn test_action_unit_all() {
529 let all = ActionUnit::all();
530 assert_eq!(all.len(), 30);
531 assert_eq!(all[0], ActionUnit::AU1);
533 assert_eq!(all[all.len() - 1], ActionUnit::AU46);
534 let numbers: Vec<u32> = all.iter().map(|au| au.number()).collect();
536 let mut sorted = numbers.clone();
537 sorted.sort_unstable();
538 sorted.dedup();
539 assert_eq!(sorted.len(), numbers.len());
540 }
541
542 #[test]
543 fn test_upper_face_units() {
544 let upper = ActionUnit::upper_face();
545 assert_eq!(upper.len(), 8);
546 assert!(upper.contains(&ActionUnit::AU1));
547 assert!(upper.contains(&ActionUnit::AU6));
548 assert!(upper.contains(&ActionUnit::AU10));
549 assert!(!upper.contains(&ActionUnit::AU12));
551 assert!(!upper.contains(&ActionUnit::AU43));
552 }
553
554 #[test]
555 fn test_lower_face_units() {
556 let lower = ActionUnit::lower_face();
557 assert_eq!(lower.len(), 16);
558 assert!(lower.contains(&ActionUnit::AU12));
559 assert!(lower.contains(&ActionUnit::AU26));
560 assert!(lower.contains(&ActionUnit::AU28));
561 assert!(!lower.contains(&ActionUnit::AU1));
563 assert!(!lower.contains(&ActionUnit::AU45));
564 }
565
566 #[test]
567 fn test_eye_units() {
568 let eyes = ActionUnit::eye_units();
569 assert_eq!(eyes.len(), 6);
570 assert!(eyes.contains(&ActionUnit::AU41));
571 assert!(eyes.contains(&ActionUnit::AU43));
572 assert!(eyes.contains(&ActionUnit::AU46));
573 assert!(!eyes.contains(&ActionUnit::AU1));
575 assert!(!eyes.contains(&ActionUnit::AU12));
576 }
577
578 #[test]
579 fn test_facs_mapper_add_and_evaluate() {
580 let mut mapper = FacsMapper::new();
581 mapper.add_mapping(ActionUnit::AU12, "smile", 0.9);
582 mapper.add_mapping(ActionUnit::AU6, "cheek", 0.7);
583
584 let mut state: FacsState = HashMap::new();
585 state.insert(ActionUnit::AU12, 1.0);
586 state.insert(ActionUnit::AU6, 0.5);
587
588 let weights = mapper.evaluate(&state);
589 let smile = *weights.get("smile").expect("smile morph missing");
590 let cheek = *weights.get("cheek").expect("cheek morph missing");
591
592 assert!((smile - 0.9).abs() < 1e-5, "smile={smile}");
593 assert!((cheek - 0.35).abs() < 1e-5, "cheek={cheek}");
594 }
595
596 #[test]
597 fn test_default_facs_mapper() {
598 let mapper = default_facs_mapper();
599
600 let m12 = mapper.mappings_for(&ActionUnit::AU12);
602 assert!(!m12.is_empty());
603 let has_smile = m12.iter().any(|(n, _)| n == "smile_mouth");
604 assert!(has_smile, "AU12 should map to smile_mouth");
605
606 let m45 = mapper.mappings_for(&ActionUnit::AU45);
608 let has_blink_l = m45.iter().any(|(n, _)| n == "blink_l");
609 let has_blink_r = m45.iter().any(|(n, _)| n == "blink_r");
610 assert!(has_blink_l, "AU45 missing blink_l");
611 assert!(has_blink_r, "AU45 missing blink_r");
612
613 let m26 = mapper.mappings_for(&ActionUnit::AU26);
615 let jaw_w = m26.iter().find(|(n, _)| n == "jaw_drop").map(|(_, w)| *w);
616 assert_eq!(jaw_w, Some(0.9));
617 }
618
619 #[test]
620 fn test_emotion_to_facs_happy() {
621 let state = emotion_to_facs("happy");
622 assert_eq!(state.get(&ActionUnit::AU6), Some(&0.8));
623 assert_eq!(state.get(&ActionUnit::AU12), Some(&0.9));
624 assert!(!state.contains_key(&ActionUnit::AU4));
626 }
627
628 #[test]
629 fn test_emotion_to_facs_angry() {
630 let state = emotion_to_facs("angry");
631 assert_eq!(state.get(&ActionUnit::AU4), Some(&0.9));
632 assert_eq!(state.get(&ActionUnit::AU5), Some(&0.6));
633 assert_eq!(state.get(&ActionUnit::AU7), Some(&0.8));
634 assert_eq!(state.get(&ActionUnit::AU23), Some(&0.7));
635 }
636
637 #[test]
638 fn test_facs_intensity_from_letter() {
639 assert_eq!(
640 FacsIntensity::from_letter('A')
641 .expect("should succeed")
642 .to_normalized(),
643 0.10
644 );
645 assert_eq!(
646 FacsIntensity::from_letter('B')
647 .expect("should succeed")
648 .to_normalized(),
649 0.30
650 );
651 assert_eq!(
652 FacsIntensity::from_letter('C')
653 .expect("should succeed")
654 .to_normalized(),
655 0.50
656 );
657 assert_eq!(
658 FacsIntensity::from_letter('D')
659 .expect("should succeed")
660 .to_normalized(),
661 0.75
662 );
663 assert_eq!(
664 FacsIntensity::from_letter('E')
665 .expect("should succeed")
666 .to_normalized(),
667 1.00
668 );
669 assert_eq!(
671 FacsIntensity::from_letter('e')
672 .expect("should succeed")
673 .to_normalized(),
674 1.00
675 );
676 assert!(FacsIntensity::from_letter('Z').is_none());
678 }
679
680 #[test]
681 fn test_facs_intensity_normalized() {
682 let fi = FacsIntensity::from_normalized(0.6);
683 assert!((fi.to_normalized() - 0.6).abs() < 1e-6);
684
685 let over = FacsIntensity::from_normalized(1.5);
687 assert_eq!(over.to_normalized(), 1.0);
688 let under = FacsIntensity::from_normalized(-0.5);
689 assert_eq!(under.to_normalized(), 0.0);
690 }
691
692 #[test]
693 fn test_parse_facs_string_simple() {
694 let state = parse_facs_string("AU12");
695 assert_eq!(state.get(&ActionUnit::AU12), Some(&1.0));
696 assert_eq!(state.len(), 1);
697 }
698
699 #[test]
700 fn test_parse_facs_string_multi() {
701 let state = parse_facs_string("AU1+AU6+AU12E");
702 assert_eq!(state.get(&ActionUnit::AU1), Some(&1.0));
703 assert_eq!(state.get(&ActionUnit::AU6), Some(&1.0));
704 assert_eq!(state.get(&ActionUnit::AU12), Some(&1.0)); assert_eq!(state.len(), 3);
706
707 let state2 = parse_facs_string("AU4A+AU12C");
709 assert_eq!(state2.get(&ActionUnit::AU4), Some(&0.10));
710 assert_eq!(state2.get(&ActionUnit::AU12), Some(&0.50));
711 }
712}