pyfaceau 1.3.6__tar.gz → 1.3.7__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.
Files changed (64) hide show
  1. {pyfaceau-1.3.6/pyfaceau.egg-info → pyfaceau-1.3.7}/PKG-INFO +1 -1
  2. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/pipeline.py +109 -100
  3. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/processor.py +29 -21
  4. {pyfaceau-1.3.6 → pyfaceau-1.3.7/pyfaceau.egg-info}/PKG-INFO +1 -1
  5. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyproject.toml +1 -1
  6. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/LICENSE +0 -0
  7. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/MANIFEST.in +0 -0
  8. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/README.md +0 -0
  9. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/__init__.py +0 -0
  10. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/alignment/__init__.py +0 -0
  11. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/alignment/calc_params.py +0 -0
  12. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/alignment/face_aligner.py +0 -0
  13. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/alignment/numba_calcparams_accelerator.py +0 -0
  14. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/alignment/paw.py +0 -0
  15. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/config.py +0 -0
  16. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/data/__init__.py +0 -0
  17. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/data/hdf5_dataset.py +0 -0
  18. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/data/quality_filter.py +0 -0
  19. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/data/training_data_generator.py +0 -0
  20. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/detectors/__init__.py +0 -0
  21. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/detectors/extract_mtcnn_weights.py +0 -0
  22. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/detectors/openface_mtcnn.py +0 -0
  23. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/detectors/pfld.py +0 -0
  24. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/detectors/pymtcnn_detector.py +0 -0
  25. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/detectors/retinaface.py +0 -0
  26. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/download_weights.py +0 -0
  27. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/features/__init__.py +0 -0
  28. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/features/histogram_median_tracker.py +0 -0
  29. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/features/pdm.py +0 -0
  30. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/features/triangulation.py +0 -0
  31. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/__init__.py +0 -0
  32. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/au_prediction_inference.py +0 -0
  33. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/au_prediction_net.py +0 -0
  34. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/fast_pipeline.py +0 -0
  35. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/landmark_pose_inference.py +0 -0
  36. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/landmark_pose_net.py +0 -0
  37. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/train_au_prediction.py +0 -0
  38. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/nn/train_landmark_pose.py +0 -0
  39. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/parallel_pipeline.py +0 -0
  40. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/prediction/__init__.py +0 -0
  41. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/prediction/au_predictor.py +0 -0
  42. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/prediction/batched_au_predictor.py +0 -0
  43. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/prediction/model_parser.py +0 -0
  44. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/prediction/online_au_correction.py +0 -0
  45. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/prediction/running_median.py +0 -0
  46. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/prediction/running_median_fallback.py +0 -0
  47. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/refinement/__init__.py +0 -0
  48. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/refinement/pdm.py +0 -0
  49. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/refinement/svr_patch_expert.py +0 -0
  50. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/refinement/targeted_refiner.py +0 -0
  51. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/utils/__init__.py +0 -0
  52. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/utils/cython_extensions/cython_histogram_median.pyx +0 -0
  53. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/utils/cython_extensions/cython_rotation_update.pyx +0 -0
  54. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau/utils/cython_extensions/setup.py +0 -0
  55. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau.egg-info/SOURCES.txt +0 -0
  56. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau.egg-info/dependency_links.txt +0 -0
  57. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau.egg-info/entry_points.txt +0 -0
  58. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau.egg-info/not-zip-safe +0 -0
  59. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau.egg-info/requires.txt +0 -0
  60. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau.egg-info/top_level.txt +0 -0
  61. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/pyfaceau_gui.py +0 -0
  62. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/requirements.txt +0 -0
  63. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/setup.cfg +0 -0
  64. {pyfaceau-1.3.6 → pyfaceau-1.3.7}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfaceau
3
- Version: 1.3.6
3
+ Version: 1.3.7
4
4
  Summary: Pure Python OpenFace 2.2 AU extraction with CLNF landmark refinement
5
5
  Home-page: https://github.com/johnwilsoniv/face-analysis
6
6
  Author: John Wilson
@@ -75,11 +75,19 @@ except ImportError:
75
75
  sys.path.insert(0, str(pyfhog_src_path))
76
76
  import pyfhog
77
77
  else:
78
- print("Error: pyfhog not found. Please install it:")
79
- print(" cd ../pyfhog && pip install -e .")
78
+ safe_print("Error: pyfhog not found. Please install it:")
79
+ safe_print(" cd ../pyfhog && pip install -e .")
80
80
  sys.exit(1)
81
81
 
82
82
 
83
+ def safe_print(*args, **kwargs):
84
+ """Print wrapper that handles BrokenPipeError in GUI subprocess contexts."""
85
+ try:
86
+ print(*args, **kwargs)
87
+ except (BrokenPipeError, IOError):
88
+ pass # Stdout disconnected (e.g., GUI subprocess terminated)
89
+
90
+
83
91
  def get_video_rotation(video_path: str) -> int:
84
92
  """
85
93
  Get video rotation from metadata using ffprobe.
@@ -284,11 +292,11 @@ class FullPythonAUPipeline:
284
292
  if self.verbose:
285
293
  thread_name = threading.current_thread().name
286
294
  is_main = threading.current_thread() == threading.main_thread()
287
- print("=" * 80)
288
- print("INITIALIZING COMPONENTS")
289
- print(f"Thread: {thread_name} (main={is_main})")
290
- print("=" * 80)
291
- print("")
295
+ safe_print("=" * 80)
296
+ safe_print("INITIALIZING COMPONENTS")
297
+ safe_print(f"Thread: {thread_name} (main={is_main})")
298
+ safe_print("=" * 80)
299
+ safe_print("")
292
300
 
293
301
  # Get initialization parameters
294
302
  mtcnn_backend = self._init_params['mtcnn_backend']
@@ -301,8 +309,8 @@ class FullPythonAUPipeline:
301
309
 
302
310
  # Component 1: Face Detection (PyMTCNN with multi-backend support)
303
311
  if self.verbose:
304
- print("[1/8] Loading face detector (PyMTCNN)...")
305
- print(f" Backend: {mtcnn_backend}")
312
+ safe_print("[1/8] Loading face detector (PyMTCNN)...")
313
+ safe_print(f" Backend: {mtcnn_backend}")
306
314
 
307
315
  if not PYMTCNN_AVAILABLE:
308
316
  raise ImportError(
@@ -320,15 +328,15 @@ class FullPythonAUPipeline:
320
328
  )
321
329
  if self.verbose:
322
330
  backend_info = self.face_detector.get_backend_info()
323
- print(f" Active backend: {backend_info}")
324
- print("Face detector loaded\n")
331
+ safe_print(f" Active backend: {backend_info}")
332
+ safe_print("Face detector loaded\n")
325
333
 
326
334
  # Component 2: Landmark Detection (pyclnf CLNF with GPU acceleration)
327
335
  if self.verbose:
328
- print("[2/8] Loading CLNF landmark detector (pyclnf)...")
329
- print(f" Max iterations: {max_clnf_iterations}")
330
- print(f" Convergence threshold: {clnf_convergence_threshold} pixels")
331
- print(f" GPU enabled: {CLNF_CONFIG.get('use_gpu', False)}")
336
+ safe_print("[2/8] Loading CLNF landmark detector (pyclnf)...")
337
+ safe_print(f" Max iterations: {max_clnf_iterations}")
338
+ safe_print(f" Convergence threshold: {clnf_convergence_threshold} pixels")
339
+ safe_print(f" GPU enabled: {CLNF_CONFIG.get('use_gpu', False)}")
332
340
 
333
341
  # Lazy import to avoid circular import (pyfaceau ↔ pyclnf)
334
342
  from pyclnf import CLNF
@@ -347,14 +355,14 @@ class FullPythonAUPipeline:
347
355
  )
348
356
 
349
357
  if self.verbose:
350
- print(f"CLNF detector loaded\n")
358
+ safe_print(f"CLNF detector loaded\n")
351
359
 
352
360
  # Component 3: PDM Parser (moved before CLNF to support PDM enforcement)
353
361
  if self.verbose:
354
- print("[3/8] Loading PDM shape model...")
362
+ safe_print("[3/8] Loading PDM shape model...")
355
363
  self.pdm_parser = PDMParser(pdm_file)
356
364
  if self.verbose:
357
- print(f"PDM loaded: {self.pdm_parser.mean_shape.shape[0]//3} landmarks\n")
365
+ safe_print(f"PDM loaded: {self.pdm_parser.mean_shape.shape[0]//3} landmarks\n")
358
366
 
359
367
  # Note: CalcParams is no longer used for geometric features
360
368
  # pyclnf's optimized params are used instead (see GEOMETRIC_FEATURES_BUG.md)
@@ -363,28 +371,28 @@ class FullPythonAUPipeline:
363
371
 
364
372
  # Component 4: Face Aligner
365
373
  if self.verbose:
366
- print("[4/8] Initializing face aligner...")
374
+ safe_print("[4/8] Initializing face aligner...")
367
375
  self.face_aligner = OpenFace22FaceAligner(
368
376
  pdm_file=pdm_file,
369
377
  sim_scale=0.7,
370
378
  output_size=(112, 112)
371
379
  )
372
380
  if self.verbose:
373
- print("Face aligner initialized\n")
381
+ safe_print("Face aligner initialized\n")
374
382
 
375
383
  # Note: CLNF landmark detector is already initialized above (Component 2)
376
384
  # No separate refiner needed - CLNF does full detection from PDM mean shape
377
385
 
378
386
  # Component 5: Triangulation
379
387
  if self.verbose:
380
- print("[5/8] Loading triangulation...")
388
+ safe_print("[5/8] Loading triangulation...")
381
389
  self.triangulation = TriangulationParser(triangulation_file)
382
390
  if self.verbose:
383
- print(f"Triangulation loaded: {len(self.triangulation.triangles)} triangles\n")
391
+ safe_print(f"Triangulation loaded: {len(self.triangulation.triangles)} triangles\n")
384
392
 
385
393
  # Component 6: AU Models
386
394
  if self.verbose:
387
- print("[6/8] Loading AU SVR models...")
395
+ safe_print("[6/8] Loading AU SVR models...")
388
396
  model_parser = OF22ModelParser(au_models_dir)
389
397
  self.au_models = model_parser.load_all_models(
390
398
  use_recommended=True,
@@ -392,30 +400,30 @@ class FullPythonAUPipeline:
392
400
  verbose=self.verbose
393
401
  )
394
402
  if self.verbose:
395
- print(f"Loaded {len(self.au_models)} AU models")
403
+ safe_print(f"Loaded {len(self.au_models)} AU models")
396
404
 
397
405
  # Initialize batched predictor if enabled
398
406
  if self.use_batched_predictor:
399
407
  self.batched_au_predictor = BatchedAUPredictor(self.au_models)
400
408
  if self.verbose:
401
- print(f"Batched AU predictor enabled (2-5x faster)")
409
+ safe_print(f"Batched AU predictor enabled (2-5x faster)")
402
410
  if self.verbose:
403
- print("")
411
+ safe_print("")
404
412
 
405
413
  # Component 7: Running Median Tracker
406
414
  if self.verbose:
407
- print("[7/8] Initializing running median tracker...")
415
+ safe_print("[7/8] Initializing running median tracker...")
408
416
  # Use locked configuration from config.py (matches C++ OpenFace)
409
417
  self.running_median = DualHistogramMedianTracker(**RUNNING_MEDIAN_CONFIG)
410
418
  if self.verbose:
411
419
  if USING_CYTHON:
412
- print("Running median tracker initialized (Cython-optimized, 260x faster)\n")
420
+ safe_print("Running median tracker initialized (Cython-optimized, 260x faster)\n")
413
421
  else:
414
- print("Running median tracker initialized (Python version)\n")
422
+ safe_print("Running median tracker initialized (Python version)\n")
415
423
 
416
424
  # Component 8: Online AU Correction (C++ CorrectOnlineAUs equivalent)
417
425
  if self.verbose:
418
- print("[8/9] Initializing online AU correction...")
426
+ safe_print("[8/9] Initializing online AU correction...")
419
427
  # Get AU names from loaded models
420
428
  au_names = list(self.au_models.keys())
421
429
  self.online_au_correction = OnlineAUCorrection(
@@ -428,15 +436,15 @@ class FullPythonAUPipeline:
428
436
  clip_values=True
429
437
  )
430
438
  if self.verbose:
431
- print(f"Online AU correction initialized for {len(au_names)} AUs\n")
439
+ safe_print(f"Online AU correction initialized for {len(au_names)} AUs\n")
432
440
 
433
441
  # Component 9: PyFHOG
434
442
  if self.verbose:
435
- print("[9/9] PyFHOG ready for HOG extraction")
436
- print("")
437
- print("All components initialized successfully")
438
- print("=" * 80)
439
- print("")
443
+ safe_print("[9/9] PyFHOG ready for HOG extraction")
444
+ safe_print("")
445
+ safe_print("All components initialized successfully")
446
+ safe_print("=" * 80)
447
+ safe_print("")
440
448
 
441
449
  self._components_initialized = True
442
450
 
@@ -564,14 +572,14 @@ class FullPythonAUPipeline:
564
572
  raise FileNotFoundError(f"Video not found: {video_path}")
565
573
 
566
574
  if self.verbose:
567
- print(f"Processing video: {video_path.name}")
568
- print("=" * 80)
569
- print("")
575
+ safe_print(f"Processing video: {video_path.name}")
576
+ safe_print("=" * 80)
577
+ safe_print("")
570
578
 
571
579
  # Detect video rotation from metadata (important for mobile videos)
572
580
  rotation = get_video_rotation(str(video_path))
573
581
  if self.verbose and rotation != 0:
574
- print(f"Detected video rotation: {rotation}°")
582
+ safe_print(f"Detected video rotation: {rotation}°")
575
583
 
576
584
  # Open video
577
585
  cap = cv2.VideoCapture(str(video_path))
@@ -582,13 +590,13 @@ class FullPythonAUPipeline:
582
590
  total_frames = min(total_frames, max_frames)
583
591
 
584
592
  if self.verbose:
585
- print(f"Video info:")
586
- print(f" FPS: {fps:.2f}")
587
- print(f" Total frames: {total_frames}")
588
- print(f" Duration: {total_frames/fps:.2f} seconds")
593
+ safe_print(f"Video info:")
594
+ safe_print(f" FPS: {fps:.2f}")
595
+ safe_print(f" Total frames: {total_frames}")
596
+ safe_print(f" Duration: {total_frames/fps:.2f} seconds")
589
597
  if rotation != 0:
590
- print(f" Rotation: {rotation}° (will be corrected)")
591
- print("")
598
+ safe_print(f" Rotation: {rotation}° (will be corrected)")
599
+ safe_print("")
592
600
 
593
601
  # Results storage
594
602
  results = []
@@ -597,6 +605,7 @@ class FullPythonAUPipeline:
597
605
  # Statistics
598
606
  total_processed = 0
599
607
  total_failed = 0
608
+ processing_start_time = time.time()
600
609
 
601
610
  try:
602
611
  while True:
@@ -619,19 +628,19 @@ class FullPythonAUPipeline:
619
628
  else:
620
629
  total_failed += 1
621
630
 
622
- # Progress update (wrapped to handle BrokenPipeError in GUI contexts)
631
+ # Progress update
623
632
  if self.verbose and (frame_idx + 1) % 10 == 0:
624
633
  progress = (frame_idx + 1) / total_frames * 100
625
- try:
626
- print(f"Progress: {frame_idx + 1}/{total_frames} frames ({progress:.1f}%) - "
627
- f"Success: {total_processed}, Failed: {total_failed}", flush=True)
628
- except (BrokenPipeError, IOError):
629
- pass # Stdout disconnected (e.g., GUI subprocess)
634
+ safe_print(f"Progress: {frame_idx + 1}/{total_frames} frames ({progress:.1f}%) - "
635
+ f"Success: {total_processed}, Failed: {total_failed}", flush=True)
630
636
 
631
637
  # GUI progress callback (called every frame for smooth updates)
632
638
  if progress_callback is not None:
633
639
  try:
634
- progress_callback(frame_idx + 1, total_frames, fps)
640
+ # Calculate actual processing FPS (not video fps)
641
+ elapsed = time.time() - processing_start_time
642
+ actual_fps = (frame_idx + 1) / elapsed if elapsed > 0 else 0.0
643
+ progress_callback(frame_idx + 1, total_frames, actual_fps)
635
644
  except Exception:
636
645
  pass # Don't let callback errors stop processing
637
646
 
@@ -646,25 +655,25 @@ class FullPythonAUPipeline:
646
655
  # Apply post-processing (cutoff adjustment, temporal smoothing)
647
656
  # This is CRITICAL for dynamic AU accuracy!
648
657
  if self.verbose:
649
- print("\nApplying post-processing (cutoff adjustment, temporal smoothing)...")
658
+ safe_print("\nApplying post-processing (cutoff adjustment, temporal smoothing)...")
650
659
  df = self.finalize_predictions(df)
651
660
 
652
661
  if self.verbose:
653
- print("")
654
- print("=" * 80)
655
- print("PROCESSING COMPLETE")
656
- print("=" * 80)
657
- print(f"Total frames processed: {total_processed}")
658
- print(f"Failed frames: {total_failed}")
659
- print(f"Success rate: {total_processed/(total_processed+total_failed)*100:.1f}%")
660
- print("")
662
+ safe_print("")
663
+ safe_print("=" * 80)
664
+ safe_print("PROCESSING COMPLETE")
665
+ safe_print("=" * 80)
666
+ safe_print(f"Total frames processed: {total_processed}")
667
+ safe_print(f"Failed frames: {total_failed}")
668
+ safe_print(f"Success rate: {total_processed/(total_processed+total_failed)*100:.1f}%")
669
+ safe_print("")
661
670
 
662
671
  # Save to CSV if requested
663
672
  if output_csv:
664
673
  df.to_csv(output_csv, index=False)
665
674
  if self.verbose:
666
- print(f"Results saved to: {output_csv}")
667
- print("")
675
+ safe_print(f"Results saved to: {output_csv}")
676
+ safe_print("")
668
677
 
669
678
  return df
670
679
 
@@ -718,7 +727,7 @@ class FullPythonAUPipeline:
718
727
  if self.track_faces and self.cached_bbox is not None:
719
728
  # Try using cached bbox (skip expensive PyMTCNN!)
720
729
  if self.verbose and frame_idx < 3:
721
- print(f"[Frame {frame_idx}] Step 1: Using cached bbox (tracking mode)")
730
+ safe_print(f"[Frame {frame_idx}] Step 1: Using cached bbox (tracking mode)")
722
731
  bbox = self.cached_bbox
723
732
  need_detection = False
724
733
  self.frames_since_detection += 1
@@ -726,10 +735,10 @@ class FullPythonAUPipeline:
726
735
  if need_detection or bbox is None:
727
736
  # First frame OR previous tracking failed - run PyMTCNN
728
737
  if self.verbose and frame_idx < 3:
729
- print(f"[Frame {frame_idx}] Step 1: Detecting face with {self.face_detector.backend}...")
738
+ safe_print(f"[Frame {frame_idx}] Step 1: Detecting face with {self.face_detector.backend}...")
730
739
  detections, _ = self.face_detector.detect_faces(frame)
731
740
  if self.verbose and frame_idx < 3:
732
- print(f"[Frame {frame_idx}] Step 1: Found {len(detections)} faces")
741
+ safe_print(f"[Frame {frame_idx}] Step 1: Found {len(detections)} faces")
733
742
 
734
743
  if len(detections) == 0:
735
744
  # No face detected - clear cache
@@ -764,7 +773,7 @@ class FullPythonAUPipeline:
764
773
  # Step 2: Detect landmarks using CLNF (OpenFace approach)
765
774
  t0 = time.time() if debug_info is not None else None
766
775
  if self.verbose and frame_idx < 3:
767
- print(f"[Frame {frame_idx}] Step 2: Detecting landmarks with CLNF...")
776
+ safe_print(f"[Frame {frame_idx}] Step 2: Detecting landmarks with CLNF...")
768
777
 
769
778
  try:
770
779
  # Convert bbox from [x1, y1, x2, y2] to [x, y, width, height] for pyclnf
@@ -781,7 +790,7 @@ class FullPythonAUPipeline:
781
790
  num_iterations = info['iterations']
782
791
 
783
792
  if self.verbose and frame_idx < 3:
784
- print(f"[Frame {frame_idx}] Step 2: Got {len(landmarks_68)} landmarks (CLNF converged: {converged}, iterations: {num_iterations})")
793
+ safe_print(f"[Frame {frame_idx}] Step 2: Got {len(landmarks_68)} landmarks (CLNF converged: {converged}, iterations: {num_iterations})")
785
794
 
786
795
  if debug_info is not None:
787
796
  debug_info['landmark_detection'] = {
@@ -795,7 +804,7 @@ class FullPythonAUPipeline:
795
804
  # Landmark detection failed with cached bbox - re-run face detection
796
805
  if self.track_faces and not need_detection:
797
806
  if self.verbose and frame_idx < 3:
798
- print(f"[Frame {frame_idx}] Step 2: Landmark detection failed with cached bbox, re-detecting face...")
807
+ safe_print(f"[Frame {frame_idx}] Step 2: Landmark detection failed with cached bbox, re-detecting face...")
799
808
  self.detection_failures += 1
800
809
  self.cached_bbox = None
801
810
 
@@ -836,7 +845,7 @@ class FullPythonAUPipeline:
836
845
  # See GEOMETRIC_FEATURES_BUG.md for details
837
846
  t0 = time.time() if debug_info is not None else None
838
847
  if self.verbose and frame_idx < 3:
839
- print(f"[Frame {frame_idx}] Step 3: Extracting pose from pyclnf params...")
848
+ safe_print(f"[Frame {frame_idx}] Step 3: Extracting pose from pyclnf params...")
840
849
 
841
850
  if 'params' in info:
842
851
  # Use params from pyclnf CLNF optimization (CORRECT approach)
@@ -870,7 +879,7 @@ class FullPythonAUPipeline:
870
879
  # Step 4: Align face
871
880
  t0 = time.time() if debug_info is not None else None
872
881
  if self.verbose and frame_idx < 3:
873
- print(f"[Frame {frame_idx}] Step 4: Aligning face...")
882
+ safe_print(f"[Frame {frame_idx}] Step 4: Aligning face...")
874
883
  aligned_face = self.face_aligner.align_face(
875
884
  image=frame,
876
885
  landmarks_68=landmarks_68,
@@ -881,7 +890,7 @@ class FullPythonAUPipeline:
881
890
  triangulation=self.triangulation
882
891
  )
883
892
  if self.verbose and frame_idx < 3:
884
- print(f"[Frame {frame_idx}] Step 4: Aligned face shape: {aligned_face.shape}")
893
+ safe_print(f"[Frame {frame_idx}] Step 4: Aligned face shape: {aligned_face.shape}")
885
894
 
886
895
  if debug_info is not None:
887
896
  debug_info['alignment'] = {
@@ -892,7 +901,7 @@ class FullPythonAUPipeline:
892
901
  # Step 5: Extract HOG features
893
902
  t0 = time.time() if debug_info is not None else None
894
903
  if self.verbose and frame_idx < 3:
895
- print(f"[Frame {frame_idx}] Step 5: Extracting HOG features...")
904
+ safe_print(f"[Frame {frame_idx}] Step 5: Extracting HOG features...")
896
905
  hog_features = pyfhog.extract_fhog_features(
897
906
  aligned_face,
898
907
  cell_size=8
@@ -900,7 +909,7 @@ class FullPythonAUPipeline:
900
909
  # pyfhog 0.1.4+ outputs in OpenFace-compatible format (no transpose needed)
901
910
  # The HOG flattening order matches C++ OpenFace Face_utils.cpp line 265
902
911
  if self.verbose and frame_idx < 3:
903
- print(f"[Frame {frame_idx}] Step 5: HOG features shape: {hog_features.shape}")
912
+ safe_print(f"[Frame {frame_idx}] Step 5: HOG features shape: {hog_features.shape}")
904
913
 
905
914
  if debug_info is not None:
906
915
  debug_info['hog_extraction'] = {
@@ -911,10 +920,10 @@ class FullPythonAUPipeline:
911
920
  # Step 6: Extract geometric features
912
921
  t0 = time.time() if debug_info is not None else None
913
922
  if self.verbose and frame_idx < 3:
914
- print(f"[Frame {frame_idx}] Step 6: Extracting geometric features...")
923
+ safe_print(f"[Frame {frame_idx}] Step 6: Extracting geometric features...")
915
924
  geom_features = self.pdm_parser.extract_geometric_features(params_local)
916
925
  if self.verbose and frame_idx < 3:
917
- print(f"[Frame {frame_idx}] Step 6: Geometric features shape: {geom_features.shape}")
926
+ safe_print(f"[Frame {frame_idx}] Step 6: Geometric features shape: {geom_features.shape}")
918
927
 
919
928
  # Ensure float32 for Cython compatibility
920
929
  hog_features = hog_features.astype(np.float32)
@@ -929,14 +938,14 @@ class FullPythonAUPipeline:
929
938
  # Step 7: Update running median
930
939
  t0 = time.time() if debug_info is not None else None
931
940
  if self.verbose and frame_idx < 3:
932
- print(f"[Frame {frame_idx}] Step 7: Updating running median...")
941
+ safe_print(f"[Frame {frame_idx}] Step 7: Updating running median...")
933
942
  # C++ increments frames_tracking BEFORE the check, so frame 0 → counter=1 → update
934
943
  # To match: update on frames 0, 2, 4, 6... (even frames)
935
944
  update_histogram = (frame_idx % 2 == 0) # Match C++ timing
936
945
  self.running_median.update(hog_features, geom_features, update_histogram=update_histogram)
937
946
  running_median = self.running_median.get_combined_median()
938
947
  if self.verbose and frame_idx < 3:
939
- print(f"[Frame {frame_idx}] Step 7: Running median shape: {running_median.shape}")
948
+ safe_print(f"[Frame {frame_idx}] Step 7: Running median shape: {running_median.shape}")
940
949
 
941
950
  if debug_info is not None:
942
951
  debug_info['running_median'] = {
@@ -952,7 +961,7 @@ class FullPythonAUPipeline:
952
961
  # Step 8: Predict AUs
953
962
  t0 = time.time() if debug_info is not None else None
954
963
  if self.verbose and frame_idx < 3:
955
- print(f"[Frame {frame_idx}] Step 8: Predicting AUs...")
964
+ safe_print(f"[Frame {frame_idx}] Step 8: Predicting AUs...")
956
965
  au_results = self._predict_aus(
957
966
  hog_features,
958
967
  geom_features,
@@ -988,7 +997,7 @@ class FullPythonAUPipeline:
988
997
 
989
998
  except Exception as e:
990
999
  if self.verbose:
991
- print(f"Warning: Frame {frame_idx} failed: {e}")
1000
+ safe_print(f"Warning: Frame {frame_idx} failed: {e}")
992
1001
 
993
1002
  return result
994
1003
 
@@ -1063,9 +1072,9 @@ class FullPythonAUPipeline:
1063
1072
  DataFrame with finalized AU predictions
1064
1073
  """
1065
1074
  if self.verbose:
1066
- print("")
1067
- print("Applying post-processing...")
1068
- print(" [1/3] Two-pass median correction...")
1075
+ safe_print("")
1076
+ safe_print("Applying post-processing...")
1077
+ safe_print(" [1/3] Two-pass median correction...")
1069
1078
 
1070
1079
  # Two-pass reprocessing: Re-predict AUs for early frames using final running median
1071
1080
  # This fixes systematic baseline offset from immature running median in early frames
@@ -1073,7 +1082,7 @@ class FullPythonAUPipeline:
1073
1082
  final_median = self.running_median.get_combined_median()
1074
1083
 
1075
1084
  if self.verbose:
1076
- print(f" Re-predicting {len(self.stored_features)} early frames with final median...")
1085
+ safe_print(f" Re-predicting {len(self.stored_features)} early frames with final median...")
1077
1086
 
1078
1087
  # Re-predict AUs for stored frames
1079
1088
  for frame_idx, hog_features, geom_features in self.stored_features:
@@ -1088,13 +1097,13 @@ class FullPythonAUPipeline:
1088
1097
  self.stored_features = []
1089
1098
 
1090
1099
  if self.verbose:
1091
- print(f" Two-pass correction complete")
1100
+ safe_print(f" Two-pass correction complete")
1092
1101
  else:
1093
1102
  if self.verbose:
1094
- print(" (No stored features - skipping)")
1103
+ safe_print(" (No stored features - skipping)")
1095
1104
 
1096
1105
  if self.verbose:
1097
- print(" [2/3] Cutoff adjustment...")
1106
+ safe_print(" [2/3] Cutoff adjustment...")
1098
1107
 
1099
1108
  # Apply cutoff adjustment for dynamic models
1100
1109
  au_cols = [col for col in df.columns if col.startswith('AU') and col.endswith('_r')]
@@ -1150,7 +1159,7 @@ class FullPythonAUPipeline:
1150
1159
  df[au_col] = np.clip(au_values - offset, 0.0, 5.0)
1151
1160
 
1152
1161
  if self.verbose:
1153
- print(" [3/3] Temporal smoothing...")
1162
+ safe_print(" [3/3] Temporal smoothing...")
1154
1163
 
1155
1164
  # Apply 3-frame moving average
1156
1165
  for au_col in au_cols:
@@ -1158,7 +1167,7 @@ class FullPythonAUPipeline:
1158
1167
  df[au_col] = smoothed
1159
1168
 
1160
1169
  if self.verbose:
1161
- print("Post-processing complete")
1170
+ safe_print("Post-processing complete")
1162
1171
 
1163
1172
  return df
1164
1173
 
@@ -1219,7 +1228,7 @@ Examples:
1219
1228
  verbose=True
1220
1229
  )
1221
1230
  except Exception as e:
1222
- print(f"Failed to initialize pipeline: {e}")
1231
+ safe_print(f"Failed to initialize pipeline: {e}")
1223
1232
  return 1
1224
1233
 
1225
1234
  # Process video
@@ -1236,28 +1245,28 @@ Examples:
1236
1245
  # Save final results
1237
1246
  df.to_csv(args.output, index=False)
1238
1247
 
1239
- print("=" * 80)
1240
- print("SUCCESS")
1241
- print("=" * 80)
1242
- print(f"Processed {len(df)} frames")
1243
- print(f"Results saved to: {args.output}")
1244
- print("")
1248
+ safe_print("=" * 80)
1249
+ safe_print("SUCCESS")
1250
+ safe_print("=" * 80)
1251
+ safe_print(f"Processed {len(df)} frames")
1252
+ safe_print(f"Results saved to: {args.output}")
1253
+ safe_print("")
1245
1254
 
1246
1255
  # Show AU statistics
1247
1256
  au_cols = [col for col in df.columns if col.startswith('AU') and col.endswith('_r')]
1248
1257
  if au_cols:
1249
- print("AU Statistics:")
1258
+ safe_print("AU Statistics:")
1250
1259
  for au_col in sorted(au_cols):
1251
1260
  success_frames = df[df['success'] == True]
1252
1261
  if len(success_frames) > 0:
1253
1262
  mean_val = success_frames[au_col].mean()
1254
1263
  max_val = success_frames[au_col].max()
1255
- print(f" {au_col}: mean={mean_val:.3f}, max={max_val:.3f}")
1264
+ safe_print(f" {au_col}: mean={mean_val:.3f}, max={max_val:.3f}")
1256
1265
 
1257
1266
  return 0
1258
1267
 
1259
1268
  except Exception as e:
1260
- print(f"Processing failed: {e}")
1269
+ safe_print(f"Processing failed: {e}")
1261
1270
  import traceback
1262
1271
  traceback.print_exc()
1263
1272
  return 1
@@ -14,6 +14,14 @@ from typing import Optional, Callable
14
14
  from .pipeline import FullPythonAUPipeline
15
15
 
16
16
 
17
+ def safe_print(*args, **kwargs):
18
+ """Print wrapper that handles BrokenPipeError in GUI subprocess contexts."""
19
+ try:
20
+ print(*args, **kwargs)
21
+ except (BrokenPipeError, IOError):
22
+ pass # Stdout disconnected (e.g., GUI subprocess terminated)
23
+
24
+
17
25
  class OpenFaceProcessor:
18
26
  """
19
27
  OpenFace 2.2-compatible AU extraction processor.
@@ -74,7 +82,7 @@ class OpenFaceProcessor:
74
82
  weights_dir = Path(weights_dir)
75
83
 
76
84
  if self.verbose:
77
- print("Initializing PyFaceAU (OpenFace 2.2 Python replacement)...")
85
+ safe_print("Initializing PyFaceAU (OpenFace 2.2 Python replacement)...")
78
86
 
79
87
  # Initialize the PyFaceAU pipeline (OpenFace-compatible: PyMTCNN → CLNF → AU)
80
88
  self.pipeline = FullPythonAUPipeline(
@@ -88,10 +96,10 @@ class OpenFaceProcessor:
88
96
  )
89
97
 
90
98
  if self.verbose:
91
- print(f" PyFaceAU initialized")
92
- print(f" CLNF refinement: {'Enabled' if use_clnf_refinement else 'Disabled'}")
93
- print(f" Expected accuracy: r > 0.92 (OpenFace 2.2 correlation)")
94
- print()
99
+ safe_print(f" PyFaceAU initialized")
100
+ safe_print(f" CLNF refinement: {'Enabled' if use_clnf_refinement else 'Disabled'}")
101
+ safe_print(f" Expected accuracy: r > 0.92 (OpenFace 2.2 correlation)")
102
+ safe_print()
95
103
 
96
104
  def process_video(
97
105
  self,
@@ -118,7 +126,7 @@ class OpenFaceProcessor:
118
126
  output_csv_path = Path(output_csv_path)
119
127
 
120
128
  if self.verbose:
121
- print(f"Processing: {video_path.name}")
129
+ safe_print(f"Processing: {video_path.name}")
122
130
 
123
131
  # Ensure output directory exists
124
132
  output_csv_path.parent.mkdir(parents=True, exist_ok=True)
@@ -136,17 +144,17 @@ class OpenFaceProcessor:
136
144
 
137
145
  if self.verbose:
138
146
  total_frames = len(df)
139
- print(f" Processed {success_count}/{total_frames} frames successfully")
147
+ safe_print(f" Processed {success_count}/{total_frames} frames successfully")
140
148
  if success_count < total_frames:
141
149
  failed = total_frames - success_count
142
- print(f" {failed} frames failed (no face detected)")
143
- print(f" Output: {output_csv_path}")
150
+ safe_print(f" {failed} frames failed (no face detected)")
151
+ safe_print(f" Output: {output_csv_path}")
144
152
 
145
153
  return int(success_count)
146
154
 
147
155
  except Exception as e:
148
156
  if self.verbose:
149
- print(f" Error processing video: {e}")
157
+ safe_print(f" Error processing video: {e}")
150
158
  raise
151
159
 
152
160
  def clear_cache(self):
@@ -205,14 +213,14 @@ def process_videos(
205
213
  output_dir='/path/to/output',
206
214
  use_clnf_refinement=True
207
215
  )
208
- print(f"Processed {count} videos")
216
+ safe_print(f"Processed {count} videos")
209
217
  ```
210
218
  """
211
219
  directory_path = Path(directory_path)
212
220
 
213
221
  # Check if directory exists
214
222
  if not directory_path.is_dir():
215
- print(f"Error: Directory '{directory_path}' does not exist.")
223
+ safe_print(f"Error: Directory '{directory_path}' does not exist.")
216
224
  return 0
217
225
 
218
226
  # Determine output directory
@@ -224,7 +232,7 @@ def process_videos(
224
232
  output_dir = Path(output_dir)
225
233
 
226
234
  output_dir.mkdir(parents=True, exist_ok=True)
227
- print(f"Output directory: {output_dir}")
235
+ safe_print(f"Output directory: {output_dir}")
228
236
 
229
237
  # Initialize processor
230
238
  processor = OpenFaceProcessor(**processor_kwargs)
@@ -238,24 +246,24 @@ def process_videos(
238
246
  if specific_files:
239
247
  # Process only the specific files
240
248
  files_to_process = [Path(f) for f in specific_files]
241
- print(f"Processing {len(files_to_process)} specific files from current session.")
249
+ safe_print(f"Processing {len(files_to_process)} specific files from current session.")
242
250
  else:
243
251
  # Process all eligible files in the directory
244
252
  files_to_process = list(directory_path.iterdir())
245
- print(f"Processing all eligible files in {directory_path}")
253
+ safe_print(f"Processing all eligible files in {directory_path}")
246
254
 
247
255
  # Process each file
248
256
  for file_path in files_to_process:
249
257
  # Skip if not a file or doesn't exist
250
258
  if not file_path.is_file():
251
- print(f"Warning: {file_path} does not exist or is not a file. Skipping.")
259
+ safe_print(f"Warning: {file_path} does not exist or is not a file. Skipping.")
252
260
  continue
253
261
 
254
262
  filename = file_path.name
255
263
 
256
264
  # Skip files with 'debug' in the filename
257
265
  if 'debug' in filename:
258
- print(f"Skipping debug file: {filename}")
266
+ safe_print(f"Skipping debug file: {filename}")
259
267
  continue
260
268
 
261
269
  # Process file with 'mirrored' in the filename
@@ -271,13 +279,13 @@ def process_videos(
271
279
 
272
280
  if frame_count > 0:
273
281
  processed_count += 1
274
- print(f"Successfully processed: {filename}\n")
282
+ safe_print(f"Successfully processed: {filename}\n")
275
283
  else:
276
- print(f"Failed to process: {filename}\n")
284
+ safe_print(f"Failed to process: {filename}\n")
277
285
 
278
286
  except Exception as e:
279
- print(f"Error processing {filename}: {e}\n")
287
+ safe_print(f"Error processing {filename}: {e}\n")
280
288
 
281
- print(f"\nProcessing complete. {processed_count} files were processed.")
289
+ safe_print(f"\nProcessing complete. {processed_count} files were processed.")
282
290
 
283
291
  return processed_count
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfaceau
3
- Version: 1.3.6
3
+ Version: 1.3.7
4
4
  Summary: Pure Python OpenFace 2.2 AU extraction with CLNF landmark refinement
5
5
  Home-page: https://github.com/johnwilsoniv/face-analysis
6
6
  Author: John Wilson
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyfaceau"
7
- version = "1.3.6"
7
+ version = "1.3.7"
8
8
  description = "Pure Python OpenFace 2.2 AU extraction with CLNF landmark refinement"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes