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