init: project structure with LAION scorer, conda setup, and research docs

This commit is contained in:
Kareem Horstink
2026-08-23 15:31:47 +00:00
commit 4f90f5838a
25 changed files with 1628 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Pi
.pi/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Imrayya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+67
View File
@@ -0,0 +1,67 @@
# Photo Judgers
Judge and shortlist 4000+ photos — primarily wedding photos, with other photo types included.
## Overview
This project designs the workflow for curating, judging, and shortlisting large batches of photos. The main use case is wedding photography, but the system should be flexible enough to handle other photo categories too.
**Key constraint:** This is a workflow design project. The actual execution runs on more capable hardware. This machine is underpowered for the workload.
## Goals
- Design a scoring pipeline to judge photos on aesthetic quality
- Support category/tagging later (initial phase focuses on scoring only)
- Shortlist the best photos from 4000+ batches
- Keep track of decisions and reasoning
## Architecture: Phase 1 — LAION Aesthetic Predictor
Based on research, Phase 1 implements the **LAION Aesthetic Predictor V2** as the first-pass scorer.
### Why LAION V2 First?
- **Simplest starting point** — one-line Python API via HuggingFace transformers
- **MIT license** — fully permissive
- **~2 GB VRAM** — lightweight, fits on consumer GPUs
- **Fast inference** — ~5-10 minutes for 4000 images on GPU
- **Good general aesthetic ranking** — trained on diverse datasets (AI-generated + professional photos)
### Limitations (to address later)
- No technical quality detection (blur, exposure, noise)
- Biased toward AI-generated aesthetic patterns
- Western/art-historical bias — may underrate candid moments and cultural shots
- No context understanding (can't distinguish emotional moments from empty frames)
### Future: Combined Approach
After LAION V2 is working, the full pipeline will combine:
- **LAION V2** — aesthetic scoring (0-10 scale)
- **BRISQUE** — technical quality (blur/noise detection, CPU-only)
- **MUSIQ** (optional) — combined aesthetic + technical scoring
A photo would be kept only if it passes both aesthetic and technical thresholds.
## Structure
```
photo_judgers/
├── test/ # Test folder
├── docs/ # Documentation
├── .pi/ # Pi config (gitignored)
├── README.md # This file
└── .gitignore
```
## Status
**Phase 1: LAION V2 implementation** — In progress.
Next steps:
- Set up Python project (or decide on language)
- Implement LAION V2 scoring pipeline
- Define scoring thresholds for shortlisting
- Build output format (CSV/JSON with scores and file paths)
+65
View File
@@ -0,0 +1,65 @@
# Output Format — JSON
## Structure
The scorer outputs a JSON file with an array of results. Each entry represents one photo.
## Schema
```json
[
{
"filename": "IMG_0001.jpg",
"filepath": "/path/to/wedding-photos/IMG_0001.jpg",
"subtype": "aesthetic",
"laion_score": 7.2,
"laion_version": "v2",
"scoring_date": "2026-01-15T10:30:00Z"
}
]
```
## Fields
| Field | Type | Description |
|-------|------|-------------|
| `filename` | string | Base filename (e.g., `IMG_0001.jpg`) |
| `filepath` | string | Full absolute path to the image |
| `subtype` | string | Classifier subtype — `aesthetic` for LAION (future: `technical`, `blur`, etc.) |
| `laion_score` | float | LAION aesthetic score (0-10 scale) |
| `laion_version` | string | Version of LAION model used (e.g., `v2`) |
| `scoring_date` | string | ISO 8601 timestamp of when scoring was performed |
## Example
```json
[
{
"filename": "DSC_0421.jpg",
"filepath": "C:/wedding-photos/day1/DSC_0421.jpg",
"subtype": "aesthetic",
"laion_score": 8.1,
"laion_version": "v2",
"scoring_date": "2026-01-15T10:30:00Z"
},
{
"filename": "DSC_0422.jpg",
"filepath": "C:/wedding-photos/day1/DSC_0422.jpg",
"subtype": "aesthetic",
"laion_score": 3.4,
"laion_version": "v2",
"scoring_date": "2026-01-15T10:30:01Z"
}
]
```
## Thresholds (TBD)
Thresholds for keep/reject/review are not yet defined. Will be determined after running initial batches and reviewing score distributions.
Suggested approach:
1. Score a sample of 100-200 photos
2. Analyze the score distribution
3. Set thresholds based on the distribution (e.g., top 25% = keep, bottom 25% = reject, middle = review)
4. Adjust based on photographer feedback
+159
View File
@@ -0,0 +1,159 @@
# BRISQUE / NIQE — Classical No-Reference IQA
## Model Info
- **BRISQUE:** "No-reference Image Quality Assessment in the Spatial Domain" — Mittal et al., IEEE TIP 2012
- **NIQE:** "Making a 'Completely Blind' Image Quality Analyzer" — Mittal et al., IEEE SPL 2013
- **Python packages:**
- `pip install brisque[opencv-python]` — Latest BRISQUE (supports opencv-contrib, opencv-headless)
- `pip install pybrisque` — Alternative BRISQUE (older, from Bukalapak)
- `pip install image-quality` — Contains `imquality.brisque` module
- `pip install pyiqa` — IQA-PyTorch toolbox (includes BRISQUE + NIQE + 30+ other metrics)
- `pip install nr-iqa` — Comprehensive library (BRISQUE + NIQE + PIQE)
- **License:** Academic (BRISQUE: BSD-like via LearnOpenCV; NIQE: academic use)
- **Dependencies:** OpenCV, NumPy, SciPy (BRISQUE); skvideo + NumPy (NIQE)
## Architecture
### BRISQUE (Blind/Referenceless Image Spatial Quality Evaluator)
- **Type:** Handcrafted features + Support Vector Regression (SVR)
- **Pipeline:**
1. **MSCN (Mean Subtracted Contrast Normalized) coefficients** — decorrelates image from local brightness/contrast
2. **GGD (Generalized Gaussian Distribution) fitting** to MSCN coefficients — extracts shape parameter alpha (tail heaviness)
3. **Pairwise products** in 4 orientations (horizontal, vertical, 2 diagonals) — 16 features
4. **AGGD (Asymmetric GGD) fitting** to pairwise products
5. **Multi-scale analysis** at 2 scales (original + 0.5x downsampled) — 36-D feature vector total (18 per scale)
6. **SVR prediction** maps 36-D features to quality score in [0, 100]
- **Training data:** TID2008 dataset (human-rated distorted images, MOS scores 0-100)
- **Score direction:** **Lower = better** (0 = perfect, 100 = worst quality)
### NIQE (Natural Image Quality Evaluator)
- **Type:** Handcrafted features + Multivariate Gaussian (MVG) model
- **Key difference from BRISQUE:** Completely opinion-unaware — does NOT need human-rated distorted images for training
- **Pipeline:**
1. Extract 18-D NSS features from 96x96 patches (same as BRISQUE scale-1)
2. Fit MVG model (mu_ref, Sigma_ref) to pristine images — the "naturalness" reference
3. Fit MVG (mu_test, Sigma_test) to test image patches
4. **Quality = Mahalanobis distance** between the two MVG models
- **Training data:** Only pristine (undistorted) images — no human ratings needed
- **Score direction:** **Lower = better** (0 = perfectly natural, higher = more distorted)
## Installation
```bash
# Option 1: Latest BRISQUE (recommended)
pip install brisque[opencv-python]
# Option 2: For servers/Docker (headless)
pip install brisque[opencv-python-headless]
# Option 3: Comprehensive NR-IQA library (BRISQUE + NIQE + PIQE)
pip install nr-iqa
# Option 4: IQA-PyTorch toolbox (BRISQUE + NIQE + 30+ other metrics)
pip install pyiqa
```
## Python Usage
```python
# --- BRISQUE via brisque package ---
from brisque import BRISQUE
import cv2
brisque = BRISQUE()
img = cv2.imread('photo.jpg')
score = brisque.score(img) # Returns float in [0, 100]
print(f"BRISQUE: {score:.2f}") # Lower = better
# --- BRISQUE via image-quality package ---
from imquality.brisque import BRISQUE
from PIL import Image
img = Image.open('photo.jpg')
score = BRISQUE().score(img) # Returns float in [0, 100]
# --- NIQE via nr-iqa package ---
from nr_iqa import NIQE
import cv2
niqe = NIQE()
img = cv2.imread('photo.jpg', cv2.IMREAD_GRAYSCALE)
score = niqe.score(img) # Returns float in [0, 100]
print(f"NIQE: {score:.4f}") # Lower = better
# --- Both via IQA-PyTorch toolbox ---
import pyiqa
device = 'cuda' if torch.cuda.is_available() else 'cpu'
brisque = pyiqa.create_metric('brisque', device=device)
niqe = pyiqa.create_metric('niqe', device=device)
score_b = brisque('./photo.jpg')
score_n = niqe('./photo.jpg')
print(f"BRISQUE: {score_b:.2f}, NIQE: {score_n:.4f}")
# Batch processing
import glob
for path in glob.glob('./wedding-photos/*.jpg'):
score = brisque(path)
```
## Score Interpretation for Wedding Photos
| BRISQUE Score | NIQE Score | Interpretation | Recommended Action |
|---------------|-----------|---------------|-------------------|
| 0-10 | 0-3 | Excellent technical quality | Auto-keep |
| 10-25 | 3-6 | Good technical quality | Keep |
| 25-40 | 6-10 | Acceptable, minor issues | Review |
| 40-60 | 10-15 | Poor technical quality | Likely reject |
| 60-100 | 15+ | Very poor (blurry, noisy, compressed) | Auto-reject |
**Recommended thresholds for wedding culling:**
- BRISQUE < 30 AND NIQE < 8 -> keep (good technical quality)
- BRISQUE > 60 OR NIQE > 15 -> reject (poor technical quality)
- In between -> manual review
## Performance Benchmarks
| Metric | SRCC vs Human | PLCC vs Human | Notes |
|--------|--------------|--------------|-------|
| BRISQUE (TID2008) | ~0.85 | ~0.87 | Trained on TID2008 |
| NIQE (KADID-10k) | ~0.83 | ~0.85 | Opinion-unaware, generalizes well |
| BRISQUE (live) | ~0.81 | ~0.83 | Live (no-reference) dataset |
| NIQE (live) | ~0.80 | ~0.82 | Live (no-reference) dataset |
## Hardware Requirements
- **CPU-only:** No GPU needed — purely classical (no neural networks)
- **Speed per image:** ~50-200ms per image on modern CPU (single-core)
- **Speed for 4,000 images:** ~3-15 minutes on CPU (single-core, depends on image size)
- **Multi-threading:** Can parallelize across CPU cores — ~1-3 minutes with 8+ cores
- **VRAM:** None required (BRISQUE/NIQE are pure NumPy/OpenCV)
- **Memory usage:** ~50-100 MB RAM for batch processing
## What It's Good At
- **No GPU needed:** Runs on any CPU, even low-end machines
- **Fast:** Good for quick technical quality screening — faster than any deep learning model
- **Detects blur and compression artifacts:** Excellent at detecting out-of-focus shots, motion blur, JPEG artifacts, noise
- **Simple:** Minimal dependencies (OpenCV + NumPy + SciPy)
- **Deterministic:** No randomness — same image always gets same score
- **Lightweight:** ~50 MB total install size, no model weights to download
- **Good complement to aesthetic models:** Catches technical rejects that aesthetic models miss
- **Works on any image type:** Not biased toward any particular visual style
## What It's Bad At
- **Purely technical:** Scores only technical quality (blur, noise, compression), not aesthetics at all
- **Poor on artistic/creative images:** Penalizes artistic choices — shallow depth of field, high contrast, film grain, vignette, intentional blur
- **Outdated methodology:** 2012-2013 techniques; inferior to modern deep learning approaches for complex distortions
- **Sensitive to intentional artistic effects:** May score a portrait with beautiful bokeh (shallow DOF) as "low quality"
- **Color-blind:** BRISQUE operates on luminance channel; NIQE on grayscale — ignores color information entirely
- **Not suitable as sole scorer:** Must be combined with an aesthetic model for wedding photo sorting
- **No multi-scale like MUSIQ:** Only processes at one scale (BRISQUE does 2-scale but simpler than MUSIQ's 3-scale)
- **Training data limitations:** BRISQUE trained on TID2008 (lab-distorted images); may not generalize well to real-world wedding photo distortions
+29
View File
@@ -0,0 +1,29 @@
# Commercial AI Photo Culling Tools — For Reference
These are not self-hosted but represent the state-of-the-art in wedding photo culling.
## FilterPixel DeepCull (2026)
- **Type:** Cloud-based, genre-specific AI
- **Speed:** 2:58 for 3,000 photos (cloud)
- **Features:** 10-parameter scoring (emotion, sharpness, composition, storytelling, etc.), memory-based learning, genre-specific models
- **Accuracy:** 85-95% agreement after training on user's past culling decisions
- **Price:** $9.99-$19/month
- **Best for:** Professional photographers needing same-day delivery
## Aftershoot
- **Type:** Local desktop application
- **Speed:** 8-12 minutes for 1,000 images (local)
- **Features:** Duplicate detection, blink detection, blur detection, highlight selection, genre selection (portrait/wedding/events)
- **Learning:** Adapts to user's editing style over time
- **Price:** $15/month
- **Best for:** Wedding photographers with multi-day turnaround
## Imagen AI
- **Type:** Cloud-based editing + culling
- **Accuracy:** 63.4% (noted as lower than FilterPixel/Aftershoot in independent testing)
- **Features:** Culling studio, AI editing, pay-per-image pricing
- **Price:** Pay per image (expensive at scale)
- **Best for:** Photographers already using Imagen for editing
+65
View File
@@ -0,0 +1,65 @@
# IQA-PyTorch Toolbox — All-in-One
## Model Info
- **GitHub:** <https://github.com/chaofengc/IQA-PyTorch>
- **PyPI:** `pip install pyiqa`
- **License:** Apache 2.0
- **Contents:** 30+ IQA metrics including MUSIQ, TOPIQ, BRISQUE, NIQE, LPIPS, FID, NIMA, DBCNN, CLIP-IQA, LIQE, Q-Align (Q-ReAlign), and more
- **Latest updates (2026):**
- Jun 2026: Added Q-ReAlign (Qwen3.5-VL backbone) — 3 sizes: mini (0.8B), lite (4B), pro (9B)
- May 2026: Added FGResQ
- Dec 2025: Added DMM, MACLIP, AFINE
- Jan 2025: Added QualiCLIP variants
- **Results calibrated:** Against official MATLAB scripts when available
- **GPU accelerated:** PyTorch implementations are faster than MATLAB counterparts
## Why It Matters
This toolbox lets you test multiple IQA models on your wedding photos without installing each separately. Useful for benchmarking and comparison. It's the single easiest way to run BRISQUE + NIQE + MUSIQ + other metrics in one codebase.
```bash
# List all available metrics
pyiqa -ls
# Test multiple metrics on a directory
pyiqa musiq niqe brisque -t ./wedding-photos/ --device cuda
# Python API — all metrics in one import
import pyiqa
import torch
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
# Create any metric
brisque = pyiqa.create_metric('brisque', device=device)
musiq = pyiqa.create_metric('musiq', device=device)
niqe = pyiqa.create_metric('niqe', device=device)
# Check scoring direction
print(f"BRISQUE lower_better: {brisque.lower_better}") # True
print(f"MUSIQ lower_better: {musiq.lower_better}") # False (higher = better)
print(f"NIQE lower_better: {niqe.lower_better}") # True
# Batch inference
import glob
for path in glob.glob('./wedding-photos/*.jpg'):
score_b = brisque(path)
score_m = musiq(path)
score_n = niqe(path)
```
## Available Metrics (relevant to wedding photo sorting)
| Metric | Type | Score Direction | GPU Required | Notes |
|--------|------|----------------|--------------|-------|
| brisque | NR-IQA | Lower better | No (CPU) | Blur/noise detection |
| niqe | NR-IQA | Lower better | No (CPU) | Blur/noise detection, opinion-unaware |
| musiq | NR-IQA (Transformer) | Higher better | Yes (~4GB) | Aesthetic + technical combined |
| toqip | NR-IQA (Transformer) | Higher better | Yes (~2GB) | Modern, self-supervised |
| clip_iqa | NR-IQA (CLIP) | Higher better | Yes (~2GB) | CLIP-based aesthetic |
| liqe | NR-IQA (CLIP) | Higher better | Yes (~2GB) | CLIP-based quality |
| lpips | FR-IQA (Perceptual) | Lower better | Yes (~1GB) | Needs reference image |
| fid | FR-IQA (Distribution) | Lower better | Yes (~5GB) | Needs reference dataset |
| nima | NR-IQA (CNN) | Higher better | Yes (~1GB) | Neural IMage Assessment |
| dbcnn | FR-IQA (CNN) | Higher better | Yes (~2GB) | Needs reference image |
+93
View File
@@ -0,0 +1,93 @@
# LAION Aesthetic Predictor — Current State
## Model Info
- **GitHub (V1):** <https://github.com/LAION-AI/aesthetic-predictor>
- **GitHub (V2/Improved):** <https://github.com/christophschuhmann/improved-aesthetic-predictor>
- **HuggingFace:** `shunk031/aesthetics-predictor-v1-vit-large-patch14`, `shunk031/aesthetics-predictor-v2-vit-large-patch14`
- **PyPI:** `simple-aesthetics-predictor` (wrapper), `aesthetic-predictor` (direct)
- **Creator:** Christoph Schuhmann / LAION
- **License:** MIT (model weights)
- **Last Update:** V2 released August 2022; no V3 released as of August 2026
- **Status:** **No newer version has been released.** LAION has not published a V3 or updated model (confirmed by CMU audit paper, FAccT 2026)
## Architecture
- **V1:** Linear layer (768->1 for ViT-L/14, 512->1 for ViT-B/32) on top of CLIP image embeddings
- **V2:** Linear model on top of CLIP ViT-L/14 embeddings (despite testing MLPs with ReLU — the linear model was subjectively preferred for visual ranking quality)
- **Input:** CLIP image embeddings (not raw pixels) — must run CLIP encoder first, then the aesthetic head
- **Output:** Single float score (0-10 scale)
- **Training Data (V2):**
- SAC (Simulacra Aesthetic Captions): ~176,000 AI-generated image ratings
- LAION-Logos: 15,000 logo image-text pairs with aesthetic ratings
- AVA (Aesthetic Visual Analysis): 250,000 professional photos with 5-10 human ratings each
- **CLIP Version:** OpenAI CLIP ViT-L/14 (2021 model)
## Variants
| Variant | CLIP Backbone | Head | Score Range | Notes |
|---------|--------------|------|-------------|-------|
| V1 (ViT-L/14) | ViT-L/14 (768d) | Linear 768->1 | 0-10 | Original, trained on 5,000 SAC samples |
| V1 (ViT-B/32) | ViT-B/32 (512d) | Linear 512->1 | 0-10 | Faster, lower capacity |
| V2 (ViT-L/14) | ViT-L/14 (768d) | Linear 768->1 | 0-10 | Trained on SAC + LAION-Logos + AVA; current best |
## Hardware Requirements
- **CLIP ViT-L/14 encoder:** ~1.8 GB VRAM (FP16)
- **Aesthetic linear head:** Negligible
- **Total per image:** ~2 GB VRAM
- **Batch size:** Can handle ~32-64 images per batch on a 24 GB GPU (RTX 4090)
- **CPU inference:** Possible but slow (~1-2 seconds per image on modern CPU)
- **Estimated time for 4,000 images:** ~5-10 minutes on GPU, ~1-2 hours on CPU
## Python Usage
```python
# Via simple-aesthetics-predictor (HuggingFace Transformers-style API)
from simple_aesthetics_predictor import AestheticsPredictorV1
from transformers import CLIPProcessor
import torch
model = AestheticsPredictorV1.from_pretrained("shunk031/aesthetics-predictor-v2-vit-large-patch14")
processor = CLIPProcessor.from_pretrained("shunk031/aesthetics-predictor-v2-vit-large-patch14")
# Batch inference
scores = model.predict(images, processor) # Returns list of scores
```
```python
# Via direct aesthetic-predictor package
from aesthetic_predictor import predict_aesthetic
from PIL import Image
score = predict_aesthetic(Image.open("photo.jpg")) # Returns float score
```
## What It's Good At
- **Speed:** Extremely fast — linear head on pre-computed embeddings
- **Simplicity:** One-line inference with HuggingFace transformers
- **Open source:** MIT license, no commercial restrictions
- **Lightweight:** ~2 GB VRAM, fits on consumer GPUs
- **Good general aesthetic ranking:** Trained on diverse datasets (AI-generated + professional photos)
- **Works on real photography:** AVA dataset contains 250K professional photos, so it generalizes to wedding photos reasonably well
## What It's Bad At
- **No technical quality detection:** Does NOT detect blur, motion blur, out-of-focus, exposure issues, noise, or composition problems. It scores "aesthetic appeal," not "technical correctness."
- **Trained heavily on AI-generated images:** 176K of 441K training samples are from SAC (AI-generated). This may bias scoring toward AI-aesthetic patterns (smooth gradients, saturated colors, idealized lighting) rather than genuine photography.
- **Western/art-historical bias:** CMU FAccT 2026 audit found LAP disproportionately favors landscapes, cityscapes, portraits from Western and Japanese artists. Reinforces "imperial and male gazes" from Western art history. May underrate candid moments, cultural ceremonies, and documentary-style wedding shots.
- **No genre awareness:** Treats all images the same. A technically perfect but emotionally empty portrait scores the same as a technically imperfect but emotionally powerful candid.
- **No context understanding:** Cannot distinguish between a "keep" (peak moment, emotional reaction) and a "reject" (duplicate, transitional moment, empty frame).
- **No blink/closed-eye detection:** Irrelevant for wedding photo sorting where you need to filter technical rejects.
- **No duplicate detection:** Cannot identify burst sequences or near-duplicates.
- **Score calibration:** Scores are relative rankings, not absolute quality measures. A score of 7 on one image doesn't mean the same as 7 on another.
## Bias Concerns (FAccT 2026 Audit)
A rigorous academic audit (CMU, published at FAccT 2026) found:
- **Gender bias:** Disproportionately filters in images with captions mentioning women; filters out images with captions mentioning men or LGBTQ+ people
- **Cultural bias:** Rates realistic images of landscapes, cityscapes, and portraits from Western and Japanese artists most highly
- **Training data bias:** Aesthetic scores used to train LAP primarily come from English-speaking photographers and Western AI-enthusiasts
- **No V3 released:** As of January 2026, LAION has not released an updated model to address these concerns
+126
View File
@@ -0,0 +1,126 @@
# MUSIQ — Multi-Scale Image Quality Transformer
## Model Info
- **Paper:** ICCV 2021 — "MUSIQ: Multi-Scale Image Quality Transformer" (Ke et al.)
- **Source:** Google Research (Pengchuan Zhang, Xiujun Li, Ping Luo, Kai Wang, Yu-Xiong Wang)
- **Official Google Repo:** <https://github.com/google-research/google-research/tree/master/musiq>
- **Google Blog:** <https://ai.googleblog.com/2021/07/musiq-assessing-image-aesthetic-and.html>
- **TensorFlow Hub:** Models available at `tfhub.dev` (official hosted checkpoints)
- **PyPI (IQA-PyTorch toolbox):** `pip install pyiqa` -> `pyiqa create_metric 'musiq'`
- **PyTorch unofficial impl:** <https://github.com/anse3832/MUSIQ> (PyTorch, works on KonIQ-10k)
- **License:** Apache 2.0 (via IQA-PyTorch reimplementation)
- **Status:** Actively maintained in IQA-PyTorch; official TensorFlow code archived but functional
## Architecture
- **Type:** Patch-based Vision Transformer (ViT) with multi-scale input
- **Backbone:** ResNet50 (ImageNet-pretrained) for feature extraction -> Transformer encoder for aggregation
- **Multi-scale processing:** Processes image at 3 scales simultaneously — native resolution, 224x224, and 384x384
- **Input:** Full-resolution images (no fixed-size constraint) — handles any aspect ratio natively
- **Output:** Single float score 0-100 (technical + aesthetic combined)
- **Training checkpoints:**
- `ava_ckpt.npz` — trained on AVA dataset (aesthetic quality)
- `koniq_ckpt.npz` — trained on KonIQ-10k (technical quality)
- `paq2piq_ckpt.npz` — trained on PaQ-2-PiQ (technical quality)
- `spaq_ckpt.npz` — trained on SPAQ (technical quality)
- `imagenet_pretrain.npz` — ImageNet pretraining only
- **Key innovation:** Patch-based design bypasses CNN fixed-size constraint; hash-based 2D spatial embedding + scale embedding for positional encoding
- **MUSIQ-single variant:** Processes only native resolution (faster, slightly lower accuracy)
## Installation
```bash
# Option 1: IQA-PyTorch toolbox (recommended — easiest, supports all metrics)
pip install pyiqa
# Option 2: Official TensorFlow code
git clone https://github.com/google-research/google-research.git
cd google-research/musiq
pip install -r requirements.txt
# Option 3: Unofficial PyTorch implementation
git clone https://github.com/anse3832/MUSIQ.git
cd MUSIQ
pip install torch torchvision einops scipy tqdm
```
## Python Usage
```python
# Via IQA-PyTorch (recommended — single API for all metrics)
import pyiqa
import torch
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
# Create MUSIQ metric (auto-downloads checkpoint on first use)
musiq = pyiqa.create_metric('musiq', device=device)
# Single image
score = musiq('./photo.jpg')
print(f"MUSIQ score: {score:.2f} / 100")
# Directory of images
import glob
scores = []
for path in glob.glob('./wedding-photos/*.jpg'):
scores.append(musiq(path))
```
```python
# Via official TensorFlow code
cd google-research/musiq
python3 -m musiq.run_predict_image \
--ckpt_path=/path/to/spaq_ckpt.npz \
--image_path=/path/to/photo.jpg
```
## Performance Benchmarks (from IQA-PyTorch docs & Google Research)
| Dataset | SRCC (rank correlation) | PLCC (Pearson correlation) |
|---------|------------------------|---------------------------|
| KonIQ-10k | ~0.90 | ~0.92 |
| PaQ-2-PiQ | ~0.85 | ~0.87 |
| SPAQ | ~0.88 | ~0.90 |
| AVA | ~0.75 | ~0.78 (lower — aesthetic-only dataset) |
## Hardware Requirements
- **VRAM:** ~3-4 GB (FP16), ~6-8 GB (FP32)
- **Batch size:** ~8-16 on 24 GB GPU (RTX 4090)
- **Speed per image:** ~50-150ms on GPU (FP16), ~200-500ms on CPU
- **Estimated time for 4,000 images:** ~15-25 minutes on GPU (FP16, batch 16)
- **Native resolution handling:** No resizing needed — processes full-resolution images directly
## Score Interpretation for Wedding Photos
| Score Range | Interpretation | Recommended Action |
|-------------|---------------|-------------------|
| 85-100 | Excellent quality | Auto-keep |
| 70-84 | Good quality | Keep, manual review if borderline |
| 50-69 | Acceptable | Review — may have minor issues |
| 30-49 | Poor quality | Likely reject |
| 0-29 | Very poor | Auto-reject |
**Recommended threshold for wedding culling:** Score >= 50 (i.e., not in the bottom third). This typically reduces 4,000 images to ~800-1,200 for manual review.
## What It's Good At
- **Technical quality detection:** Better than LAION predictors at detecting blur, noise, compression artifacts, over/under exposure
- **Full-resolution input:** No resizing artifacts — important for wedding photos with varying aspect ratios (portrait, landscape, square)
- **Combined aesthetic + technical scoring:** Not just "pretty" but also "technically sound" — single model for both dimensions
- **Google-backed research:** Well-documented, peer-reviewed, actively maintained in IQA-PyTorch
- **Apache 2.0 license:** Fully permissive for any use
- **Multiple training checkpoints:** Can choose checkpoint based on use case (aesthetic vs. technical focus)
- **Handles diverse aspect ratios:** Native resolution processing means no distortion from forced resizing
## What It's Bad At
- **Still no genre awareness:** General-purpose model, not wedding-specific
- **No context understanding:** Same as LAION — cannot distinguish emotional moments from empty frames
- **Slower than CLIP-based:** Transformer architecture is heavier (~50-150ms vs ~10-30ms for LAION V2)
- **Score range 0-100:** Different from LAION's 0-10 scale (requires threshold adjustment if combining)
- **Limited community adoption:** Less community testing and discussion compared to LAION predictors
- **Checkpoint selection matters:** Different checkpoints optimized for different datasets — no single "best" checkpoint for all use cases
- **TF/HF ecosystem split:** Official code is TensorFlow; PyTorch users need IQA-PyTorch or unofficial impl
+40
View File
@@ -0,0 +1,40 @@
# Research — Overview & Use Case Context
**Date:** 2026-08-23
**Source:** GitHub, HuggingFace, arXiv, LAION Blog, PyPI, Commercial Tool Benchmarks, Reddit, IQA Toolboxes
## Use Case Context
Sorting ~4,000 wedding photos — need to separate keeps from rejects based on aesthetic/technical quality. Requirements: batch processing, self-hosted or lightweight, no cloud dependency, fast inference on 4,000 images.
## Models Covered
1. **LAION Aesthetic Predictor** — CLIP-based aesthetic scoring (V1 & V2)
2. **SigLIP-Based Aesthetic Predictor V2.5** — SigLIP alternative to OpenAI CLIP
3. **MUSIQ** — Multi-Scale Image Quality Transformer (Google Research)
4. **Q-Align** — Qwen-based Visual Scorer
5. **BRISQUE / NIQE** — Classical no-reference IQA (CPU-only)
6. **Commercial Tools** — FilterPixel, Aftershoot, Imagen AI (for reference)
7. **IQA-PyTorch Toolbox** — All-in-one metric library
## Key Takeaways
1. **LAION Aesthetic Predictor V2 is the simplest starting point** — one-line Python API, MIT license, ~2 GB VRAM, fast inference. But it scores "aesthetic appeal," not "technical quality." It will NOT detect blur, out-of-focus shots, or bad exposure.
2. **Best approach: Combine models.** Use BRISQUE/NIQE for technical quality (blur/noise detection) + LAION V2 or MUSIQ for aesthetic scoring. A photo should be kept only if it passes BOTH thresholds.
3. **MUSIQ is the best single-model option** if you want one model that captures both aesthetic and technical quality. It's from Google Research, handles full-resolution images, and has Apache 2.0 license.
4. **SigLIP V2.5 is a modest improvement over LAION V2** — better on illustrations/art, but same fundamental limitations for real photography. Worth trying if you have BF16 GPU support.
5. **No open-source model understands wedding context.** None of these can distinguish a "peak moment" from a "transitional shot" or detect emotional content. For that, you need commercial tools like FilterPixel DeepCull (genre-specific AI) or Aftershoot (learning-based).
6. **For 4,000 wedding photos on a consumer GPU (RTX 4090):**
- LAION V2 batch: ~5-10 minutes
- MUSIQ batch: ~15-20 minutes
- BRISQUE (CPU): ~10-20 minutes
- Combined approach (LAION + BRISQUE): ~15-25 minutes total
7. **Bias warning:** LAION V2 has documented Western/cultural bias. For culturally diverse weddings (e.g., Indonesian, South Asian, African), scores may not accurately reflect the quality of ceremony shots, cultural attire, or traditional poses.
8. **Practical recommendation:** Start with LAION V2 + BRISQUE as a first-pass filter. Score all 4,000 images, set thresholds (e.g., LAION > 5.5 AND BRISQUE < 30), and manually review the borderline cases. This reduces 4,000 images to ~500-800 for manual review.
+37
View File
@@ -0,0 +1,37 @@
# Q-Align — Qwen-based Visual Scorer
## Model Info
- **GitHub:** <https://github.com/Q-Future/Q-Align>
- **Paper:** ICML 2024 — "Teaching LMMs for Visual Scoring via Discrete Text-Defined Levels"
- **HuggingFace:** Multiple checkpoints available
- **License:** Apache 2.0
- **Latest:** Q-ReAlign (Qwen3.5-VL backbone) — June 2026 update in IQA-PyTorch
## Architecture
- **Type:** Vision-Language Model (VLM) fine-tuned for visual scoring
- **Backbone:** Qwen LLM + vision encoder
- **Output:** Discrete score levels (text-defined)
- **Innovation:** Uses reinforcement learning to align model scoring with human perception
## Hardware Requirements
- **Mini (0.8B):** ~3 GB VRAM
- **Lite (4B):** ~10 GB VRAM
- **Pro (9B):** ~20 GB VRAM
- **Batch size:** 1-4 (VLMs are memory-intensive)
## What It's Good At
- **Human-aligned scoring:** Trained to match human subjective ratings
- **Explainable:** Can generate text descriptions of why an image scored high/low
- **Multi-task:** Can do IQA, IAA (Image Aesthetic Assessment), and VQA
- **Fine-tunable:** Can be adapted to specific domains (e.g., wedding photos)
## What It's Bad At
- **Slower inference:** VLM architecture is significantly slower than CLIP-based models
- **Higher VRAM:** Even the mini variant needs 3 GB; not ideal for batch processing thousands of images
- **Still no wedding-specific training:** General-purpose model
- **Complex setup:** Requires more dependencies and configuration than LAION predictors
+55
View File
@@ -0,0 +1,55 @@
# SigLIP-Based Aesthetic Predictor V2.5
## Model Info
- **GitHub:** <https://github.com/discus0434/aesthetic-predictor-v2-5>
- **PyPI:** `aesthetic-predictor-v2-5`
- **License:** MIT
- **Updated:** December 2024
- **Improvement over V2:** Uses SigLIP (Google's CLIP alternative) instead of OpenAI CLIP; better at illustrations and diverse image domains
## Architecture
- **Backbone:** SigLIP (Google) — superior to OpenAI CLIP on many benchmarks
- **Head:** Linear layer on SigLIP embeddings
- **Output:** Single float score (0-10 scale)
- **Score threshold:** 5.5+ is considered "great" (vs. 6+ for V2)
## Hardware Requirements
- **SigLIP ViT:** ~2-3 GB VRAM (slightly larger than CLIP ViT-L/14)
- **Precision:** Requires BF16 for best results
- **Batch size:** ~16-32 on 24 GB GPU
## Python Usage
```python
from aesthetic_predictor_v2_5 import convert_v2_5_from_siglip
from PIL import Image
import torch
model, preprocessor = convert_v2_5_from_siglip(low_cpu_mem_usage=True)
model = model.to(torch.bfloat16).cuda()
image = Image.open("photo.jpg").convert("RGB")
pixel_values = preprocessor(images=image, return_tensors="pt").pixel_values.to(torch.bfloat16).cuda()
with torch.inference_mode():
score = model(pixel_values).logits.squeeze().float().cpu().numpy()
print(f"Aesthetics score: {score:.2f}")
```
## What It's Good At
- **Better than V2 on illustrations/art:** SigLIP trained on larger, more diverse dataset
- **Same simplicity:** One-line HuggingFace-style API
- **Same speed:** Linear head on embeddings
- **Slightly better generalization:** SigLIP's training data is more diverse than OpenAI CLIP's
## What It's Bad At
- **Same fundamental limitations as V2:** No technical quality detection, no blur/exposure detection, no genre awareness
- **Still not trained on real photography:** Same bias toward AI-generated aesthetic patterns
- **BF16 requirement:** Needs GPU with BF16 support (RTX 30-series+); not ideal for older hardware
- **Community model:** Not an official LAION release; maintained by a third party
+40
View File
@@ -0,0 +1,40 @@
# Alternatives Comparison & Summary
## Comparison Table
| Model | Type | Score Scale | Technical Quality | Aesthetic Quality | GPU Required | Speed (4K images) | Self-Hosted | License | Wedding-Specific |
|-------|------|-------------|-------------------|-------------------|--------------|-------------------|-------------|---------|------------------|
| **LAION V2 (CLIP)** | CLIP+Linear | 0-10 | No | Basic | Yes (~2GB) | ~5-10 min | Yes | MIT | No |
| **SigLIP V2.5** | SigLIP+Linear | 0-10 | No | Better | Yes (~3GB) | ~5-10 min | Yes | MIT | No |
| **MUSIQ** | Transformer | 0-100 | Yes | Combined | Yes (~4GB) | ~15-20 min | Yes | Apache 2.0 | No |
| **Q-Align Mini** | VLM (0.8B) | Text levels | Yes | Human-aligned | Yes (~3GB) | ~30-60 min | Yes | Apache 2.0 | No |
| **BRISQUE** | Handcrafted | Inverted | Yes | No | No (CPU) | ~10-20 min | Yes | Academic | No |
| **NIQE** | Handcrafted | Direct | Yes | No | No (CPU) | ~10-20 min | Yes | Academic | No |
| **FilterPixel DeepCull** | Cloud AI | 10 params | Yes | Genre-aware | N/A | ~5 min | No | Commercial | Yes |
| **Aftershoot** | Desktop AI | Stars | Yes | Genre-aware | Yes | ~10 min | Yes (local) | Commercial | Yes |
## Key Takeaways for Wedding Photo Sorting
1. **LAION Aesthetic Predictor V2 is the simplest starting point** — one-line Python API, MIT license, ~2 GB VRAM, fast inference. But it scores "aesthetic appeal," not "technical quality." It will NOT detect blur, out-of-focus shots, or bad exposure.
2. **Best approach: Combine models.** Use BRISQUE/NIQE for technical quality (blur/noise detection) + LAION V2 or MUSIQ for aesthetic scoring. A photo should be kept only if it passes BOTH thresholds.
3. **MUSIQ is the best single-model option** if you want one model that captures both aesthetic and technical quality. It's from Google Research, handles full-resolution images, and has Apache 2.0 license.
4. **SigLIP V2.5 is a modest improvement over LAION V2** — better on illustrations/art, but same fundamental limitations for real photography. Worth trying if you have BF16 GPU support.
5. **No open-source model understands wedding context.** None of these can distinguish a "peak moment" from a "transitional shot" or detect emotional content. For that, you need commercial tools like FilterPixel DeepCull (genre-specific AI) or Aftershoot (learning-based).
6. **For 4,000 wedding photos on a consumer GPU (RTX 4090):**
- LAION V2 batch: ~5-10 minutes
- MUSIQ batch: ~15-20 minutes
- BRISQUE (CPU): ~10-20 minutes
- Combined approach (LAION + BRISQUE): ~15-25 minutes total
7. **Bias warning:** LAION V2 has documented Western/cultural bias. For culturally diverse weddings (e.g., Indonesian, South Asian, African), scores may not accurately reflect the quality of ceremony shots, cultural attire, or traditional poses.
8. **Practical recommendation:** Start with LAION V2 + BRISQUE as a first-pass filter. Score all 4,000 images, set thresholds (e.g., LAION > 5.5 AND BRISQUE < 30), and manually review the borderline cases. This reduces 4,000 images to ~500-800 for manual review.
## Update Log
- **2026-08-23:** Initial research for wedding photo sorting use case (~4,000 images). Covered LAION V1/V2, SigLIP V2.5, MUSIQ, Q-Align, BRISQUE, NIQE, commercial tools (FilterPixel, Aftershoot, Imagen AI), and IQA-PyTorch toolbox.
+85
View File
@@ -0,0 +1,85 @@
# Project Scripts
## Setup Scripts
### `setup.bat` (Windows) / `setup.sh` (Linux/Mac)
Creates a new conda environment and installs all required packages from `requirements.txt`.
**What it does:**
1. Checks that conda is installed
2. Creates a conda environment named `photo_judgers` with Python 3.11
3. Installs all packages from `requirements.txt`:
- `torch`, `torchvision`, `torchaudio` (PyTorch with CUDA 12.1)
- `transformers` (HuggingFace)
- `simple-aesthetics-predictor` (LAION aesthetic predictor)
- `Pillow` (image processing)
- `tqdm` (progress bars)
- `numpy` (numerical operations)
**Usage:**
```bash
# Windows
setup.bat
# Linux/Mac
chmod +x setup.sh
./setup.sh
```
## Run Scripts
### `run.bat` (Windows) / `run.sh` (Linux/Mac)
Activates the conda environment and runs the scorer.
**What it does:**
1. Activates the `photo_judgers` conda environment
2. Runs `python -m src.scorer` with any additional arguments passed through
**Usage:**
```bash
# Windows
run.bat
# Linux/Mac
chmod +x run.sh
./run.sh
```
**Optional arguments:**
- `--input <path>` — Directory or file to score
- `--output <path>` — Output JSON file (default: `output.json`)
- `--threshold <value>` — Score threshold for keep/reject (default: TBD)
## Project Structure
```
photo_judgers/
├── src/
│ ├── __init__.py
│ ├── scorer.py # Menu + proxy (entry point)
│ └── scorers/
│ ├── __init__.py
│ ├── base.py # BaseScorer abstract class
│ └── laion.py # LAION V2 implementation
├── requirements.txt
├── setup.bat / setup.sh
├── run.bat / run.sh
├── output.json # Generated by scorer
├── docs/
├── test/
└── .pi/
```
## Adding a New Scorer
1. Create `src/scorers/<name>.py` with a class that follows the LAION pattern
2. Register it in `src/scorer.py` by adding to the `SCORERS` list
3. Add any new dependencies to `requirements.txt`
4. Update `src/scorers/__init__.py` to export the new class
+12
View File
@@ -0,0 +1,12 @@
# Photo Judgers — Requirements
# Install in conda env: pip install -r requirements.txt
# Core dependencies
simple-aesthetics-predictor
transformers
torch
torchvision
torchaudio
Pillow
tqdm
numpy
+19
View File
@@ -0,0 +1,19 @@
@echo off
REM Photo Judgers — Run LAION Scorer (Windows)
REM Activates the conda env and runs the scorer
echo ========================================
echo Photo Judgers — LAION Scorer
echo ========================================
echo.
call conda activate photo_judgers
echo Running scorer...
echo.
python -m src.scorer %*
echo.
echo Done. Check output.json for results.
pause
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Photo Judgers — Run LAION Scorer (Linux/Mac)
# Activates the conda env and runs the scorer
set -e
echo "========================================"
echo " Photo Judgers — LAION Scorer"
echo "========================================"
echo ""
source $(conda info --base)/etc/profile.d/conda.sh
conda activate photo_judgers
echo "Running scorer..."
echo ""
python -m src.scorer "$@"
echo ""
echo "Done. Check output.json for results."
+283
View File
@@ -0,0 +1,283 @@
"""
Photo Judgers — Scorer
Interactive scorer with menu for selecting which model to run.
Tracks progress by reading existing JSON output to avoid re-scoring.
Usage:
python scorer.py
"""
from __future__ import annotations
import glob
import json
import os
import sys
from datetime import datetime, timezone
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from types import NoneType
# ---------------------------------------------------------------------------
# Scorer Base
# ---------------------------------------------------------------------------
class BaseScorer:
"""Base class for all scorers."""
name: str = "Base"
description: str = ""
def load(self) -> None:
"""Load model weights / dependencies. Override in subclasses."""
def score(self, image_path: str) -> dict | None:
"""
Score a single image.
Returns a dict with scorer-specific fields, or None on failure.
"""
raise NotImplementedError
def unload(self) -> None:
"""Clean up resources. Override in subclasses."""
# ---------------------------------------------------------------------------
# LAION Scorer
# ---------------------------------------------------------------------------
class LaionScorer(BaseScorer):
name = "LAION Aesthetic Predictor V2"
description = "CLIP-based aesthetic scoring (0-10 scale)"
def __init__(self) -> None:
self.model: object = None # type: ignore[assignment]
self.processor: object = None # type: ignore[assignment]
self._version = "v2"
def load(self) -> None:
try:
import torch # noqa: F401
from simple_aesthetics_predictor import AestheticsPredictorV1 # noqa: F401
from transformers import CLIPProcessor # noqa: F401
print(" Loading LAION V2 model... (first run downloads ~2GB)")
from simple_aesthetics_predictor import AestheticsPredictorV1
from transformers import CLIPProcessor
self.model = AestheticsPredictorV1.from_pretrained(
"shunk031/aesthetics-predictor-v2-vit-large-patch14"
)
self.processor = CLIPProcessor.from_pretrained(
"shunk031/aesthetics-predictor-v2-vit-large-patch14"
)
self.model.eval()
print(" Model loaded.")
except ImportError:
print(" ERROR: Required packages not installed.")
print(
" Run: pip install simple-aesthetics-predictor transformers "
"torch torchvision Pillow tqdm"
)
sys.exit(1)
def score(self, image_path: str) -> dict | None:
try:
import torch # noqa: F401
from PIL import Image
assert self.processor is not None, "Model not loaded. Call load() first."
assert self.model is not None, "Model not loaded. Call load() first."
image = Image.open(image_path).convert("RGB")
inputs = self.processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = self.model(**inputs)
score = outputs.logits.squeeze().item()
return {
"laion_score": round(score, 2),
"laion_version": self._version,
}
except Exception as e:
print(f" Warning: Failed to score {image_path}: {e}")
return None
def unload(self) -> None:
self.model = None # type: ignore[assignment]
self.processor = None # type: ignore[assignment]
# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------
SCORERS: list[type[BaseScorer]] = [
LaionScorer,
# Add new scorers here:
# NimaScorer,
# MusiqScorer,
# BrisqueScorer,
]
def show_scorer_menu() -> type[BaseScorer]:
"""Display scorer selection menu and return the chosen scorer class."""
print()
print("=" * 50)
print(" Photo Judgers — Select Scorer")
print("=" * 50)
print()
for i, scorer_cls in enumerate(SCORERS, 1):
print(f" {i}. {scorer_cls.name}")
print(f" {scorer_cls.description}")
print()
while True:
choice = input(f" Choose [1-{len(SCORERS)}]: ").strip()
try:
idx = int(choice) - 1
if 0 <= idx < len(SCORERS):
return SCORERS[idx]
except ValueError:
pass
print(" Invalid choice. Try again.")
def ask_folder() -> str:
"""Prompt user for the input folder path."""
while True:
folder = input("\n Enter folder path to score: ").strip().strip('"\'')
if not folder:
print(" Path cannot be empty.")
continue
if os.path.isdir(folder):
return folder
print(f" Folder not found: {folder}")
def ask_output() -> str:
"""Prompt user for the output JSON file path."""
default = "output.json"
answer = input(f"\n Output JSON file [{default}]: ").strip()
return answer if answer else default
def load_existing_results(output_path: str) -> dict:
"""Load existing results from JSON file. Returns {filepath: result}."""
if os.path.exists(output_path):
try:
with open(output_path, encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return {entry["filepath"]: entry for entry in data}
except (json.JSONDecodeError, OSError) as e:
print(f" Warning: Could not read existing results: {e}")
return {}
def get_image_files(folder: str) -> list[str]:
"""Get all image files from folder (recursively)."""
extensions = {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".webp", ".gif"}
files: list[str] = []
for ext in extensions:
files.extend(glob.glob(os.path.join(folder, f"**/*{ext}"), recursive=True))
files.extend(glob.glob(os.path.join(folder, f"*{ext}")))
return sorted(set(files))
def run_scorer(scorer_cls: type[BaseScorer], folder: str, output_path: str) -> None:
"""Run the selected scorer on all images in the folder."""
print(f"\n Scorer: {scorer_cls.name}")
print(f" Folder: {folder}")
print(f" Output: {output_path}")
print()
# Load existing results
existing = load_existing_results(output_path)
print(f" Already scored: {len(existing)} images")
# Get all image files
all_files = get_image_files(folder)
remaining = [f for f in all_files if f not in existing]
print(f" Remaining to score: {len(remaining)} images")
print()
if not remaining:
print(" Nothing new to score. Done!")
return
# Load the model
scorer = scorer_cls()
scorer.load()
# Process images
results = list(existing.values()) # Start with existing
skipped = 0
from tqdm import tqdm
for image_path in tqdm(remaining, desc="Scoring"):
result = scorer.score(image_path)
if result:
entry = {
"filename": os.path.basename(image_path),
"filepath": image_path,
"subtype": scorer_cls.name.lower().replace(" ", "_"),
**result,
"scoring_date": datetime.now(timezone.utc).isoformat(),
}
results.append(entry)
else:
skipped += 1
scorer.unload()
# Save results
try:
with open(output_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
except OSError as e:
print(f" ERROR: Could not write output file: {e}")
return
print()
print(f" Scored: {len(remaining) - skipped} images")
if skipped:
print(f" Skipped (failed): {skipped} images")
print(f" Total in output: {len(results)} images")
print(f" Saved to: {output_path}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print()
print("=" * 50)
print(" Photo Judgers")
print("=" * 50)
# Select scorer
scorer_cls = show_scorer_menu()
# Get folder
folder = ask_folder()
# Get output file
output_path = ask_output()
# Run
run_scorer(scorer_cls, folder, output_path)
if __name__ == "__main__":
main()
+54
View File
@@ -0,0 +1,54 @@
@echo off
REM Photo Judgers — Conda Environment Setup (Windows)
REM Run this from the project root: photo_judgers\setup.bat
echo ========================================
echo Photo Judgers — Setup
echo ========================================
echo.
REM Check if conda is available
where conda >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo ERROR: conda not found. Please install Miniconda or Anaconda first.
echo Download from: https://docs.conda.io/en/latest/miniconda.html
pause
exit /b 1
)
echo Creating conda environment: photo_judgers
echo Python version: 3.11
echo.
REM Create the conda environment
conda create -y -n photo_judgers python=3.11
if %ERRORLEVEL% NEQ 0 (
echo ERROR: Failed to create conda environment.
pause
exit /b 1
)
echo.
echo Activating environment...
call conda activate photo_judgers
echo.
echo Installing packages...
echo.
REM Install from requirements.txt
pip install -r requirements.txt
echo.
echo ========================================
echo Setup complete!
echo ========================================
echo.
echo To activate the environment:
echo conda activate photo_judgers
echo.
echo To run the scorer:
echo run.bat
echo.
pause
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Photo Judgers — Conda Environment Setup (Linux/Mac)
# Run this from the project root: ./setup.sh
set -e
echo "========================================"
echo " Photo Judgers — Setup"
echo "========================================"
echo ""
# Check if conda is available
if ! command -v conda &>/dev/null; then
echo "ERROR: conda not found. Please install Miniconda or Anaconda first."
echo "Download from: https://docs.conda.io/en/latest/miniconda.html"
exit 1
fi
echo "Creating conda environment: photo_judgers"
echo "Python version: 3.11"
echo ""
# Create the conda environment
conda create -y -n photo_judgers python=3.11
echo ""
echo "Activating environment..."
source $(conda info --base)/etc/profile.d/conda.sh
conda activate photo_judgers
echo ""
echo "Installing packages..."
echo ""
# Install from requirements.txt
pip install -r requirements.txt
echo ""
echo "========================================"
echo " Setup complete!"
echo "========================================"
echo ""
echo "To activate the environment:"
echo " conda activate photo_judgers"
echo ""
echo "To run the scorer:"
echo " ./run.sh"
echo ""
View File
+206
View File
@@ -0,0 +1,206 @@
"""
Photo Judgers — Scorer
Interactive menu + proxy for running scorers.
Tracks progress by reading existing JSON output to avoid re-scoring.
Usage:
python -m src.scorer
"""
from __future__ import annotations
import glob
import json
import os
import sys
from datetime import datetime, timezone
# Add project root to path so we can import src
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.scorers.base import BaseScorer
from src.scorers.laion import LaionScorer
# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------
SCORERS: list[type[BaseScorer]] = [
LaionScorer,
# Add new scorers here:
# NimaScorer,
# MusiqScorer,
# BrisqueScorer,
]
# ---------------------------------------------------------------------------
# Menu & Prompts
# ---------------------------------------------------------------------------
def show_scorer_menu() -> type[BaseScorer]:
"""Display scorer selection menu and return the chosen scorer class."""
print()
print("=" * 50)
print(" Photo Judgers — Select Scorer")
print("=" * 50)
print()
for i, scorer_cls in enumerate(SCORERS, 1):
print(f" {i}. {scorer_cls.name}")
print(f" {scorer_cls.description}")
print()
while True:
choice = input(f" Choose [1-{len(SCORERS)}]: ").strip()
try:
idx = int(choice) - 1
if 0 <= idx < len(SCORERS):
return SCORERS[idx]
except ValueError:
pass
print(" Invalid choice. Try again.")
def ask_folder() -> str:
"""Prompt user for the input folder path."""
while True:
folder = input("\n Enter folder path to score: ").strip().strip('"\'')
if not folder:
print(" Path cannot be empty.")
continue
if os.path.isdir(folder):
return folder
print(f" Folder not found: {folder}")
def ask_output() -> str:
"""Prompt user for the output JSON file path."""
default = "output.json"
answer = input(f"\n Output JSON file [{default}]: ").strip()
return answer if answer else default
# ---------------------------------------------------------------------------
# Progress / IO
# ---------------------------------------------------------------------------
def load_existing_results(output_path: str) -> dict:
"""Load existing results from JSON file. Returns {filepath: result}."""
if os.path.exists(output_path):
try:
with open(output_path, encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return {entry["filepath"]: entry for entry in data}
except (json.JSONDecodeError, OSError) as e:
print(f" Warning: Could not read existing results: {e}")
return {}
def get_image_files(folder: str) -> list[str]:
"""Get all image files from folder (recursively)."""
extensions = {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".webp", ".gif"}
files: list[str] = []
for ext in extensions:
files.extend(glob.glob(os.path.join(folder, f"**/*{ext}"), recursive=True))
files.extend(glob.glob(os.path.join(folder, f"*{ext}")))
return sorted(set(files))
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
def run_scorer(scorer_cls: type, folder: str, output_path: str) -> None:
"""Run the selected scorer on all images in the folder."""
print(f"\n Scorer: {scorer_cls.name}")
print(f" Folder: {folder}")
print(f" Output: {output_path}")
print()
# Load existing results
existing = load_existing_results(output_path)
print(f" Already scored: {len(existing)} images")
# Get all image files
all_files = get_image_files(folder)
remaining = [f for f in all_files if f not in existing]
print(f" Remaining to score: {len(remaining)} images")
print()
if not remaining:
print(" Nothing new to score. Done!")
return
# Load the model
scorer = scorer_cls()
scorer.load()
# Process images with tqdm
results = list(existing.values()) # Start with existing
skipped = 0
from tqdm import tqdm
for image_path in tqdm(remaining, desc="Scoring"):
result = scorer.score(image_path)
if result:
entry = {
"filename": os.path.basename(image_path),
"filepath": image_path,
"subtype": scorer_cls.name.lower().replace(" ", "_"),
**result,
"scoring_date": datetime.now(timezone.utc).isoformat(),
}
results.append(entry)
else:
skipped += 1
scorer.unload()
# Save results
try:
with open(output_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
except OSError as e:
print(f" ERROR: Could not write output file: {e}")
return
print()
print(f" Scored: {len(remaining) - skipped} images")
if skipped:
print(f" Skipped (failed): {skipped} images")
print(f" Total in output: {len(results)} images")
print(f" Saved to: {output_path}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print()
print("=" * 50)
print(" Photo Judgers")
print("=" * 50)
# Select scorer
scorer_cls = show_scorer_menu()
# Get folder
folder = ask_folder()
# Get output file
output_path = ask_output()
# Run
run_scorer(scorer_cls, folder, output_path)
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
from src.scorers.base import BaseScorer
from src.scorers.laion import LaionScorer
__all__ = ["BaseScorer", "LaionScorer"]
+26
View File
@@ -0,0 +1,26 @@
"""Base class for all scorers."""
from abc import ABC, abstractmethod
from typing import Optional
class BaseScorer(ABC):
"""Base class for all scorers."""
name: str = "Base"
description: str = ""
@abstractmethod
def load(self) -> None:
"""Load model weights / dependencies. Override in subclasses."""
@abstractmethod
def score(self, image_path: str) -> Optional[dict]:
"""
Score a single image.
Returns a dict with scorer-specific fields, or None on failure.
"""
def unload(self) -> None:
"""Clean up resources. Override in subclasses."""
+71
View File
@@ -0,0 +1,71 @@
"""LAION Aesthetic Predictor V2 scorer."""
from typing import Optional
class LaionScorer:
"""LAION V2 aesthetic scorer — CLIP-based, 0-10 scale."""
name = "LAION Aesthetic Predictor V2"
description = "CLIP-based aesthetic scoring (0-10 scale)"
def __init__(self) -> None:
self.model = None # type: ignore[assignment]
self.processor = None # type: ignore[assignment]
self._version = "v2"
def load(self) -> None:
"""Load model weights (downloads ~2GB on first run)."""
try:
import torch # noqa: F401
from simple_aesthetics_predictor import AestheticsPredictorV1 # noqa: F401
from transformers import CLIPProcessor # noqa: F401
print(" Loading LAION V2 model... (first run downloads ~2GB)")
from simple_aesthetics_predictor import AestheticsPredictorV1
from transformers import CLIPProcessor
self.model = AestheticsPredictorV1.from_pretrained(
"shunk031/aesthetics-predictor-v2-vit-large-patch14"
)
self.processor = CLIPProcessor.from_pretrained(
"shunk031/aesthetics-predictor-v2-vit-large-patch14"
)
self.model.eval()
print(" Model loaded.")
except ImportError:
print(" ERROR: Required packages not installed.")
print(
" Run: pip install simple-aesthetics-predictor transformers "
"torch torchvision Pillow tqdm"
)
raise
def score(self, image_path: str) -> Optional[dict]:
"""Score a single image. Returns dict with score or None on failure."""
try:
import torch # noqa: F401
from PIL import Image
if self.processor is None or self.model is None:
raise RuntimeError("Model not loaded. Call load() first.")
image = Image.open(image_path).convert("RGB")
inputs = self.processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = self.model(**inputs)
score = outputs.logits.squeeze().item()
return {
"laion_score": round(score, 2),
"laion_version": self._version,
}
except Exception as e:
print(f" Warning: Failed to score {image_path}: {e}")
return None
def unload(self) -> None:
"""Clean up model references."""
self.model = None # type: ignore[assignment]
self.processor = None # type: ignore[assignment]