pyfaceau 1.3.0__tar.gz → 1.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyfaceau-1.3.3/PKG-INFO +84 -0
- pyfaceau-1.3.3/README.md +39 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/alignment/calc_params.py +40 -49
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/alignment/face_aligner.py +170 -30
- pyfaceau-1.3.3/pyfaceau/config.py +118 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/data/hdf5_dataset.py +335 -20
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/data/training_data_generator.py +14 -18
- pyfaceau-1.3.3/pyfaceau/features/triangulation.py +109 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/nn/au_prediction_net.py +315 -0
- pyfaceau-1.3.3/pyfaceau/nn/fast_pipeline.py +364 -0
- pyfaceau-1.3.3/pyfaceau/nn/landmark_pose_net.py +972 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/nn/train_au_prediction.py +137 -24
- pyfaceau-1.3.3/pyfaceau/nn/train_landmark_pose.py +819 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/pipeline.py +238 -54
- pyfaceau-1.3.3/pyfaceau/prediction/online_au_correction.py +256 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/utils/cython_extensions/cython_rotation_update.pyx +36 -46
- pyfaceau-1.3.3/pyfaceau.egg-info/PKG-INFO +84 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau.egg-info/SOURCES.txt +3 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyproject.toml +1 -1
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/setup.py +1 -1
- pyfaceau-1.3.0/PKG-INFO +0 -478
- pyfaceau-1.3.0/README.md +0 -433
- pyfaceau-1.3.0/pyfaceau/features/triangulation.py +0 -64
- pyfaceau-1.3.0/pyfaceau/nn/landmark_pose_net.py +0 -497
- pyfaceau-1.3.0/pyfaceau/nn/train_landmark_pose.py +0 -508
- pyfaceau-1.3.0/pyfaceau.egg-info/PKG-INFO +0 -478
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/LICENSE +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/MANIFEST.in +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/alignment/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/alignment/numba_calcparams_accelerator.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/alignment/paw.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/data/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/data/quality_filter.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/detectors/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/detectors/extract_mtcnn_weights.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/detectors/openface_mtcnn.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/detectors/pfld.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/detectors/pymtcnn_detector.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/detectors/retinaface.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/download_weights.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/features/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/features/histogram_median_tracker.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/features/pdm.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/nn/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/nn/au_prediction_inference.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/nn/landmark_pose_inference.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/parallel_pipeline.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/prediction/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/prediction/au_predictor.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/prediction/batched_au_predictor.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/prediction/model_parser.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/prediction/running_median.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/prediction/running_median_fallback.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/processor.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/refinement/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/refinement/pdm.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/refinement/svr_patch_expert.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/refinement/targeted_refiner.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/utils/__init__.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/utils/cython_extensions/cython_histogram_median.pyx +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau/utils/cython_extensions/setup.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau.egg-info/dependency_links.txt +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau.egg-info/entry_points.txt +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau.egg-info/not-zip-safe +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau.egg-info/requires.txt +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau.egg-info/top_level.txt +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/pyfaceau_gui.py +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/requirements.txt +0 -0
- {pyfaceau-1.3.0 → pyfaceau-1.3.3}/setup.cfg +0 -0
pyfaceau-1.3.3/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyfaceau
|
|
3
|
+
Version: 1.3.3
|
|
4
|
+
Summary: Pure Python OpenFace 2.2 AU extraction with CLNF landmark refinement
|
|
5
|
+
Home-page: https://github.com/johnwilsoniv/face-analysis
|
|
6
|
+
Author: John Wilson
|
|
7
|
+
Author-email:
|
|
8
|
+
License: CC BY-NC 4.0
|
|
9
|
+
Project-URL: Homepage, https://github.com/johnwilsoniv/pyfaceau
|
|
10
|
+
Project-URL: Documentation, https://github.com/johnwilsoniv/pyfaceau
|
|
11
|
+
Project-URL: Repository, https://github.com/johnwilsoniv/pyfaceau
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/johnwilsoniv/pyfaceau/issues
|
|
13
|
+
Keywords: facial-action-units,openface,computer-vision,facial-analysis,emotion-recognition
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
18
|
+
Classifier: License :: Other/Proprietary License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.20.0
|
|
28
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
29
|
+
Requires-Dist: pandas>=1.3.0
|
|
30
|
+
Requires-Dist: onnxruntime>=1.10.0
|
|
31
|
+
Requires-Dist: scipy>=1.7.0
|
|
32
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
33
|
+
Requires-Dist: tqdm>=4.62.0
|
|
34
|
+
Requires-Dist: pyfhog>=0.1.0
|
|
35
|
+
Requires-Dist: pyclnf>=0.2.0
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: flake8>=4.0.0; extra == "dev"
|
|
40
|
+
Provides-Extra: accel
|
|
41
|
+
Requires-Dist: onnxruntime-coreml>=1.10.0; extra == "accel"
|
|
42
|
+
Dynamic: home-page
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
Dynamic: requires-python
|
|
45
|
+
|
|
46
|
+
# pyfaceau
|
|
47
|
+
|
|
48
|
+
Python implementation of OpenFace 2.2's Facial Action Unit extraction pipeline.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pyfaceau
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pyfaceau import FaceAnalyzer
|
|
60
|
+
|
|
61
|
+
analyzer = FaceAnalyzer()
|
|
62
|
+
result = analyzer.analyze(image)
|
|
63
|
+
|
|
64
|
+
print(result.au_intensities) # 17 action unit intensities
|
|
65
|
+
print(result.landmarks) # 68 facial landmarks
|
|
66
|
+
print(result.pose) # head pose
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## What it does
|
|
70
|
+
|
|
71
|
+
- Extracts 17 facial action units (AU01, AU02, AU04, AU05, AU06, AU07, AU09, AU10, AU12, AU14, AU15, AU17, AU20, AU23, AU25, AU26, AU45)
|
|
72
|
+
- Detects 68 facial landmarks via [pyclnf](https://github.com/johnwilsoniv/pyclnf)
|
|
73
|
+
- Estimates 3D head pose
|
|
74
|
+
- No C++ compilation required
|
|
75
|
+
|
|
76
|
+
## Citation
|
|
77
|
+
|
|
78
|
+
If you use this in research, please cite:
|
|
79
|
+
|
|
80
|
+
> Wilson IV, J., Rosenberg, J., Gray, M. L., & Razavi, C. R. (2025). A split-face computer vision/machine learning assessment of facial paralysis using facial action units. *Facial Plastic Surgery & Aesthetic Medicine*. https://doi.org/10.1177/26893614251394382
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
CC BY-NC 4.0 — free for non-commercial use with attribution.
|
pyfaceau-1.3.3/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pyfaceau
|
|
2
|
+
|
|
3
|
+
Python implementation of OpenFace 2.2's Facial Action Unit extraction pipeline.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pyfaceau
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from pyfaceau import FaceAnalyzer
|
|
15
|
+
|
|
16
|
+
analyzer = FaceAnalyzer()
|
|
17
|
+
result = analyzer.analyze(image)
|
|
18
|
+
|
|
19
|
+
print(result.au_intensities) # 17 action unit intensities
|
|
20
|
+
print(result.landmarks) # 68 facial landmarks
|
|
21
|
+
print(result.pose) # head pose
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
- Extracts 17 facial action units (AU01, AU02, AU04, AU05, AU06, AU07, AU09, AU10, AU12, AU14, AU15, AU17, AU20, AU23, AU25, AU26, AU45)
|
|
27
|
+
- Detects 68 facial landmarks via [pyclnf](https://github.com/johnwilsoniv/pyclnf)
|
|
28
|
+
- Estimates 3D head pose
|
|
29
|
+
- No C++ compilation required
|
|
30
|
+
|
|
31
|
+
## Citation
|
|
32
|
+
|
|
33
|
+
If you use this in research, please cite:
|
|
34
|
+
|
|
35
|
+
> Wilson IV, J., Rosenberg, J., Gray, M. L., & Razavi, C. R. (2025). A split-face computer vision/machine learning assessment of facial paralysis using facial action units. *Facial Plastic Surgery & Aesthetic Medicine*. https://doi.org/10.1177/26893614251394382
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
CC BY-NC 4.0 — free for non-commercial use with attribution.
|
|
@@ -17,6 +17,7 @@ Date: 2025-10-29
|
|
|
17
17
|
import numpy as np
|
|
18
18
|
from scipy import linalg
|
|
19
19
|
import cv2
|
|
20
|
+
import os
|
|
20
21
|
|
|
21
22
|
# Try to import Cython-optimized rotation update for 99.9% accuracy
|
|
22
23
|
try:
|
|
@@ -26,10 +27,12 @@ try:
|
|
|
26
27
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
27
28
|
from cython_rotation_update import update_rotation_cython
|
|
28
29
|
CYTHON_AVAILABLE = True
|
|
29
|
-
|
|
30
|
+
if os.environ.get('PYFACEAU_VERBOSE', '0') == '1':
|
|
31
|
+
print("Cython rotation update module loaded - targeting 99.9% accuracy")
|
|
30
32
|
except ImportError:
|
|
31
33
|
CYTHON_AVAILABLE = False
|
|
32
|
-
|
|
34
|
+
if os.environ.get('PYFACEAU_VERBOSE', '0') == '1':
|
|
35
|
+
print("Warning: Cython rotation update not available - using Python (99.45% accuracy)")
|
|
33
36
|
|
|
34
37
|
# Try to import Numba JIT-accelerated CalcParams functions for 2-5x speedup
|
|
35
38
|
try:
|
|
@@ -41,7 +44,6 @@ try:
|
|
|
41
44
|
NUMBA_AVAILABLE = True
|
|
42
45
|
except ImportError:
|
|
43
46
|
NUMBA_AVAILABLE = False
|
|
44
|
-
print("Warning: Numba JIT accelerator not available - using standard Python (slower)")
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
class CalcParams:
|
|
@@ -97,10 +99,10 @@ class CalcParams:
|
|
|
97
99
|
@staticmethod
|
|
98
100
|
def rotation_matrix_to_euler(R):
|
|
99
101
|
"""
|
|
100
|
-
Convert 3x3 rotation matrix to Euler angles
|
|
102
|
+
Convert 3x3 rotation matrix to Euler angles
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
Uses
|
|
104
|
+
EXACTLY matches RotationMatrix2Euler() from RotationHelpers.h lines 73-90
|
|
105
|
+
Uses simple quaternion extraction (assumes trace+1 > 0)
|
|
104
106
|
|
|
105
107
|
Args:
|
|
106
108
|
R: 3x3 rotation matrix
|
|
@@ -108,47 +110,34 @@ class CalcParams:
|
|
|
108
110
|
Returns:
|
|
109
111
|
(rx, ry, rz) Euler angles in radians
|
|
110
112
|
"""
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
s = np.sqrt(1.0 + R[0,0] - R[1,1] - R[2,2]) * 2.0 # s = 4*q1
|
|
125
|
-
q0 = (R[2,1] - R[1,2]) / s
|
|
126
|
-
q1 = 0.25 * s
|
|
127
|
-
q2 = (R[0,1] + R[1,0]) / s
|
|
128
|
-
q3 = (R[0,2] + R[2,0]) / s
|
|
129
|
-
elif R[1,1] > R[2,2]:
|
|
130
|
-
# q2 is largest component
|
|
131
|
-
s = np.sqrt(1.0 + R[1,1] - R[0,0] - R[2,2]) * 2.0 # s = 4*q2
|
|
132
|
-
q0 = (R[0,2] - R[2,0]) / s
|
|
133
|
-
q1 = (R[0,1] + R[1,0]) / s
|
|
134
|
-
q2 = 0.25 * s
|
|
135
|
-
q3 = (R[1,2] + R[2,1]) / s
|
|
136
|
-
else:
|
|
137
|
-
# q3 is largest component
|
|
138
|
-
s = np.sqrt(1.0 + R[2,2] - R[0,0] - R[1,1]) * 2.0 # s = 4*q3
|
|
139
|
-
q0 = (R[1,0] - R[0,1]) / s
|
|
140
|
-
q1 = (R[0,2] + R[2,0]) / s
|
|
141
|
-
q2 = (R[1,2] + R[2,1]) / s
|
|
142
|
-
q3 = 0.25 * s
|
|
143
|
-
|
|
144
|
-
# Quaternion to Euler angles
|
|
113
|
+
# EXACT C++ implementation from RotationHelpers.h
|
|
114
|
+
# float q0 = sqrt(1 + rotation_matrix(0, 0) + rotation_matrix(1, 1) + rotation_matrix(2, 2)) / 2.0f;
|
|
115
|
+
q0 = np.sqrt(1.0 + R[0,0] + R[1,1] + R[2,2]) / 2.0
|
|
116
|
+
|
|
117
|
+
# float q1 = (rotation_matrix(2, 1) - rotation_matrix(1, 2)) / (4.0f*q0);
|
|
118
|
+
q1 = (R[2,1] - R[1,2]) / (4.0 * q0)
|
|
119
|
+
# float q2 = (rotation_matrix(0, 2) - rotation_matrix(2, 0)) / (4.0f*q0);
|
|
120
|
+
q2 = (R[0,2] - R[2,0]) / (4.0 * q0)
|
|
121
|
+
# float q3 = (rotation_matrix(1, 0) - rotation_matrix(0, 1)) / (4.0f*q0);
|
|
122
|
+
q3 = (R[1,0] - R[0,1]) / (4.0 * q0)
|
|
123
|
+
|
|
124
|
+
# Quaternion to Euler angles (exactly as in C++)
|
|
125
|
+
# float t1 = 2.0f * (q0*q2 + q1*q3);
|
|
145
126
|
t1 = 2.0 * (q0*q2 + q1*q3)
|
|
146
|
-
t1 =
|
|
127
|
+
# if (t1 > 1) t1 = 1.0f; if (t1 < -1) t1 = -1.0f;
|
|
128
|
+
if t1 > 1.0:
|
|
129
|
+
t1 = 1.0
|
|
130
|
+
if t1 < -1.0:
|
|
131
|
+
t1 = -1.0
|
|
147
132
|
|
|
133
|
+
# float yaw = asin(t1);
|
|
148
134
|
yaw = np.arcsin(t1)
|
|
135
|
+
# float pitch = atan2(2.0f * (q0*q1 - q2*q3), q0*q0 - q1*q1 - q2*q2 + q3*q3);
|
|
149
136
|
pitch = np.arctan2(2.0 * (q0*q1 - q2*q3), q0*q0 - q1*q1 - q2*q2 + q3*q3)
|
|
137
|
+
# float roll = atan2(2.0f * (q0*q3 - q1*q2), q0*q0 + q1*q1 - q2*q2 - q3*q3);
|
|
150
138
|
roll = np.arctan2(2.0 * (q0*q3 - q1*q2), q0*q0 + q1*q1 - q2*q2 - q3*q3)
|
|
151
139
|
|
|
140
|
+
# return cv::Vec3f(pitch, yaw, roll);
|
|
152
141
|
return np.array([pitch, yaw, roll], dtype=np.float32)
|
|
153
142
|
|
|
154
143
|
@staticmethod
|
|
@@ -381,15 +370,15 @@ class CalcParams:
|
|
|
381
370
|
euler_new = update_rotation_cython(euler_current, delta_rotation)
|
|
382
371
|
updated_global[1:4] = euler_new
|
|
383
372
|
else:
|
|
384
|
-
# Fallback to Python implementation
|
|
373
|
+
# Fallback to Python implementation matching C++ EXACTLY
|
|
385
374
|
# Get current rotation matrix
|
|
386
375
|
euler_current = params_global[1:4]
|
|
387
376
|
R1 = self.euler_to_rotation_matrix(euler_current)
|
|
388
377
|
|
|
389
378
|
# Construct incremental rotation matrix R'
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
379
|
+
# R2(1,2) = -1.0*(R2(2,1) = delta_p.at<float>(1,0)); // wx
|
|
380
|
+
# R2(2,0) = -1.0*(R2(0,2) = delta_p.at<float>(2,0)); // wy
|
|
381
|
+
# R2(0,1) = -1.0*(R2(1,0) = delta_p.at<float>(3,0)); // wz
|
|
393
382
|
R2 = np.eye(3, dtype=np.float32)
|
|
394
383
|
R2[1, 2] = -delta_p[1] # -wx
|
|
395
384
|
R2[2, 1] = delta_p[1] # wx
|
|
@@ -404,10 +393,12 @@ class CalcParams:
|
|
|
404
393
|
# Combine rotations
|
|
405
394
|
R3 = R1 @ R2
|
|
406
395
|
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
|
|
396
|
+
# C++ uses: RotationMatrix2AxisAngle -> AxisAngle2Euler
|
|
397
|
+
# cv::Vec3f axis_angle = Utilities::RotationMatrix2AxisAngle(R3);
|
|
398
|
+
# cv::Vec3f euler = Utilities::AxisAngle2Euler(axis_angle);
|
|
399
|
+
# This is: Rodrigues(R3) -> Rodrigues(axis_angle) -> RotationMatrix2Euler
|
|
400
|
+
axis_angle = self.rotation_matrix_to_axis_angle(R3)
|
|
401
|
+
euler_new = self.axis_angle_to_euler(axis_angle)
|
|
411
402
|
|
|
412
403
|
# Handle numerical instability
|
|
413
404
|
if np.any(np.isnan(euler_new)):
|
|
@@ -37,7 +37,7 @@ class OpenFace22FaceAligner:
|
|
|
37
37
|
# Testing shows removing eyes improves STABILITY but ruins MAGNITUDE (31° vs 5°)
|
|
38
38
|
RIGID_INDICES = [1, 2, 3, 4, 12, 13, 14, 15, 27, 28, 29, 31, 32, 33, 34, 35, 36, 39, 40, 41, 42, 45, 46, 47]
|
|
39
39
|
|
|
40
|
-
def __init__(self, pdm_file: str, sim_scale: float = 0.7, output_size: Tuple[int, int] = (112, 112)):
|
|
40
|
+
def __init__(self, pdm_file: str, sim_scale: float = 0.7, output_size: Tuple[int, int] = (112, 112), y_offset: float = 0.0):
|
|
41
41
|
"""
|
|
42
42
|
Initialize face aligner with PDM reference shape
|
|
43
43
|
|
|
@@ -45,9 +45,12 @@ class OpenFace22FaceAligner:
|
|
|
45
45
|
pdm_file: Path to PDM model file (e.g., "pdm_68_multi_pie.txt")
|
|
46
46
|
sim_scale: Scaling factor for reference shape (default: 0.7 for AU analysis)
|
|
47
47
|
output_size: Output aligned face size in pixels (default: 112×112)
|
|
48
|
+
y_offset: Y-axis offset for centering (negative shifts face UP, default: 0.0)
|
|
49
|
+
Note: Non-zero values can disrupt HOG feature alignment with C++ models.
|
|
48
50
|
"""
|
|
49
51
|
self.sim_scale = sim_scale
|
|
50
52
|
self.output_width, self.output_height = output_size
|
|
53
|
+
self.y_offset = y_offset
|
|
51
54
|
|
|
52
55
|
# Load PDM and extract mean shape
|
|
53
56
|
print(f"Loading PDM from: {pdm_file}")
|
|
@@ -56,15 +59,17 @@ class OpenFace22FaceAligner:
|
|
|
56
59
|
# Preprocess mean shape: 204 values (68 landmarks × 3D) → 68 landmarks × 2D
|
|
57
60
|
# OpenFace C++ logic (Face_utils.cpp:112-119):
|
|
58
61
|
# 1. Scale mean shape by sim_scale
|
|
59
|
-
# 2.
|
|
60
|
-
# 3.
|
|
62
|
+
# 2. Extract X and Y coordinates (grouped format)
|
|
63
|
+
# 3. Stack to (68, 2) format
|
|
61
64
|
#
|
|
62
|
-
# CRITICAL: PDM stores as
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
# CRITICAL FIX: PDM stores as GROUPED format:
|
|
66
|
+
# [x0, x1, ..., x67, y0, y1, ..., y67, z0, z1, ..., z67]
|
|
67
|
+
# NOT interleaved: [x0, y0, x1, y1, ...]
|
|
68
|
+
# So we must: take first 68 as X, next 68 as Y, stack them
|
|
69
|
+
mean_shape_scaled = pdm.mean_shape.flatten() * sim_scale # (204,)
|
|
70
|
+
x_coords = mean_shape_scaled[:68] # First 68 = all X values
|
|
71
|
+
y_coords = mean_shape_scaled[68:136] # Next 68 = all Y values
|
|
72
|
+
self.reference_shape = np.column_stack([x_coords, y_coords]) # (68, 2)
|
|
68
73
|
|
|
69
74
|
print(f"Face aligner initialized")
|
|
70
75
|
print(f" Sim scale: {sim_scale}")
|
|
@@ -74,7 +79,8 @@ class OpenFace22FaceAligner:
|
|
|
74
79
|
|
|
75
80
|
def align_face(self, image: np.ndarray, landmarks_68: np.ndarray,
|
|
76
81
|
pose_tx: float, pose_ty: float, p_rz: float = 0.0,
|
|
77
|
-
apply_mask: bool = False, triangulation=None
|
|
82
|
+
apply_mask: bool = False, triangulation=None,
|
|
83
|
+
mask_style: str = 'detected') -> np.ndarray:
|
|
78
84
|
"""
|
|
79
85
|
Align face to canonical 112×112 reference frame
|
|
80
86
|
|
|
@@ -86,6 +92,8 @@ class OpenFace22FaceAligner:
|
|
|
86
92
|
p_rz: Pose rotation Z in radians (from OpenFace params_global[3])
|
|
87
93
|
apply_mask: If True, mask out regions outside the face (like OpenFace C++)
|
|
88
94
|
triangulation: TriangulationParser object (required if apply_mask=True)
|
|
95
|
+
mask_style: 'detected' uses warped detected landmarks (C++ OpenFace style),
|
|
96
|
+
'reference' uses reference shape (legacy behavior)
|
|
89
97
|
|
|
90
98
|
Returns:
|
|
91
99
|
aligned_face: 112×112 aligned face image (BGR format)
|
|
@@ -100,22 +108,12 @@ class OpenFace22FaceAligner:
|
|
|
100
108
|
source_rigid = self._extract_rigid_points(landmarks_68)
|
|
101
109
|
dest_rigid = self._extract_rigid_points(self.reference_shape)
|
|
102
110
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
# We need rotation FROM tilted TO canonical, which is -p_rz
|
|
110
|
-
angle = -p_rz
|
|
111
|
-
cos_a = np.cos(angle)
|
|
112
|
-
sin_a = np.sin(angle)
|
|
113
|
-
|
|
114
|
-
R = np.array([[cos_a, -sin_a],
|
|
115
|
-
[sin_a, cos_a]], dtype=np.float32)
|
|
116
|
-
|
|
117
|
-
# Combine scale and rotation
|
|
118
|
-
scale_rot_matrix = scale * R
|
|
111
|
+
# Match C++ exactly: use AlignShapesWithScale to compute BOTH scale and rotation
|
|
112
|
+
# via Kabsch algorithm. This does NOT use p_rz - the rotation comes from
|
|
113
|
+
# finding the optimal alignment between source and destination rigid points.
|
|
114
|
+
# C++ code: Face_utils.cpp line 127:
|
|
115
|
+
# cv::Matx22f scale_rot_matrix = Utilities::AlignShapesWithScale(source_landmarks, destination_landmarks);
|
|
116
|
+
scale_rot_matrix = self._align_shapes_with_scale(source_rigid, dest_rigid)
|
|
119
117
|
|
|
120
118
|
# Build 2×3 affine warp matrix using pose translation
|
|
121
119
|
warp_matrix = self._build_warp_matrix(scale_rot_matrix, pose_tx, pose_ty)
|
|
@@ -133,8 +131,20 @@ class OpenFace22FaceAligner:
|
|
|
133
131
|
if triangulation is None:
|
|
134
132
|
raise ValueError("triangulation required when apply_mask=True")
|
|
135
133
|
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
if mask_style == 'detected':
|
|
135
|
+
# C++ OpenFace style: transform detected landmarks by warp matrix
|
|
136
|
+
# This adapts the mask per-frame based on actual face shape
|
|
137
|
+
# Reference: Face_utils.cpp::AlignFaceMask() lines 186-209
|
|
138
|
+
warp_2d = scale_rot_matrix
|
|
139
|
+
translation = np.array([warp_matrix[0, 2], warp_matrix[1, 2]])
|
|
140
|
+
aligned_landmarks = landmarks_68 @ warp_2d.T + translation
|
|
141
|
+
else:
|
|
142
|
+
# Legacy style: use reference shape (consistent mask across frames)
|
|
143
|
+
center = np.array([self.output_width / 2, self.output_height / 2])
|
|
144
|
+
aligned_landmarks = self.reference_shape + center
|
|
145
|
+
# Apply correction shift for reference shape centering
|
|
146
|
+
aligned_landmarks[:, 0] += 5.0
|
|
147
|
+
aligned_landmarks[:, 1] += 3.0
|
|
138
148
|
|
|
139
149
|
# Adjust eyebrow landmarks upward to include forehead (like C++)
|
|
140
150
|
# Indices 17-26 are eyebrows, 0 and 16 are jaw corners
|
|
@@ -154,6 +164,80 @@ class OpenFace22FaceAligner:
|
|
|
154
164
|
|
|
155
165
|
return aligned_face
|
|
156
166
|
|
|
167
|
+
def align_face_with_matrix(self, image: np.ndarray, landmarks_68: np.ndarray,
|
|
168
|
+
pose_tx: float, pose_ty: float, p_rz: float = 0.0,
|
|
169
|
+
apply_mask: bool = False, triangulation=None,
|
|
170
|
+
mask_style: str = 'detected') -> tuple:
|
|
171
|
+
"""
|
|
172
|
+
Align face and return both the aligned image and the warp matrix.
|
|
173
|
+
|
|
174
|
+
This is the same as align_face() but also returns the 2x3 affine transform
|
|
175
|
+
matrix used for alignment, which can be used to transform landmarks from
|
|
176
|
+
original frame coordinates to aligned face coordinates.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
tuple: (aligned_face, warp_matrix)
|
|
180
|
+
- aligned_face: 112×112 aligned face image (BGR format)
|
|
181
|
+
- warp_matrix: (2, 3) affine transform matrix
|
|
182
|
+
"""
|
|
183
|
+
# Ensure landmarks are (68, 2) shape
|
|
184
|
+
if landmarks_68.shape == (136,):
|
|
185
|
+
landmarks_68 = landmarks_68.reshape(68, 2)
|
|
186
|
+
elif landmarks_68.shape != (68, 2):
|
|
187
|
+
raise ValueError(f"landmarks_68 must be (68, 2) or (136,), got {landmarks_68.shape}")
|
|
188
|
+
|
|
189
|
+
# Extract rigid points from both source and destination
|
|
190
|
+
source_rigid = self._extract_rigid_points(landmarks_68)
|
|
191
|
+
dest_rigid = self._extract_rigid_points(self.reference_shape)
|
|
192
|
+
|
|
193
|
+
# Compute scale-rotation matrix using Kabsch algorithm
|
|
194
|
+
scale_rot_matrix = self._align_shapes_with_scale(source_rigid, dest_rigid)
|
|
195
|
+
|
|
196
|
+
# Build 2×3 affine warp matrix using pose translation
|
|
197
|
+
warp_matrix = self._build_warp_matrix(scale_rot_matrix, pose_tx, pose_ty)
|
|
198
|
+
|
|
199
|
+
# Apply affine transformation
|
|
200
|
+
aligned_face = cv2.warpAffine(
|
|
201
|
+
image,
|
|
202
|
+
warp_matrix,
|
|
203
|
+
(self.output_width, self.output_height),
|
|
204
|
+
flags=cv2.INTER_LINEAR
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Apply face mask if requested
|
|
208
|
+
if apply_mask:
|
|
209
|
+
if triangulation is None:
|
|
210
|
+
raise ValueError("triangulation required when apply_mask=True")
|
|
211
|
+
|
|
212
|
+
if mask_style == 'detected':
|
|
213
|
+
# C++ OpenFace style: transform detected landmarks by warp matrix
|
|
214
|
+
warp_2d = scale_rot_matrix
|
|
215
|
+
translation = np.array([warp_matrix[0, 2], warp_matrix[1, 2]])
|
|
216
|
+
aligned_landmarks = landmarks_68 @ warp_2d.T + translation
|
|
217
|
+
else:
|
|
218
|
+
# Legacy style: use reference shape
|
|
219
|
+
center = np.array([self.output_width / 2, self.output_height / 2])
|
|
220
|
+
aligned_landmarks = self.reference_shape + center
|
|
221
|
+
aligned_landmarks[:, 0] += 5.0
|
|
222
|
+
aligned_landmarks[:, 1] += 3.0
|
|
223
|
+
|
|
224
|
+
# Adjust eyebrow landmarks upward to include forehead
|
|
225
|
+
forehead_offset = (30 / 0.7) * self.sim_scale
|
|
226
|
+
for idx in [0, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]:
|
|
227
|
+
aligned_landmarks[idx, 1] -= forehead_offset
|
|
228
|
+
|
|
229
|
+
# Create mask
|
|
230
|
+
mask = triangulation.create_face_mask(
|
|
231
|
+
aligned_landmarks,
|
|
232
|
+
self.output_width,
|
|
233
|
+
self.output_height
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Apply mask to each channel
|
|
237
|
+
aligned_face = cv2.bitwise_and(aligned_face, aligned_face, mask=mask)
|
|
238
|
+
|
|
239
|
+
return aligned_face, warp_matrix
|
|
240
|
+
|
|
157
241
|
def _transform_landmarks(self, landmarks: np.ndarray, warp_matrix: np.ndarray) -> np.ndarray:
|
|
158
242
|
"""
|
|
159
243
|
Transform landmarks using affine warp matrix
|
|
@@ -197,6 +281,60 @@ class OpenFace22FaceAligner:
|
|
|
197
281
|
|
|
198
282
|
return s_dst / s_src
|
|
199
283
|
|
|
284
|
+
def _align_shapes_with_scale(self, src: np.ndarray, dst: np.ndarray) -> np.ndarray:
|
|
285
|
+
"""
|
|
286
|
+
Compute scale AND rotation using Kabsch algorithm (like C++ AlignShapesWithScale)
|
|
287
|
+
|
|
288
|
+
This is the correct approach used by OpenFace C++:
|
|
289
|
+
1. Mean-normalize both point sets
|
|
290
|
+
2. Compute RMS scale for each
|
|
291
|
+
3. Normalize to unit scale
|
|
292
|
+
4. Use Kabsch/SVD to find optimal rotation
|
|
293
|
+
5. Return scale * rotation matrix
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
src: (N, 2) source points (detected landmarks)
|
|
297
|
+
dst: (N, 2) destination points (reference shape)
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
(2, 2) scale-rotation matrix
|
|
301
|
+
"""
|
|
302
|
+
n = src.shape[0]
|
|
303
|
+
|
|
304
|
+
# 1. Mean normalize both
|
|
305
|
+
src_mean = src.mean(axis=0)
|
|
306
|
+
dst_mean = dst.mean(axis=0)
|
|
307
|
+
src_centered = src - src_mean
|
|
308
|
+
dst_centered = dst - dst_mean
|
|
309
|
+
|
|
310
|
+
# 2. Compute RMS scale for each
|
|
311
|
+
s_src = np.sqrt(np.sum(src_centered ** 2) / n)
|
|
312
|
+
s_dst = np.sqrt(np.sum(dst_centered ** 2) / n)
|
|
313
|
+
|
|
314
|
+
# 3. Normalize to unit scale
|
|
315
|
+
src_normed = src_centered / s_src
|
|
316
|
+
dst_normed = dst_centered / s_dst
|
|
317
|
+
|
|
318
|
+
# 4. Kabsch algorithm (SVD) to find optimal rotation
|
|
319
|
+
H = src_normed.T @ dst_normed
|
|
320
|
+
U, S, Vt = np.linalg.svd(H)
|
|
321
|
+
|
|
322
|
+
# Handle reflection (ensure proper rotation) - check BEFORE computing R
|
|
323
|
+
d = np.linalg.det(Vt.T @ U.T)
|
|
324
|
+
corr = np.eye(2)
|
|
325
|
+
if d < 0:
|
|
326
|
+
corr[1, 1] = -1
|
|
327
|
+
|
|
328
|
+
R = Vt.T @ corr @ U.T
|
|
329
|
+
|
|
330
|
+
# Note: NOT transposing R - testing if direct Kabsch matches C++
|
|
331
|
+
|
|
332
|
+
# 5. Combine scale and rotation
|
|
333
|
+
scale = s_dst / s_src
|
|
334
|
+
scale_rot = scale * R
|
|
335
|
+
|
|
336
|
+
return scale_rot.astype(np.float32)
|
|
337
|
+
|
|
200
338
|
def _build_warp_matrix(self, scale_rot: np.ndarray, pose_tx: float, pose_ty: float) -> np.ndarray:
|
|
201
339
|
"""
|
|
202
340
|
Build 2×3 affine warp matrix from 2×2 scale-rotation matrix and pose translation
|
|
@@ -233,9 +371,11 @@ class OpenFace22FaceAligner:
|
|
|
233
371
|
# C++ code (lines 142-143):
|
|
234
372
|
# warp_matrix(0,2) = -T(0) + out_width/2;
|
|
235
373
|
# warp_matrix(1,2) = -T(1) + out_height/2;
|
|
236
|
-
#
|
|
374
|
+
# We add y_offset to shift the face up slightly (negative = up)
|
|
375
|
+
# to account for small differences between Python CalcParams and C++ CLNF fitting
|
|
376
|
+
|
|
237
377
|
warp_matrix[0, 2] = -T_transformed[0] + self.output_width / 2
|
|
238
|
-
warp_matrix[1, 2] = -T_transformed[1] + self.output_height / 2
|
|
378
|
+
warp_matrix[1, 2] = -T_transformed[1] + self.output_height / 2 + self.y_offset
|
|
239
379
|
|
|
240
380
|
return warp_matrix
|
|
241
381
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canonical Configuration for PyFaceAU Pipeline
|
|
3
|
+
|
|
4
|
+
These settings match C++ OpenFace 2.2 defaults for accurate AU extraction.
|
|
5
|
+
DO NOT modify without thorough testing against C++ reference output.
|
|
6
|
+
|
|
7
|
+
Configuration locked on: Dec 5, 2025
|
|
8
|
+
Tested against: IMG_0942.MOV (1110 frames), IMG_0422.MOV (bearded)
|
|
9
|
+
Target accuracy: Sub-pixel landmark error (<1.0 px), AU correlation >0.95
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# CLNF Landmark Detection Configuration
|
|
14
|
+
# =============================================================================
|
|
15
|
+
CLNF_CONFIG = {
|
|
16
|
+
'max_iterations': 10,
|
|
17
|
+
'convergence_threshold': 0.005, # Gold standard (stricter than 0.01)
|
|
18
|
+
'sigma': 2.25, # C++ CECLM default (1.5 × 1.5 scale factor)
|
|
19
|
+
'use_eye_refinement': True, # Enable hierarchical eye model refinement
|
|
20
|
+
'convergence_profile': 'video', # Enable template tracking + scale adaptation
|
|
21
|
+
'detector': False, # Disable built-in detector (pyfaceau handles)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# MTCNN Face Detection Configuration
|
|
26
|
+
# =============================================================================
|
|
27
|
+
MTCNN_CONFIG = {
|
|
28
|
+
'backend': 'coreml', # Deterministic backend for reproducibility
|
|
29
|
+
'confidence_threshold': 0.5, # Face confidence threshold
|
|
30
|
+
'nms_threshold': 0.7, # Non-max suppression threshold
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# HOG Feature Extraction Configuration
|
|
35
|
+
# =============================================================================
|
|
36
|
+
HOG_CONFIG = {
|
|
37
|
+
'hog_dim': 4464, # 56×14 cell grid × 9 bins (4464 total)
|
|
38
|
+
'hog_bins': 1000, # Histogram bins for running median
|
|
39
|
+
'hog_min': -0.005, # CRITICAL: NOT 0.0 - matches C++ OpenFace
|
|
40
|
+
'hog_max': 1.0, # Maximum HOG value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Geometric Feature Configuration
|
|
45
|
+
# =============================================================================
|
|
46
|
+
GEOM_CONFIG = {
|
|
47
|
+
'geom_dim': 238, # 34 PDM params × 7 derivatives
|
|
48
|
+
'geom_bins': 10000, # Histogram bins for running median
|
|
49
|
+
'geom_min': -60.0, # Minimum geometric feature value
|
|
50
|
+
'geom_max': 60.0, # Maximum geometric feature value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# AU Prediction Configuration
|
|
55
|
+
# =============================================================================
|
|
56
|
+
AU_CONFIG = {
|
|
57
|
+
# Online AU correction (C++ CorrectOnlineAUs equivalent)
|
|
58
|
+
'num_bins': 200, # C++ default
|
|
59
|
+
'min_val': -3.0, # C++ default
|
|
60
|
+
'max_val': 5.0, # C++ default
|
|
61
|
+
'cutoff_ratio': 0.10, # 10th percentile baseline
|
|
62
|
+
'min_frames': 10, # Minimum frames before correction
|
|
63
|
+
'skip_au17_cutoff': True, # AU17 exception (unusual weight distribution)
|
|
64
|
+
'apply_online_dyn_shift': False, # Online 10% shift (no impact in testing)
|
|
65
|
+
|
|
66
|
+
# Two-pass processing
|
|
67
|
+
'max_stored_frames': 3000, # OpenFace default for re-prediction
|
|
68
|
+
|
|
69
|
+
# AU-specific cutoff overrides
|
|
70
|
+
# Python raw predictions are systematically higher than C++ for certain AUs.
|
|
71
|
+
# These adjusted cutoffs compensate to match C++ behavior.
|
|
72
|
+
# See diagnose_raw_prediction_diff.py for derivation.
|
|
73
|
+
'cutoff_overrides': {
|
|
74
|
+
'AU20_r': 0.40, # Original: 0.65 -> 0.9729 correlation (PASS)
|
|
75
|
+
'AU26_r': 0.12, # Original: 0.30 -> 0.9317 correlation (best achievable)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# =============================================================================
|
|
80
|
+
# Running Median Tracker Configuration
|
|
81
|
+
# =============================================================================
|
|
82
|
+
RUNNING_MEDIAN_CONFIG = {
|
|
83
|
+
'hog_dim': HOG_CONFIG['hog_dim'],
|
|
84
|
+
'geom_dim': GEOM_CONFIG['geom_dim'],
|
|
85
|
+
'hog_bins': HOG_CONFIG['hog_bins'],
|
|
86
|
+
'hog_min': HOG_CONFIG['hog_min'],
|
|
87
|
+
'hog_max': HOG_CONFIG['hog_max'],
|
|
88
|
+
'geom_bins': GEOM_CONFIG['geom_bins'],
|
|
89
|
+
'geom_min': GEOM_CONFIG['geom_min'],
|
|
90
|
+
'geom_max': GEOM_CONFIG['geom_max'],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# CLNF Optimizer Defaults (in pyclnf/clnf.py)
|
|
95
|
+
# =============================================================================
|
|
96
|
+
# These are documented here for reference - actual defaults are in pyclnf:
|
|
97
|
+
# regularization: 22.5 # C++ CECLM: 25.0 base × 0.9 = 22.5
|
|
98
|
+
# sigma: 2.25 # C++ CECLM: 1.5 base × 1.5 = 2.25
|
|
99
|
+
# weight_multiplier: 0.0 # C++ disables NU-RLMS weighting
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# Known Fixes Applied
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# 1. Optimizer defaults: reg=22.5, sigma=2.25 (in pyclnf/clnf.py)
|
|
105
|
+
# 2. PDM epsilon: No +1e-10 in eigenvalue regularization (in pyclnf/core/pdm.py)
|
|
106
|
+
# 3. BORDER_REPLICATE: Used in patch extraction (in pyclnf/core/optimizer.py)
|
|
107
|
+
# 4. Template tracking: Enabled in video mode (in pyclnf/clnf.py)
|
|
108
|
+
# 5. PyMTCNN bbox: [x,y,w,h] format handled correctly (in pymtcnn_detector.py)
|
|
109
|
+
# 6. HOG min: -0.005 (NOT 0.0) matches C++ OpenFace
|
|
110
|
+
|
|
111
|
+
# =============================================================================
|
|
112
|
+
# Validation Targets
|
|
113
|
+
# =============================================================================
|
|
114
|
+
VALIDATION_TARGETS = {
|
|
115
|
+
'max_landmark_error_px': 1.0, # Mean error threshold
|
|
116
|
+
'min_au_correlation': 0.95, # For expressed AUs (std > 0.02)
|
|
117
|
+
'test_video': 'IMG_0942.MOV', # Primary test video
|
|
118
|
+
}
|