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