pyfaceau 1.0.3__cp313-cp313-win_amd64.whl

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.
Files changed (40) hide show
  1. pyfaceau/__init__.py +19 -0
  2. pyfaceau/alignment/__init__.py +0 -0
  3. pyfaceau/alignment/calc_params.py +671 -0
  4. pyfaceau/alignment/face_aligner.py +352 -0
  5. pyfaceau/alignment/numba_calcparams_accelerator.py +244 -0
  6. pyfaceau/cython_histogram_median.cp313-win_amd64.pyd +0 -0
  7. pyfaceau/cython_rotation_update.cp313-win_amd64.pyd +0 -0
  8. pyfaceau/detectors/__init__.py +0 -0
  9. pyfaceau/detectors/pfld.py +128 -0
  10. pyfaceau/detectors/retinaface.py +352 -0
  11. pyfaceau/download_weights.py +134 -0
  12. pyfaceau/features/__init__.py +0 -0
  13. pyfaceau/features/histogram_median_tracker.py +335 -0
  14. pyfaceau/features/pdm.py +269 -0
  15. pyfaceau/features/triangulation.py +64 -0
  16. pyfaceau/parallel_pipeline.py +462 -0
  17. pyfaceau/pipeline.py +1083 -0
  18. pyfaceau/prediction/__init__.py +0 -0
  19. pyfaceau/prediction/au_predictor.py +434 -0
  20. pyfaceau/prediction/batched_au_predictor.py +269 -0
  21. pyfaceau/prediction/model_parser.py +337 -0
  22. pyfaceau/prediction/running_median.py +318 -0
  23. pyfaceau/prediction/running_median_fallback.py +200 -0
  24. pyfaceau/processor.py +270 -0
  25. pyfaceau/refinement/__init__.py +12 -0
  26. pyfaceau/refinement/svr_patch_expert.py +361 -0
  27. pyfaceau/refinement/targeted_refiner.py +362 -0
  28. pyfaceau/utils/__init__.py +0 -0
  29. pyfaceau/utils/cython_extensions/cython_histogram_median.c +35391 -0
  30. pyfaceau/utils/cython_extensions/cython_histogram_median.pyx +316 -0
  31. pyfaceau/utils/cython_extensions/cython_rotation_update.c +32262 -0
  32. pyfaceau/utils/cython_extensions/cython_rotation_update.pyx +211 -0
  33. pyfaceau/utils/cython_extensions/setup.py +47 -0
  34. pyfaceau-1.0.3.data/scripts/pyfaceau_gui.py +302 -0
  35. pyfaceau-1.0.3.dist-info/METADATA +466 -0
  36. pyfaceau-1.0.3.dist-info/RECORD +40 -0
  37. pyfaceau-1.0.3.dist-info/WHEEL +5 -0
  38. pyfaceau-1.0.3.dist-info/entry_points.txt +3 -0
  39. pyfaceau-1.0.3.dist-info/licenses/LICENSE +40 -0
  40. pyfaceau-1.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,362 @@
1
+ """
2
+ Targeted CLNF refinement for critical landmarks
3
+
4
+ This module implements focused CLNF refinement for brow landmarks (17-26) and
5
+ lip corners (48, 54) to improve AU01, AU02, and AU23 detection accuracy.
6
+ """
7
+
8
+ import numpy as np
9
+ import cv2
10
+ from typing import Dict, Tuple, Optional, Any
11
+
12
+ # Handle both relative and absolute imports
13
+ if __name__ == '__main__':
14
+ import sys
15
+ import os
16
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
17
+ from pyfaceau.refinement.svr_patch_expert import SVRPatchExpert, SVRPatchExpertLoader
18
+ else:
19
+ from .svr_patch_expert import SVRPatchExpert, SVRPatchExpertLoader
20
+
21
+
22
+ class TargetedCLNFRefiner:
23
+ """
24
+ Targeted CLNF landmark refinement for critical AU landmarks.
25
+
26
+ Refines only brow landmarks (17-26) and lip corners (48, 54) using
27
+ OpenFace SVR patch experts. This focused approach maintains speed
28
+ while improving accuracy for AU01, AU02, and AU23.
29
+
30
+ Optionally enforces PDM (Point Distribution Model) constraints to ensure
31
+ refined landmarks remain anatomically plausible.
32
+ """
33
+
34
+ # Critical landmarks for AU improvement
35
+ CRITICAL_LANDMARKS = [17, 18, 19, 20, 21, 22, 26, 48, 54]
36
+
37
+ def __init__(self, patch_expert_file: str, search_window: int = 3,
38
+ pdm: Optional[Any] = None, enforce_pdm: bool = False):
39
+ """
40
+ Initialize targeted CLNF refiner.
41
+
42
+ Args:
43
+ patch_expert_file: Path to svr_patches_*.txt file
44
+ search_window: Search radius around initial landmark (pixels)
45
+ pdm: Optional PDM for shape constraint enforcement
46
+ enforce_pdm: Whether to project refined landmarks onto PDM
47
+ """
48
+ self.search_window = search_window
49
+ self.pdm = pdm
50
+ self.enforce_pdm = enforce_pdm
51
+
52
+ # Load only critical patch experts
53
+ loader = SVRPatchExpertLoader(patch_expert_file)
54
+ self.patch_experts = loader.load(target_landmarks=self.CRITICAL_LANDMARKS)
55
+
56
+ if self.enforce_pdm and self.pdm is None:
57
+ print("⚠️ Warning: enforce_pdm=True but no PDM provided. PDM constraints disabled.")
58
+ self.enforce_pdm = False
59
+
60
+ pdm_status = " + PDM constraints" if self.enforce_pdm else ""
61
+ print(f"Loaded {len(self.patch_experts)} patch experts for refinement{pdm_status}")
62
+
63
+ def refine_landmarks(self, image: np.ndarray, landmarks: np.ndarray) -> np.ndarray:
64
+ """
65
+ Refine critical landmarks using CLNF patch experts.
66
+
67
+ Args:
68
+ image: Grayscale image (H, W) as uint8
69
+ landmarks: Initial 68 landmarks as (68, 2) array
70
+
71
+ Returns:
72
+ Refined landmarks (68, 2) array
73
+ """
74
+ # Convert to grayscale if needed
75
+ if len(image.shape) == 3:
76
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
77
+
78
+ # Ensure float image for processing
79
+ if image.dtype == np.uint8:
80
+ image_float = image.astype(np.float32) / 255.0
81
+ else:
82
+ image_float = image.astype(np.float32)
83
+
84
+ # Copy landmarks for refinement
85
+ refined = landmarks.copy()
86
+
87
+ # Refine each critical landmark
88
+ for landmark_idx in self.CRITICAL_LANDMARKS:
89
+ if landmark_idx not in self.patch_experts:
90
+ continue
91
+
92
+ expert = self.patch_experts[landmark_idx]
93
+ initial_pos = refined[landmark_idx]
94
+
95
+ # Search for best position
96
+ refined_pos = self._search_for_best_position(
97
+ image_float,
98
+ initial_pos,
99
+ expert
100
+ )
101
+
102
+ refined[landmark_idx] = refined_pos
103
+
104
+ # Optional: Enforce PDM constraints to ensure anatomically plausible shape
105
+ if self.enforce_pdm and self.pdm is not None:
106
+ refined = self._project_to_pdm(refined)
107
+
108
+ return refined
109
+
110
+ def _project_to_pdm(self, landmarks: np.ndarray) -> np.ndarray:
111
+ """
112
+ Project refined landmarks onto PDM to enforce shape constraints.
113
+
114
+ This ensures refined landmarks remain anatomically plausible by:
115
+ 1. Finding best-fit PDM parameters using calc_params
116
+ 2. Reconstructing landmarks from those parameters using calc_shape_3d
117
+
118
+ Args:
119
+ landmarks: Refined landmarks (68, 2)
120
+
121
+ Returns:
122
+ PDM-constrained landmarks (68, 2)
123
+ """
124
+ # Find best PDM parameters (calc_params accepts (68, 2) landmarks)
125
+ params_global, params_local = self.pdm.calc_params(landmarks)
126
+
127
+ # Reconstruct 3D landmarks from parameters
128
+ # calc_shape_3d returns flat array (204,) = 68 landmarks * 3 coords (X, Y, Z)
129
+ landmarks_3d_flat = self.pdm.calc_shape_3d(params_local)
130
+
131
+ # Reshape to (68, 3)
132
+ landmarks_3d = landmarks_3d_flat.reshape(68, 3)
133
+
134
+ # Project back to 2D using global params
135
+ # For simplicity, just use X and Y coordinates
136
+ constrained = landmarks_3d[:, :2] # Take only X, Y (drop Z)
137
+
138
+ return constrained
139
+
140
+ def _search_for_best_position(
141
+ self,
142
+ image: np.ndarray,
143
+ initial_pos: np.ndarray,
144
+ expert: SVRPatchExpert
145
+ ) -> np.ndarray:
146
+ """
147
+ Search for optimal landmark position using patch expert.
148
+
149
+ Args:
150
+ image: Grayscale image (normalized to 0-1)
151
+ initial_pos: Initial landmark position (x, y)
152
+ expert: SVR patch expert for this landmark
153
+
154
+ Returns:
155
+ Refined position (x, y)
156
+ """
157
+ best_response = -float('inf')
158
+ best_pos = initial_pos.copy()
159
+
160
+ # Search in window around initial position
161
+ for dx in range(-self.search_window, self.search_window + 1):
162
+ for dy in range(-self.search_window, self.search_window + 1):
163
+ candidate = initial_pos + np.array([dx, dy], dtype=np.float32)
164
+
165
+ # Extract patch at candidate position
166
+ patch = self._extract_patch(image, candidate, expert)
167
+
168
+ if patch is None:
169
+ continue
170
+
171
+ # Compute patch expert response
172
+ response = self._compute_response(patch, expert)
173
+
174
+ if response > best_response:
175
+ best_response = response
176
+ best_pos = candidate
177
+
178
+ return best_pos
179
+
180
+ def _extract_patch(
181
+ self,
182
+ image: np.ndarray,
183
+ position: np.ndarray,
184
+ expert: SVRPatchExpert
185
+ ) -> Optional[np.ndarray]:
186
+ """
187
+ Extract patch around landmark position.
188
+
189
+ Args:
190
+ image: Grayscale image (normalized to 0-1)
191
+ position: Landmark position (x, y)
192
+ expert: Patch expert (determines patch type)
193
+
194
+ Returns:
195
+ Feature vector for patch, or None if out of bounds
196
+ """
197
+ # Patch experts use 11x11 patches
198
+ patch_size = expert.weights.shape[0] # Should be 11
199
+ half_size = patch_size // 2
200
+
201
+ # Get integer position
202
+ x, y = int(round(position[0])), int(round(position[1]))
203
+
204
+ # Check bounds
205
+ if (x - half_size < 0 or x + half_size >= image.shape[1] or
206
+ y - half_size < 0 or y + half_size >= image.shape[0]):
207
+ return None
208
+
209
+ # Extract patch
210
+ patch = image[y - half_size:y + half_size + 1,
211
+ x - half_size:x + half_size + 1]
212
+
213
+ # Convert to features based on patch expert type
214
+ if expert.type == 0:
215
+ # Raw pixel features
216
+ features = patch.flatten()
217
+ else:
218
+ # Gradient features (type == 1)
219
+ # Compute image gradients
220
+ grad_x = cv2.Sobel(patch, cv2.CV_32F, 1, 0, ksize=3)
221
+ grad_y = cv2.Sobel(patch, cv2.CV_32F, 0, 1, ksize=3)
222
+
223
+ # Concatenate gradient features
224
+ features = np.concatenate([grad_x.flatten(), grad_y.flatten()])
225
+
226
+ return features
227
+
228
+ def _compute_response(
229
+ self,
230
+ patch_features: np.ndarray,
231
+ expert: SVRPatchExpert
232
+ ) -> float:
233
+ """
234
+ Compute patch expert response using SVR.
235
+
236
+ This is identical to AU prediction:
237
+ response = features @ weights + bias
238
+
239
+ Args:
240
+ patch_features: Extracted patch features
241
+ expert: SVR patch expert with weights
242
+
243
+ Returns:
244
+ Response score (higher = better landmark position)
245
+ """
246
+ # Flatten weights for dot product
247
+ weights_flat = expert.weights.flatten()
248
+
249
+ # Ensure feature dimensions match
250
+ if len(patch_features) != len(weights_flat):
251
+ # If patch expert expects raw pixels but we have gradients (or vice versa)
252
+ # This shouldn't happen if patch extraction is correct
253
+ return -float('inf')
254
+
255
+ # SVR prediction: features @ weights + bias
256
+ response = np.dot(patch_features, weights_flat) + expert.bias
257
+
258
+ # Apply logistic scaling (optional, makes response 0-1)
259
+ # OpenFace uses: 1.0 / (1.0 + exp(-scaling * response))
260
+ response = 1.0 / (1.0 + np.exp(-expert.scaling * response))
261
+
262
+ return response
263
+
264
+
265
+ def test_refiner():
266
+ """Test the targeted CLNF refiner on a sample image"""
267
+ import os
268
+ import sys
269
+
270
+ # Add parent directory to path to enable absolute imports
271
+ parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))
272
+ if parent_dir not in sys.path:
273
+ sys.path.insert(0, parent_dir)
274
+
275
+ from pyfaceau.detectors.pfld import CunjianPFLDDetector
276
+ from pyfaceau.detectors.retinaface import OptimizedFaceDetector
277
+
278
+ print("="*80)
279
+ print("TESTING TARGETED CLNF REFINER")
280
+ print("="*80)
281
+
282
+ # Initialize detectors
283
+ current_dir = os.path.dirname(os.path.abspath(__file__))
284
+ weights_dir = os.path.join(current_dir, '../../weights')
285
+ retinaface_model = os.path.join(weights_dir, 'retinaface_mobilenet025_coreml.onnx')
286
+ pfld_model = os.path.join(weights_dir, 'pfld_cunjian.onnx')
287
+ patch_expert_file = os.path.join(weights_dir, 'svr_patches_0.25_general.txt')
288
+
289
+ print("\n1. Loading models...")
290
+ face_detector = OptimizedFaceDetector(retinaface_model)
291
+ landmark_detector = CunjianPFLDDetector(pfld_model)
292
+ refiner = TargetedCLNFRefiner(patch_expert_file, search_window=3)
293
+
294
+ # Load test image
295
+ test_video = '/Users/johnwilsoniv/Documents/SplitFace Open3/D Normal Pts/IMG_0434.MOV'
296
+ cap = cv2.VideoCapture(test_video)
297
+
298
+ if not cap.isOpened():
299
+ print(f"Could not open video: {test_video}")
300
+ return
301
+
302
+ # Read first frame
303
+ ret, frame = cap.read()
304
+ cap.release()
305
+
306
+ if not ret:
307
+ print("Could not read frame from video")
308
+ return
309
+
310
+ print(f" Frame shape: {frame.shape}")
311
+
312
+ # Detect face
313
+ print("\n2. Detecting face...")
314
+ detections, img_raw = face_detector.detect_faces(frame)
315
+
316
+ if len(detections) == 0:
317
+ print(" No faces detected")
318
+ return
319
+
320
+ # Extract bbox coordinates (first 4 values: x_min, y_min, x_max, y_max)
321
+ first_face = detections[0]
322
+ bbox = first_face[:4] # x_min, y_min, x_max, y_max
323
+ print(f" Face bbox: {bbox}")
324
+
325
+ # Detect landmarks with PFLD
326
+ print("\n3. Detecting initial landmarks with PFLD...")
327
+ pfld_landmarks, confidence = landmark_detector.detect_landmarks(frame, bbox)
328
+ print(f" PFLD landmarks shape: {pfld_landmarks.shape}")
329
+ print(f" Sample landmarks (17-22):")
330
+ for i in range(17, 23):
331
+ print(f" {i}: ({pfld_landmarks[i, 0]:.1f}, {pfld_landmarks[i, 1]:.1f})")
332
+
333
+ # Refine landmarks with CLNF
334
+ print("\n4. Refining landmarks with CLNF...")
335
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
336
+ refined_landmarks = refiner.refine_landmarks(gray, pfld_landmarks)
337
+
338
+ print(f" Refined landmarks shape: {refined_landmarks.shape}")
339
+ print(f" Sample refined landmarks (17-22):")
340
+ for i in range(17, 23):
341
+ delta_x = refined_landmarks[i, 0] - pfld_landmarks[i, 0]
342
+ delta_y = refined_landmarks[i, 1] - pfld_landmarks[i, 1]
343
+ print(f" {i}: ({refined_landmarks[i, 0]:.1f}, {refined_landmarks[i, 1]:.1f}) "
344
+ f"[Δx={delta_x:+.1f}, Δy={delta_y:+.1f}]")
345
+
346
+ # Compute displacement statistics
347
+ print("\n5. Refinement statistics:")
348
+ displacements = []
349
+ for idx in refiner.CRITICAL_LANDMARKS:
350
+ delta = refined_landmarks[idx] - pfld_landmarks[idx]
351
+ displacement = np.linalg.norm(delta)
352
+ displacements.append(displacement)
353
+
354
+ print(f" Mean displacement: {np.mean(displacements):.2f} pixels")
355
+ print(f" Max displacement: {np.max(displacements):.2f} pixels")
356
+ print(f" Min displacement: {np.min(displacements):.2f} pixels")
357
+
358
+ print("\nTargeted CLNF refiner test complete!")
359
+
360
+
361
+ if __name__ == '__main__':
362
+ test_refiner()
File without changes