Files
hermes-config/skills/mlops/computer-vision-inference/references/immich-yolo-pipeline.py
T
2026-07-12 10:17:17 -04:00

117 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
Immich YOLO Photo Classifier Pipeline
Classifies photos as: has-people, scenic, or neither.
Moves non-people/non-scenic photos to NO PEOPLE PHOTOS folder.
Usage:
/mnt/storage/yolo_venv/bin/python3 this_script.py
Requirements:
- /mnt/storage/yolo_venv/ with ultralytics installed
- ~/yolov8n_openvino_model/ (exported from yolov8n.pt)
- Immich PostgreSQL container running
- Photos on /mnt/wd-passport/immich/photos/
"""
import multiprocessing, os, time, warnings, subprocess, shutil
warnings.filterwarnings("ignore")
PHOTO_BASE = "/mnt/wd-passport/immich/photos"
NO_PEOPLE_DIR = "/mnt/wd-passport/immich/NO PEOPLE PHOTOS"
DB_QUERY = '''docker exec immich_postgres psql -U postgres -d immich -t -A -c "SELECT \\\"originalPath\\\" FROM asset WHERE type='IMAGE' AND visibility='timeline' AND \\\"originalPath\\\" LIKE '/data/library/%' ORDER BY \\\"originalPath\\\";" 2>/dev/null'''
PERSON_CLASS = 0
NATURE_CLASSES = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 58, 77, 80}
MOVE_CLASSES = {56, 57, 59, 60, 61, 62, 63, 64, 65, 67, 70, 71, 72, 73, 74, 75, 76, 78, 79,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
def worker(args):
model_path, paths = args
from ultralytics import YOLO
import warnings
warnings.filterwarnings("ignore")
model = YOLO(model_path, task="detect")
results = []
for ap in paths:
try:
r = model(ap, verbose=False)
has_person = has_nature = has_move = False
for r2 in r:
if r2.boxes:
for box in r2.boxes:
cls = int(box.cls[0])
if cls == PERSON_CLASS: has_person = True
elif cls in NATURE_CLASSES: has_nature = True
elif cls in MOVE_CLASSES: has_move = True
if has_person: results.append((ap, "keep_people"))
elif has_nature: results.append((ap, "keep_scenic"))
elif has_move: results.append((ap, "move"))
else: results.append((ap, "check_size"))
except Exception as e:
results.append((ap, f"error:{e}"))
return results
if __name__ == "__main__":
multiprocessing.set_start_method("forkserver", force=True)
num_workers = min(multiprocessing.cpu_count(), 8)
os.makedirs(NO_PEOPLE_DIR, exist_ok=True)
model_path = os.path.expanduser("~/yolov8n_openvino_model/")
print(f"[1] Querying DB...", flush=True)
result = subprocess.run(DB_QUERY, shell=True, capture_output=True, text=True, timeout=120)
db_paths = [p.strip() for p in result.stdout.strip().split('\n') if p.strip()]
print(f"[2] Mapping {len(db_paths)} paths...", flush=True)
actual = []
for p in db_paths:
ap = PHOTO_BASE + "/" + p[6:]
if os.path.exists(ap):
actual.append(ap)
print(f" {len(actual)} accessible files", flush=True)
print(f"[3] Classifying with {num_workers} workers...", flush=True)
chunks = [actual[i::num_workers] for i in range(num_workers)]
chunk_args = [(model_path, c) for c in chunks]
t_start = time.time()
with multiprocessing.Pool(num_workers) as pool:
all_results = pool.map(worker, chunk_args)
elapsed = time.time() - t_start
keep_people = keep_scenic = errors = 0
to_move, check_size = [], []
for chunk in all_results:
for ap, d in chunk:
if d == "keep_people": keep_people += 1
elif d == "keep_scenic": keep_scenic += 1
elif d == "move": to_move.append(ap)
elif d == "check_size": check_size.append(ap)
else: errors += 1
print(f"\n YOLO: {elapsed/60:.1f}min ({len(actual)/elapsed:.1f} img/s)", flush=True)
print(f" People:{keep_people} Scenic:{keep_scenic} Move:{len(to_move)} Check:{len(check_size)} Err:{errors}", flush=True)
print(f"[4] Size check on {len(check_size)} undetected...", flush=True)
for ap in check_size:
try:
if os.path.getsize(ap) < 400000:
to_move.append(ap)
except: pass
print(f" Now {len(to_move)} to move", flush=True)
print(f"[5] Moving {len(to_move)} photos...", flush=True)
moved = file_errors = 0
for ap in to_move:
try:
rel = os.path.relpath(ap, PHOTO_BASE)
dest = os.path.join(NO_PEOPLE_DIR, rel)
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.move(ap, dest)
moved += 1
except Exception as e:
file_errors += 1
print(f"\nDONE! Moved {moved} photos", flush=True)
print(f" Total:{len(actual)} Kept(people):{keep_people} Kept(scenic):{keep_scenic} Moved:{moved} Errors:{file_errors}", flush=True)
print(f" Time: {(time.time()-t_start)/60:.1f}min", flush=True)
print(f" Output: {NO_PEOPLE_DIR}", flush=True)