From b69f77637856f4b98017c5418c1a4cba2104ceb5 Mon Sep 17 00:00:00 2001 From: Holden Date: Wed, 17 Jun 2026 17:18:43 +0000 Subject: [PATCH] fix: surface skip reasons and suppress norm_crop FutureWarning process_face_mode now returns a descriptive string instead of None for filtered-out faces ("face too small 45x38px, min 90px", "no face metadata"), so the executor can print a useful reason rather than the generic "no usable face data". Also suppresses the InsightFace norm_crop FutureWarning about deprecated estimate usage, which was noisy at INFO level on every aligned crop. --- winnow/executor.py | 3 ++- winnow/image_processing.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/winnow/executor.py b/winnow/executor.py index 1aa9877..6eb0a9c 100644 --- a/winnow/executor.py +++ b/winnow/executor.py @@ -202,8 +202,9 @@ def execute_jobs(jobs: list[dict]) -> None: count += 1 else: + reason = saved if isinstance(saved, str) else "no usable face data" progress.console.print( - f"[yellow]Skipped {asset['id']} (no usable face data)[/yellow]" + f"[yellow]Skipped {asset['id']} ({reason})[/yellow]" ) except Exception as e: logger.error("Failed to process asset %s: %s", asset.get("id", ""), e) diff --git a/winnow/image_processing.py b/winnow/image_processing.py index 23c3f0e..c9c9b85 100644 --- a/winnow/image_processing.py +++ b/winnow/image_processing.py @@ -48,7 +48,9 @@ def align_face(img: Image.Image, landmarks: list[list[float]] | np.ndarray) -> I if lm.shape != (5, 2): logger.debug("Invalid landmark shape: %s, expected (5, 2)", lm.shape) return None - aligned = norm_crop(img_np, lm) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=".*estimate.*is deprecated", category=FutureWarning) + aligned = norm_crop(img_np, lm) return Image.fromarray(aligned) except ImportError: logger.debug("InsightFace not available for face alignment") @@ -66,10 +68,11 @@ def process_face_mode( count: int, min_width: int | None = None, insightface_app=None, -) -> tuple[int, int] | None: +) -> tuple[int, int] | str | None: """Crop face based on Immich metadata and save to output directory. - Returns (width, height) of the saved crop, or None if no crop was saved. + Returns (width, height) of the saved crop, a skip-reason string if the + face was filtered out, or None if no crop was saved for other reasons. When insightface_app is provided and ENABLE_FACE_ALIGNMENT is True, re-detects the face in the Immich bbox region using InsightFace to get precise landmarks for a proper 112x112 aligned crop. Falls back to @@ -89,7 +92,7 @@ def process_face_mode( if not face_info: logger.debug("No face info for %s in asset %s", person.get("name"), asset.get("id")) - return None + return "no face metadata" img_w, img_h = img.size meta_w = face_info.get("imageWidth") or 0 @@ -108,7 +111,7 @@ def process_face_mode( face_w, face_h = x2 - x1, y2 - y1 if face_w < min_width or face_h < min_width: logger.debug("Face too small (%.1fx%.1f)", face_w, face_h) - return None + return f"face too small ({face_w:.0f}x{face_h:.0f}px, min {min_width}px)" # Re-detect face with InsightFace for landmark-based alignment. # Immich's /api/faces endpoint does not include landmarks, so the