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()
|