A lightweight aesthetic/quality scoring model trained on human image preferences using frozen DINOv2 ViT-B/14 embeddings with a learned MLP head.
The output is a scalar score where higher values indicate higher aesthetic quality.
-
Human preference pairs — Collected via the image-preference-labeler tool. Labelers were shown two images generated from the same prompt (but different models/variations) and asked which they preferred. These are stored in a SQLite database (comparisons + decisions tables organized by case_id/prompt).
-
Pseudo-pairs from scored images — A collection of ~1,100+ curated AI-generated and digital artwork images with Elo-style scores (derived from multiple comparisons). Pseudo-pairs were sampled from this scored set with a minimum score gap of 0.15 to ensure meaningful distinctions.
Pairs were split 80/10/10 train/val/test using deterministic hash-based partitioning on case_id/prompt to avoid data leakage.
Images were pre-processed through the frozen DINOv2 ViT-B/14 backbone (timm) to extract L2-normalized 768-dimensional embeddings. This was done once and cached (embeddings_dinov3_timm_vitb.npz) so the head could be trained without keeping the vision backbone in GPU memory.
1from pathlib import Path
2import torch
3import torch.nn as nn
4from PIL import Image
5import timm
6from timm.data import create_transform, resolve_model_data_config
7
8# --- AestheticHead definition (matches training) ---
9class AestheticHead(nn.Module):
10 def __init__(self, input_dim: int = 768, hidden_dim: int = 256, dropout: float = 0.1):
11 super().__init__()
12 self.net = nn.Sequential(
13 nn.LayerNorm(input_dim),
14 nn.Linear(input_dim, hidden_dim),
15 nn.GELU(),
16 nn.Dropout(dropout),
17 nn.Linear(hidden_dim, 1),
18 )
19
20 def forward(self, features):
21 return self.net(features).squeeze(-1)
22
23# --- Load backbone ---
24device = "cuda" if torch.cuda.is_available() else "cpu"
25backbone = timm.create_model("vit_base_patch16_dinov3.lvd1689m", pretrained=True, num_classes=0)
26config = resolve_model_data_config(backbone)
27transform = create_transform(**config, is_training=False)
28backbone.to(device)
29backbone.eval()
30
31# --- Load trained head ---
32checkpoint = torch.load("v1_dinov3_vitb.pt", map_location=device)
33head = AestheticHead(
34 input_dim=checkpoint["input_dim"],
35 hidden_dim=checkpoint["hidden_dim"],
36 dropout=checkpoint["dropout"],
37)
38head.load_state_dict(checkpoint["state_dict"])
39head.to(device)
40head.eval()
41
42# --- Score an image ---
43def score_image(image_path: str) -> float:
44 image = Image.open(image_path).convert("RGB")
45 tensor = transform(image).unsqueeze(0).to(device)
46 with torch.inference_mode():
47 features = backbone(tensor)
48 features = nn.functional.normalize(features.float(), dim=-1)
49 score = head(features).item()
50 return score
51
52print(score_image("example.png")) # Higher = more aesthetically pleasing