Mavthunder commited on
Commit
d379ce4
Β·
verified Β·
1 Parent(s): f129981

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -216
app.py CHANGED
@@ -1,13 +1,17 @@
1
  import gradio as gr
2
  import numpy as np
3
  from PIL import Image, ImageFilter
 
 
 
 
 
4
  import time
5
 
6
- # ---------- small image utils ----------
7
 
8
  def pil_to_np(img_pil):
9
- arr = np.asarray(img_pil.convert("RGB")).astype(np.float32) / 255.0
10
- return arr
11
 
12
  def np_to_pil(arr):
13
  arr = np.clip(arr * 255.0, 0, 255).astype(np.uint8)
@@ -20,256 +24,163 @@ def resize_max_side(img_pil, max_side=1600):
20
  return img_pil.resize((int(w*scale), int(h*scale)), Image.LANCZOS)
21
  return img_pil
22
 
23
- def rgb_to_hsv_np(rgb):
24
- # rgb: HxWx3 in [0,1]
25
- r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
26
- mx = np.max(rgb, axis=-1)
27
- mn = np.min(rgb, axis=-1)
28
- diff = mx - mn + 1e-8
29
-
30
- # Hue
31
- h = np.zeros_like(mx)
32
- mask = diff > 1e-8
33
- r_is_max = (mx == r) & mask
34
- g_is_max = (mx == g) & mask
35
- b_is_max = (mx == b) & mask
36
- h[r_is_max] = (g[r_is_max] - b[r_is_max]) / diff[r_is_max]
37
- h[g_is_max] = 2.0 + (b[g_is_max] - r[g_is_max]) / diff[g_is_max]
38
- h[b_is_max] = 4.0 + (r[b_is_max] - g[b_is_max]) / diff[b_is_max]
39
- h = (h / 6.0) % 1.0
40
-
41
- # Saturation
42
- s = np.where(mx <= 1e-8, 0, diff / (mx + 1e-8))
43
- v = mx
44
- return np.stack([h, s, v], axis=-1)
45
-
46
- def hsv_to_rgb_np(hsv):
47
- h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]
48
- i = np.floor(h * 6).astype(int)
49
- f = h * 6 - i
50
- p = v * (1 - s)
51
- q = v * (1 - f * s)
52
- t = v * (1 - (1 - f) * s)
53
- i_mod = i % 6
54
-
55
- r = np.select(
56
- [i_mod == 0, i_mod == 1, i_mod == 2, i_mod == 3, i_mod == 4, i_mod == 5],
57
- [v, q, p, p, t, v])
58
- g = np.select(
59
- [i_mod == 0, i_mod == 1, i_mod == 2, i_mod == 3, i_mod == 4, i_mod == 5],
60
- [t, v, v, q, p, p])
61
- b = np.select(
62
- [i_mod == 0, i_mod == 1, i_mod == 2, i_mod == 3, i_mod == 4, i_mod == 5],
63
- [p, p, t, v, v, q])
64
- rgb = np.stack([r, g, b], axis=-1)
65
- return np.clip(rgb, 0, 1)
66
-
67
  def unsharp_mask(img_pil, radius=1.2, amount=0.7):
68
- # classic local contrast boost
69
  blurred = img_pil.filter(ImageFilter.GaussianBlur(radius=radius))
70
  arr = pil_to_np(img_pil)
71
  arr_blur = pil_to_np(blurred)
72
  out = np.clip(arr + amount * (arr - arr_blur), 0, 1)
73
  return np_to_pil(out)
74
 
75
- # ---------- core adjustments ----------
76
-
77
- def apply_adjustments(img_pil,
78
- exposure_stops=0.0,
79
- contrast=0.0,
80
- saturation=0.0,
81
- warmth=0.0, # + warm, - cool
82
- hue_shift_deg=0.0,
83
- gamma=1.0,
84
- clarity=0.0,
85
- lift=0.0): # lift blacks / fade
86
- """All params are gentle, designed to stay natural."""
87
- img = resize_max_side(img_pil)
88
- arr = pil_to_np(img)
89
-
90
- # exposure (stops)
91
- if abs(exposure_stops) > 1e-6:
92
- arr = np.clip(arr * (2.0 ** exposure_stops), 0, 1)
93
-
94
- # contrast (simple S-curve around mid 0.5)
95
- if abs(contrast) > 1e-6:
96
- arr = np.clip((arr - 0.5) * (1.0 + contrast) + 0.5, 0, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- # lift blacks (fade)
99
- if abs(lift) > 1e-6:
100
- arr = np.clip(arr + lift * (1.0 - arr), 0, 1)
101
-
102
- # warmth (white-balance tilt)
103
- if abs(warmth) > 1e-6:
104
- wb = np.array([1.0 + warmth, 1.0, 1.0 - warmth], dtype=np.float32)
105
- arr = np.clip(arr * wb, 0, 1)
106
-
107
- # HSV tweaks (saturation + hue shift + gamma)
108
- hsv = rgb_to_hsv_np(arr)
109
- if abs(saturation) > 1e-6:
110
- hsv[..., 1] = np.clip(hsv[..., 1] * (1.0 + saturation), 0, 1)
111
- if abs(hue_shift_deg) > 1e-6:
112
- hsv[..., 0] = (hsv[..., 0] + hue_shift_deg / 360.0) % 1.0
113
- if abs(gamma - 1.0) > 1e-6:
114
- hsv[..., 2] = np.clip(hsv[..., 2] ** (1.0 / gamma), 0, 1)
115
- arr = hsv_to_rgb_np(hsv)
116
 
 
 
 
 
 
 
 
 
 
 
117
  out = np_to_pil(arr)
118
- # clarity via unsharp mask
119
- if abs(clarity) > 1e-6:
120
- out = unsharp_mask(out, radius=1.2, amount=clarity)
121
  return out
122
 
123
- # ---------- aesthetic scoring (fast heuristic) ----------
124
-
125
- def aesthetic_score_fast(img_pil):
126
- arr = pil_to_np(img_pil)
127
- # luminance
128
- Y = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2]
129
- brightness = float(np.mean(Y))
130
- contrast = float(np.std(Y))
131
- # saturation
132
- s = rgb_to_hsv_np(arr)[..., 1]
133
- sat = float(np.mean(s))
134
-
135
- # targets tuned for mass-appeal feed aesthetics (roughly)
136
- target_b = 0.62
137
- target_sat = 0.35
138
 
139
- score_b = 1.0 - min(abs(brightness - target_b) / 0.62, 1.0)
140
- score_c = min(max((contrast - 0.04) / 0.26, 0.0), 1.0)
141
- score_s = 1.0 - min(abs(sat - target_sat) / 0.35, 1.0)
142
-
143
- # clipping penalties
144
- clip_hi = float((Y > 0.98).mean())
145
- clip_lo = float((Y < 0.02).mean())
146
- penalty_clip = min(clip_hi * 4.0 + clip_lo * 2.5, 1.5)
147
-
148
- # white balance cast penalty (channel means too far apart)
149
- means = arr.reshape(-1, 3).mean(axis=0)
150
- cast = float(np.max(means) - np.min(means))
151
- penalty_cast = min(cast * 2.0, 1.0)
152
-
153
- # simple skin guard: if skin-ish pixels oversaturated, penalize
154
- hsv = rgb_to_hsv_np(arr)
155
- h, s_, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]
156
- skin_mask = (h < (50/360)) | (h > (345/360))
157
- skin_mask &= (s_ > 0.23) & (v > 0.35)
158
- skin_sat = float(s_[skin_mask].mean()) if np.any(skin_mask) else 0.0
159
- penalty_skin = max(0.0, (skin_sat - 0.65) * 2.0)
160
-
161
- raw = 0.4 * score_b + 0.35 * score_c + 0.25 * score_s
162
- penalties = penalty_clip + penalty_cast + penalty_skin
163
- final = max(0.0, min(1.0, raw - 0.4 * penalties))
164
- return final, {
165
- "brightness": round(brightness, 3),
166
- "contrast": round(contrast, 3),
167
- "saturation": round(sat, 3),
168
- "clip_hi%": round(clip_hi * 100, 2),
169
- "clip_lo%": round(clip_lo * 100, 2)
170
- }
171
-
172
- # ---------- vibe presets ----------
173
-
174
- VIBES = {
175
- "Natural": dict(exposure_stops=0.10, contrast=0.08, saturation=0.06, warmth=0.02, clarity=0.06, gamma=1.0, lift=0.00),
176
- "Film": dict(exposure_stops=0.05, contrast=-0.03, saturation=-0.02, warmth=0.05, clarity=0.03, gamma=0.95, lift=0.06),
177
- "Pop": dict(exposure_stops=0.00, contrast=0.15, saturation=0.12, warmth=0.00, clarity=0.15, gamma=1.0, lift=0.00),
178
- "Moody": dict(exposure_stops=-0.15,contrast=0.10, saturation=-0.08,warmth=-0.03, clarity=0.05, gamma=1.05, lift=0.02),
179
- "Pastel": dict(exposure_stops=0.10, contrast=-0.10, saturation=-0.15,warmth=0.03, clarity=0.02, gamma=0.90, lift=0.08),
180
- }
181
-
182
- # Keep recent result in memory so feedback buttons can store something meaningful
183
- LAST_RESULT = {"winner": None, "scores": None}
184
 
185
  def process(image, intensity):
186
  if image is None:
187
  raise gr.Error("Please upload a photo first.")
 
 
 
188
 
189
- # generate candidates
190
  candidates = []
191
- scores = []
192
- metrics = []
193
- for name, params in VIBES.items():
194
- out = apply_adjustments(image, **params)
195
- score, met = aesthetic_score_fast(out)
196
  candidates.append((name, out, score))
197
- scores.append(score)
198
- metrics.append((name, met))
199
-
200
- # pick winner
201
  candidates.sort(key=lambda x: x[2], reverse=True)
202
  winner_name, winner_img, winner_score = candidates[0]
203
 
204
- # blend intensity with original (0..100)
205
- t = float(intensity) / 100.0
206
  base = resize_max_side(image)
207
- wnp = pil_to_np(winner_img)
208
- onp = pil_to_np(base)
209
- blended = np_to_pil(onp * (1 - t) + wnp * t)
210
-
211
- # gallery: show all looks with their scores
212
- gallery = []
213
- for name, img, score in candidates:
214
- caption = f"{name} β€” score {score:.2f}"
215
- gallery.append((img, caption))
216
-
217
- # remember
218
- LAST_RESULT["winner"] = {
219
- "name": winner_name,
220
- "score": float(winner_score),
221
- "when": time.strftime("%Y-%m-%d %H:%M:%S")
222
- }
223
- LAST_RESULT["scores"] = {name: float(s) for name, _, s in candidates}
224
 
225
- # metrics text
226
- metrics_top = next(m for n, m in metrics if n == winner_name)
227
- info = f"Picked: **{winner_name}** (score {winner_score:.2f})"
228
- info += f"\n\nBrightness: {metrics_top['brightness']} | Contrast: {metrics_top['contrast']} | Saturation: {metrics_top['saturation']}"
229
- info += f"\nClipped Highlights: {metrics_top['clip_hi%']}% | Deep Shadows: {metrics_top['clip_lo%']}%"
230
-
231
- return blended, gallery, info
232
 
233
  def feedback(good):
234
  if LAST_RESULT["winner"] is None:
235
- return "Upload a photo and generate a result first."
236
- # Append a tiny log in Space storage (ephemeral on free tier, good enough for MVP)
237
  try:
238
- with open("feedback_log.csv", "a", encoding="utf-8") as f:
239
- f.write(
240
- f"{LAST_RESULT['winner']['when']},{LAST_RESULT['winner']['name']},{LAST_RESULT['winner']['score']},{'up' if good else 'down'}\n"
241
- )
242
- except Exception:
243
  pass
244
  return "Thanks for the feedback! ✨"
245
 
246
- # ---------- UI ----------
247
 
248
- with gr.Blocks(title="One-Click Aesthetic") as demo:
249
- gr.Markdown(
250
- """
251
- # One-Click Aesthetic ✨
252
- Upload a photo and hit **Make it Aesthetic**.
253
- The app tries a few tasteful looks and picks the one with the best predicted mass-appeal score.
254
- Use the **Intensity** slider to control how strong the look is.
255
- """
256
- )
257
  with gr.Row():
258
  inp = gr.Image(label="Upload photo", type="pil")
259
- out = gr.Image(label="Aesthetic result")
260
-
261
- intensity = gr.Slider(0, 100, value=80, step=1, label="Intensity (blend)")
262
-
263
- go = gr.Button("Make it Aesthetic", variant="primary")
264
  info = gr.Markdown()
265
- gallery = gr.Gallery(label="Tried looks (ranked high β†’ low)", show_label=True, columns=5, height="auto")
266
-
267
  with gr.Row():
268
- up = gr.Button("πŸ‘ Looks great")
269
- down = gr.Button("πŸ‘Ž Needs work")
270
 
271
- go.click(process, inputs=[inp, intensity], outputs=[out, gallery, info])
272
- up.click(lambda: feedback(True), inputs=None, outputs=info)
273
- down.click(lambda: feedback(False), inputs=None, outputs=info)
274
 
275
  demo.launch()
 
1
  import gradio as gr
2
  import numpy as np
3
  from PIL import Image, ImageFilter
4
+ import torch
5
+ import torch.nn as nn
6
+ from transformers import AutoProcessor, AutoModel
7
+ from huggingface_hub import hf_hub_download
8
+ import cv2
9
  import time
10
 
11
+ # ------------------ Utility functions ------------------
12
 
13
  def pil_to_np(img_pil):
14
+ return np.asarray(img_pil.convert("RGB")).astype(np.float32) / 255.0
 
15
 
16
  def np_to_pil(arr):
17
  arr = np.clip(arr * 255.0, 0, 255).astype(np.uint8)
 
24
  return img_pil.resize((int(w*scale), int(h*scale)), Image.LANCZOS)
25
  return img_pil
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def unsharp_mask(img_pil, radius=1.2, amount=0.7):
 
28
  blurred = img_pil.filter(ImageFilter.GaussianBlur(radius=radius))
29
  arr = pil_to_np(img_pil)
30
  arr_blur = pil_to_np(blurred)
31
  out = np.clip(arr + amount * (arr - arr_blur), 0, 1)
32
  return np_to_pil(out)
33
 
34
+ # ------------------ Zero-DCE++ Model ------------------
35
+
36
+ class EnhanceNet(nn.Module):
37
+ def __init__(self):
38
+ super(EnhanceNet, self).__init__()
39
+ number_f = 32
40
+ self.e_conv1 = nn.Conv2d(3, number_f, 3, 1, 1, bias=True)
41
+ self.e_conv2 = nn.Conv2d(number_f, number_f, 3, 1, 1, bias=True)
42
+ self.e_conv3 = nn.Conv2d(number_f, number_f, 3, 1, 1, bias=True)
43
+ self.e_conv4 = nn.Conv2d(number_f, number_f, 3, 1, 1, bias=True)
44
+ self.e_conv5 = nn.Conv2d(number_f*2, number_f, 3, 1, 1, bias=True)
45
+ self.e_conv6 = nn.Conv2d(number_f*2, number_f, 3, 1, 1, bias=True)
46
+ self.e_conv7 = nn.Conv2d(number_f*2, 24, 3, 1, 1, bias=True)
47
+ self.relu = nn.ReLU(inplace=True)
48
+
49
+ def forward(self, x):
50
+ x1 = self.relu(self.e_conv1(x))
51
+ x2 = self.relu(self.e_conv2(x1))
52
+ x3 = self.relu(self.e_conv3(x2))
53
+ x4 = self.relu(self.e_conv4(x3))
54
+ x5 = self.relu(self.e_conv5(torch.cat([x3, x4], 1)))
55
+ x6 = self.relu(self.e_conv6(torch.cat([x2, x5], 1)))
56
+ x_r = torch.tanh(self.e_conv7(torch.cat([x1, x6], 1)))
57
+ r1, r2, r3, r4, r5, r6, r7, r8 = torch.split(x_r, 3, dim=1)
58
+ x = x + r1 * (torch.pow(x, 2) - x)
59
+ x = x + r2 * (torch.pow(x, 2) - x)
60
+ x = x + r3 * (torch.pow(x, 2) - x)
61
+ enhance_image_1 = x + r4 * (torch.pow(x, 2) - x)
62
+ enhance_image_2 = enhance_image_1 + r5 * (torch.pow(enhance_image_1, 2) - enhance_image_1)
63
+ enhance_image_3 = enhance_image_2 + r6 * (torch.pow(enhance_image_2, 2) - enhance_image_2)
64
+ enhance_image_4 = enhance_image_3 + r7 * (torch.pow(enhance_image_3, 2) - enhance_image_3)
65
+ return enhance_image_4
66
+
67
+ device = "cuda" if torch.cuda.is_available() else "cpu"
68
+ model_path = hf_hub_download("LLVIP/Zero-DCEpp", "zerodcepp.pth")
69
+ zero_dce = EnhanceNet().to(device)
70
+ zero_dce.load_state_dict(torch.load(model_path, map_location=device))
71
+ zero_dce.eval()
72
+
73
+ def zero_dce_enhance(img_pil):
74
+ img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
75
+ img = cv2.resize(img, (400, 400)) # small resize for speed
76
+ img = img.astype(np.float32) / 255.0
77
+ inp = torch.from_numpy(img).permute(2,0,1).unsqueeze(0).to(device)
78
+ with torch.no_grad():
79
+ out = zero_dce(inp)
80
+ out = out.squeeze(0).permute(1,2,0).cpu().numpy()
81
+ out = np.clip(out * 255.0, 0, 255).astype(np.uint8)
82
+ return Image.fromarray(cv2.cvtColor(out, cv2.COLOR_BGR2RGB))
83
+
84
+ # ------------------ Aesthetic Predictor ------------------
85
+
86
+ predictor_name = "shunk031/aesthetic-predictor-v2"
87
+ processor = AutoProcessor.from_pretrained(predictor_name)
88
+ model_pred = AutoModel.from_pretrained(predictor_name).to(device)
89
+ model_pred.eval()
90
+
91
+ def aesthetic_score_ai(img_pil):
92
+ inputs = processor(images=img_pil, return_tensors="pt").to(device)
93
+ with torch.no_grad():
94
+ outputs = model_pred(**inputs)
95
+ score = outputs.logits.mean().item()
96
+ return float(score)
97
+
98
+ # ------------------ Vibes ------------------
99
 
100
+ VIBES = {
101
+ "Natural": dict(exposure=0.05, contrast=0.08, saturation=0.08, sharp=0.05),
102
+ "Film": dict(exposure=0.0, contrast=-0.05, saturation=-0.02, sharp=0.03),
103
+ "Pop": dict(exposure=0.1, contrast=0.15, saturation=0.20, sharp=0.15),
104
+ "Moody": dict(exposure=-0.1, contrast=0.10, saturation=-0.08, sharp=0.05),
105
+ "Pastel": dict(exposure=0.1, contrast=-0.08, saturation=-0.15, sharp=0.02),
106
+ }
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ def apply_vibe(img_pil, vibe):
109
+ arr = pil_to_np(img_pil)
110
+ # Exposure
111
+ arr = np.clip(arr * (1.0 + vibe["exposure"]), 0, 1)
112
+ # Contrast
113
+ arr = np.clip((arr - 0.5) * (1.0 + vibe["contrast"]) + 0.5, 0, 1)
114
+ # Saturation (HSV)
115
+ hsv = cv2.cvtColor((arr*255).astype(np.uint8), cv2.COLOR_RGB2HSV).astype(np.float32)
116
+ hsv[...,1] = np.clip(hsv[...,1] * (1.0 + vibe["saturation"]), 0, 255)
117
+ arr = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB).astype(np.float32)/255.0
118
  out = np_to_pil(arr)
119
+ if vibe["sharp"] > 0:
120
+ out = unsharp_mask(out, amount=vibe["sharp"])
 
121
  return out
122
 
123
+ # ------------------ Processing ------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
+ LAST_RESULT = {"winner": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  def process(image, intensity):
128
  if image is None:
129
  raise gr.Error("Please upload a photo first.")
130
+
131
+ # Step 1: enhance with Zero-DCE++
132
+ enhanced = zero_dce_enhance(image)
133
 
134
+ # Step 2: apply vibes + score them
135
  candidates = []
136
+ for name, vibe in VIBES.items():
137
+ out = apply_vibe(enhanced, vibe)
138
+ score = aesthetic_score_ai(out)
 
 
139
  candidates.append((name, out, score))
140
+
 
 
 
141
  candidates.sort(key=lambda x: x[2], reverse=True)
142
  winner_name, winner_img, winner_score = candidates[0]
143
 
144
+ # Intensity blend
145
+ t = intensity / 100.0
146
  base = resize_max_side(image)
147
+ out_np = pil_to_np(winner_img)
148
+ base_np = pil_to_np(base)
149
+ blended = np_to_pil(base_np * (1-t) + out_np * t)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ gallery = [(img, f"{name}: {score:.2f}") for name, img, score in candidates]
152
+
153
+ LAST_RESULT["winner"] = {"name": winner_name, "score": winner_score, "when": time.strftime("%Y-%m-%d %H:%M:%S")}
154
+
155
+ return blended, gallery, f"Chosen: **{winner_name}** (score {winner_score:.2f})"
 
 
156
 
157
  def feedback(good):
158
  if LAST_RESULT["winner"] is None:
159
+ return "Generate a result first!"
 
160
  try:
161
+ with open("feedback_log.csv", "a") as f:
162
+ f.write(f"{LAST_RESULT['winner']['when']},{LAST_RESULT['winner']['name']},{LAST_RESULT['winner']['score']},{'up' if good else 'down'}\n")
163
+ except:
 
 
164
  pass
165
  return "Thanks for the feedback! ✨"
166
 
167
+ # ------------------ UI ------------------
168
 
169
+ with gr.Blocks(title="One-Click Aesthetic AI") as demo:
170
+ gr.Markdown("# One-Click Aesthetic ✨\nUpload a photo β†’ AI enhances it (Zero-DCE++) β†’ tries vibes β†’ ranks with an AI aesthetic predictor.")
 
 
 
 
 
 
 
171
  with gr.Row():
172
  inp = gr.Image(label="Upload photo", type="pil")
173
+ out = gr.Image(label="Result")
174
+ intensity = gr.Slider(0, 100, value=80, label="Intensity")
175
+ go = gr.Button("Make it Aesthetic")
 
 
176
  info = gr.Markdown()
177
+ gallery = gr.Gallery(label="Tried Looks", columns=5)
 
178
  with gr.Row():
179
+ up = gr.Button("πŸ‘ Good")
180
+ down = gr.Button("πŸ‘Ž Bad")
181
 
182
+ go.click(process, [inp, intensity], [out, gallery, info])
183
+ up.click(lambda: feedback(True), None, info)
184
+ down.click(lambda: feedback(False), None, info)
185
 
186
  demo.launch()