Spaces:
Sleeping
Sleeping
| # Complete Enhanced Russian/Eastern European 2000s Photo Filter | |
| import gradio as gr | |
| from PIL import Image, ImageOps, ImageFilter, ImageDraw, ImageFont, ImageEnhance | |
| import numpy as np | |
| import cv2 | |
| import io | |
| import random | |
| import math | |
| # ---------------------- | |
| # Utilities | |
| # ---------------------- | |
| def to_np(img: Image.Image): | |
| return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) | |
| def to_pil(arr: np.ndarray): | |
| return Image.fromarray(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB)) | |
| def clamp_u8(x): | |
| return np.clip(x, 0, 255).astype(np.uint8) | |
| def smoothstep(x, edge0, edge1): | |
| t = np.clip((x - edge0) / (edge1 - edge0 + 1e-6), 0, 1) | |
| return t * t * (3 - 2 * t) | |
| # ---------------------- | |
| # Core look functions | |
| # ---------------------- | |
| def crop_4_3(img: Image.Image): | |
| """Slightly biased crop (rule-of-thirds feel) but guaranteed 4:3.""" | |
| w, h = img.size | |
| target_ratio = 4/3 | |
| cur_ratio = w/h | |
| if cur_ratio > target_ratio: | |
| new_w = int(h * target_ratio) | |
| left = max(0, int((w - new_w) * 0.4)) | |
| return img.crop((left, 0, left + new_w, h)) | |
| else: | |
| new_h = int(w / target_ratio) | |
| top = max(0, int((h - new_h) * 0.3)) | |
| return img.crop((0, top, w, top + new_h)) | |
| def apply_lens_distortion(bgr, strength=0.01): | |
| if strength <= 0: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| y, x = np.ogrid[:h, :w] | |
| cx, cy = w/2, h/2 | |
| x_norm = (x - cx) / cx | |
| y_norm = (y - cy) / cy | |
| r = np.sqrt(x_norm**2 + y_norm**2) | |
| # slight pincushion | |
| distortion = 1 + strength * r**2 | |
| map_x = (x_norm * distortion * cx + cx).astype(np.float32) | |
| map_y = (y_norm * distortion * cy + cy).astype(np.float32) | |
| return cv2.remap(bgr, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101) | |
| def enhanced_vignette(bgr, strength=0.15, feather=1.8): | |
| if strength <= 0: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| y, x = np.ogrid[:h, :w] | |
| cx, cy = w/2, h/2 | |
| x_norm = (x - cx) / (w/2) | |
| y_norm = (y - cy) / (h/2) | |
| dist = np.sqrt(x_norm**2 + y_norm**2) | |
| mask = 1 - strength * (dist ** feather) | |
| mask = np.clip(mask, 0.6, 1.0).astype(np.float32) | |
| out = bgr.astype(np.float32).copy() | |
| out *= mask[..., None] | |
| return clamp_u8(out) | |
| def realistic_film_grain(bgr, grain_strength=8, grain_size=1.1): | |
| if grain_strength < 2: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| fine = np.random.normal(0, grain_strength * 0.5, (h, w)).astype(np.float32) | |
| if grain_size > 1.0: | |
| ch, cw = max(1, int(h/grain_size)), max(1, int(w/grain_size)) | |
| coarse = np.random.normal(0, grain_strength * 0.2, (ch, cw)).astype(np.float32) | |
| coarse = cv2.resize(coarse, (w, h), interpolation=cv2.INTER_LINEAR) | |
| fine += coarse | |
| yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV).astype(np.float32) | |
| yuv[:, :, 0] += fine * 0.6 | |
| yuv[:, :, 1] += fine * 0.2 | |
| yuv[:, :, 2] += fine * 0.2 | |
| out = cv2.cvtColor(clamp_u8(yuv), cv2.COLOR_YUV2BGR) | |
| return out | |
| def enhanced_chroma_noise(bgr, amount=4.0): | |
| if amount <= 0: | |
| return bgr | |
| ycrcb = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb).astype(np.float32) | |
| y, cr, cb = cv2.split(ycrcb) | |
| h, w = cr.shape | |
| cr_n = np.random.normal(0, amount * 0.5, (h, w)).astype(np.float32) | |
| cb_n = np.random.normal(0, amount * 0.5, (h, w)).astype(np.float32) | |
| cb_n = cb_n * 0.7 + cr_n * 0.3 # slight correlation | |
| cr = np.clip(cr + cr_n, 0, 255) | |
| cb = np.clip(cb + cb_n, 0, 255) | |
| return cv2.cvtColor(np.stack([y, cr, cb], axis=-1).astype(np.uint8), cv2.COLOR_YCrCb2BGR) | |
| def authentic_2000s_tone_curve(bgr, amount=1.0): | |
| """Apply a mild early-2000s S-curve, blended by amount (0..1).""" | |
| if amount <= 0: | |
| return bgr | |
| x = np.linspace(0, 1, 256) | |
| tone = np.where( | |
| x < 0.5, | |
| 0.18 + 0.60 * (2 * x) ** 0.9, | |
| 0.82 - 0.15 * (2 * (1 - x)) ** 1.1 | |
| ) | |
| lut = (np.clip(tone, 0, 1) * 255).astype(np.uint8) | |
| curved = np.empty_like(bgr) | |
| for c in range(3): | |
| curved[:, :, c] = cv2.LUT(bgr[:, :, c], lut) | |
| return (bgr.astype(np.float32) * (1 - amount) + curved.astype(np.float32) * amount).astype(np.uint8) | |
| def early_digital_wb(bgr, preset="auto"): | |
| presets = { | |
| "auto": {"temp_shift": 8, "tint_shift": 4, "saturation": 0.88}, | |
| "daylight": {"temp_shift": 0, "tint_shift": 2, "saturation": 0.95}, | |
| "cloudy": {"temp_shift": -6, "tint_shift": 1, "saturation": 0.92}, | |
| "tungsten": {"temp_shift": 25,"tint_shift": 8, "saturation": 0.85}, | |
| "fluorescent": {"temp_shift": 15,"tint_shift": -5, "saturation": 0.90}, | |
| } | |
| s = presets.get(preset, presets["auto"]) | |
| b, g, r = cv2.split(bgr.astype(np.int16)) | |
| if s["temp_shift"] > 0: # cooler | |
| b = np.clip(b + s["temp_shift"], 0, 255) | |
| r = np.clip(r - s["temp_shift"] // 2, 0, 255) | |
| else: # warmer | |
| r = np.clip(r - s["temp_shift"], 0, 255) | |
| b = np.clip(b + s["temp_shift"] // 2, 0, 255) | |
| g = np.clip(g + s["tint_shift"], 0, 255) | |
| result = cv2.merge([b.astype(np.uint8), g.astype(np.uint8), r.astype(np.uint8)]) | |
| hsv = cv2.cvtColor(result, cv2.COLOR_BGR2HSV).astype(np.float32) | |
| hsv[:, :, 1] *= s["saturation"] | |
| hsv[:, :, 1] = np.clip(hsv[:, :, 1], 0, 255) | |
| return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) | |
| def ccd_blooming_effect(bgr, threshold=240, bloom_size=2): | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) | |
| mask = (gray > threshold).astype(np.uint8) | |
| if not np.any(mask): | |
| return bgr | |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (bloom_size, bloom_size)) | |
| bloomed = cv2.dilate(mask, kernel, iterations=1) | |
| out = bgr.astype(np.float32) | |
| bloom_factor = 1.08 | |
| for i in range(3): | |
| out[:, :, i] = np.where(bloomed > 0, np.minimum(out[:, :, i] * bloom_factor, 255), out[:, :, i]) | |
| return out.astype(np.uint8) | |
| def enhanced_center_sharpness(pil_img: Image.Image, strength=0.3): | |
| arr = np.array(pil_img) | |
| h, w = arr.shape[:2] | |
| kernel = np.array([[-0.1, -0.1, -0.1], | |
| [-0.1, 2.2, -0.1], | |
| [-0.1, -0.1, -0.1]]) | |
| sharp = cv2.filter2D(arr, -1, kernel) | |
| y, x = np.ogrid[:h, :w] | |
| cx, cy = w/2, h/2 | |
| dist = np.sqrt((x - cx)**2 + (y - cy)**2) | |
| mask = 1 - (dist / np.sqrt(cx**2 + cy**2)) | |
| mask = np.clip(mask, 0, 1) ** 2 | |
| res = arr.astype(np.float32) * (1 - mask[..., None] * strength) + sharp.astype(np.float32) * (mask[..., None] * strength) | |
| return Image.fromarray(np.clip(res, 0, 255).astype(np.uint8)) | |
| def authentic_jpeg_compression(pil_img: Image.Image, quality=55, add_artifacts=False): | |
| def compress_once(im, q): | |
| buf = io.BytesIO() | |
| im.save(buf, format='JPEG', quality=q, subsampling=2, optimize=False) | |
| buf.seek(0) | |
| return Image.open(buf).convert("RGB") | |
| out = compress_once(pil_img, int(quality)) | |
| if add_artifacts: | |
| out = compress_once(out, int(min(95, quality + 10))) | |
| return out | |
| # ---------------------- | |
| # NEW: Russian Film Stocks | |
| # ---------------------- | |
| def authentic_russian_film_stocks(bgr, stock="svema", strength=0.5): | |
| """Simulate popular Russian/Soviet film stocks""" | |
| if strength <= 0: | |
| return bgr | |
| stocks = { | |
| "svema": { | |
| # SVEMA color negative - greenish shadows, warm highlights | |
| "shadow_tint": (0, 8, -3), # slight green in shadows | |
| "highlight_tint": (5, -2, 8), # warm highlights | |
| "saturation": 0.92, | |
| "contrast": 1.08 | |
| }, | |
| "orwo": { | |
| # East German ORWO - cooler, more contrasty | |
| "shadow_tint": (-2, 3, 6), | |
| "highlight_tint": (2, 0, -4), | |
| "saturation": 0.95, | |
| "contrast": 1.12 | |
| }, | |
| "tasma": { | |
| # Soviet TASMA - muted, slightly magenta | |
| "shadow_tint": (2, -1, 4), | |
| "highlight_tint": (3, 2, -1), | |
| "saturation": 0.88, | |
| "contrast": 1.05 | |
| } | |
| } | |
| if stock not in stocks: | |
| stock = "svema" | |
| s = stocks[stock] | |
| result = bgr.astype(np.float32) | |
| # Create luminance mask for shadows/highlights | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0 | |
| shadow_mask = np.maximum(0, 1 - gray * 2) # stronger in dark areas | |
| highlight_mask = np.maximum(0, (gray - 0.5) * 2) # stronger in bright areas | |
| # Apply color tints | |
| for i, (shadow_shift, highlight_shift) in enumerate(zip(s["shadow_tint"], s["highlight_tint"])): | |
| result[:,:,i] += shadow_mask * shadow_shift * strength | |
| result[:,:,i] += highlight_mask * highlight_shift * strength | |
| result = np.clip(result, 0, 255).astype(np.uint8) | |
| # Adjust saturation and contrast | |
| hsv = cv2.cvtColor(result, cv2.COLOR_BGR2HSV).astype(np.float32) | |
| hsv[:,:,1] *= (s["saturation"] ** strength) | |
| hsv[:,:,2] *= (s["contrast"] ** (strength * 0.5)) | |
| hsv = np.clip(hsv, 0, 255) | |
| return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) | |
| # ---------------------- | |
| # NEW: Authentic Lighting | |
| # ---------------------- | |
| def add_tungsten_indoor_warmth(bgr, strength=0.3): | |
| """Simulate warm tungsten bulbs common in Russian homes""" | |
| if strength <= 0: | |
| return bgr | |
| # Create depth-based mask (assume farther = darker = more tungsten influence) | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0 | |
| depth_proxy = 1 - gray # darker areas = "deeper" | |
| result = bgr.astype(np.float32) | |
| # Add warm cast to darker areas (tungsten falloff) | |
| warm_mask = depth_proxy * strength | |
| result[:,:,2] += warm_mask * 25 # more red | |
| result[:,:,1] += warm_mask * 12 # some green | |
| result[:,:,0] -= warm_mask * 8 # less blue | |
| return np.clip(result, 0, 255).astype(np.uint8) | |
| def add_fluorescent_flicker(bgr, strength=0.2): | |
| """Simulate old fluorescent tube flicker""" | |
| if strength <= 0: | |
| return bgr | |
| # Random brightness variation (simulates 50Hz flicker) | |
| flicker = 1 + np.random.normal(0, strength * 0.05) | |
| flicker = np.clip(flicker, 0.85, 1.15) | |
| result = bgr.astype(np.float32) * flicker | |
| # Slight green tint variation | |
| green_var = np.random.normal(1, strength * 0.03) | |
| result[:,:,1] *= green_var | |
| return np.clip(result, 0, 255).astype(np.uint8) | |
| def add_party_atmosphere(bgr, strength=0.3): | |
| """Enhance for typical Russian gathering photos""" | |
| if strength <= 0: | |
| return bgr | |
| # Slight overexposure (flash + alcohol = shaky hands) | |
| result = bgr.astype(np.float32) | |
| result *= (1 + strength * 0.15) | |
| # Warm skin tone enhancement | |
| # Simple skin detection based on color range | |
| hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) | |
| # Skin color range (approximate) | |
| lower_skin = np.array([0, 25, 50]) | |
| upper_skin = np.array([25, 255, 255]) | |
| skin_mask = cv2.inRange(hsv, lower_skin, upper_skin).astype(np.float32) / 255.0 | |
| # Warm up skin tones | |
| result[:,:,2] += skin_mask * strength * 15 # more red | |
| result[:,:,1] += skin_mask * strength * 8 # slight green | |
| return np.clip(result, 0, 255).astype(np.uint8) | |
| # ---------------------- | |
| # NEW: Scene Presets | |
| # ---------------------- | |
| def apply_scene_preset(bgr, scene="none", intensity=1.0): | |
| """Apply scene-specific authentic looks""" | |
| if scene == "none": | |
| return bgr | |
| result = bgr.copy() | |
| if scene == "kitchen_party": | |
| result = authentic_russian_film_stocks(result, "svema", intensity * 0.6) | |
| result = add_tungsten_indoor_warmth(result, intensity * 0.4) | |
| result = add_party_atmosphere(result, intensity * 0.5) | |
| elif scene == "winter_street": | |
| result = authentic_russian_film_stocks(result, "orwo", intensity * 0.7) | |
| # Add slight blue cast for winter | |
| result = result.astype(np.float32) | |
| result[:,:,0] += intensity * 8 # more blue | |
| result = np.clip(result, 0, 255).astype(np.uint8) | |
| elif scene == "apartment_interior": | |
| result = authentic_russian_film_stocks(result, "tasma", intensity * 0.5) | |
| result = add_tungsten_indoor_warmth(result, intensity * 0.3) | |
| result = add_fluorescent_flicker(result, intensity * 0.2) | |
| elif scene == "dacha_summer": | |
| result = authentic_russian_film_stocks(result, "svema", intensity * 0.4) | |
| # Enhance greens for summer | |
| hsv = cv2.cvtColor(result, cv2.COLOR_BGR2HSV).astype(np.float32) | |
| # Boost saturation slightly in green ranges | |
| green_mask = ((hsv[:,:,0] > 40) & (hsv[:,:,0] < 80)).astype(np.float32) | |
| hsv[:,:,1] += green_mask * intensity * 15 | |
| hsv = np.clip(hsv, 0, 255) | |
| result = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) | |
| return result | |
| # ---------------------- | |
| # Video/TV effects (from original) | |
| # ---------------------- | |
| def radial_chromatic_aberration(bgr, pixels=1.0): | |
| """Shift R outward and B inward radially (cheap lens look).""" | |
| if pixels <= 0: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| y, x = np.indices((h, w), dtype=np.float32) | |
| cx, cy = np.float32(w / 2.0), np.float32(h / 2.0) | |
| dx = x - cx | |
| dy = y - cy | |
| r = np.sqrt(dx * dx + dy * dy) + 1e-6 | |
| r_norm = r / np.sqrt(cx * cx + cy * cy) | |
| shift = (np.float32(pixels) * r_norm) | |
| ux = dx / r | |
| uy = dy / r | |
| map_x_out = np.ascontiguousarray((x + ux * shift).astype(np.float32)) | |
| map_y_out = np.ascontiguousarray((y + uy * shift).astype(np.float32)) | |
| map_x_in = np.ascontiguousarray((x - ux * shift).astype(np.float32)) | |
| map_y_in = np.ascontiguousarray((y - uy * shift).astype(np.float32)) | |
| b, g, rch = cv2.split(bgr) | |
| rch = cv2.remap(rch, map_x_out, map_y_out, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101) | |
| b = cv2.remap(b, map_x_in, map_y_in, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101) | |
| return cv2.merge([b, g, rch]) | |
| def composite_chroma_bleed(bgr, amount=0.3, offset_px=1): | |
| """SECAM/Composite-ish horizontal chroma blur + phase offset.""" | |
| if amount <= 0: | |
| return bgr | |
| ycrcb = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb).astype(np.float32) | |
| y, cr, cb = cv2.split(ycrcb) | |
| k = max(1, int(3 + amount * 12)) | |
| cr_b = cv2.blur(cr, (k, 1)) | |
| cb_b = cv2.blur(cb, (k, 1)) | |
| if offset_px != 0: | |
| M = np.float32([[1, 0, offset_px], [0, 1, 0]]) | |
| cr_b = cv2.warpAffine(cr_b, M, (cr.shape[1], cr.shape[0]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE) | |
| cb_b = cv2.warpAffine(cb_b, M, (cb.shape[1], cb.shape[0]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE) | |
| out = cv2.cvtColor(np.stack([y, cr_b, cb_b], axis=-1).astype(np.uint8), cv2.COLOR_YCrCb2BGR) | |
| return out | |
| def add_interlace_combing(bgr, amount=0.3, horiz_px=2): | |
| """Offset every other scanline horizontally and lower its contrast.""" | |
| if amount <= 0: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| out = bgr.copy() | |
| delta = int(max(1, horiz_px * amount * 5)) | |
| out[::2] = np.roll(out[::2], shift=delta, axis=1) | |
| lines = np.ones((h, 1, 1), np.float32) | |
| lines[::2] *= (1.0 - 0.15 * amount) | |
| out = clamp_u8(out.astype(np.float32) * lines) | |
| return out | |
| def add_tv_scanlines(bgr, strength=0.02): | |
| """CRT-like scanlines as a slider now (not a checkbox).""" | |
| if strength <= 0: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| lines = np.ones((h, 1, 1), np.float32) | |
| darken = np.clip(strength, 0.0, 0.35) | |
| lines[::2] *= (1.0 - darken) | |
| out = clamp_u8(bgr.astype(np.float32) * lines) | |
| return out | |
| def add_low_bitrate_artifacts(bgr, strength=0.3, block_size=16, ringing=0.3): | |
| """Coarse 'MPEG-like' macroblocking + ringing + recompression.""" | |
| if strength <= 0: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| factor = max(1, int(block_size * (0.8 + 1.7 * strength))) | |
| small_w = max(1, w // factor) | |
| small_h = max(1, h // factor) | |
| small = cv2.resize(bgr, (small_w, small_h), interpolation=cv2.INTER_LINEAR) | |
| up = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST) | |
| if ringing > 0: | |
| blur = cv2.GaussianBlur(up, (0, 0), 0.8 + 1.6 * ringing) | |
| up = cv2.addWeighted(up, 1 + 0.9 * ringing, blur, -0.9 * ringing, 0) | |
| pil = to_pil(up) | |
| q = int(np.clip(48 - 28 * strength, 8, 60)) | |
| pil = authentic_jpeg_compression(pil, quality=q, add_artifacts=True) | |
| return to_np(pil) | |
| def add_print_border(pil_img: Image.Image, enable=False, width_rel=0.04, color=(245, 245, 245)): | |
| """Optional 10x15 minilab-ish border.""" | |
| if not enable or width_rel <= 0: | |
| return pil_img | |
| w, h = pil_img.size | |
| border = int(min(w, h) * width_rel) | |
| canvas = Image.new("RGB", (w + border * 2, h + int(border * 2.2)), color) | |
| canvas.paste(pil_img, (border, border)) | |
| return canvas | |
| def lab_color_cast(bgr, preset="none", amount=0.3): | |
| if amount <= 0 or preset == "none": | |
| return bgr | |
| # luminance proxy | |
| y = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb)[:, :, 0].astype(np.float32) / 255.0 | |
| r, g, b = bgr[:, :, 2].astype(np.float32), bgr[:, :, 1].astype(np.float32), bgr[:, :, 0].astype(np.float32) | |
| if preset == "fuji_warm_magenta_shadows": | |
| t_high = smoothstep(y, 0.55, 0.95) | |
| t_shad = 1.0 - smoothstep(y, 0.15, 0.45) | |
| r += amount * (22.0 * t_high + 12.0 * t_shad) | |
| g += amount * (14.0 * t_high - 8.0 * t_shad) | |
| b += amount * (0.0 * t_high + 10.0 * t_shad) | |
| elif preset == "kodak_cool_mids": | |
| t_mid = np.exp(-((y - 0.55) ** 2) / (2 * 0.12 ** 2)) | |
| r -= amount * (12.0 * t_mid) | |
| g += amount * (6.0 * t_mid) | |
| b += amount * (16.0 * t_mid) | |
| elif preset == "minilab_greenish": | |
| t_all = smoothstep(y, 0.2, 0.9) | |
| g += amount * (18.0 * t_all) | |
| r -= amount * (6.0 * (1 - t_all)) | |
| hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV).astype(np.float32) | |
| hsv[:, :, 1] *= (1 - 0.06 * amount) | |
| bgr = cv2.cvtColor(clamp_u8(hsv), cv2.COLOR_HSV2BGR) | |
| r, g, b = bgr[:, :, 2].astype(np.float32), bgr[:, :, 1].astype(np.float32), bgr[:, :, 0].astype(np.float32) | |
| out = np.stack([clamp_u8(b), clamp_u8(g), clamp_u8(r)], axis=-1) | |
| return out | |
| def add_scan_dust_hairs(pil_img: Image.Image, density=0.25, strength=0.6, hair_prob=0.25, size_factor=1.0): | |
| if density <= 0 or strength <= 0: | |
| return pil_img | |
| w, h = pil_img.size | |
| area = w * h | |
| n = int(max(1, (area / 55000.0) * float(density))) | |
| dark = Image.new("L", (w, h), 0) | |
| bright = Image.new("L", (w, h), 0) | |
| ddraw = ImageDraw.Draw(dark) | |
| bdraw = ImageDraw.Draw(bright) | |
| for _ in range(n): | |
| if random.random() < hair_prob: | |
| x0 = random.randint(0, w - 1) | |
| y0 = random.randint(0, h - 1) | |
| length = int(random.uniform(30, 120) * size_factor) | |
| angle = random.uniform(0, math.pi) | |
| x1 = int(np.clip(x0 + length * math.cos(angle), 0, w - 1)) | |
| y1 = int(np.clip(y0 + length * math.sin(angle), 0, h - 1)) | |
| width = random.choice([1, 1, 2]) | |
| if random.random() < 0.6: | |
| ddraw.line((x0, y0, x1, y1), fill=random.randint(160, 255), width=width) | |
| else: | |
| bdraw.line((x0, y0, x1, y1), fill=random.randint(140, 220), width=width) | |
| else: | |
| cx = random.randint(0, w - 1) | |
| cy = random.randint(0, h - 1) | |
| r = int(random.uniform(1, 3.5) * size_factor) | |
| bbox = (cx - r, cy - r, cx + r, cy + r) | |
| if random.random() < 0.5: | |
| ddraw.ellipse(bbox, fill=random.randint(160, 255)) | |
| else: | |
| bdraw.ellipse(bbox, fill=random.randint(140, 220)) | |
| dark = dark.filter(ImageFilter.GaussianBlur(radius=0.8 + 1.2 * strength)) | |
| bright = bright.filter(ImageFilter.GaussianBlur(radius=0.8 + 1.2 * strength)) | |
| base = np.array(pil_img).astype(np.float32) | |
| d = np.array(dark).astype(np.float32) / 255.0 | |
| b = np.array(bright).astype(np.float32) / 255.0 | |
| amt = 28.0 * float(strength) | |
| base -= d[..., None] * amt | |
| base += b[..., None] * (amt * 0.9) | |
| base = np.clip(base, 0, 255).astype(np.uint8) | |
| return Image.fromarray(base) | |
| def apply_chaos(bgr, amount=0.2): | |
| if amount <= 0: | |
| return bgr | |
| h, w = bgr.shape[:2] | |
| out = bgr.copy() | |
| max_shift = 2.0 * amount | |
| tx = np.random.uniform(-max_shift, max_shift) | |
| ty = np.random.uniform(-max_shift, max_shift) | |
| M = np.float32([[1, 0, tx], [0, 1, ty]]) | |
| out = cv2.warpAffine(out, M, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101) | |
| amp = 2.0 * amount | |
| freq = np.random.uniform(1.0, 3.0) | |
| phase = np.random.uniform(0, 2*np.pi) | |
| shifts = (amp * np.sin(phase + (np.arange(h) / max(h,1)) * 2*np.pi*freq)).astype(np.int32) | |
| for y in range(h): | |
| if shifts[y] != 0: | |
| out[y] = np.roll(out[y], shifts[y], axis=0) | |
| n_hot = int(amount * w * h * 0.00005) | |
| for _ in range(n_hot): | |
| y = random.randint(0, h - 1) | |
| x = random.randint(0, w - 1) | |
| color = random.choice([(255, 255, 255), (255, 240, 220), (255, 255, 200)]) | |
| out[y, x] = color | |
| if n_hot > 0: | |
| out = cv2.GaussianBlur(out, (0, 0), 0.25 + 0.6 * amount) | |
| return out | |
| # ---------------------- | |
| # Timestamp helpers | |
| # ---------------------- | |
| def add_russian_timestamp_styles(pil_img: Image.Image, date_text: str, style="russian"): | |
| months = ["ЯНВ","ФЕВ","МАР","АПР","МАЙ","ИЮН","ИЮЛ","АВГ","СЕН","ОКТ","НОЯ","ДЕК"] | |
| try: | |
| d, m, y = date_text.split(".") | |
| m_i = int(m) | |
| rus = f"{int(d):02d} {months[m_i-1]} {int(y)}" | |
| except Exception: | |
| rus = date_text | |
| draw = ImageDraw.Draw(pil_img) | |
| w, h = pil_img.size | |
| font_size = max(12, min(w, h) // 40) | |
| try: | |
| font = ImageFont.truetype("DejaVuSansMono.ttf", font_size) | |
| except: | |
| try: | |
| font = ImageFont.truetype("courier.ttf", font_size) | |
| except: | |
| font = ImageFont.load_default() | |
| x_pos, y_pos = w - 10, h - 10 | |
| for dx in (-1, 0, 1): | |
| for dy in (-1, 0, 1): | |
| if dx or dy: | |
| draw.text((x_pos + dx, y_pos + dy), rus, anchor="rd", fill=(0, 0, 0), font=font) | |
| draw.text((x_pos, y_pos), rus, anchor="rd", fill=(255, 200, 0), font=font) | |
| return pil_img | |
| def add_authentic_timestamp(pil_img: Image.Image, date_text: str, style="digital"): | |
| draw = ImageDraw.Draw(pil_img) | |
| w, h = pil_img.size | |
| font_size = max(12, min(w, h) // 40) | |
| try: | |
| font = ImageFont.truetype("DejaVuSansMono.ttf", font_size) | |
| except: | |
| try: | |
| font = ImageFont.truetype("courier.ttf", font_size) | |
| except: | |
| font = ImageFont.load_default() | |
| if style == "digital": | |
| x_pos, y_pos = w - 10, h - 10 | |
| for dx in (-1, 0, 1): | |
| for dy in (-1, 0, 1): | |
| if dx or dy: | |
| draw.text((x_pos + dx, y_pos + dy), date_text, anchor="rd", fill=(0, 0, 0), font=font) | |
| draw.text((x_pos, y_pos), date_text, anchor="rd", fill=(255, 200, 0), font=font) | |
| else: # "film_lab" | |
| try: | |
| small_font = ImageFont.truetype("DejaVuSansMono.ttf", max(8, font_size - 4)) | |
| except: | |
| small_font = font | |
| draw.text((10, h - 10), date_text, anchor="ld", fill=(255, 255, 255), font=small_font) | |
| return pil_img | |
| def add_motion_blur(pil_img: Image.Image, strength=0.8): | |
| """Directional motion blur to simulate handshake.""" | |
| if strength <= 0: | |
| return pil_img | |
| k = max(3, int(3 + strength * 6)) | |
| kernel = np.zeros((k, k), np.float32) | |
| kernel[k // 2, :] = 1.0 / k | |
| arr = np.array(pil_img) | |
| blurred = cv2.filter2D(arr, -1, kernel) | |
| return Image.fromarray(blurred) | |
| def add_cheap_flash_effect(bgr, strength=0.08): | |
| """Harsh on-camera flash: lift mids, cool/green cast, flatten shadows.""" | |
| if strength <= 0: | |
| return bgr | |
| out = bgr.astype(np.float32) | |
| out = out * (1.0 + strength * 0.3) | |
| out[:, :, 0] += 12 * strength | |
| out[:, :, 1] += 8 * strength | |
| out = np.clip(out, 0, 255).astype(np.uint8) | |
| lut = np.arange(256, dtype=np.float32) | |
| lut = np.clip(lut + (30 * strength) * (1 - (lut / 255.0)), 0, 255).astype(np.uint8) | |
| for c in range(3): | |
| out[:, :, c] = cv2.LUT(out[:, :, c], lut) | |
| return out | |
| # ---------------------- | |
| # Intensity mapping | |
| # ---------------------- | |
| def map_intensity(intensity_0_10: float): | |
| base = float(np.clip(intensity_0_10 / 3.0, 0.0, 1.0)) | |
| s = 1.0 - (1.0 - base) ** 3 | |
| extra = float(np.clip((intensity_0_10 - 3.0) / 7.0, 0.0, 1.0)) | |
| boost = 1.0 + 2.8 * (extra ** 1.2) | |
| return s, boost | |
| # ---------------------- | |
| # Main pipeline | |
| # ---------------------- | |
| def process_image( | |
| image, | |
| intensity, # 0..10 | |
| wb_preset, | |
| add_date, | |
| date_style, | |
| custom_date, | |
| grain_amount, | |
| compression_level, | |
| flash_effect, | |
| motion_blur_strength, | |
| # NEW: Scene and film controls | |
| scene_preset, | |
| film_stock, | |
| lighting_condition, | |
| # Video controls | |
| macroblock_strength, | |
| block_size, | |
| ringing_strength, | |
| interlace_amount, | |
| chroma_bleed_amount, | |
| scanlines_amount, | |
| # Optics/print | |
| chrom_ab_px, | |
| print_border_enable, | |
| print_border_width, | |
| # Lab & scan | |
| lab_preset, | |
| lab_amount, | |
| dust_enable, | |
| dust_density, | |
| dust_strength, | |
| hair_prob, | |
| speck_size, | |
| # Chaos | |
| chaos_amount, | |
| # Options | |
| keep_ratio, | |
| timestamp_layer, | |
| russian_style | |
| ): | |
| if image is None: | |
| return None | |
| # master scaling | |
| s, boost = map_intensity(float(intensity)) | |
| # pick working base respecting aspect ratio | |
| original = image.convert("RGB") | |
| pil = original.copy() if keep_ratio else crop_4_3(original) | |
| # --- optional: bake timestamp BEFORE effects --- | |
| if add_date and timestamp_layer == "baked": | |
| if not custom_date: | |
| year = random.choice([1998, 1999, 2000, 2001, 2002]) | |
| month = random.randint(1, 12) | |
| day = random.randint(1, 28) | |
| date_text = f"{day:02d}.{month:02d}.{year}" | |
| else: | |
| date_text = custom_date.strip() | |
| if russian_style and date_style == "digital": | |
| pil = add_russian_timestamp_styles(pil, date_text, style="russian") | |
| else: | |
| pil = add_authentic_timestamp(pil, date_text, style=date_style) | |
| # pre-effects | |
| mb = min(3.0, float(motion_blur_strength) * 0.25 * s * boost) | |
| if mb > 0.01: | |
| pil = add_motion_blur(pil, strength=mb) | |
| pil = enhanced_center_sharpness(pil, strength=min(0.45, 0.15 * s * boost)) | |
| bgr = to_np(pil) | |
| # WB | |
| bgr = early_digital_wb(bgr, wb_preset) | |
| # NEW: Apply scene preset | |
| bgr = apply_scene_preset(bgr, scene_preset, intensity=s) | |
| # NEW: Apply film stock (if not handled by scene preset) | |
| if scene_preset == "none" and film_stock != "none": | |
| bgr = authentic_russian_film_stocks(bgr, film_stock, strength=0.6 * s) | |
| # NEW: Apply lighting conditions | |
| if lighting_condition == "tungsten_warmth": | |
| bgr = add_tungsten_indoor_warmth(bgr, strength=0.4 * s) | |
| elif lighting_condition == "fluorescent_flicker": | |
| bgr = add_fluorescent_flicker(bgr, strength=0.3 * s) | |
| # lab cast | |
| bgr = lab_color_cast(bgr, preset=lab_preset, amount=float(lab_amount) * (0.6 + 0.6 * s)) | |
| # tone curve | |
| bgr = authentic_2000s_tone_curve(bgr, amount=min(1.0, 0.4 * s * (0.9 + 0.5 * (boost - 1)))) | |
| # cheap flash | |
| if flash_effect: | |
| bgr = add_cheap_flash_effect(bgr, strength=min(0.25, 0.05 * s * boost)) | |
| # blooming | |
| bgr = ccd_blooming_effect(bgr, threshold=242, bloom_size=2) | |
| # optics | |
| bgr = apply_lens_distortion(bgr, strength=min(0.03, 0.004 * s * boost)) | |
| bgr = radial_chromatic_aberration(bgr, pixels=min(3.0, float(chrom_ab_px) * (0.7 + 0.3 * s))) | |
| # vignette | |
| bgr = enhanced_vignette(bgr, strength=min(0.4, 0.06 * s * boost), feather=1.8) | |
| # grain & chroma noise | |
| g_strength = min(30.0, (float(grain_amount) * 0.35 + 1.5) * s * boost) | |
| bgr = realistic_film_grain(bgr, grain_strength=g_strength, grain_size=1.05) | |
| bgr = enhanced_chroma_noise(bgr, amount=min(12.0, 1.6 * s * boost)) | |
| # composite bleed, interlace, scanlines | |
| bgr = composite_chroma_bleed(bgr, amount=float(chroma_bleed_amount) * (0.4 + 0.8 * s), offset_px=1) | |
| bgr = add_interlace_combing(bgr, amount=float(interlace_amount), horiz_px=2) | |
| bgr = add_tv_scanlines(bgr, strength=float(scanlines_amount) * 0.25) | |
| # low bitrate macroblocking | |
| bgr = add_low_bitrate_artifacts( | |
| bgr, | |
| strength=float(macroblock_strength) * (0.5 + 0.8 * s), | |
| block_size=int(block_size), | |
| ringing=float(ringing_strength) | |
| ) | |
| # JPEG pre-pass | |
| pil_mid = to_pil(bgr) | |
| comp_norm = (float(compression_level) - 0.3) / (1.5 - 0.3) | |
| comp_norm = float(np.clip(comp_norm, 0, 1)) | |
| q = int(92 - (92 - 68) * comp_norm * min(1.5, s * (0.8 + 0.6 * (boost - 1)))) | |
| add_2pass = (compression_level > 1.0) or (s > 0.7) | |
| pil_mid = authentic_jpeg_compression(pil_mid, quality=int(np.clip(q, 30, 92)), add_artifacts=add_2pass) | |
| # final blend | |
| orig_aligned = original if keep_ratio else crop_4_3(original) | |
| mix = float(np.clip(0.08 + 0.67 * s * (0.9 + 0.6 * (boost - 1)), 0.08, 0.92)) | |
| processed = Image.blend(orig_aligned, pil_mid, alpha=mix) | |
| # CHAOS | |
| if chaos_amount > 0: | |
| bgr_chaos = to_np(processed) | |
| bgr_chaos = apply_chaos(bgr_chaos, amount=float(chaos_amount)) | |
| processed = to_pil(bgr_chaos) | |
| # timestamp ON TOP | |
| if add_date and timestamp_layer == "top": | |
| if not custom_date: | |
| year = random.choice([1998, 1999, 2000, 2001, 2002]) | |
| month = random.randint(1, 12) | |
| day = random.randint(1, 28) | |
| date_text = f"{day:02d}.{month:02d}.{year}" | |
| else: | |
| date_text = custom_date.strip() | |
| if russian_style and date_style == "digital": | |
| processed = add_russian_timestamp_styles(processed, date_text, style="russian") | |
| else: | |
| processed = add_authentic_timestamp(processed, date_text, style=date_style) | |
| # print border | |
| processed = add_print_border(processed, enable=bool(print_border_enable), width_rel=float(print_border_width)) | |
| # scan dust/hairs | |
| if dust_enable: | |
| processed = add_scan_dust_hairs( | |
| processed, | |
| density=float(dust_density), | |
| strength=float(dust_strength), | |
| hair_prob=float(hair_prob), | |
| size_factor=float(speck_size) | |
| ) | |
| return processed | |
| # ---------------------- | |
| # Enhanced UI | |
| # ---------------------- | |
| with gr.Blocks(title="Complete Russian/Eastern European 2000s Photo Filter", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(""" | |
| # 📷 Complete Authentic Russian/Eastern European 2000s Photo Filter | |
| Transform your photos with authentic Russian film stocks, lighting conditions, and cultural atmosphere from ~25 years ago. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| input_image = gr.Image(type="pil", label="📸 Upload Your Photo") | |
| with gr.Accordion("🎛️ Basic Settings", open=True): | |
| intensity = gr.Slider(0, 10, value=3, step=0.1, label="Overall Effect Intensity (0–10; 3 ≈ moderate)") | |
| wb_preset = gr.Dropdown( | |
| choices=["auto", "daylight", "cloudy", "tungsten", "fluorescent"], | |
| value="auto", | |
| label="White Balance Preset" | |
| ) | |
| grain_amount = gr.Slider(2, 15, value=6, step=1, label="Film Grain Amount") | |
| compression_level = gr.Slider(0.3, 1.5, value=0.8, step=0.1, label="JPEG Compression Level") | |
| keep_ratio = gr.Checkbox(value=True, label="Keep Original Aspect Ratio (no 4:3 crop)") | |
| with gr.Accordion("🇷🇺 Russian/Eastern European Features", open=True): | |
| scene_preset = gr.Dropdown( | |
| choices=["none", "kitchen_party", "winter_street", "apartment_interior", "dacha_summer"], | |
| value="none", | |
| label="Scene Preset (Authentic Russian Atmosphere)" | |
| ) | |
| film_stock = gr.Dropdown( | |
| choices=["none", "svema", "orwo", "tasma"], | |
| value="svema", | |
| label="Russian/Soviet Film Stock" | |
| ) | |
| lighting_condition = gr.Dropdown( | |
| choices=["none", "tungsten_warmth", "fluorescent_flicker"], | |
| value="none", | |
| label="Period Lighting Conditions" | |
| ) | |
| russian_style = gr.Checkbox(label="Russian Date Format (Cyrillic months)", value=False) | |
| flash_effect = gr.Checkbox(label="Cheap Camera Flash", value=True) | |
| motion_blur_strength = gr.Slider(0, 3, value=1, step=0.5, label="Motion Blur (low light shake)") | |
| with gr.Accordion("📼 Video / TV Artifacts", open=True): | |
| macroblock_strength = gr.Slider(0, 1, value=0.4, step=0.05, label="Macroblocking Strength") | |
| block_size = gr.Slider(1, 32, value=16, step=1, label="Block Size (px)") | |
| ringing_strength = gr.Slider(0, 1, value=0.35, step=0.05, label="Ringing / Edge Halos") | |
| interlace_amount = gr.Slider(0, 1, value=0.15, step=0.05, label="Interlace Combing") | |
| chroma_bleed_amount = gr.Slider(0, 1, value=0.2, step=0.05, label="Chroma Bleed (composite/SECAM)") | |
| scanlines_amount = gr.Slider(0, 1, value=0.15, step=0.05, label="CRT Scanlines") | |
| with gr.Accordion("🔧 Optics & Print", open=False): | |
| chrom_ab_px = gr.Slider(0, 2.0, value=0.6, step=0.1, label="Chromatic Aberration (px)") | |
| print_border_enable = gr.Checkbox(label="Add 10×15 Minilab Border", value=False) | |
| print_border_width = gr.Slider(0.02, 0.08, value=0.04, step=0.005, label="Border Width (relative)") | |
| with gr.Accordion("🧪 Lab & Scan Look", open=False): | |
| lab_preset = gr.Dropdown( | |
| choices=["none", "fuji_warm_magenta_shadows", "kodak_cool_mids", "minilab_greenish"], | |
| value="none", | |
| label="Lab Color Cast Preset" | |
| ) | |
| lab_amount = gr.Slider(0, 1, value=0.3, step=0.05, label="Lab Cast Amount") | |
| dust_enable = gr.Checkbox(label="Add Scan Dust & Hairs", value=False) | |
| dust_density = gr.Slider(0, 1, value=0.25, step=0.05, label="Dust/Hair Density") | |
| dust_strength = gr.Slider(0, 1, value=0.6, step=0.05, label="Dust/Hair Contrast") | |
| hair_prob = gr.Slider(0, 1, value=0.25, step=0.05, label="Hair Probability") | |
| speck_size = gr.Slider(0.8, 2.5, value=1.0, step=0.1, label="Speck Size Factor") | |
| with gr.Accordion("🎲 Chaos", open=False): | |
| chaos_amount = gr.Slider(0, 1, value=0.2, step=0.05, label="Micro Jitter, Wobble & Hot Pixels") | |
| with gr.Accordion("📅 Timestamp Options", open=True): | |
| add_date = gr.Checkbox(label="Add Date Timestamp", value=True) | |
| date_style = gr.Radio(choices=["digital", "film_lab"], value="digital", label="Timestamp Style") | |
| custom_date = gr.Textbox( | |
| label="Custom Date (dd.mm.yyyy)", | |
| placeholder="14.08.2000", | |
| info="Leave empty for random date from 1998–2002" | |
| ) | |
| timestamp_layer = gr.Radio( | |
| choices=["top", "baked"], | |
| value="top", | |
| label="Timestamp Layer", | |
| info="'top' = after effects; 'baked' = before effects" | |
| ) | |
| with gr.Column(scale=1): | |
| output_image = gr.Image(type="pil", label="✨ Processed Photo", interactive=False) | |
| with gr.Row(): | |
| process_btn = gr.Button("🎬 Apply Complete Russian 2000s Filter", variant="primary") | |
| process_btn.click( | |
| fn=process_image, | |
| inputs=[ | |
| input_image, intensity, wb_preset, add_date, date_style, custom_date, | |
| grain_amount, compression_level, flash_effect, motion_blur_strength, | |
| scene_preset, film_stock, lighting_condition, # NEW | |
| macroblock_strength, block_size, ringing_strength, interlace_amount, | |
| chroma_bleed_amount, scanlines_amount, | |
| chrom_ab_px, print_border_enable, print_border_width, | |
| lab_preset, lab_amount, dust_enable, dust_density, dust_strength, hair_prob, speck_size, | |
| chaos_amount, | |
| keep_ratio, timestamp_layer, russian_style | |
| ], | |
| outputs=[output_image] | |
| ) | |
| gr.Markdown(""" | |
| ### 🎯 NEW Russian Authenticity Features: | |
| - **Scene Presets**: Kitchen Party, Winter Street, Apartment Interior, Dacha Summer | |
| - **Film Stocks**: SVEMA (greenish shadows), ORWO (contrasty), TASMA (muted/magenta) | |
| - **Lighting**: Tungsten warmth, Fluorescent flicker simulation | |
| - **Cultural Atmosphere**: Party overexposure, skin tone warming | |
| ### 💡 Quick Preset Guide: | |
| - **Family gathering**: Kitchen Party + tungsten + flash + Russian date | |
| - **Winter scene**: Winter Street + ORWO + grain 8 + intensity 4-5 | |
| - **Home interior**: Apartment Interior + TASMA + fluorescent + motion blur | |
| - **Summer countryside**: Dacha Summer + SVEMA + daylight WB | |
| - **Video capture**: Any scene + macroblocks 0.6 + scanlines 0.3 + interlace 0.2 | |
| ### 📷 Film Stock Characteristics: | |
| - **SVEMA**: Most popular Soviet color film - greenish shadows, warm highlights | |
| - **ORWO**: East German - cooler tones, higher contrast, professional look | |
| - **TASMA**: Soviet black & white heritage - muted colors, slight magenta cast | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch() |