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:
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user