initial commit
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user