chore: remove dead embedding paths and suppress insightface FutureWarning in image_processing

- immich_api.py: remove FaceData.embedding field and numpy import — Immich face
  embeddings were never consumed after being fetched; bbox/confidence is all that's used
- embeddings.py: remove immich_embedding param from get_embedding() — never passed by
  any caller; simplify docstring accordingly
- image_processing.py: wrap insightface_app.get() in warnings filter to suppress the
  scikit-image FutureWarning about estimate being deprecated (already suppressed in
  embeddings.py for the diversity path, was leaking from the crop-alignment path)
- README.md: remove two stale "object mode" / "object classification" fragments
This commit is contained in:
2026-06-14 17:47:30 +00:00
parent 72dbbfa18a
commit 0ef15c5c12
4 changed files with 9 additions and 23 deletions
+2 -2
View File
@@ -9,7 +9,7 @@
**Docs:** [Setup](https://github.com/sudolulo/winnow/wiki/Setup) · [Troubleshooting](https://github.com/sudolulo/winnow/wiki/Troubleshooting) · [FAQ](https://github.com/sudolulo/winnow/wiki/FAQ) **Docs:** [Setup](https://github.com/sudolulo/winnow/wiki/Setup) · [Troubleshooting](https://github.com/sudolulo/winnow/wiki/Troubleshooting) · [FAQ](https://github.com/sudolulo/winnow/wiki/FAQ)
`winnow` pulls photos from your [Immich](https://immich.app) library, selects the most diverse and highest-quality subset using AI embeddings, and delivers them as training data for [Frigate](https://frigate.video)'s face recognition and object classification models. `winnow` pulls photos from your [Immich](https://immich.app) library, selects the most diverse and highest-quality subset using AI embeddings, and delivers them as training data for [Frigate](https://frigate.video)'s face recognition.
The best Frigate training data is images you curate manually — photos taken specifically for recognition, in controlled conditions, uploaded directly through Frigate's UI. For people you can do that for, do it. winnow is for everyone else: people in your library you want Frigate to recognise but don't have dedicated training photos for. It mines your existing Immich library for the most diverse spread of real-world appearances and fills the gap. The best Frigate training data is images you curate manually — photos taken specifically for recognition, in controlled conditions, uploaded directly through Frigate's UI. For people you can do that for, do it. winnow is for everyone else: people in your library you want Frigate to recognise but don't have dedicated training photos for. It mines your existing Immich library for the most diverse spread of real-world appearances and fills the gap.
@@ -257,7 +257,7 @@ When run with a terminal attached, winnow starts an interactive session: select
## Requirements ## Requirements
- **Immich** v1.106+ - **Immich** v1.106+
- **Frigate** v0.16+ (face mode only — object mode has no Frigate dependency) - **Frigate** v0.16+
- **GPU** recommended: NVIDIA (CUDA), AMD (ROCm), or Intel (Arc / iGPU via OpenVINO) - **GPU** recommended: NVIDIA (CUDA), AMD (ROCm), or Intel (Arc / iGPU via OpenVINO)
- **Python** 3.13+ - **Python** 3.13+
+2 -10
View File
@@ -205,25 +205,17 @@ def get_face_embedding(img_pil: Image.Image) -> np.ndarray | None:
def get_embedding( def get_embedding(
img_pil: Image.Image, img_pil: Image.Image,
asset_id: str | None = None, asset_id: str | None = None,
immich_embedding: np.ndarray | None = None,
) -> np.ndarray | None: ) -> np.ndarray | None:
"""Get embedding for a face image. """Get embedding for a face image.
Priority: Checks disk cache first (if enabled and asset_id provided),
1. Pre-fetched Immich embedding (if provided) then falls back to local InsightFace computation.
2. Disk cache (if enabled and asset_id provided)
3. Local InsightFace computation
""" """
from .config import Config from .config import Config
use_cache = Config.ENABLE_CACHE and asset_id is not None use_cache = Config.ENABLE_CACHE and asset_id is not None
cache = get_cache(Config.CACHE_DIR) if use_cache else None cache = get_cache(Config.CACHE_DIR) if use_cache else None
if immich_embedding is not None:
if cache:
cache.put(asset_id, immich_embedding, "insightface")
return immich_embedding
if cache: if cache:
cached = cache.get(asset_id, "insightface") cached = cache.get(asset_id, "insightface")
if cached is not None: if cached is not None:
+3
View File
@@ -2,6 +2,7 @@
import logging import logging
import os import os
import warnings
import numpy as np import numpy as np
from PIL import Image from PIL import Image
@@ -113,6 +114,8 @@ def process_face_mode(
min(img_h, y2 + pad_y), min(img_h, y2 + pad_y),
) )
search_crop = img.crop(search_box) search_crop = img.crop(search_box)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*estimate.*is deprecated", category=FutureWarning)
detected = insightface_app.get(np.asarray(search_crop)) detected = insightface_app.get(np.asarray(search_crop))
if detected: if detected:
cx, cy = search_crop.width / 2, search_crop.height / 2 cx, cy = search_crop.width / 2, search_crop.height / 2
+1 -10
View File
@@ -5,7 +5,6 @@ from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from io import BytesIO from io import BytesIO
import numpy as np
import requests import requests
from PIL import Image, ImageOps from PIL import Image, ImageOps
@@ -21,7 +20,6 @@ _MAX_ASSETS_PER_PERSON = 5000 # Stop fetching after this many — diversity poo
class FaceData: class FaceData:
"""Pre-computed face data from Immich.""" """Pre-computed face data from Immich."""
embedding: np.ndarray | None
bbox: tuple[float, float, float, float] # (x1, y1, x2, y2) bbox: tuple[float, float, float, float] # (x1, y1, x2, y2)
confidence: float | None confidence: float | None
image_width: int image_width: int
@@ -120,7 +118,7 @@ def fetch_face_data(asset_id: str, person_id: str | None = None) -> FaceData | N
person_id: Optional person ID to match the specific face person_id: Optional person ID to match the specific face
Returns: Returns:
FaceData with embedding, bbox, and confidence, or None if unavailable FaceData with bbox and confidence, or None if unavailable
""" """
try: try:
resp = requests.get( resp = requests.get(
@@ -148,12 +146,6 @@ def fetch_face_data(asset_id: str, person_id: str | None = None) -> FaceData | N
if face is None: if face is None:
face = faces[0] # Fall back to first/largest face face = faces[0] # Fall back to first/largest face
# Extract embedding if available
embedding = None
if "embedding" in face:
embedding = np.array(face["embedding"], dtype=np.float32)
# Extract bounding box
bbox = ( bbox = (
face.get("boundingBoxX1", 0), face.get("boundingBoxX1", 0),
face.get("boundingBoxY1", 0), face.get("boundingBoxY1", 0),
@@ -163,7 +155,6 @@ def fetch_face_data(asset_id: str, person_id: str | None = None) -> FaceData | N
score = face.get("score") score = face.get("score")
return FaceData( return FaceData(
embedding=embedding,
bbox=bbox, bbox=bbox,
confidence=score if score is not None else face.get("confidence"), confidence=score if score is not None else face.get("confidence"),
image_width=face.get("imageWidth", 0), image_width=face.get("imageWidth", 0),