tatyafanas's picture
Update app.py
060f40f verified
#!/usr/bin/env python3
"""
Enhanced Russian 2000s Photo Filter with Aggressive VHS Video Still Effects
Incorporates dramatic VHS degradation, color corruption, and period-accurate effects
"""
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
# ----------------------
# Utility Functions
# ----------------------
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):
"""Smooth interpolation function"""
t = np.clip((x - edge0) / (edge1 - edge0 + 1e-6), 0, 1)
return t * t * (3 - 2 * t)
def crop_4_3(img: Image.Image):
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))
# ----------------------
# Aggressive VHS Effects
# ----------------------
def add_dramatic_vhs_color_cast(bgr, cast_type="random", strength=0.8):
"""Add dramatic VHS color casts like in the reference images"""
if strength <= 0:
return bgr
result = bgr.astype(np.float32)
h, w = result.shape[:2]
# Define different color cast types based on reference images
cast_types = {
"red_magenta": {
"red": 1.4 + strength * 0.8, # Heavy red boost
"green": 0.7 - strength * 0.2, # Reduce green
"blue": 0.6 - strength * 0.3, # Reduce blue significantly
"magenta_shift": strength * 60 # Add magenta to highlights
},
"cyan_blue": {
"red": 0.5 - strength * 0.3, # Reduce red significantly
"green": 0.8 - strength * 0.1, # Slight green reduction
"blue": 1.3 + strength * 0.7, # Heavy blue boost
"cyan_shift": strength * 50
},
"green_yellow": {
"red": 0.8 - strength * 0.2,
"green": 1.5 + strength * 0.8, # Heavy green boost
"blue": 0.6 - strength * 0.4,
"yellow_shift": strength * 40
},
"purple_violet": {
"red": 1.2 + strength * 0.6,
"green": 0.5 - strength * 0.3,
"blue": 1.3 + strength * 0.7, # Purple = red + blue
"purple_shift": strength * 55
}
}
if cast_type == "random":
cast_type = random.choice(list(cast_types.keys()))
cast = cast_types.get(cast_type, cast_types["red_magenta"])
# Apply color multipliers
result[:,:,2] *= cast["red"] # Red channel
result[:,:,1] *= cast["green"] # Green channel
result[:,:,0] *= cast["blue"] # Blue channel
# Add color shifts to different tonal ranges
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
if "magenta_shift" in cast:
# Add magenta to highlights (like image 2)
highlight_mask = smoothstep(gray, 0.4, 0.9)
result[:,:,2] += highlight_mask * cast["magenta_shift"] # Red
result[:,:,0] += highlight_mask * cast["magenta_shift"] # Blue
elif "cyan_shift" in cast:
# Add cyan to midtones
midtone_mask = np.exp(-((gray - 0.5) ** 2) / (2 * 0.2 ** 2))
result[:,:,1] += midtone_mask * cast["cyan_shift"] # Green
result[:,:,0] += midtone_mask * cast["cyan_shift"] # Blue
elif "yellow_shift" in cast:
# Add yellow to shadows
shadow_mask = 1.0 - smoothstep(gray, 0.2, 0.6)
result[:,:,2] += shadow_mask * cast["yellow_shift"] # Red
result[:,:,1] += shadow_mask * cast["yellow_shift"] # Green
elif "purple_shift" in cast:
# Add purple throughout
result[:,:,2] += cast["purple_shift"] # Red
result[:,:,0] += cast["purple_shift"] # Blue
return np.clip(result, 0, 255).astype(np.uint8)
def add_vhs_color_bleeding(bgr, amount=0.4):
"""Add VHS-style color bleeding and chroma smearing - AGGRESSIVE VERSION"""
if amount <= 0:
return bgr
# Convert to YUV for chroma manipulation
yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV).astype(np.float32)
y, u, v = cv2.split(yuv)
# MUCH more aggressive horizontal chroma bleeding
blur_kernel_size = max(5, int(amount * 40)) # Increased from 15
if blur_kernel_size % 2 == 0:
blur_kernel_size += 1
# Multiple passes of bleeding for more dramatic effect
u_blurred = u.copy()
v_blurred = v.copy()
for _ in range(3): # Multiple bleeding passes
u_blurred = cv2.GaussianBlur(u_blurred, (blur_kernel_size, 1), 0)
v_blurred = cv2.GaussianBlur(v_blurred, (blur_kernel_size, 1), 0)
# Much stronger mix - almost completely replace chroma
mix_strength = min(0.9, amount * 1.5) # Up to 90% replacement
u = u * (1 - mix_strength) + u_blurred * mix_strength
v = v * (1 - mix_strength) + v_blurred * mix_strength
# Add horizontal chroma shift (like bad VHS tracking)
shift_amount = int(amount * 8)
if shift_amount > 0:
u = np.roll(u, shift_amount, axis=1)
v = np.roll(v, -shift_amount, axis=1)
# Recombine and convert back
yuv_result = cv2.merge([y, u, v])
bgr_result = cv2.cvtColor(np.clip(yuv_result, 0, 255).astype(np.uint8), cv2.COLOR_YUV2BGR)
return bgr_result
def add_vhs_tracking_lines(bgr, intensity=0.3, line_count=None):
"""Add VHS tracking distortion lines - AGGRESSIVE VERSION"""
if intensity <= 0:
return bgr
h, w = bgr.shape[:2]
if line_count is None:
line_count = int(h * intensity * 0.15) # Much more lines
result = bgr.copy()
for _ in range(line_count):
y = random.randint(0, h-1)
# Much more aggressive horizontal displacement
displacement = int(random.uniform(-30, 30) * intensity) # Increased from 10
if displacement != 0:
# Shift the line
result[y] = np.roll(result[y], displacement, axis=0)
# Add much more noise to the line
noise = np.random.normal(0, 40 * intensity, (w, 3)) # Increased from 15
result[y] = np.clip(result[y].astype(np.float32) + noise, 0, 255).astype(np.uint8)
# Sometimes add complete line corruption
if random.random() < intensity * 0.3:
# Corrupt entire line with static
static = np.random.randint(0, 255, (w, 3))
blend_factor = random.uniform(0.3, 0.8)
result[y] = cv2.addWeighted(result[y], 1-blend_factor, static.astype(np.uint8), blend_factor, 0)
return result
def add_vhs_tape_artifacts(bgr, wear_level=0.3):
"""Add VHS tape wear artifacts - AGGRESSIVE VERSION"""
if wear_level <= 0:
return bgr
h, w = bgr.shape[:2]
result = bgr.astype(np.float32)
# Much more aggressive dropout artifacts
dropout_count = int(w * h * wear_level * 0.0005) # 5x more dropouts
for _ in range(dropout_count):
x = random.randint(0, w-1)
y = random.randint(0, h-1)
size = random.randint(2, 8) # Larger dropouts
# Create dropout
y_start = max(0, y - size)
y_end = min(h, y + size + 1)
x_start = max(0, x - size)
x_end = min(w, x + size + 1)
if random.random() < 0.6:
# Dark dropout - much more aggressive
result[y_start:y_end, x_start:x_end] *= random.uniform(0.0, 0.2) # Almost black
else:
# Bright dropout - blown out
result[y_start:y_end, x_start:x_end] = np.minimum(
result[y_start:y_end, x_start:x_end] + random.uniform(100, 255), 255
)
# Much more prominent vertical streaks
streak_count = int(wear_level * 15) # 5x more streaks
for _ in range(streak_count):
x = random.randint(0, w-1)
streak_width = random.randint(1, 4) # Wider streaks
streak_intensity = random.uniform(0.3, 1.8) # More extreme intensity
x_start = max(0, x)
x_end = min(w, x + streak_width)
result[:, x_start:x_end] *= streak_intensity
# Sometimes add color corruption to streaks
if random.random() < 0.4:
# Corrupt one color channel in the streak
channel = random.randint(0, 2)
result[:, x_start:x_end, channel] *= random.uniform(0.2, 2.0)
# Add horizontal tape damage bands
band_count = int(wear_level * 8)
for _ in range(band_count):
y = random.randint(0, h-1)
band_height = random.randint(1, 5)
y_start = max(0, y)
y_end = min(h, y + band_height)
# Corrupt entire horizontal band
corruption_type = random.choice(['dark', 'bright', 'noisy', 'color_shift'])
if corruption_type == 'dark':
result[y_start:y_end, :] *= random.uniform(0.1, 0.4)
elif corruption_type == 'bright':
result[y_start:y_end, :] = np.minimum(result[y_start:y_end, :] + random.uniform(50, 150), 255)
elif corruption_type == 'noisy':
noise = np.random.normal(0, 60, (y_end - y_start, w, 3))
result[y_start:y_end, :] += noise
elif corruption_type == 'color_shift':
# Shift one color channel dramatically
channel = random.randint(0, 2)
result[y_start:y_end, :, channel] *= random.uniform(0.2, 2.5)
return np.clip(result, 0, 255).astype(np.uint8)
def simulate_vhs_resolution_loss(pil_img: Image.Image, horizontal_res=240, add_softness=True):
"""Simulate VHS resolution limitations - MUCH MORE AGGRESSIVE"""
original_size = pil_img.size
# Much more aggressive resolution reduction
reduced_height = max(120, horizontal_res) # Even lower minimum
aspect_ratio = original_size[0] / original_size[1]
reduced_width = int(reduced_height * aspect_ratio)
# Scale down with more aggressive interpolation
reduced = pil_img.resize((reduced_width, reduced_height), Image.Resampling.NEAREST)
if add_softness:
# Much more blur to simulate VHS softness
reduced = reduced.filter(ImageFilter.GaussianBlur(radius=1.5)) # Increased from 0.5
# Add additional motion blur effect
reduced = reduced.filter(ImageFilter.BoxBlur(radius=1))
# Scale back up with even lower quality interpolation
result = reduced.resize(original_size, Image.Resampling.NEAREST) # Changed from BILINEAR
return result
def add_aggressive_vhs_interlacing(bgr, field_offset=True, blend_amount=0.5):
"""Add much more aggressive VHS interlacing effects"""
if blend_amount <= 0:
return bgr
h, w = bgr.shape[:2]
result = bgr.copy()
if field_offset:
# Much more aggressive field offset
offset_lines = bgr.copy()
# Shift odd and even lines in opposite directions
for y in range(0, h, 2):
if y < h:
offset_lines[y] = np.roll(offset_lines[y], 2, axis=0) # Increased shift
for y in range(1, h, 2):
if y < h:
offset_lines[y] = np.roll(offset_lines[y], -2, axis=0) # Opposite direction
# Stronger blend
result = cv2.addWeighted(bgr, 1 - blend_amount, offset_lines, blend_amount, 0)
# Much more aggressive line-by-line variation
brightness_var = np.ones((h, 1, 1), dtype=np.float32)
for y in range(0, h, 2):
brightness_var[y] *= (1 - blend_amount * 0.4) # Much stronger variation
for y in range(1, h, 2):
brightness_var[y] *= (1 + blend_amount * 0.3) # Opposite variation
result = np.clip(result.astype(np.float32) * brightness_var, 0, 255).astype(np.uint8)
# Add scanline-like artifacts
for y in range(0, h, 4): # Every 4th line
if random.random() < blend_amount:
# Make some lines much darker or brighter
intensity = random.choice([0.3, 1.7]) # Very dark or very bright
result[y, :] = np.clip(result[y, :].astype(np.float32) * intensity, 0, 255).astype(np.uint8)
return result
def add_vhs_rf_interference(bgr, intensity=0.2):
"""Add RF interference patterns common in VHS"""
if intensity <= 0:
return bgr
h, w = bgr.shape[:2]
# Create interference pattern
y_coords, x_coords = np.ogrid[:h, :w]
# Multiple frequency interference
pattern1 = np.sin(x_coords * 0.1 + y_coords * 0.05) * intensity * 10
pattern2 = np.sin(x_coords * 0.03 + y_coords * 0.1) * intensity * 8
pattern3 = np.sin(x_coords * 0.2) * intensity * 5
interference = pattern1 + pattern2 + pattern3
# Apply interference
result = bgr.astype(np.float32)
result += interference[..., np.newaxis]
return np.clip(result, 0, 255).astype(np.uint8)
def add_vhs_head_switching_noise(bgr, intensity=0.2):
"""Add VHS head switching noise (horizontal band at bottom)"""
if intensity <= 0:
return bgr
h, w = bgr.shape[:2]
result = bgr.copy()
# Head switching occurs in bottom portion of frame
noise_start = int(h * 0.85)
noise_height = int(h * 0.1)
if noise_height > 0:
# Add horizontal noise band
noise = np.random.normal(0, intensity * 30, (noise_height, w, 3))
noise_region = result[noise_start:noise_start+noise_height].astype(np.float32)
noise_region += noise
result[noise_start:noise_start+noise_height] = np.clip(noise_region, 0, 255).astype(np.uint8)
# Add some horizontal lines
for i in range(2):
y = noise_start + random.randint(0, noise_height-1)
cv2.line(result, (0, y), (w, y), (128, 128, 128), 1)
return result
# ----------------------
# VHS UI Elements
# ----------------------
def add_vhs_camcorder_ui(pil_img: Image.Image, style="classic", enable_ui=True):
"""Add authentic VHS camcorder UI overlay"""
if not enable_ui:
return pil_img
# Create UI overlay
w, h = pil_img.size
overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
# UI colors and styles
ui_styles = {
"classic": {"bg": (40, 40, 40, 200), "text": (255, 255, 255, 255), "accent": (255, 0, 0, 255)},
"sony": {"bg": (20, 20, 80, 180), "text": (200, 200, 255, 255), "accent": (255, 255, 0, 255)},
"panasonic": {"bg": (80, 20, 20, 180), "text": (255, 200, 200, 255), "accent": (0, 255, 0, 255)}
}
colors = ui_styles.get(style, ui_styles["classic"])
# Top status bar
draw.rectangle([(0, 0), (w, 35)], fill=colors["bg"])
# REC indicator
rec_x = w - 80
draw.ellipse([(rec_x, 8), (rec_x + 20, 28)], fill=colors["accent"])
# Font setup
try:
font = ImageFont.truetype("DejaVuSansMono.ttf", 14)
small_font = ImageFont.truetype("DejaVuSansMono.ttf", 10)
except:
font = ImageFont.load_default()
small_font = font
# UI elements
draw.text((rec_x + 25, 12), "REC", fill=colors["text"], font=font, anchor="lm")
draw.text((10, 12), "VIDEO", fill=colors["text"], font=font, anchor="lm")
# Side UI elements
ui_height = h // 8
ui_y_start = h // 3
# Left side buttons
buttons = ["MENU", "ZOOM", "T", "W"]
for i, btn in enumerate(buttons):
y = ui_y_start + i * (ui_height // 2)
# Button background
draw.rectangle([(5, y), (45, y + 25)], fill=colors["bg"], outline=colors["text"])
draw.text((25, y + 12), btn, fill=colors["text"], font=small_font, anchor="mm")
# Right side elements
draw.text((w - 10, ui_y_start), "LIGHT", fill=colors["text"], font=small_font, anchor="rm")
draw.text((w - 10, ui_y_start + 30), "TITLER", fill=colors["text"], font=small_font, anchor="rm")
draw.text((w - 10, ui_y_start + 60), "PLAY", fill=colors["text"], font=small_font, anchor="rm")
# Blend overlay
result = Image.alpha_composite(pil_img.convert("RGBA"), overlay)
return result.convert("RGB")
def add_vhs_video_timestamp(pil_img: Image.Image, timestamp_style="camcorder", custom_time=""):
"""Add VHS-style video timestamp"""
draw = ImageDraw.Draw(pil_img)
w, h = pil_img.size
try:
font = ImageFont.truetype("DejaVuSansMono.ttf", max(12, min(w, h) // 35))
except:
font = ImageFont.load_default()
if not custom_time:
# Generate random VHS-era timestamp
year = random.choice([1997, 1998, 1999, 2000, 2001, 2002])
month = random.randint(1, 12)
day = random.randint(1, 28)
hour = random.randint(0, 23)
minute = random.randint(0, 59)
if timestamp_style == "camcorder":
# Create multiline timestamp for camcorder style
line1 = f"PM {hour:02d}:{minute:02d}"
line2 = f"{month:02d}/{day:02d}/{year}"
timestamp_lines = [line1, line2]
elif timestamp_style == "security":
timestamp_lines = [f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:00"]
else: # european
timestamp_lines = [f"{day:02d}.{month:02d}.{year} {hour:02d}:{minute:02d}"]
else:
# Handle custom timestamp - split by newlines if present
timestamp_lines = custom_time.split('\n')
# Position and draw timestamp
if timestamp_style == "camcorder":
x_pos, y_pos = 15, h - 45
# Draw each line separately for multiline support
line_height = 18
for i, line in enumerate(timestamp_lines):
current_y = y_pos + (i * line_height)
# Add black outline for readability
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
if dx != 0 or dy != 0:
draw.text((x_pos + dx, current_y + dy), line,
fill=(0, 0, 0), font=font)
# Main text
draw.text((x_pos, current_y), line, fill=(255, 255, 255), font=font)
elif timestamp_style == "security":
# Single line, top-right
line = timestamp_lines[0]
bbox = draw.textbbox((0, 0), line, font=font)
text_width = bbox[2] - bbox[0]
x_pos, y_pos = w - text_width - 15, 15
# Add black outline
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
if dx != 0 or dy != 0:
draw.text((x_pos + dx, y_pos + dy), line,
fill=(0, 0, 0), font=font)
# Main text
draw.text((x_pos, y_pos), line, fill=(255, 255, 255), font=font)
else: # european
# Single line, bottom-right
line = timestamp_lines[0]
bbox = draw.textbbox((0, 0), line, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x_pos, y_pos = w - text_width - 15, h - text_height - 15
# Add black outline
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
if dx != 0 or dy != 0:
draw.text((x_pos + dx, y_pos + dy), line,
fill=(0, 0, 0), font=font)
# Main text
draw.text((x_pos, y_pos), line, fill=(255, 255, 255), font=font)
return pil_img
# ----------------------
# Basic Image Processing
# ----------------------
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 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
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 Processing Pipeline
# ----------------------
def process_image_with_vhs(
image,
intensity,
# VHS Video Effects
enable_vhs_mode,
vhs_tracking_lines,
vhs_color_bleeding,
vhs_tape_wear,
vhs_interlacing,
vhs_head_noise,
vhs_rf_interference,
vhs_resolution_loss,
vhs_ui_overlay,
vhs_ui_style,
vhs_timestamp_style,
vhs_custom_timestamp,
# Basic settings
grain_amount,
compression_level,
keep_ratio
):
if image is None:
return None
# Master scaling
s, boost = map_intensity(float(intensity))
# Working image
original = image.convert("RGB")
pil = original.copy() if keep_ratio else crop_4_3(original)
# STEP 1: VHS Resolution Loss (do this early for authentic low-res look)
if enable_vhs_mode and vhs_resolution_loss > 0:
target_resolution = int(480 - (vhs_resolution_loss * 200)) # 480 down to 280 lines
pil = simulate_vhs_resolution_loss(pil, horizontal_res=target_resolution, add_softness=True)
# Convert to BGR for OpenCV operations
bgr = to_np(pil)
# STEP 2: VHS-specific effects - MUCH MORE AGGRESSIVE
if enable_vhs_mode:
# Dramatic color cast FIRST (like the red image)
bgr = add_dramatic_vhs_color_cast(bgr, cast_type="random", strength=0.8)
# VHS color bleeding (do early to affect subsequent processing)
if vhs_color_bleeding > 0:
bgr = add_vhs_color_bleeding(bgr, amount=min(1.0, vhs_color_bleeding * 1.5))
# VHS tape artifacts - much more aggressive
if vhs_tape_wear > 0:
bgr = add_vhs_tape_artifacts(bgr, wear_level=min(1.0, vhs_tape_wear * 1.8))
# VHS tracking issues - much more prominent
if vhs_tracking_lines > 0:
bgr = add_vhs_tracking_lines(bgr, intensity=min(1.0, vhs_tracking_lines * 2.0))
# VHS interlacing - more aggressive
if vhs_interlacing > 0:
bgr = add_aggressive_vhs_interlacing(bgr, field_offset=True, blend_amount=min(1.0, vhs_interlacing * 1.5))
# RF interference - stronger
if vhs_rf_interference > 0:
bgr = add_vhs_rf_interference(bgr, intensity=min(1.0, vhs_rf_interference * 2.0))
# Head switching noise - more prominent
if vhs_head_noise > 0:
bgr = add_vhs_head_switching_noise(bgr, intensity=min(1.0, vhs_head_noise * 2.5))
# STEP 3: Standard processing (reduced for VHS mode)
if enable_vhs_mode:
# Lighter processing for VHS mode
reduced_s = s * 0.6 # Reduce standard effects when VHS mode is on
reduced_boost = 1 + (boost - 1) * 0.4
else:
reduced_s, reduced_boost = s, boost
# Vignette
bgr = enhanced_vignette(bgr, strength=min(0.4, 0.06 * reduced_s * reduced_boost), feather=1.8)
# Grain (adjusted for VHS)
g_strength = min(30.0, (float(grain_amount) * 0.35 + 1.5) * reduced_s * reduced_boost)
if enable_vhs_mode:
g_strength *= 0.7 # Less grain for VHS mode
bgr = realistic_film_grain(bgr, grain_strength=g_strength, grain_size=1.05)
# Convert back to PIL
pil_mid = to_pil(bgr)
# JPEG compression (adjusted for VHS)
comp_level = compression_level
if enable_vhs_mode:
comp_level = min(compression_level * 1.2, 1.5) # More compression for VHS
comp_norm = (float(comp_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, reduced_s * (0.8 + 0.6 * (reduced_boost - 1))))
add_2pass = (comp_level > 1.0) or (reduced_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)
if enable_vhs_mode:
mix = float(np.clip(0.15 + 0.85 * reduced_s * (0.9 + 0.6 * (reduced_boost - 1)), 0.15, 0.95))
else:
mix = float(np.clip(0.08 + 0.67 * reduced_s * (0.9 + 0.6 * (reduced_boost - 1)), 0.08, 0.92))
processed = Image.blend(orig_aligned, pil_mid, alpha=mix)
# STEP 4: VHS UI and timestamp overlays (do last)
if enable_vhs_mode:
# Add VHS timestamp
if vhs_timestamp_style != "none":
processed = add_vhs_video_timestamp(
processed,
timestamp_style=vhs_timestamp_style,
custom_time=vhs_custom_timestamp
)
# Add VHS UI overlay
if vhs_ui_overlay:
processed = add_vhs_camcorder_ui(processed, style=vhs_ui_style, enable_ui=True)
return processed
# ----------------------
# Gradio Interface
# ----------------------
with gr.Blocks(title="Russian 2000s Filter with Aggressive VHS Effects", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# 📷 Russian 2000s Filter with Aggressive VHS Video Effects
Transform your photos into heavily degraded VHS video stills with dramatic color corruption and period-accurate artifacts.
""")
with gr.Row():
with gr.Column(scale=1):
input_image = gr.Image(type="pil", label="📸 Upload Your Photo")
with gr.Column(scale=1):
output_image = gr.Image(type="pil", label="✨ Degraded VHS Video Still", interactive=False)
# Main processing button
with gr.Row():
process_btn = gr.Button("🎥 Apply Aggressive VHS Filter", variant="primary", size="lg")
with gr.Row():
with gr.Column(scale=1):
with gr.Accordion("📼 Aggressive VHS Video Effects", open=True):
gr.Markdown("""
**Transform photos into heavily corrupted VHS video stills**
- Dramatic color casts (red/magenta like your examples)
- Severe resolution loss and pixelation
- Heavy tracking errors and line displacement
- Aggressive tape wear and corruption
""")
enable_vhs_mode = gr.Checkbox(
label="🎥 Enable Aggressive VHS Mode",
value=True,
info="Master switch for extreme VHS degradation"
)
with gr.Row():
with gr.Column():
vhs_ui_overlay = gr.Checkbox(label="Camcorder UI Overlay", value=True)
vhs_ui_style = gr.Dropdown(
choices=["classic", "sony", "panasonic"],
value="classic",
label="UI Style"
)
vhs_timestamp_style = gr.Dropdown(
choices=["none", "camcorder", "security", "european"],
value="camcorder",
label="Timestamp Style"
)
vhs_custom_timestamp = gr.Textbox(
label="Custom Timestamp",
placeholder="Leave empty for random",
info="Custom time/date text"
)
with gr.Column():
vhs_resolution_loss = gr.Slider(
0, 1, value=0.8, step=0.1,
label="Resolution Loss",
info="Simulates VHS 120-240 line resolution"
)
vhs_color_bleeding = gr.Slider(
0, 1, value=0.7, step=0.1,
label="Color Bleeding",
info="Aggressive horizontal chroma smearing"
)
vhs_tracking_lines = gr.Slider(
0, 1, value=0.6, step=0.1,
label="Tracking Issues",
info="Heavy horizontal line displacement"
)
vhs_interlacing = gr.Slider(
0, 1, value=0.5, step=0.1,
label="Interlacing Effects",
info="Aggressive field offset and artifacts"
)
with gr.Row():
vhs_tape_wear = gr.Slider(
0, 1, value=0.5, step=0.1,
label="Tape Wear",
info="Heavy dropouts, streaks, and corruption"
)
vhs_head_noise = gr.Slider(
0, 1, value=0.3, step=0.05,
label="Head Switching Noise",
info="Aggressive noise band at bottom"
)
vhs_rf_interference = gr.Slider(
0, 1, value=0.2, step=0.05,
label="RF Interference",
info="Strong wavy interference patterns"
)
with gr.Accordion("🎛️ Basic Settings", open=True):
intensity = gr.Slider(0, 10, value=3.5, step=0.1, label="Overall Effect Intensity (0–10)")
grain_amount = gr.Slider(2, 15, value=7, step=1, label="Film Grain Amount")
compression_level = gr.Slider(0.3, 1.5, value=1.1, step=0.1, label="Compression Level")
keep_ratio = gr.Checkbox(value=False, label="Keep Original Aspect Ratio")
# Connect processing button
process_btn.click(
fn=process_image_with_vhs,
inputs=[
input_image, intensity,
# VHS controls
enable_vhs_mode, vhs_tracking_lines, vhs_color_bleeding, vhs_tape_wear,
vhs_interlacing, vhs_head_noise, vhs_rf_interference, vhs_resolution_loss,
vhs_ui_overlay, vhs_ui_style, vhs_timestamp_style, vhs_custom_timestamp,
# Basic settings
grain_amount, compression_level, keep_ratio
],
outputs=[output_image]
)
gr.Markdown("""
### 📼 AGGRESSIVE VHS Video Still Features:
**🎥 Dramatic Color Effects:**
- **Heavy Color Casts**: Random dramatic color shifts (red/magenta, cyan/blue, green/yellow, purple)
- **Severe Color Bleeding**: Massive horizontal chroma smearing and displacement
- **Channel Corruption**: Individual color channels get corrupted and shifted
- **Tonal Range Shifts**: Different colors applied to shadows, midtones, highlights
**📺 Extreme VHS Degradation:**
- **Resolution Destruction**: Down to 120-line resolution with heavy pixelation
- **Massive Tracking Errors**: Lines displaced by 30+ pixels with static corruption
- **Aggressive Interlacing**: Heavy field offset with alternating line brightness
- **Tape Damage**: Extensive dropouts, vertical streaks, horizontal bands
- **Static Corruption**: Random lines replaced with pure static
**⚡ Interference & Artifacts:**
- **RF Interference**: Strong wavy patterns throughout image
- **Head Switching**: Prominent noise bands and corruption
- **Dropout Artifacts**: Large black/white spots and missing sections
- **Color Channel Shifts**: Dramatic red/green/blue displacement
**🎯 Perfect for Recreating:**
- Heavily degraded VHS tapes (like your red-cast example)
- Old security camera footage with severe artifacts
- Damaged home video recordings
- Worn-out rental VHS tapes
- Bootleg video recordings
**💡 New Default Settings:**
- **Resolution Loss**: 0.8 (very low resolution like VHS)
- **Color Bleeding**: 0.7 (heavy chroma smearing)
- **Tracking Lines**: 0.6 (prominent displacement)
- **Tape Wear**: 0.5 (significant degradation)
- **All effects amplified 1.5-2.5x** for maximum VHS authenticity
**🔧 Technical Improvements:**
- **Dramatic color casts** applied first (like the red example image)
- **Multiple blur passes** for extreme color bleeding
- **Static line replacement** for severe tracking errors
- **Aggressive resolution loss** with nearest-neighbor scaling
- **Color channel corruption** for authentic VHS color errors
""")
if __name__ == "__main__":
demo.launch()