""" ✨ AI Photo Studio — Works Great With or Without CodeFormer Full enhancement pipeline: AI or advanced OpenCV """ import gradio as gr import cv2 import numpy as np import time import logging import tempfile import os from PIL import Image, ImageEnhance, ImageFilter logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') logger = logging.getLogger(__name__) try: import spaces except ImportError: class spaces: @staticmethod def GPU(fn=None, **kwargs): if fn is None: return lambda f: f return fn try: from gradio_client import Client as HFClient # Try to import handle_file, fall back to string path try: from gradio_client import handle_file as _handle_file def make_file_handle(path): return _handle_file(path) logger.info("✅ gradio_client + handle_file available") except ImportError: def make_file_handle(path): return path # Older gradio_client accepts string paths logger.info("✅ gradio_client available (no handle_file, using string paths)") HAS_CLIENT = True except ImportError: HAS_CLIENT = False def make_file_handle(path): return path logger.error("❌ gradio_client not available") STATE = {'client': None, 'connected': False} HF_TOKEN = os.environ.get("HF_TOKEN", "") # ═══════════════════════════════════════════════════════════════ # CODEFORMER # ═══════════════════════════════════════════════════════════════ def connect(): if not HAS_CLIENT: logger.error("❌ gradio_client not available") return False try: if HF_TOKEN: logger.info("🔌 Connecting to CodeFormer with HF_TOKEN...") STATE['client'] = HFClient("sczhou/CodeFormer", hf_token=HF_TOKEN) else: logger.info("🔌 Connecting to CodeFormer (no token)...") STATE['client'] = HFClient("sczhou/CodeFormer") STATE['connected'] = True logger.info("✅ Connected to CodeFormer!") return True except Exception as e: logger.error(f"❌ CodeFormer connection failed: {e}") return False def call_codeformer(pil_img): """Try CodeFormer with multiple parameter combinations""" c = STATE.get('client') if not c: return None configs = [ {'upscale': 2, 'fidelity': 0.1}, {'upscale': 2, 'fidelity': 0.5}, {'upscale': 4, 'fidelity': 0.1}, ] for cfg in configs: t = tempfile.NamedTemporaryFile(suffix='.png', delete=False) pil_img.save(t.name, 'PNG') t.close() try: file_arg = make_file_handle(t.name) logger.info(f"Calling CodeFormer: upscale={cfg['upscale']}, fidelity={cfg['fidelity']}, file_type={type(file_arg)}") r = c.predict( image=file_arg, face_align=True, background_enhance=True, face_upsample=True, upscale=cfg['upscale'], codeformer_fidelity=cfg['fidelity'], api_name="/inference" ) d = r[0] if isinstance(r, (list, tuple)) else r if isinstance(d, dict): d = d.get('path') or d.get('url') if isinstance(d, str) and os.path.exists(d): logger.info(f"✅ CodeFormer success! Output: {d}") return Image.open(d) elif isinstance(d, str): # Try downloading from URL logger.info(f"CodeFormer returned URL: {d[:100]}") try: import urllib.request dl = tempfile.NamedTemporaryFile(suffix='.png', delete=False) urllib.request.urlretrieve(d, dl.name) dl.close() return Image.open(dl.name) except Exception as e2: logger.warning(f"Download failed: {e2}") except Exception as e: logger.warning(f"CodeFormer failed (upscale={cfg['upscale']}): {e}") finally: try: os.unlink(t.name) except: pass return None # ═══════════════════════════════════════════════════════════════ # ADVANCED OPENCV PIPELINE (when CodeFormer unavailable) # ═══════════════════════════════════════════════════════════════ def detect_faces(img): h, w = img.shape[:2] ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) m1 = cv2.inRange(ycrcb, np.array([0,133,77]), np.array([255,173,127])) m2 = cv2.inRange(hsv, np.array([0,15,60]), np.array([30,255,255])) skin = cv2.bitwise_and(m1, m2) k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7,7)) skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, k, iterations=3) skin = cv2.morphologyEx(skin, cv2.MORPH_OPEN, k, iterations=2) n,_,stats,_ = cv2.connectedComponentsWithStats(skin, 8) faces = [] for i in range(1, n): a = stats[i, cv2.CC_STAT_AREA] if a > (h*w)*0.005: x,y = stats[i,cv2.CC_STAT_LEFT], stats[i,cv2.CC_STAT_TOP] bw,bh = stats[i,cv2.CC_STAT_WIDTH], stats[i,cv2.CC_STAT_HEIGHT] if 0.4 < bw/max(bh,1) < 2.5: p = int(max(bw,bh)*0.15) faces.append([max(0,x-p), max(0,y-p), min(w,x+bw+p), min(h,y+bh+p)]) return faces def get_skin_mask(img): ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) m1 = cv2.inRange(ycrcb, np.array([0,133,77]), np.array([255,173,127])) m2 = cv2.inRange(hsv, np.array([0,15,60]), np.array([30,255,255])) kn = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)) sk = cv2.morphologyEx(cv2.bitwise_and(m1,m2), cv2.MORPH_CLOSE, kn, iterations=2) sk = cv2.morphologyEx(sk, cv2.MORPH_OPEN, kn, iterations=1) return cv2.GaussianBlur(sk, (15,15), 0).astype(np.float32)/255.0 def opencv_full_enhance(img_cv): """Complete OpenCV enhancement pipeline — no AI needed""" h, w = img_cv.shape[:2] r = img_cv.copy() # ── 1. Strong denoise ── r = cv2.fastNlMeansDenoisingColored(r, None, 8, 8, 7, 21) # ── 2. HDR-like tone mapping ── lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) lf = l.astype(np.float32) base = cv2.bilateralFilter(lf, -1, 50, 50) detail = lf - base l_new = np.clip(base * 0.7 + 128 * 0.3 + detail * 1.4, 0, 255).astype(np.uint8) r = cv2.cvtColor(cv2.merge([l_new, a, b]), cv2.COLOR_LAB2BGR) # ── 3. CLAHE contrast ── lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) l = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8,8)).apply(l) r = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR) # ── 4. Gamma correction ── gray = cv2.cvtColor(r, cv2.COLOR_BGR2GRAY) mean_b = gray.mean() if mean_b < 115: gamma = 1.0 + (115 - mean_b) / 115 * 0.4 elif mean_b > 180: gamma = 1.0 - (mean_b - 180) / 180 * 0.2 else: gamma = 1.0 if gamma != 1.0: table = np.array([((i/255.0)**(1.0/gamma))*255 for i in range(256)]).astype(np.uint8) r = cv2.LUT(r, table) # ── 5. White balance (percentile) ── f = r.astype(np.float32) for c in range(3): lo, hi = np.percentile(f[:,:,c], 1), np.percentile(f[:,:,c], 99) if hi > lo: f[:,:,c] = np.clip((f[:,:,c]-lo)/(hi-lo)*255, 0, 255) r = f.astype(np.uint8) # ── 6. Skin smoothing (light) ── sk = get_skin_mask(r) smoothed = cv2.bilateralFilter(r, 7, 25, 25) alpha = np.expand_dims(sk * 0.2, 2) r = np.clip(r.astype(np.float32)*(1-alpha) + smoothed.astype(np.float32)*alpha, 0, 255).astype(np.uint8) # Texture restore detail = r.astype(np.float32) - cv2.GaussianBlur(r, (0,0), 1.5).astype(np.float32) r = np.clip(r.astype(np.float32) + detail * 0.5 * np.expand_dims(sk, 2), 0, 255).astype(np.uint8) # ── 7. Face-specific sharpening ── faces = detect_faces(r) if faces: for x1,y1,x2,y2 in faces: face = r[y1:y2, x1:x2].copy() if face.size == 0: continue # Strong unsharp on face g = cv2.GaussianBlur(face, (0,0), 2.0) sharpened = cv2.addWeighted(face, 1.7, g, -0.7, 0) # Detail kernel kernel = np.array([[0,-0.5,0],[-0.5,3.0,-0.5],[0,-0.5,0]]) sharpened = cv2.filter2D(sharpened, -1, kernel) # Blend back fh, fw = sharpened.shape[:2] mask = np.ones((fh,fw), dtype=np.float32) border = int(min(fh,fw)*0.15) for i in range(border): al = i/border mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al mask = cv2.GaussianBlur(mask, (11,11), 0) m3 = np.expand_dims(mask, 2) region = r[y1:y2, x1:x2].astype(np.float32) r[y1:y2, x1:x2] = np.clip(region*(1-m3) + sharpened.astype(np.float32)*m3, 0, 255).astype(np.uint8) else: # No faces — sharpen entire image g = cv2.GaussianBlur(r, (0,0), 2.0) r = cv2.addWeighted(r, 1.5, g, -0.5, 0) kernel = np.array([[0,-0.4,0],[-0.4,2.6,-0.4],[0,-0.4,0]]) r = cv2.filter2D(r, -1, kernel) # ── 8. Skin tone fix (prevent blue) ── if faces: for x1,y1,x2,y2 in faces: face = r[y1:y2, x1:x2].copy() if face.size == 0: continue sk_face = get_skin_mask(face) sk_bool = sk_face > 0.5 if np.sum(sk_bool) < 100: continue avg_b = np.mean(face[:,:,0][sk_bool]) avg_r = np.mean(face[:,:,2][sk_bool]) if avg_b > avg_r * 0.85: correction = np.ones_like(face, dtype=np.float32) correction[:,:,0] = 0.92 correction[:,:,2] = 1.05 sk3 = np.expand_dims(sk_face, 2) corrected = face.astype(np.float32)*(1-sk3*0.5) + (face.astype(np.float32)*correction)*sk3*0.5 face_fixed = np.clip(corrected, 0, 255).astype(np.uint8) fh, fw = face_fixed.shape[:2] mask = np.ones((fh,fw), dtype=np.float32) border = int(min(fh,fw)*0.12) for i in range(border): al = i/border mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al mask = cv2.GaussianBlur(mask, (9,9), 0) m3 = np.expand_dims(mask, 2) region = r[y1:y2, x1:x2].astype(np.float32) r[y1:y2, x1:x2] = np.clip(region*(1-m3) + face_fixed.astype(np.float32)*m3, 0, 255).astype(np.uint8) # ── 9. Warm color grading ── lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB).astype(np.float32) lab[:,:,1] = np.clip(lab[:,:,1] + 0.5, 0, 255) lab[:,:,2] = np.clip(lab[:,:,2] + 0.3, 0, 255) r = cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2BGR) # ── 10. Saturation ── hsv = cv2.cvtColor(r, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.08, 0, 255) r = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) # ── 11. Vignette ── Y, X = np.ogrid[:h,:w] dist = np.sqrt(((X-w/2)/(w/2))**2 + ((Y-h/2)/(h/2))**2) vig = np.clip(np.expand_dims(1 - 0.04*(dist**2), 2), 0, 1) r = np.clip(r.astype(np.float32) * vig, 0, 255).astype(np.uint8) return r def upscale_smart(img_cv, min_size=1024): """Smart multi-step upscaling""" h, w = img_cv.shape[:2] if max(h, w) >= min_size: return img_cv scale = min_size / max(h, w) # Multi-step for better quality if scale > 2.5: # Step 1: 2x img_cv = cv2.resize(img_cv, (w*2, h*2), interpolation=cv2.INTER_LANCZOS4) remaining = scale / 2.0 h, w = img_cv.shape[:2] img_cv = cv2.resize(img_cv, (int(w*remaining), int(h*remaining)), interpolation=cv2.INTER_LANCZOS4) else: img_cv = cv2.resize(img_cv, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_LANCZOS4) # Unsharp mask g = cv2.GaussianBlur(img_cv, (0,0), 2.0) img_cv = cv2.addWeighted(img_cv, 1.5, g, -0.5, 0) img_cv = cv2.fastNlMeansDenoisingColored(img_cv, None, 3, 3, 7, 21) return img_cv def skin_smooth(img): """Light skin smoothing with texture preservation""" h, w = img.shape[:2] if h < 50 or w < 50: return img sk = get_skin_mask(img) smoothed = cv2.bilateralFilter(img, 7, 22, 22) alpha = np.expand_dims(sk * 0.2, 2) result = np.clip(img.astype(np.float32)*(1-alpha) + smoothed.astype(np.float32)*alpha, 0, 255).astype(np.uint8) detail = result.astype(np.float32) - cv2.GaussianBlur(result, (0,0), 1.5).astype(np.float32) result = np.clip(result.astype(np.float32) + detail * 0.5 * np.expand_dims(sk, 2), 0, 255).astype(np.uint8) return result def face_sharpen(img): """Sharpen face regions""" faces = detect_faces(img) if not faces: g = cv2.GaussianBlur(img, (0,0), 2.0) img = cv2.addWeighted(img, 1.5, g, -0.5, 0) kernel = np.array([[0,-0.4,0],[-0.4,2.6,-0.4],[0,-0.4,0]]) return cv2.filter2D(img, -1, kernel) for x1,y1,x2,y2 in faces: face = img[y1:y2, x1:x2].copy() if face.size == 0: continue g = cv2.GaussianBlur(face, (0,0), 2.0) sharpened = cv2.addWeighted(face, 1.6, g, -0.6, 0) kernel = np.array([[0,-0.5,0],[-0.5,3.0,-0.5],[0,-0.5,0]]) sharpened = cv2.filter2D(sharpened, -1, kernel) fh, fw = sharpened.shape[:2] mask = np.ones((fh,fw), dtype=np.float32) border = int(min(fh,fw)*0.15) for i in range(border): al = i/border mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al mask = cv2.GaussianBlur(mask, (11,11), 0) m3 = np.expand_dims(mask, 2) region = img[y1:y2, x1:x2].astype(np.float32) img[y1:y2, x1:x2] = np.clip(region*(1-m3) + sharpened.astype(np.float32)*m3, 0, 255).astype(np.uint8) return img def studio_grade(img): """Studio color grading""" r = img.copy() h, w = r.shape[:2] f = r.astype(np.float32) for c in range(3): lo, hi = np.percentile(f[:,:,c], 1), np.percentile(f[:,:,c], 99) if hi > lo: f[:,:,c] = np.clip((f[:,:,c]-lo)/(hi-lo)*255, 0, 255) r = f.astype(np.uint8) lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)).apply(l) r = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR) lab = cv2.cvtColor(r, cv2.COLOR_BGR2LAB).astype(np.float32) lab[:,:,1] = np.clip(lab[:,:,1] + 0.5, 0, 255) lab[:,:,2] = np.clip(lab[:,:,2] + 0.3, 0, 255) r = cv2.cvtColor(lab.astype(np.uint8), cv2.COLOR_LAB2BGR) hsv = cv2.cvtColor(r, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.06, 0, 255) r = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) Y, X = np.ogrid[:h,:w] dist = np.sqrt(((X-w/2)/(w/2))**2 + ((Y-h/2)/(h/2))**2) vig = np.clip(np.expand_dims(1 - 0.04*(dist**2), 2), 0, 1) r = np.clip(r.astype(np.float32) * vig, 0, 255).astype(np.uint8) return r def pil_enhance(pil_img): img = pil_img.copy() img = ImageEnhance.Contrast(img).enhance(1.08) img = ImageEnhance.Color(img).enhance(1.06) img = ImageEnhance.Brightness(img).enhance(1.03) img = ImageEnhance.Sharpness(img).enhance(1.15) img = img.filter(ImageFilter.DETAIL) img = img.filter(ImageFilter.UnsharpMask(radius=1.5, percent=40, threshold=3)) return img def fix_skin_tone(img): faces = detect_faces(img) if not faces: return img for x1,y1,x2,y2 in faces: face = img[y1:y2, x1:x2].copy() if face.size == 0: continue sk = get_skin_mask(face) sk_bool = sk > 0.5 if np.sum(sk_bool) < 100: continue avg_b = np.mean(face[:,:,0][sk_bool]) avg_r = np.mean(face[:,:,2][sk_bool]) if avg_b > avg_r * 0.85: correction = np.ones_like(face, dtype=np.float32) correction[:,:,0] = 0.92; correction[:,:,2] = 1.05 sk3 = np.expand_dims(sk, 2) corrected = face.astype(np.float32)*(1-sk3*0.5) + (face.astype(np.float32)*correction)*sk3*0.5 face_fixed = np.clip(corrected, 0, 255).astype(np.uint8) fh, fw = face_fixed.shape[:2] mask = np.ones((fh,fw), dtype=np.float32) border = int(min(fh,fw)*0.12) for i in range(border): al = i/border mask[i,:]*=al; mask[-(i+1),:]*=al; mask[:,i]*=al; mask[:,-(i+1)]*=al mask = cv2.GaussianBlur(mask, (9,9), 0) m3 = np.expand_dims(mask, 2) region = img[y1:y2, x1:x2].astype(np.float32) img[y1:y2, x1:x2] = np.clip(region*(1-m3) + face_fixed.astype(np.float32)*m3, 0, 255).astype(np.uint8) return img # ═══════════════════════════════════════════════════════════════ # MAIN PIPELINE # ═══════════════════════════════════════════════════════════════ # Dummy GPU function to satisfy ZeroGPU requirement (if hardware is ZeroGPU) # The actual enhance function runs on CPU - CodeFormer uses REMOTE GPU @spaces.GPU(duration=5) def _gpu_placeholder(): """Dummy function for ZeroGPU compatibility. Does nothing.""" return True # NOTE: The actual enhance function runs on CPU. # CodeFormer AI runs on the REMOTE Space's GPU (sczhou/CodeFormer). # Set Space hardware to "CPU basic" for unlimited free usage. def enhance(image_pil, progress=gr.Progress()): start = time.time() steps = [] if image_pil.mode != 'RGB': image_pil = image_pil.convert('RGB') oh, ow = image_pil.size[1], image_pil.size[0] try: # Try CodeFormer progress(0.05, desc="🔌 Connecting to AI...") if not STATE.get('connected'): connect() progress(0.1, desc="🤖 AI face restoration...") cf_result = None debug_info = f"connected={STATE.get('connected')}, has_client={STATE.get('client') is not None}, has_gradio={HAS_CLIENT}" if STATE.get('connected'): cf_result = call_codeformer(image_pil) if cf_result: debug_info += ", cf=SUCCESS" else: debug_info += ", cf=FAILED" else: debug_info += ", NOT_CONNECTED" if cf_result: steps.append("🤖 CodeFormer AI (fidelity=0.1, 4x)") img_cv = cv2.cvtColor(np.array(cf_result), cv2.COLOR_RGB2BGR) else: # ═══ FULL OPENCV PIPELINE ═══ steps.append("🔧 Advanced OpenCV pipeline (11 stages)") img_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR) progress(0.2, desc="🔧 Full enhancement...") img_cv = opencv_full_enhance(img_cv) steps.append(" ✓ Denoise + HDR + CLAHE + Gamma + WB") steps.append(" ✓ Skin smooth + Face sharpen + Tone fix") steps.append(" ✓ Color grade + Saturation + Vignette") # Upscale if needed progress(0.5, desc="⬆️ Resolution...") img_cv = upscale_smart(img_cv, 1024) rh, rw = img_cv.shape[:2] steps.append(f"⬆️ {rw}×{rh}") # Skin smooth (if CodeFormer was used) if cf_result: progress(0.6, desc="✨ Skin...") img_cv = skin_smooth(img_cv) steps.append("✨ Skin smoothing") progress(0.65, desc="🔍 Sharpen...") img_cv = face_sharpen(img_cv) steps.append("🔍 Face sharpen") progress(0.7, desc="🎨 Color...") img_cv = studio_grade(img_cv) steps.append("🎨 Studio color grading") progress(0.75, desc="⚖️ Tone...") img_cv = fix_skin_tone(img_cv) steps.append("⚖️ Skin tone fix") # PIL polish progress(0.85, desc="🖼️ Final polish...") result_pil = Image.fromarray(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)) result_pil = pil_enhance(result_pil) steps.append("🖼️ PIL polish") # Save PNG progress(0.95, desc="💾 Saving...") tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False) result_pil.save(tmp.name, format='PNG') final = Image.open(tmp.name) except Exception as e: logger.error(f"Error: {e}") steps.append(f"⚠️ Error: {str(e)[:60]}") final = image_pil.copy() elapsed = (time.time()-start)*1000 rw, rh = final.size progress(1.0, desc=f"✅ {elapsed:.0f}ms") lines = [f"## ✨ Enhanced in {elapsed:.0f}ms!\n", f"| Before | After |\n|---|---|\n| {ow}×{oh} | **{rw}×{rh}** |\n", f"*Debug: {debug_info}*", "### Pipeline:"] for s in steps: lines.append(f"- {s}") if not cf_result: lines.append("\n> 💡 **Tip:** Add `HF_TOKEN` in Space Settings → Secrets for AI-powered face restoration (even better results)") return final, "\n".join(lines) # ═══════════════════════════════════════════════════════════════ # UI # ═══════════════════════════════════════════════════════════════ _T = gr.themes.Soft(primary_hue="purple", secondary_hue="pink") _CSS = ".hdr{text-align:center;margin-bottom:12px}.hdr h1{background:linear-gradient(135deg,#7c5cfc,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:2.2em;font-weight:800}.hdr p{color:#888}footer{display:none!important}.gradio-container{max-width:900px!important;margin:0 auto!important}" def build_app(): with gr.Blocks(title="✨ AI Photo Studio", theme=_T, css=_CSS) as app: gr.HTML('
Upload any photo → Get enhanced result → Download PNG