Skip to main content

oxihuman_morph/
parametric_face.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Parametric face model with FACS action units and expression composition.
5
6use std::collections::HashMap;
7
8/// A single named face parameter.
9#[allow(dead_code)]
10#[derive(Clone, Debug)]
11pub struct FaceParam {
12    pub name: String,
13    /// Normalized value: 0..1, or −1..1 for bilateral parameters.
14    pub value: f32,
15    /// `true` = both sides move together.
16    pub symmetric: bool,
17}
18
19/// Container of all face parameters.
20#[allow(dead_code)]
21#[derive(Clone, Debug, Default)]
22pub struct FaceModel {
23    pub params: HashMap<String, FaceParam>,
24}
25
26/// A FACS action unit that drives a set of morph weights.
27#[allow(dead_code)]
28#[derive(Clone, Debug)]
29pub struct FaceActionUnit {
30    /// FACS AU number (e.g. 1 = inner brow raise).
31    pub au_id: u8,
32    pub name: String,
33    pub description: String,
34    /// Morph parameter → base weight mapping.
35    pub morph_weights: HashMap<String, f32>,
36    pub bilateral: bool,
37}
38
39// ── standard params ───────────────────────────────────────────────────────────
40
41/// Return at least 15 standard face parameters.
42#[allow(dead_code)]
43pub fn standard_face_params() -> Vec<FaceParam> {
44    fn p(name: &str, sym: bool) -> FaceParam {
45        FaceParam {
46            name: name.to_owned(),
47            value: 0.0,
48            symmetric: sym,
49        }
50    }
51    vec![
52        p("brow_raise_l", false),
53        p("brow_raise_r", false),
54        p("brow_furrow", true),
55        p("eye_open_l", false),
56        p("eye_open_r", false),
57        p("eye_squint", true),
58        p("cheek_puff", true),
59        p("lip_corner_pull", true),
60        p("lip_pucker", true),
61        p("jaw_open", true),
62        p("chin_raise", true),
63        p("nose_wrinkle", true),
64        p("upper_lid_l", false),
65        p("upper_lid_r", false),
66        p("smile_l", false),
67        p("smile_r", false),
68    ]
69}
70
71/// Return at least 10 FACS action units mapped to morph weights.
72#[allow(dead_code)]
73pub fn standard_face_action_units() -> Vec<FaceActionUnit> {
74    fn au(
75        id: u8,
76        name: &str,
77        desc: &str,
78        weights: &[(&str, f32)],
79        bilateral: bool,
80    ) -> FaceActionUnit {
81        FaceActionUnit {
82            au_id: id,
83            name: name.to_owned(),
84            description: desc.to_owned(),
85            morph_weights: weights.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
86            bilateral,
87        }
88    }
89    vec![
90        au(
91            1,
92            "AU1",
93            "Inner brow raise",
94            &[("brow_raise_l", 0.5), ("brow_raise_r", 0.5)],
95            true,
96        ),
97        au(
98            2,
99            "AU2",
100            "Outer brow raise",
101            &[("brow_raise_l", 1.0), ("brow_raise_r", 1.0)],
102            true,
103        ),
104        au(4, "AU4", "Brow lowerer", &[("brow_furrow", 1.0)], true),
105        au(
106            5,
107            "AU5",
108            "Upper lid raiser",
109            &[("upper_lid_l", 1.0), ("upper_lid_r", 1.0)],
110            true,
111        ),
112        au(6, "AU6", "Cheek raiser", &[("cheek_puff", 0.8)], true),
113        au(7, "AU7", "Lid tightener", &[("eye_squint", 1.0)], true),
114        au(
115            10,
116            "AU10",
117            "Upper lip raiser",
118            &[("lip_corner_pull", 0.5)],
119            true,
120        ),
121        au(
122            12,
123            "AU12",
124            "Lip corner puller",
125            &[("smile_l", 1.0), ("smile_r", 1.0)],
126            true,
127        ),
128        au(17, "AU17", "Chin raiser", &[("chin_raise", 1.0)], true),
129        au(
130            25,
131            "AU25",
132            "Lips part",
133            &[("jaw_open", 0.4), ("lip_pucker", -0.2)],
134            true,
135        ),
136    ]
137}
138
139// ── FaceModel impl ────────────────────────────────────────────────────────────
140
141impl FaceModel {
142    /// Create an empty face model.
143    #[allow(dead_code)]
144    pub fn new() -> Self {
145        Self {
146            params: HashMap::new(),
147        }
148    }
149
150    /// Set a parameter value (inserts if absent).
151    #[allow(dead_code)]
152    pub fn set_param(&mut self, name: &str, value: f32) {
153        self.params
154            .entry(name.to_owned())
155            .and_modify(|p| p.value = value)
156            .or_insert(FaceParam {
157                name: name.to_owned(),
158                value,
159                symmetric: false,
160            });
161    }
162
163    /// Get a parameter value (returns 0.0 if absent).
164    #[allow(dead_code)]
165    pub fn get_param(&self, name: &str) -> f32 {
166        self.params.get(name).map(|p| p.value).unwrap_or(0.0)
167    }
168
169    /// Apply a FACS action unit at the given intensity (0..1).
170    #[allow(dead_code)]
171    pub fn apply_action_unit(&mut self, au: &FaceActionUnit, intensity: f32) {
172        for (param, &weight) in &au.morph_weights {
173            let v = weight * intensity;
174            self.set_param(param, v);
175        }
176    }
177
178    /// Flatten all parameters to a morph weight map.
179    #[allow(dead_code)]
180    pub fn compose_expression(&self) -> HashMap<String, f32> {
181        self.params
182            .iter()
183            .map(|(k, p)| (k.clone(), p.value))
184            .collect()
185    }
186
187    /// Reset all parameters to 0.0.
188    #[allow(dead_code)]
189    pub fn reset(&mut self) {
190        for p in self.params.values_mut() {
191            p.value = 0.0;
192        }
193    }
194}
195
196// ── free functions ────────────────────────────────────────────────────────────
197
198/// Linearly interpolate all parameters between models `a` and `b` at position `t`.
199#[allow(dead_code)]
200pub fn blend_face_params(a: &FaceModel, b: &FaceModel, t: f32) -> FaceModel {
201    let t = t.clamp(0.0, 1.0);
202    let mut result = FaceModel::new();
203    // collect all param names
204    let names: std::collections::HashSet<&String> =
205        a.params.keys().chain(b.params.keys()).collect();
206    for name in names {
207        let va = a.get_param(name);
208        let vb = b.get_param(name);
209        let sym = a
210            .params
211            .get(name)
212            .or_else(|| b.params.get(name))
213            .map(|p| p.symmetric)
214            .unwrap_or(false);
215        result.params.insert(
216            name.clone(),
217            FaceParam {
218                name: name.clone(),
219                value: va + (vb - va) * t,
220                symmetric: sym,
221            },
222        );
223    }
224    result
225}
226
227/// Return expression presets (at least 6).
228#[allow(dead_code)]
229pub fn expression_presets() -> HashMap<String, HashMap<String, f32>> {
230    let mut map: HashMap<String, HashMap<String, f32>> = HashMap::new();
231
232    // neutral — all zero
233    map.insert("neutral".to_owned(), HashMap::new());
234
235    // happy
236    map.insert(
237        "happy".to_owned(),
238        [
239            ("smile_l", 0.9),
240            ("smile_r", 0.9),
241            ("cheek_puff", 0.3),
242            ("eye_squint", 0.2),
243            ("lip_corner_pull", 0.8),
244        ]
245        .iter()
246        .map(|(k, v)| (k.to_string(), *v))
247        .collect(),
248    );
249
250    // sad
251    map.insert(
252        "sad".to_owned(),
253        [
254            ("brow_furrow", 0.6),
255            ("brow_raise_l", -0.3),
256            ("brow_raise_r", -0.3),
257            ("lip_corner_pull", -0.5),
258            ("jaw_open", 0.1),
259        ]
260        .iter()
261        .map(|(k, v)| (k.to_string(), *v))
262        .collect(),
263    );
264
265    // surprised
266    map.insert(
267        "surprised".to_owned(),
268        [
269            ("brow_raise_l", 1.0),
270            ("brow_raise_r", 1.0),
271            ("eye_open_l", 1.0),
272            ("eye_open_r", 1.0),
273            ("jaw_open", 0.6),
274        ]
275        .iter()
276        .map(|(k, v)| (k.to_string(), *v))
277        .collect(),
278    );
279
280    // angry
281    map.insert(
282        "angry".to_owned(),
283        [
284            ("brow_furrow", 1.0),
285            ("eye_squint", 0.5),
286            ("nose_wrinkle", 0.6),
287            ("lip_corner_pull", -0.4),
288        ]
289        .iter()
290        .map(|(k, v)| (k.to_string(), *v))
291        .collect(),
292    );
293
294    // disgusted
295    map.insert(
296        "disgusted".to_owned(),
297        [
298            ("nose_wrinkle", 1.0),
299            ("lip_pucker", 0.4),
300            ("lip_corner_pull", -0.5),
301            ("chin_raise", 0.3),
302        ]
303        .iter()
304        .map(|(k, v)| (k.to_string(), *v))
305        .collect(),
306    );
307
308    // fearful (7th preset)
309    map.insert(
310        "fearful".to_owned(),
311        [
312            ("brow_raise_l", 0.7),
313            ("brow_raise_r", 0.7),
314            ("eye_open_l", 0.8),
315            ("eye_open_r", 0.8),
316            ("jaw_open", 0.3),
317            ("lip_corner_pull", -0.2),
318        ]
319        .iter()
320        .map(|(k, v)| (k.to_string(), *v))
321        .collect(),
322    );
323
324    map
325}
326
327/// Apply an expression preset to a face model.
328#[allow(dead_code)]
329pub fn apply_expression_preset(model: &mut FaceModel, preset: &HashMap<String, f32>) {
330    for (name, &value) in preset {
331        model.set_param(name, value);
332    }
333}
334
335// ── tests ─────────────────────────────────────────────────────────────────────
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    // 1. standard_face_params >= 15
342    #[test]
343    fn test_standard_face_params_count() {
344        assert!(standard_face_params().len() >= 15);
345    }
346
347    // 2. standard_face_action_units >= 10
348    #[test]
349    fn test_standard_face_action_units_count() {
350        assert!(standard_face_action_units().len() >= 10);
351    }
352
353    // 3. FaceModel::new empty params
354    #[test]
355    fn test_face_model_new_empty() {
356        let m = FaceModel::new();
357        assert!(m.params.is_empty());
358    }
359
360    // 4. set_param / get_param round-trip
361    #[test]
362    fn test_set_get_param() {
363        let mut m = FaceModel::new();
364        m.set_param("jaw_open", 0.7);
365        assert!((m.get_param("jaw_open") - 0.7).abs() < 1e-6);
366    }
367
368    // 5. get_param returns 0.0 for absent key
369    #[test]
370    fn test_get_param_absent() {
371        let m = FaceModel::new();
372        assert!((m.get_param("nonexistent") - 0.0).abs() < 1e-6);
373    }
374
375    // 6. apply_action_unit scales morph weights by intensity
376    #[test]
377    fn test_apply_action_unit_scales() {
378        let aus = standard_face_action_units();
379        let au12 = aus
380            .iter()
381            .find(|au| au.au_id == 12)
382            .expect("should succeed");
383        let mut model = FaceModel::new();
384        model.apply_action_unit(au12, 0.5);
385        // AU12 drives smile_l with weight 1.0 × 0.5 = 0.5
386        assert!((model.get_param("smile_l") - 0.5).abs() < 1e-5);
387    }
388
389    // 7. compose_expression returns map with all params
390    #[test]
391    fn test_compose_expression_contains_params() {
392        let mut m = FaceModel::new();
393        m.set_param("brow_furrow", 0.3);
394        m.set_param("jaw_open", 0.6);
395        let expr = m.compose_expression();
396        assert!(expr.contains_key("brow_furrow"));
397        assert!(expr.contains_key("jaw_open"));
398    }
399
400    // 8. reset zeros all params
401    #[test]
402    fn test_reset_zeros_all() {
403        let mut m = FaceModel::new();
404        m.set_param("smile_l", 0.8);
405        m.set_param("jaw_open", 0.4);
406        m.reset();
407        for p in m.params.values() {
408            assert!((p.value).abs() < 1e-6, "param {} should be 0", p.name);
409        }
410    }
411
412    // 9. blend at t=0 returns a
413    #[test]
414    fn test_blend_t0_is_a() {
415        let mut a = FaceModel::new();
416        a.set_param("x", 1.0);
417        let mut b = FaceModel::new();
418        b.set_param("x", 0.0);
419        let result = blend_face_params(&a, &b, 0.0);
420        assert!((result.get_param("x") - 1.0).abs() < 1e-5);
421    }
422
423    // 10. blend at t=1 returns b
424    #[test]
425    fn test_blend_t1_is_b() {
426        let mut a = FaceModel::new();
427        a.set_param("x", 0.0);
428        let mut b = FaceModel::new();
429        b.set_param("x", 1.0);
430        let result = blend_face_params(&a, &b, 1.0);
431        assert!((result.get_param("x") - 1.0).abs() < 1e-5);
432    }
433
434    // 11. expression_presets has >= 6 presets
435    #[test]
436    fn test_expression_presets_count() {
437        let presets = expression_presets();
438        assert!(
439            presets.len() >= 6,
440            "expected >= 6 presets, got {}",
441            presets.len()
442        );
443    }
444
445    // 12. apply_expression_preset applies values
446    #[test]
447    fn test_apply_expression_preset_applies() {
448        let presets = expression_presets();
449        let happy = &presets["happy"];
450        let mut model = FaceModel::new();
451        apply_expression_preset(&mut model, happy);
452        // smile_l should be set
453        assert!(
454            model.get_param("smile_l") > 0.0,
455            "happy preset should set smile_l"
456        );
457    }
458
459    // 13. neutral preset — explicitly all-zero (no entries)
460    #[test]
461    fn test_neutral_preset_all_zero() {
462        let presets = expression_presets();
463        let neutral = &presets["neutral"];
464        for &v in neutral.values() {
465            assert!((v).abs() < 1e-6, "neutral preset should be all-zero");
466        }
467    }
468
469    // 14. bilateral param flag set correctly for symmetric params
470    #[test]
471    fn test_bilateral_param_symmetric_flag() {
472        let params = standard_face_params();
473        let jaw = params
474            .iter()
475            .find(|p| p.name == "jaw_open")
476            .expect("should succeed");
477        assert!(jaw.symmetric, "jaw_open should be symmetric");
478        let brow_l = params
479            .iter()
480            .find(|p| p.name == "brow_raise_l")
481            .expect("should succeed");
482        assert!(!brow_l.symmetric, "brow_raise_l should not be symmetric");
483    }
484
485    // 15. FaceActionUnit bilateral flag set for AU12
486    #[test]
487    fn test_action_unit_bilateral_flag() {
488        let aus = standard_face_action_units();
489        let au12 = aus
490            .iter()
491            .find(|au| au.au_id == 12)
492            .expect("should succeed");
493        assert!(au12.bilateral);
494    }
495
496    // 16. blend midpoint
497    #[test]
498    fn test_blend_midpoint() {
499        let mut a = FaceModel::new();
500        a.set_param("x", 0.0);
501        let mut b = FaceModel::new();
502        b.set_param("x", 1.0);
503        let result = blend_face_params(&a, &b, 0.5);
504        assert!((result.get_param("x") - 0.5).abs() < 1e-5);
505    }
506}