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