--- name: photo-processing description: "Process photo directories to detect duplicates, burst sequences, sharpness-ranked keepers, and cleanup candidates. Handles perceptual hashing, EXIF-based burst clustering, edge-sharpness scoring, Syncthing conflicts, and safe staged deletion. Use when asked to deduplicate photos, find blurry shots, clean up burst sequences, detect near-duplicates, or organize camera imports. Triggers on: photo cleanup, deduplicate images, find duplicates, burst shots, sharpness ranking, photo processing, camera import cleanup, remove blurry photos." --- # Photo Processing Clean up photo directories by detecting duplicates, burst sequences, and blurry/outlier frames. Provide actionable keep/delete recommendations with safe staged deletion. ## Reference Files - [Auto-enhance](references/auto-enhance.md) — standalone ImageMagick 7 batch script (`auto-enhance-photos.sh`) for auto-orientation, auto-levels, auto-gamma, mild brightness/saturation. Use this to *improve* surviving keepers, separate from the culling workflow below. ## When to Use - User asks to **deduplicate**, **find duplicates**, or **detect near-duplicates** in a photo folder. - User asks to **clean up burst shots**, **shutter-speed sequences**, or **keep only sharpest** frames. - User asks to **remove blurry photos**, **find out-of-focus shots**, or **rank by sharpness**. - User mentions **Syncthing conflicts** in a photo directory. - User imports a **camera card** or **Fujifilm/Canon/Sony/Nikon** folder and wants cleanup. - Triggers: *photo cleanup*, *deduplicate images*, *find duplicates*, *burst shots*, *sharpness ranking*, *photo processing*, *camera import cleanup*, *remove blurry photos*. ## Required Tools - `exiftool` — EXIF metadata extraction (timestamps, burst grouping) - `python3` + `PIL/Pillow` — perceptual hashing, sharpness scoring - `ImageFilter.FIND_EDGES` from Pillow — edge-based sharpness metric - `fdupes` — exact byte-level duplicate detection (optional fallback) - Standard shell tools (`ls`, `stat`, `md5sum`, `mv`, `mkdir`) ## Core Techniques ### 1. Exact Duplicate Detection Run `fdupes -r -s -S ` first. If it finds groups, those are byte-identical duplicates — safe to delete all but one without visual review. ### 2. Near-Duplicate Detection (Perceptual Hashing) Use **average hash (aHash)** on a 16×16 grayscale resize: ```python from PIL import Image def ahash(path, size=16): with Image.open(path) as im: im = im.convert('L').resize((size, size), Image.Resampling.LANCZOS) pixels = list(im.getdata()) avg = sum(pixels) / len(pixels) bits = ''.join('1' if p > avg else '0' for p in pixels) return int(bits, 2) def hamming_distance(a, b): x = a ^ b dist = 0 while x: dist += 1 x &= x - 1 return dist ``` - **hamming = 0**: identical or nearly identical - **hamming ≤ 5**: very similar, likely burst variants - **hamming ≤ 10**: similar composition, review recommended - **hamming > 20**: different shots Compare only nearby filename ranges (e.g., ±15 frames) to avoid O(n²) on large sets. ### 3. Burst Sequence Detection (EXIF Timestamp Clustering) Use `exiftool -DateTimeOriginal -SubSecTimeOriginal` to extract capture time. Group frames where consecutive timestamps differ by ≤ 3 seconds. A burst is defined as ≥ 3 consecutive frames within that gap. ```python from datetime import datetime from collections import defaultdict def parse_exif_datetime(raw): # 2026:06:07 13:19:22 return datetime.strptime(raw, '%Y:%m:%d %H:%M:%S') ``` Also report **same-second shots** (≥ 2 frames with identical `HH:MM:SS`) — these are always burst/machine-gun frames. ### 4. Sharpness Ranking (Keep the Best, Delete the Rest) Compute **variance of edge intensities** as a proxy for sharpness. A sharper image has stronger high-frequency detail, which shows up after `ImageFilter.FIND_EDGES`. ```python from PIL import Image, ImageFilter def sharpness_score(path): with Image.open(path) as im: im = im.convert('L').resize((1024, 1024), Image.Resampling.LANCZOS) edges = im.filter(ImageFilter.FIND_EDGES) pixels = list(edges.getdata()) mean = sum(pixels) / len(pixels) var = sum((p - mean) ** 2 for p in pixels) / len(pixels) return var ``` Within each burst: 1. Compute sharpness for every frame 2. Sort descending by sharpness 3. Recommend **keeping top 2–5** depending on burst size 4. Label remainder as **delete candidates** **File size as secondary signal:** larger JPEGs in a burst often correlate with more detail (less motion blur, higher complexity), but sharpness score is the primary metric. ### 5. Syncthing Conflict Handling Files matching `*.sync-conflict-*.JPG` are Syncthing duplicates. For each conflict: - Find the non-conflict original by stripping `.sync-conflict-` from the basename - Compare perceptual hashes: - If hamming ≈ 0: conflict is identical → delete the conflict - If hamming is small but sizes differ: keep the larger/better one, delete the other - If no original exists: the conflict is an orphan → move to `to_delete/` for manual review ### 6. Safe Staged Deletion Workflow **Never delete immediately.** Always: 1. Compute all metrics and present a ranked summary to the user 2. Confirm the delete candidate list 3. `mkdir -p to_delete/` 4. `mv to_delete/` 5. Report what was moved and total space freed 6. User can later `rm -rf to_delete/` after review, or `mv to_delete/* .` to restore ## Typical Workflow 1. **Scan directory** — count files, check for exact duplicates (`fdupes`) 2. **Hash all images** — compute aHash for perceptual similarity 3. **Extract EXIF timestamps** — group into burst sequences by time gaps ≤ 3s 4. **Compute sharpness** — rank each burst by edge variance 5. **Identify conflicts** — flag Syncthing orphans 6. **Present summary** — bursts found, keepers, delete candidates, space estimate 7. **Move to `to_delete/`** after user confirmation ## Output Format For each burst, present: - Burst size and time range - Top N keepers with sharpness score + file size - Bottom N delete candidates with sharpness score + file size - Clear recommendation ("Keep top 3, delete the other 12") ## Caveats - **Low-resolution thumbnails** or heavily compressed frames may have low file size but still be sharp — trust the sharpness score over size when they disagree. - **Monochrome/B&W images** have different perceptual hash behavior than color; the edge-sharpness metric still works. - **Portraits with shallow DOF** may have a blurry background but sharp subject. The global edge-variance metric can mis-rank these if the background dominates. For critical portrait bursts, the user should visually review the top-ranked frames rather than auto-deleting. - **Different compositions** within a short time window (e.g. wide shot → close-up) may cluster as a single burst by timestamp. The hamming distance check catches this — if consecutive frames differ by > 20, they are different compositions and should not be auto-deleted. ## Auto-Enhancement After culling, optionally improve the surviving keepers with the standalone ImageMagick batch script. See [references/auto-enhance.md](references/auto-enhance.md) for the full `auto-enhance-photos.sh` script, a per-flag explanation, output naming, and saturation tuning.