waybackRu2000v2 / app.py
tatyafanas's picture
Create app.py
01b79ea verified
# 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()