An open-source Python tool that automatically sorts photos into folders like selected, blurry, closed_eye, and duplicates.
Perfect for photographers who want a simple, local version of tools like AfterShoot.
- Sharpness detection β filters out blurry images
- Face & eye detection β marks images with closed eyes
- Duplicate detection β finds near-identical shots
- Automatic organization β outputs sorted folders
- Local & private β runs on your machine only
git clone https://github.com/your-username/photo-culler.git
cd photo-culler
pip install -r requirements.txt
python photo_culler.py -i ./photos -o ./culled
culled/
βββ selected/ β
best photos
βββ blurry/ π«οΈ out of focus
βββ closed_eye/ π΄ eyes closed
βββ duplicates/ π similar shots
βββ others/ β uncategorized
Option | Short | Description |
---|---|---|
--input |
-i |
Input folder with photos (required) |
--output |
-o |
Output folder for results (required) |
--sharpness-threshold |
Sharpness sensitivity (default: 50) | |
--verbose |
-v |
Print detailed classification info |
# Minimal command
python photo_culler.py -i photos -o sorted
# All options specified
python photo_culler.py \
--input /path/to/photos \
--output /path/to/sorted \
--sharpness-threshold 45.5 \
--verbose
output_directory/
βββ selected/ # β
High-quality images
β βββ IMG_001.jpg # Sharp, open eyes, no duplicates
β βββ IMG_045.jpg
βββ blurry/ # π«οΈ Low sharpness images
β βββ IMG_012.jpg # Below sharpness threshold
β βββ IMG_089.jpg
βββ closed_eye/ # π΄ Eyes closed images
β βββ IMG_034.jpg # Majority of eyes closed
β βββ IMG_067.jpg
βββ duplicates/ # π Near-duplicate images
β βββ IMG_023.jpg # Similar to images in other folders
β βββ IMG_024.jpg
βββ others/ # β Miscellaneous images
βββ IMG_078.jpg # No faces detected
βββ IMG_091.jpg # Processing errors
Processing Flow:
- Duplicate Check: Uses perceptual hashing (pHash) with Hamming distance β€ 8 to identify near-duplicates.
- Sharpness Analysis: Multi-method approach (Laplacian, Sobel, Tenengrad) with configurable threshold.
- Face Detection: Utilizes MediaPipe face mesh with a confidence threshold for accurate detection.
- Eye State Analysis: Calculates Eye Aspect Ratio (EAR) to detect closed eyes in faces.
π§ Troubleshooting
# Error: No module named 'cv2'
pip install opencv-python
# Error: No module named 'mediapipe'
pip install mediapipe
# Error: No module named 'PIL'
pip install pillow
# Error: No module named 'imagehash'
pip install imagehash
# Lower the threshold
python photo_culler.py -i photos -o sorted --sharpness-threshold 25 -v
# Use verbose mode to debug
python photo_culler.py -i photos -o sorted -v
# Check eye ratio values in output
# Edit script: change hamming_distance <= 8 to <= 5
# Run with verbose output
python photo_culler.py -i ./photos -o ./sorted -v
# Sample verbose output:
# IMG_001.jpg:
# Category: selected
# Reason: Passed all quality checks
# Sharpness (combined): 67.45
# left eye ratio: 0.234
# right eye ratio: 0.241
#
# IMG_002.jpg:
# Category: closed_eye
# Reason: Majority of eyes closed: 100%
# Sharpness (combined): 78.23
# left eye ratio: 0.089
# right eye ratio: 0.102
# Eye detection sensitivity (line ~150)
if left_ear < 0.15: # Lower = more sensitive to closed eyes
# Closed eye majority threshold (line ~280)
if closed_ratio > 0.6: # Lower = stricter about closed eyes
# Duplicate detection sensitivity (line ~320)
if hamming_distance <= 8: # Lower = stricter duplicate detection
# Sharpness calculation weights (line ~95)
combined_score = (laplacian_var * 0.6 + sobel_mean * 0.4)
categories = ['selected', 'blurry', 'closed_eye', 'duplicates', 'others', 'custom_category']
def classify_image(self, image_path, duplicates):
# ... existing logic ...
# Your custom logic
if your_custom_condition:
return 'custom_category', debug_info
#!/bin/bash
#!/bin/bash
# auto_cull.sh - Wrapper script for photo culling
PHOTOS_DIR="\$1"
OUTPUT_DIR="\$2"
THRESHOLD="${3:-50}"
if [ -z "$$PHOTOS_DIR" ] || [ -z "$$OUTPUT_DIR" ]; then
echo "Usage: \$0 <photos_directory> <output_directory> [threshold]"
echo "Example: \$0 ./photos ./sorted 45"
exit 1
fi
echo "π Starting photo culling..."
echo "π Input: $PHOTOS_DIR"
echo "π Output: $OUTPUT_DIR"
echo "π― Threshold: $THRESHOLD"
python photo_culler.py \
-i "$PHOTOS_DIR" \
-o "$OUTPUT_DIR" \
--sharpness-threshold "$THRESHOLD" \
-v
if [ $? -eq 0 ]; then
echo "β
Culling completed successfully!"
echo "π Results:"
ls -la "$OUTPUT_DIR"
else
echo "β Culling failed!"
exit 1
fi
import subprocess
import sys
from pathlib import Path
def cull_photos(input_dir, output_dir, threshold=50.0, verbose=False):
"""
Wrapper function to call photo culler from another Python script.
Returns:
tuple: (success, stdout, stderr)
"""
cmd = [
sys.executable, 'photo_culler.py',
'-i', str(input_dir),
'-o', str(output_dir),
'--sharpness-threshold', str(threshold)
]
if verbose:
cmd.append('-v')
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
return result.returncode == 0, result.stdout, result.stderr
except subprocess.TimeoutExpired:
return False, "", "Process timed out after 1 hour"
# Usage example
if __name__ == "__main__":
success, output, error = cull_photos("./photos", "./sorted", threshold=45, verbose=True)
if success:
print("β
Photo culling completed!")
else:
print(f"β Error: {error}")
min_detection_confidence=0.1 # Default: 0.3
# Instead of processing 10,000 images at once
find photos -name "*.jpg" | split -l 1000 - batch_
for batch in batch_*; do
# Process each batch separately
done
- Large collections: Process images in batches of 1000β2000 for efficiency.
- High-resolution images: Resize images for analysis to reduce memory usage.
- Multiple runs: Clear output directories before each run to avoid mixing results.
Setting | Speed | Accuracy | Recommended Use Case |
---|---|---|---|
min_detection_confidence=0.1 |
Fast | Lower | Quick sorting |
min_detection_confidence=0.3 |
Medium | Good | Balanced |
min_detection_confidence=0.5 |
Slow | High | Precise sorting |
π Starting improved photo culling process...
π Input directory: ./photos
π Output directory: ./culled
π― Sharpness threshold: 50.0
Setting up output directories in: ./culled
β Created: selected/
β Created: blurry/
β Created: closed_eye/
β Created: duplicates/
β Created: others/
Scanning for images in: ./photos
Found 150 image files
Calculating perceptual hashes for duplicate detection...
Found 12 duplicate images
Processing 150 images...
Processed 50/150 images
Processed 100/150 images
Processed 150/150 images
β
Photo culling completed!
{
"improved_photo_culling_summary": {
"input_directory": "./photos",
"output_directory": "./culled",
"sharpness_threshold": 50.0,
"statistics": {
"total_processed": 150,
"selected": 89,
"blurry": 23,
"closed_eye": 15,
"duplicates": 18,
"others": 5
},
"percentages": {
"selected": 59.3,
"blurry": 15.3,
"closed_eye": 10.0,
"duplicates": 12.0,
"others": 3.3
}
}
}
# Process RAW files keeping original format
python photo_culler_raw.py -i ./raw_photos -o ./sorted_raw -v
# Convert RAW to JPEG while sorting
python photo_culler_raw.py -i ./raw_photos -o ./sorted_jpeg --convert-to-jpeg --jpeg-quality 95
# Professional workflow with high standards
python photo_culler_raw.py -i ./shoot_photos -o ./processed --sharpness-threshold 50 --convert-to-jpeg -v
When reporting bugs, please include:
- Python version:
python --version
- Operating system: Windows/macOS/Linux + version
- Package versions:
pip list | grep -E "(opencv|mediapipe|pillow|imagehash)"
- Full error message with stack trace
- Command used and expected vs actual behavior
- Describe the use case and problem it solves
- Provide example scenarios
- Suggest implementation approach if you have ideas
This project is licensed under the MIT License.
- OpenCV for computer vision algorithms
- MediaPipe for face and landmark detection
- ImageHash for perceptual hashing
- Pillow for image processing
Last updated: 2025
Version: 1.0
An open-source Python tool that automatically sorts photos into folders like selected, blurry, closed_eye, and duplicates. Perfect for photographers who want a simple, local version of tools.
...
π Happy Photo Culling!
Transform your chaotic photo collection into an organized masterpiece! πΈβ¨