Update app.py
Browse files
app.py
CHANGED
|
@@ -10,8 +10,12 @@ from huggingface_hub import hf_hub_download
|
|
| 10 |
import pandas as pd
|
| 11 |
import tempfile
|
| 12 |
import shutil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
# Utility classes and functions from provided code
|
| 15 |
class MLP(torch.nn.Module):
|
| 16 |
def __init__(self, input_size, xcol='emb', ycol='avg_rating', batch_norm=True):
|
| 17 |
super().__init__()
|
|
@@ -43,90 +47,100 @@ class MLP(torch.nn.Module):
|
|
| 43 |
def forward(self, x):
|
| 44 |
return self.layers(x)
|
| 45 |
|
| 46 |
-
|
| 47 |
class WaifuScorer(object):
|
| 48 |
def __init__(self, model_path=None, device='cuda', cache_dir=None, verbose=False):
|
| 49 |
self.verbose = verbose
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
@torch.no_grad()
|
| 80 |
def __call__(self, images):
|
|
|
|
|
|
|
|
|
|
| 81 |
if isinstance(images, Image.Image):
|
| 82 |
images = [images]
|
| 83 |
n = len(images)
|
| 84 |
if n == 1:
|
| 85 |
-
images = images*2
|
| 86 |
-
|
| 87 |
-
# Preprocess and encode images
|
| 88 |
image_tensors = [self.preprocess(img).unsqueeze(0) for img in images]
|
| 89 |
image_batch = torch.cat(image_tensors).to(self.device)
|
| 90 |
image_features = self.model2.encode_image(image_batch)
|
| 91 |
-
|
| 92 |
-
# Normalize features
|
| 93 |
l2 = image_features.norm(2, dim=-1, keepdim=True)
|
| 94 |
l2[l2 == 0] = 1
|
| 95 |
im_emb_arr = (image_features / l2).to(device=self.device, dtype=self.dtype)
|
| 96 |
-
|
| 97 |
-
# Get predictions
|
| 98 |
predictions = self.mlp(im_emb_arr)
|
| 99 |
scores = predictions.clamp(0, 10).cpu().numpy().reshape(-1).tolist()
|
| 100 |
-
|
| 101 |
-
# Return only the requested number of scores
|
| 102 |
-
return scores[:n]
|
| 103 |
|
|
|
|
| 104 |
|
| 105 |
def load_aesthetic_predictor_v2_5():
|
| 106 |
-
|
| 107 |
-
# The actual implementation would import and use aesthetic_predictor_v2_5
|
| 108 |
-
# We'll simulate the model with a dummy implementation
|
| 109 |
-
|
| 110 |
-
class AestheticPredictorV2_5:
|
| 111 |
def __init__(self):
|
| 112 |
print("Loading Aesthetic Predictor V2.5...")
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
def load_anime_aesthetic_model():
|
| 125 |
model_path = hf_hub_download(repo_id="skytnt/anime-aesthetic", filename="model.onnx")
|
| 126 |
model = rt.InferenceSession(model_path, providers=['CPUExecutionProvider'])
|
| 127 |
return model
|
| 128 |
|
| 129 |
-
|
| 130 |
def predict_anime_aesthetic(img, model):
|
| 131 |
img = np.array(img).astype(np.float32) / 255
|
| 132 |
s = 768
|
|
@@ -140,224 +154,354 @@ def predict_anime_aesthetic(img, model):
|
|
| 140 |
pred = model.run(None, {"img": img_input})[0].item()
|
| 141 |
return pred
|
| 142 |
|
| 143 |
-
|
| 144 |
class ImageEvaluationTool:
|
| 145 |
def __init__(self):
|
| 146 |
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 147 |
print(f"Using device: {self.device}")
|
| 148 |
-
|
| 149 |
-
# Load all models
|
| 150 |
print("Loading models... This may take some time.")
|
| 151 |
-
|
| 152 |
-
# 1. Aesthetic Shadow
|
| 153 |
print("Loading Aesthetic Shadow model...")
|
| 154 |
self.aesthetic_shadow = pipeline("image-classification", model="NeoChen1024/aesthetic-shadow-v2-backup", device=self.device)
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
self.waifu_scorer = WaifuScorer(device=self.device, verbose=True)
|
| 160 |
-
except Exception as e:
|
| 161 |
-
print(f"Error loading Waifu Scorer: {e}")
|
| 162 |
-
self.waifu_scorer = None
|
| 163 |
-
|
| 164 |
-
# 3. Aesthetic Predictor V2.5 (placeholder)
|
| 165 |
print("Loading Aesthetic Predictor V2.5...")
|
| 166 |
self.aesthetic_predictor_v2_5 = load_aesthetic_predictor_v2_5()
|
| 167 |
-
|
| 168 |
-
# 4. Cafe Aesthetic models
|
| 169 |
-
print("Loading Cafe Aesthetic models...")
|
| 170 |
-
self.cafe_aesthetic = pipeline("image-classification", "cafeai/cafe_aesthetic")
|
| 171 |
-
self.cafe_style = pipeline("image-classification", "cafeai/cafe_style")
|
| 172 |
-
self.cafe_waifu = pipeline("image-classification", "cafeai/cafe_waifu")
|
| 173 |
-
|
| 174 |
-
# 5. Anime Aesthetic
|
| 175 |
print("Loading Anime Aesthetic model...")
|
| 176 |
self.anime_aesthetic = load_anime_aesthetic_model()
|
| 177 |
-
|
| 178 |
print("All models loaded successfully!")
|
| 179 |
-
|
| 180 |
-
# Create temp directory for storing processed images
|
| 181 |
self.temp_dir = tempfile.mkdtemp()
|
| 182 |
-
|
| 183 |
def evaluate_image(self, image):
|
| 184 |
-
"""Evaluate a single image with all models"""
|
| 185 |
results = {}
|
| 186 |
-
|
| 187 |
-
# Convert to PIL Image if not already
|
| 188 |
if not isinstance(image, Image.Image):
|
| 189 |
image = Image.fromarray(image)
|
| 190 |
-
|
| 191 |
-
# 1. Aesthetic Shadow
|
| 192 |
try:
|
| 193 |
shadow_result = self.aesthetic_shadow(images=[image])[0]
|
| 194 |
hq_score = [p for p in shadow_result if p['label'] == 'hq'][0]['score']
|
| 195 |
-
|
|
|
|
|
|
|
| 196 |
except Exception as e:
|
| 197 |
print(f"Error in Aesthetic Shadow: {e}")
|
| 198 |
results['aesthetic_shadow'] = None
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
results['waifu_scorer'] = None
|
| 208 |
-
else:
|
| 209 |
results['waifu_scorer'] = None
|
| 210 |
-
|
| 211 |
-
# 3. Aesthetic Predictor V2.5
|
| 212 |
try:
|
| 213 |
v2_5_score = self.aesthetic_predictor_v2_5.inference(image)
|
| 214 |
-
|
|
|
|
|
|
|
| 215 |
except Exception as e:
|
| 216 |
print(f"Error in Aesthetic Predictor V2.5: {e}")
|
| 217 |
results['aesthetic_predictor_v2_5'] = None
|
| 218 |
-
|
| 219 |
-
# 4. Cafe Aesthetic
|
| 220 |
-
try:
|
| 221 |
-
cafe_aesthetic_result = self.cafe_aesthetic(image, top_k=2)
|
| 222 |
-
cafe_aesthetic_score = {d["label"]: round(d["score"], 2) for d in cafe_aesthetic_result}
|
| 223 |
-
results['cafe_aesthetic_good'] = cafe_aesthetic_score.get('good', 0)
|
| 224 |
-
results['cafe_aesthetic_bad'] = cafe_aesthetic_score.get('bad', 0)
|
| 225 |
-
|
| 226 |
-
cafe_style_result = self.cafe_style(image, top_k=1)
|
| 227 |
-
results['cafe_style'] = cafe_style_result[0]["label"]
|
| 228 |
-
|
| 229 |
-
cafe_waifu_result = self.cafe_waifu(image, top_k=1)
|
| 230 |
-
results['cafe_waifu'] = cafe_waifu_result[0]["label"]
|
| 231 |
-
except Exception as e:
|
| 232 |
-
print(f"Error in Cafe Aesthetic: {e}")
|
| 233 |
-
results['cafe_aesthetic_good'] = None
|
| 234 |
-
results['cafe_aesthetic_bad'] = None
|
| 235 |
-
results['cafe_style'] = None
|
| 236 |
-
results['cafe_waifu'] = None
|
| 237 |
-
|
| 238 |
-
# 5. Anime Aesthetic
|
| 239 |
try:
|
| 240 |
img_array = np.array(image)
|
| 241 |
anime_score = predict_anime_aesthetic(img_array, self.anime_aesthetic)
|
| 242 |
-
|
|
|
|
|
|
|
| 243 |
except Exception as e:
|
| 244 |
print(f"Error in Anime Aesthetic: {e}")
|
| 245 |
results['anime_aesthetic'] = None
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
return results
|
| 248 |
-
|
| 249 |
-
def
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
results = []
|
| 252 |
-
|
| 253 |
for i, file_path in enumerate(image_files):
|
| 254 |
try:
|
| 255 |
-
# Open image
|
| 256 |
img = Image.open(file_path).convert("RGB")
|
| 257 |
-
|
| 258 |
-
# Get image evaluation results
|
| 259 |
eval_results = self.evaluate_image(img)
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
# Add file info and thumbnail path to results
|
| 267 |
result = {
|
| 268 |
'file_name': os.path.basename(file_path),
|
| 269 |
-
'
|
| 270 |
**eval_results
|
| 271 |
}
|
| 272 |
results.append(result)
|
| 273 |
-
|
| 274 |
except Exception as e:
|
| 275 |
print(f"Error processing {file_path}: {e}")
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
return results
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
def cleanup(self):
|
| 280 |
-
"""Clean up temporary files"""
|
| 281 |
if os.path.exists(self.temp_dir):
|
| 282 |
shutil.rmtree(self.temp_dir)
|
| 283 |
|
|
|
|
|
|
|
| 284 |
|
| 285 |
-
# Create the Gradio interface
|
| 286 |
def create_interface():
|
|
|
|
|
|
|
| 287 |
evaluator = ImageEvaluationTool()
|
| 288 |
-
|
|
|
|
| 289 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 290 |
gr.Markdown("""
|
| 291 |
# Comprehensive Image Evaluation Tool
|
| 292 |
-
|
| 293 |
Upload images to evaluate them using multiple aesthetic and quality prediction models:
|
| 294 |
-
|
| 295 |
-
- **Aesthetic Shadow**: Evaluates high-quality vs low-quality images
|
| 296 |
- **Waifu Scorer**: Rates anime/illustration quality from 0-10
|
| 297 |
-
- **Aesthetic Predictor V2.5**: General aesthetic quality prediction
|
| 298 |
-
- **
|
| 299 |
-
- **
|
| 300 |
-
|
| 301 |
-
Upload multiple images to get a comprehensive evaluation table.
|
| 302 |
""")
|
| 303 |
-
|
| 304 |
with gr.Row():
|
| 305 |
with gr.Column(scale=1):
|
| 306 |
input_images = gr.Files(label="Upload Images")
|
|
|
|
| 307 |
process_btn = gr.Button("Evaluate Images", variant="primary")
|
| 308 |
clear_btn = gr.Button("Clear Results")
|
| 309 |
-
|
| 310 |
with gr.Column(scale=2):
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
def
|
| 315 |
-
|
| 316 |
file_paths = [f.name for f in files]
|
| 317 |
-
|
| 318 |
-
#
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
def clear_results():
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
demo.load(lambda: None, inputs=None, outputs=None)
|
| 350 |
-
|
| 351 |
gr.Markdown("""
|
| 352 |
### Notes
|
| 353 |
- The evaluation may take some time depending on the number and size of images
|
| 354 |
- For best results, use high-quality images
|
| 355 |
-
- Scores are
|
|
|
|
|
|
|
|
|
|
| 356 |
""")
|
| 357 |
-
|
| 358 |
return demo
|
| 359 |
|
| 360 |
-
# Launch the interface
|
| 361 |
if __name__ == "__main__":
|
| 362 |
demo = create_interface()
|
| 363 |
demo.queue().launch()
|
|
|
|
| 10 |
import pandas as pd
|
| 11 |
import tempfile
|
| 12 |
import shutil
|
| 13 |
+
import base64
|
| 14 |
+
from io import BytesIO
|
| 15 |
+
|
| 16 |
+
# Import necessary function from aesthetic_predictor_v2_5
|
| 17 |
+
from aesthetic_predictor_v2_5 import convert_v2_5_from_siglip
|
| 18 |
|
|
|
|
| 19 |
class MLP(torch.nn.Module):
|
| 20 |
def __init__(self, input_size, xcol='emb', ycol='avg_rating', batch_norm=True):
|
| 21 |
super().__init__()
|
|
|
|
| 47 |
def forward(self, x):
|
| 48 |
return self.layers(x)
|
| 49 |
|
|
|
|
| 50 |
class WaifuScorer(object):
|
| 51 |
def __init__(self, model_path=None, device='cuda', cache_dir=None, verbose=False):
|
| 52 |
self.verbose = verbose
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
import clip
|
| 56 |
+
|
| 57 |
+
if model_path is None:
|
| 58 |
+
model_path = "Eugeoter/waifu-scorer-v3/model.pth"
|
| 59 |
+
if self.verbose:
|
| 60 |
+
print(f"model path not set, switch to default: `{model_path}`")
|
| 61 |
+
|
| 62 |
+
if not os.path.isfile(model_path):
|
| 63 |
+
split = model_path.split("/")
|
| 64 |
+
username, repo_id, model_name = split[-3], split[-2], split[-1]
|
| 65 |
+
model_path = hf_hub_download(f"{username}/{repo_id}", model_name, cache_dir=cache_dir)
|
| 66 |
+
|
| 67 |
+
print(f"Loading WaifuScorer model from `{model_path}`")
|
| 68 |
+
|
| 69 |
+
self.mlp = MLP(input_size=768)
|
| 70 |
+
if model_path.endswith(".safetensors"):
|
| 71 |
+
from safetensors.torch import load_file
|
| 72 |
+
state_dict = load_file(model_path)
|
| 73 |
+
else:
|
| 74 |
+
state_dict = torch.load(model_path, map_location=device)
|
| 75 |
+
self.mlp.load_state_dict(state_dict)
|
| 76 |
+
self.mlp.to(device)
|
| 77 |
+
|
| 78 |
+
self.model2, self.preprocess = clip.load("ViT-L/14", device=device)
|
| 79 |
+
self.device = device
|
| 80 |
+
self.dtype = torch.float32
|
| 81 |
+
self.mlp.eval()
|
| 82 |
+
self.available = True
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"Unable to initialize WaifuScorer: {e}")
|
| 85 |
+
self.available = False
|
| 86 |
|
| 87 |
@torch.no_grad()
|
| 88 |
def __call__(self, images):
|
| 89 |
+
if not self.available:
|
| 90 |
+
return [None] * (1 if not isinstance(images, list) else len(images))
|
| 91 |
+
|
| 92 |
if isinstance(images, Image.Image):
|
| 93 |
images = [images]
|
| 94 |
n = len(images)
|
| 95 |
if n == 1:
|
| 96 |
+
images = images*2
|
| 97 |
+
|
|
|
|
| 98 |
image_tensors = [self.preprocess(img).unsqueeze(0) for img in images]
|
| 99 |
image_batch = torch.cat(image_tensors).to(self.device)
|
| 100 |
image_features = self.model2.encode_image(image_batch)
|
| 101 |
+
|
|
|
|
| 102 |
l2 = image_features.norm(2, dim=-1, keepdim=True)
|
| 103 |
l2[l2 == 0] = 1
|
| 104 |
im_emb_arr = (image_features / l2).to(device=self.device, dtype=self.dtype)
|
| 105 |
+
|
|
|
|
| 106 |
predictions = self.mlp(im_emb_arr)
|
| 107 |
scores = predictions.clamp(0, 10).cpu().numpy().reshape(-1).tolist()
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
return scores[:n]
|
| 110 |
|
| 111 |
def load_aesthetic_predictor_v2_5():
|
| 112 |
+
class AestheticPredictorV2_5_Impl: # Renamed class to avoid confusion
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
def __init__(self):
|
| 114 |
print("Loading Aesthetic Predictor V2.5...")
|
| 115 |
+
self.model, self.preprocessor = convert_v2_5_from_siglip(
|
| 116 |
+
low_cpu_mem_usage=True,
|
| 117 |
+
trust_remote_code=True,
|
| 118 |
+
)
|
| 119 |
+
if torch.cuda.is_available():
|
| 120 |
+
self.model = self.model.to(torch.bfloat16).cuda()
|
| 121 |
+
|
| 122 |
+
def inference(self, image: Image.Image) -> float:
|
| 123 |
+
# preprocess image
|
| 124 |
+
pixel_values = self.preprocessor(
|
| 125 |
+
images=image.convert("RGB"), return_tensors="pt"
|
| 126 |
+
).pixel_values
|
| 127 |
+
|
| 128 |
+
if torch.cuda.is_available():
|
| 129 |
+
pixel_values = pixel_values.to(torch.bfloat16).cuda()
|
| 130 |
+
|
| 131 |
+
# predict aesthetic score
|
| 132 |
+
with torch.inference_mode():
|
| 133 |
+
score = self.model(pixel_values).logits.squeeze().float().cpu().numpy()
|
| 134 |
|
| 135 |
+
return score
|
| 136 |
+
|
| 137 |
+
return AestheticPredictorV2_5_Impl() # Return an instance of the implementation class
|
| 138 |
|
| 139 |
def load_anime_aesthetic_model():
|
| 140 |
model_path = hf_hub_download(repo_id="skytnt/anime-aesthetic", filename="model.onnx")
|
| 141 |
model = rt.InferenceSession(model_path, providers=['CPUExecutionProvider'])
|
| 142 |
return model
|
| 143 |
|
|
|
|
| 144 |
def predict_anime_aesthetic(img, model):
|
| 145 |
img = np.array(img).astype(np.float32) / 255
|
| 146 |
s = 768
|
|
|
|
| 154 |
pred = model.run(None, {"img": img_input})[0].item()
|
| 155 |
return pred
|
| 156 |
|
|
|
|
| 157 |
class ImageEvaluationTool:
|
| 158 |
def __init__(self):
|
| 159 |
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 160 |
print(f"Using device: {self.device}")
|
| 161 |
+
|
|
|
|
| 162 |
print("Loading models... This may take some time.")
|
| 163 |
+
|
|
|
|
| 164 |
print("Loading Aesthetic Shadow model...")
|
| 165 |
self.aesthetic_shadow = pipeline("image-classification", model="NeoChen1024/aesthetic-shadow-v2-backup", device=self.device)
|
| 166 |
+
|
| 167 |
+
print("Loading Waifu Scorer model...")
|
| 168 |
+
self.waifu_scorer = WaifuScorer(device=self.device, verbose=True)
|
| 169 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
print("Loading Aesthetic Predictor V2.5...")
|
| 171 |
self.aesthetic_predictor_v2_5 = load_aesthetic_predictor_v2_5()
|
| 172 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
print("Loading Anime Aesthetic model...")
|
| 174 |
self.anime_aesthetic = load_anime_aesthetic_model()
|
| 175 |
+
|
| 176 |
print("All models loaded successfully!")
|
| 177 |
+
|
|
|
|
| 178 |
self.temp_dir = tempfile.mkdtemp()
|
| 179 |
+
|
| 180 |
def evaluate_image(self, image):
|
|
|
|
| 181 |
results = {}
|
| 182 |
+
|
|
|
|
| 183 |
if not isinstance(image, Image.Image):
|
| 184 |
image = Image.fromarray(image)
|
| 185 |
+
|
|
|
|
| 186 |
try:
|
| 187 |
shadow_result = self.aesthetic_shadow(images=[image])[0]
|
| 188 |
hq_score = [p for p in shadow_result if p['label'] == 'hq'][0]['score']
|
| 189 |
+
# Scale aesthetic_shadow to 0-10 and clamp
|
| 190 |
+
aesthetic_shadow_score = np.clip(hq_score * 10.0, 0.0, 10.0)
|
| 191 |
+
results['aesthetic_shadow'] = aesthetic_shadow_score
|
| 192 |
except Exception as e:
|
| 193 |
print(f"Error in Aesthetic Shadow: {e}")
|
| 194 |
results['aesthetic_shadow'] = None
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
waifu_score = self.waifu_scorer([image])[0]
|
| 198 |
+
# Clamp waifu_score
|
| 199 |
+
waifu_score_clamped = np.clip(waifu_score, 0.0, 10.0)
|
| 200 |
+
results['waifu_scorer'] = waifu_score_clamped
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f"Error in Waifu Scorer: {e}")
|
|
|
|
|
|
|
| 203 |
results['waifu_scorer'] = None
|
| 204 |
+
|
|
|
|
| 205 |
try:
|
| 206 |
v2_5_score = self.aesthetic_predictor_v2_5.inference(image)
|
| 207 |
+
# Clamp v2.5 score
|
| 208 |
+
v2_5_score_clamped = np.clip(v2_5_score, 0.0, 10.0)
|
| 209 |
+
results['aesthetic_predictor_v2_5'] = float(np.round(v2_5_score_clamped, 4)) # Keep 4 decimal places after clamping
|
| 210 |
except Exception as e:
|
| 211 |
print(f"Error in Aesthetic Predictor V2.5: {e}")
|
| 212 |
results['aesthetic_predictor_v2_5'] = None
|
| 213 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
try:
|
| 215 |
img_array = np.array(image)
|
| 216 |
anime_score = predict_anime_aesthetic(img_array, self.anime_aesthetic)
|
| 217 |
+
# Scale Anime Score to 0-10 and clamp
|
| 218 |
+
anime_score_scaled = np.clip(anime_score * 10.0, 0.0, 10.0)
|
| 219 |
+
results['anime_aesthetic'] = anime_score_scaled
|
| 220 |
except Exception as e:
|
| 221 |
print(f"Error in Anime Aesthetic: {e}")
|
| 222 |
results['anime_aesthetic'] = None
|
| 223 |
+
|
| 224 |
+
# Calculate Final Score (simple average of available scores)
|
| 225 |
+
valid_scores = [v for v in results.values() if v is not None]
|
| 226 |
+
if valid_scores:
|
| 227 |
+
final_score = np.mean(valid_scores)
|
| 228 |
+
results['final_score'] = np.clip(final_score, 0.0, 10.0) # Clamp final score too
|
| 229 |
+
else:
|
| 230 |
+
results['final_score'] = None
|
| 231 |
+
|
| 232 |
return results
|
| 233 |
+
|
| 234 |
+
def image_to_base64(self, image):
|
| 235 |
+
buffered = BytesIO()
|
| 236 |
+
image.save(buffered, format="JPEG")
|
| 237 |
+
return base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 238 |
+
|
| 239 |
+
def process_single_image(self, file_path):
|
| 240 |
+
try:
|
| 241 |
+
img = Image.open(file_path).convert("RGB")
|
| 242 |
+
eval_results = self.evaluate_image(img)
|
| 243 |
+
thumbnail = img.copy()
|
| 244 |
+
thumbnail.thumbnail((200, 200))
|
| 245 |
+
img_base64 = self.image_to_base64(thumbnail)
|
| 246 |
+
result = {
|
| 247 |
+
'file_name': os.path.basename(file_path),
|
| 248 |
+
'img_data': img_base64,
|
| 249 |
+
**eval_results
|
| 250 |
+
}
|
| 251 |
+
return result
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"Error processing {file_path}: {e}")
|
| 254 |
+
return None
|
| 255 |
+
|
| 256 |
+
def process_images_evaluation(self, image_files): # Renamed and now for evaluation only
|
| 257 |
results = []
|
| 258 |
+
|
| 259 |
for i, file_path in enumerate(image_files):
|
| 260 |
try:
|
|
|
|
| 261 |
img = Image.open(file_path).convert("RGB")
|
|
|
|
|
|
|
| 262 |
eval_results = self.evaluate_image(img)
|
| 263 |
+
|
| 264 |
+
thumbnail = img.copy()
|
| 265 |
+
thumbnail.thumbnail((200, 200))
|
| 266 |
+
|
| 267 |
+
img_base64 = self.image_to_base64(thumbnail)
|
| 268 |
+
|
|
|
|
| 269 |
result = {
|
| 270 |
'file_name': os.path.basename(file_path),
|
| 271 |
+
'img_data': img_base64,
|
| 272 |
**eval_results
|
| 273 |
}
|
| 274 |
results.append(result)
|
| 275 |
+
|
| 276 |
except Exception as e:
|
| 277 |
print(f"Error processing {file_path}: {e}")
|
| 278 |
+
|
| 279 |
+
return results
|
| 280 |
+
|
| 281 |
+
def sort_results(self, results, sort_by="Final Score"): # New function for sorting
|
| 282 |
+
def sort_key(res): # Define a sorting key function
|
| 283 |
+
sort_value = res.get(sort_by.lower().replace(" ", "_"), None) # Handle spaces and case
|
| 284 |
+
if sort_value is None: # Put N/A at the end
|
| 285 |
+
return -float('inf') if sort_by == "File Name" else float('inf') # File Name sort N/A at end alphabetically
|
| 286 |
+
return sort_value
|
| 287 |
+
|
| 288 |
+
results.sort(key=sort_key, reverse=sort_by != "File Name") # Sort results, reverse for score columns
|
| 289 |
return results
|
| 290 |
+
|
| 291 |
+
def generate_html_table(self, results):
|
| 292 |
+
html = """
|
| 293 |
+
<style>
|
| 294 |
+
.results-table {
|
| 295 |
+
width: 100%;
|
| 296 |
+
border-collapse: collapse;
|
| 297 |
+
margin: 20px 0;
|
| 298 |
+
font-family: Arial, sans-serif;
|
| 299 |
+
background-color: transparent;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.results-table th,
|
| 303 |
+
.results-table td {
|
| 304 |
+
color: #eee;
|
| 305 |
+
border: 1px solid #ddd;
|
| 306 |
+
padding: 8px;
|
| 307 |
+
text-align: center;
|
| 308 |
+
background-color: transparent;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.results-table th {
|
| 312 |
+
font-weight: bold;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.results-table tr:nth-child(even) {
|
| 316 |
+
background-color: transparent;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.results-table tr:hover {
|
| 320 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.image-preview {
|
| 324 |
+
max-width: 150px;
|
| 325 |
+
max-height: 150px;
|
| 326 |
+
display: block;
|
| 327 |
+
margin: 0 auto;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.good-score {
|
| 331 |
+
color: #0f0;
|
| 332 |
+
font-weight: bold;
|
| 333 |
+
}
|
| 334 |
+
.bad-score {
|
| 335 |
+
color: #f00;
|
| 336 |
+
font-weight: bold;
|
| 337 |
+
}
|
| 338 |
+
.medium-score {
|
| 339 |
+
color: orange;
|
| 340 |
+
font-weight: bold;
|
| 341 |
+
}
|
| 342 |
+
</style>
|
| 343 |
+
|
| 344 |
+
<table class="results-table">
|
| 345 |
+
<thead>
|
| 346 |
+
<tr>
|
| 347 |
+
<th>Image</th>
|
| 348 |
+
<th>File Name</th>
|
| 349 |
+
<th>Aesthetic Shadow</th>
|
| 350 |
+
<th>Waifu Scorer</th>
|
| 351 |
+
<th>Aesthetic V2.5</th>
|
| 352 |
+
<th>Anime Score</th>
|
| 353 |
+
<th>Final Score</th>
|
| 354 |
+
</tr>
|
| 355 |
+
</thead>
|
| 356 |
+
<tbody>
|
| 357 |
+
"""
|
| 358 |
+
|
| 359 |
+
for result in results:
|
| 360 |
+
html += "<tr>"
|
| 361 |
+
html += f'<td><img src="data:image/jpeg;base64,{result["img_data"]}" class="image-preview"></td>'
|
| 362 |
+
html += f'<td>{result["file_name"]}</td>'
|
| 363 |
+
|
| 364 |
+
score = result["aesthetic_shadow"]
|
| 365 |
+
score_class = "good-score" if score and score >= 7 else "medium-score" if score and score >= 4 else "bad-score"
|
| 366 |
+
html += f'<td class="{score_class}">{score if score is not None else "N/A":.4f}</td>' # Format to 4 decimal places
|
| 367 |
+
|
| 368 |
+
score = result["waifu_scorer"]
|
| 369 |
+
score_class = "good-score" if score and score >= 7 else "medium-score" if score and score >= 5 else "bad-score"
|
| 370 |
+
html += f'<td class="{score_class}">{score if score is not None else "N/A":.4f}</td>' # Format to 4 decimal places
|
| 371 |
+
|
| 372 |
+
score = result["aesthetic_predictor_v2_5"]
|
| 373 |
+
score_class = "good-score" if score and score >= 7 else "medium-score" if score and score >= 5 else "bad-score"
|
| 374 |
+
html += f'<td class="{score_class}">{score if score is not None else "N/A":.4f}</td>' # Format to 4 decimal places
|
| 375 |
+
|
| 376 |
+
score = result["anime_aesthetic"]
|
| 377 |
+
score_class = "good-score" if score and score >= 7 else "medium-score" if score and score >= 5 else "bad-score"
|
| 378 |
+
html += f'<td class="{score_class}">{score if score is not None else "N/A":.4f}</td>' # Format to 4 decimal places
|
| 379 |
+
|
| 380 |
+
score = result["final_score"]
|
| 381 |
+
score_class = "good-score" if score and score >= 7 else "medium-score" if score and score >= 5 else "bad-score"
|
| 382 |
+
html += f'<td class="{score_class}">{score if score is not None else "N/A":.4f}</td>' # Format to 4 decimal places
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
html += "</tr>"
|
| 386 |
+
|
| 387 |
+
html += """
|
| 388 |
+
</tbody>
|
| 389 |
+
</table>
|
| 390 |
+
"""
|
| 391 |
+
|
| 392 |
+
return html
|
| 393 |
+
|
| 394 |
def cleanup(self):
|
|
|
|
| 395 |
if os.path.exists(self.temp_dir):
|
| 396 |
shutil.rmtree(self.temp_dir)
|
| 397 |
|
| 398 |
+
# Global variable to store evaluation results
|
| 399 |
+
global_results = None
|
| 400 |
|
|
|
|
| 401 |
def create_interface():
|
| 402 |
+
global global_results # Use the global variable
|
| 403 |
+
|
| 404 |
evaluator = ImageEvaluationTool()
|
| 405 |
+
sort_options = ["Final Score", "File Name", "Aesthetic Shadow", "Waifu Scorer", "Aesthetic V2.5", "Anime Score"] # Sort options
|
| 406 |
+
|
| 407 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 408 |
gr.Markdown("""
|
| 409 |
# Comprehensive Image Evaluation Tool
|
| 410 |
+
|
| 411 |
Upload images to evaluate them using multiple aesthetic and quality prediction models:
|
| 412 |
+
|
| 413 |
+
- **Aesthetic Shadow**: Evaluates high-quality vs low-quality images (scaled to 0-10)
|
| 414 |
- **Waifu Scorer**: Rates anime/illustration quality from 0-10
|
| 415 |
+
- **Aesthetic Predictor V2.5**: General aesthetic quality prediction (clamped to 0-10)
|
| 416 |
+
- **Anime Aesthetic**: Specific model for anime style images (scaled and clamped to 0-10)
|
| 417 |
+
- **Final Score**: Average of available scores (clamped to 0-10)
|
| 418 |
+
|
| 419 |
+
Upload multiple images to get a comprehensive evaluation table. Scores are clamped to the range 0.0000 - 10.0000.
|
| 420 |
""")
|
| 421 |
+
|
| 422 |
with gr.Row():
|
| 423 |
with gr.Column(scale=1):
|
| 424 |
input_images = gr.Files(label="Upload Images")
|
| 425 |
+
sort_dropdown = gr.Dropdown(sort_options, value="Final Score", label="Sort by") # Dropdown for sorting
|
| 426 |
process_btn = gr.Button("Evaluate Images", variant="primary")
|
| 427 |
clear_btn = gr.Button("Clear Results")
|
| 428 |
+
|
| 429 |
with gr.Column(scale=2):
|
| 430 |
+
progress_html = gr.HTML(label="Progress") # Keep progress_html if you want to show initial progress
|
| 431 |
+
output_html = gr.HTML(label="Evaluation Results")
|
| 432 |
+
|
| 433 |
+
def process_images_and_update(files): # Renamed and simplified
|
| 434 |
+
global global_results
|
| 435 |
file_paths = [f.name for f in files]
|
| 436 |
+
total = len(file_paths)
|
| 437 |
+
progress_html_content = "" # Initialize progress content
|
| 438 |
+
|
| 439 |
+
if not file_paths: # Handle no files uploaded
|
| 440 |
+
global_results = []
|
| 441 |
+
return progress_html_content, evaluator.generate_html_table([]) # Empty table
|
| 442 |
+
|
| 443 |
+
progress_html_content = ""
|
| 444 |
+
for i, file_path in enumerate(file_paths):
|
| 445 |
+
percent = (i / total) * 100
|
| 446 |
+
progress_bar = f"""
|
| 447 |
+
<div>
|
| 448 |
+
<p>Processing {os.path.basename(file_path)}</p>
|
| 449 |
+
<progress value="{percent}" max="100"></progress>
|
| 450 |
+
<p>{percent:.1f}% complete</p>
|
| 451 |
+
</div>
|
| 452 |
+
"""
|
| 453 |
+
progress_html_content = progress_bar # Update progress content
|
| 454 |
+
yield progress_html_content, gr.update() # Yield progress update
|
| 455 |
+
# No need to process and sort here, just evaluate
|
| 456 |
+
global_results = evaluator.process_images_evaluation(file_paths) # Evaluate all images and store
|
| 457 |
+
sorted_results = evaluator.sort_results(global_results, sort_by="Final Score") # Initial sort by Final Score
|
| 458 |
+
html_table = evaluator.generate_html_table(sorted_results)
|
| 459 |
+
yield "<p>Processing complete</p>", html_table # Final progress and table
|
| 460 |
+
|
| 461 |
+
def update_table_sort(sort_by_column): # New function for sorting update
|
| 462 |
+
global global_results
|
| 463 |
+
if global_results is None:
|
| 464 |
+
return "No images evaluated yet." # Or handle case when no images are evaluated
|
| 465 |
+
sorted_results = evaluator.sort_results(global_results, sort_by=sort_by_column)
|
| 466 |
+
html_table = evaluator.generate_html_table(sorted_results)
|
| 467 |
+
return html_table
|
| 468 |
+
|
| 469 |
def clear_results():
|
| 470 |
+
global global_results
|
| 471 |
+
global_results = None # Clear stored results
|
| 472 |
+
return gr.update(value=""), gr.update(value="")
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
process_btn.click(
|
| 476 |
+
process_images_and_update,
|
| 477 |
+
inputs=[input_images],
|
| 478 |
+
outputs=[progress_html, output_html]
|
| 479 |
+
)
|
| 480 |
+
sort_dropdown.change( # Only update table on sort change
|
| 481 |
+
update_table_sort,
|
| 482 |
+
inputs=[sort_dropdown],
|
| 483 |
+
outputs=[output_html] # Only update output_html
|
| 484 |
+
)
|
| 485 |
+
clear_btn.click(
|
| 486 |
+
clear_results,
|
| 487 |
+
inputs=[],
|
| 488 |
+
outputs=[progress_html, output_html]
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
demo.load(lambda: None, inputs=None, outputs=None)
|
| 492 |
+
|
| 493 |
gr.Markdown("""
|
| 494 |
### Notes
|
| 495 |
- The evaluation may take some time depending on the number and size of images
|
| 496 |
- For best results, use high-quality images
|
| 497 |
+
- Scores are color-coded: green for good (>=7), orange for medium (>=5), and red for poor scores (<5, or <4 for Aesthetic Shadow)
|
| 498 |
+
- Some models may fail for certain image types, shown as "N/A" in the results
|
| 499 |
+
- "Final Score" is a simple average of available model scores.
|
| 500 |
+
- Table is sortable by clicking the dropdown above the "Evaluate Images" button. Default sort is by "Final Score". Sorting happens instantly without re-evaluating images.
|
| 501 |
""")
|
| 502 |
+
|
| 503 |
return demo
|
| 504 |
|
|
|
|
| 505 |
if __name__ == "__main__":
|
| 506 |
demo = create_interface()
|
| 507 |
demo.queue().launch()
|