Mavthunder commited on
Commit
edf1515
Β·
verified Β·
1 Parent(s): fbba29a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +275 -0
app.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
14
+ return Image.fromarray(arr)
15
+
16
+ def resize_max_side(img_pil, max_side=1600):
17
+ w, h = img_pil.size
18
+ scale = min(1.0, max_side / max(w, h))
19
+ if scale < 1.0:
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()