| """ |
| Visual effects and enhancements for BackgroundFX Pro. |
| Implements professional-grade effects for background replacement. |
| """ |
|
|
| import cv2 |
| import numpy as np |
| import torch |
| import torch.nn.functional as F |
| from typing import Dict, List, Optional, Tuple, Union |
| from dataclasses import dataclass |
| from enum import Enum |
| import logging |
| from scipy.ndimage import gaussian_filter, map_coordinates |
|
|
| from utils.logger import setup_logger |
| from utils.device import DeviceManager |
| from core.quality import QualityAnalyzer |
|
|
| logger = setup_logger(__name__) |
|
|
|
|
| class EffectType(Enum): |
| """Available effect types.""" |
| BLUR = "blur" |
| BOKEH = "bokeh" |
| COLOR_SHIFT = "color_shift" |
| LIGHT_WRAP = "light_wrap" |
| SHADOW = "shadow" |
| REFLECTION = "reflection" |
| GLOW = "glow" |
| CHROMATIC_ABERRATION = "chromatic_aberration" |
| VIGNETTE = "vignette" |
| FILM_GRAIN = "film_grain" |
| MOTION_BLUR = "motion_blur" |
| DEPTH_OF_FIELD = "depth_of_field" |
|
|
|
|
| @dataclass |
| class EffectConfig: |
| """Configuration for visual effects.""" |
| blur_strength: float = 15.0 |
| bokeh_size: int = 21 |
| bokeh_brightness: float = 1.5 |
| light_wrap_intensity: float = 0.3 |
| light_wrap_width: int = 10 |
| shadow_opacity: float = 0.5 |
| shadow_blur: float = 10.0 |
| shadow_offset: Tuple[int, int] = (5, 5) |
| glow_intensity: float = 0.5 |
| glow_radius: int = 20 |
| chromatic_shift: float = 2.0 |
| vignette_strength: float = 0.3 |
| grain_intensity: float = 0.1 |
| motion_blur_angle: float = 0.0 |
| motion_blur_size: int = 15 |
|
|
|
|
| class BackgroundEffects: |
| """Apply effects to background images.""" |
| |
| def __init__(self, config: Optional[EffectConfig] = None): |
| self.config = config or EffectConfig() |
| self.device_manager = DeviceManager() |
| |
| def apply_blur(self, image: np.ndarray, |
| strength: Optional[float] = None, |
| mask: Optional[np.ndarray] = None) -> np.ndarray: |
| """ |
| Apply Gaussian blur to image. |
| |
| Args: |
| image: Input image |
| strength: Blur strength |
| mask: Optional mask for selective blur |
| |
| Returns: |
| Blurred image |
| """ |
| strength = strength or self.config.blur_strength |
| |
| if strength <= 0: |
| return image |
| |
| |
| kernel_size = int(strength * 2) + 1 |
| |
| |
| blurred = cv2.GaussianBlur(image, (kernel_size, kernel_size), strength) |
| |
| |
| if mask is not None: |
| mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2) |
| if mask_3ch.max() > 1: |
| mask_3ch = mask_3ch / 255.0 |
| |
| blurred = image * (1 - mask_3ch) + blurred * mask_3ch |
| blurred = blurred.astype(np.uint8) |
| |
| return blurred |
| |
| def apply_bokeh(self, image: np.ndarray, |
| depth_map: Optional[np.ndarray] = None) -> np.ndarray: |
| """ |
| Apply bokeh effect to simulate depth of field. |
| |
| Args: |
| image: Input image |
| depth_map: Optional depth map for varying blur |
| |
| Returns: |
| Image with bokeh effect |
| """ |
| h, w = image.shape[:2] |
| |
| |
| if depth_map is None: |
| |
| center_x, center_y = w // 2, h // 2 |
| Y, X = np.ogrid[:h, :w] |
| dist = np.sqrt((X - center_x)**2 + (Y - center_y)**2) |
| depth_map = dist / dist.max() |
| |
| |
| if depth_map.max() > 1: |
| depth_map = depth_map / 255.0 |
| |
| |
| kernel_size = self.config.bokeh_size |
| kernel = self._create_bokeh_kernel(kernel_size) |
| |
| |
| result = np.zeros_like(image, dtype=np.float32) |
| |
| |
| blur_levels = 5 |
| for i in range(blur_levels): |
| blur_strength = (i + 1) * (kernel_size // blur_levels) |
| |
| if blur_strength > 0: |
| blurred = cv2.filter2D(image, -1, kernel[:blur_strength, :blur_strength]) |
| else: |
| blurred = image |
| |
| |
| depth_min = i / blur_levels |
| depth_max = (i + 1) / blur_levels |
| mask = ((depth_map >= depth_min) & (depth_map < depth_max)).astype(np.float32) |
| |
| |
| mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2) |
| |
| |
| result += blurred * mask_3ch |
| |
| |
| result = self._add_bokeh_highlights(result, depth_map) |
| |
| return np.clip(result, 0, 255).astype(np.uint8) |
| |
| def _create_bokeh_kernel(self, size: int) -> np.ndarray: |
| """Create hexagonal bokeh kernel.""" |
| kernel = np.zeros((size, size), dtype=np.float32) |
| center = size // 2 |
| radius = center - 1 |
| |
| |
| for i in range(size): |
| for j in range(size): |
| x, y = i - center, j - center |
| |
| if abs(x) <= radius and abs(y) <= radius * np.sqrt(3) / 2: |
| if abs(y) <= (radius * np.sqrt(3) / 2 - abs(x) * np.sqrt(3) / 2): |
| kernel[i, j] = 1.0 |
| |
| |
| kernel /= kernel.sum() |
| |
| return kernel |
| |
| def _add_bokeh_highlights(self, image: np.ndarray, |
| depth_map: np.ndarray) -> np.ndarray: |
| """Add bright bokeh spots to out-of-focus areas.""" |
| |
| gray = cv2.cvtColor(image.astype(np.uint8), cv2.COLOR_BGR2GRAY) |
| _, bright_mask = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) |
| |
| |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) |
| bright_mask = cv2.dilate(bright_mask, kernel, iterations=2) |
| |
| |
| bright_mask = (bright_mask * depth_map).astype(np.uint8) |
| |
| |
| glow = cv2.GaussianBlur(bright_mask, (21, 21), 10) |
| glow = cv2.cvtColor(glow, cv2.COLOR_GRAY2BGR) / 255.0 |
| |
| |
| result = image + glow * self.config.bokeh_brightness * 50 |
| |
| return result |
| |
| def apply_light_wrap(self, foreground: np.ndarray, |
| background: np.ndarray, |
| mask: np.ndarray) -> np.ndarray: |
| """ |
| Apply light wrap effect for better compositing. |
| |
| Args: |
| foreground: Foreground image |
| background: Background image |
| mask: Foreground mask |
| |
| Returns: |
| Foreground with light wrap |
| """ |
| |
| if len(mask.shape) == 3: |
| mask = mask[:, :, 0] |
| |
| |
| if mask.max() > 1: |
| mask = mask / 255.0 |
| |
| |
| kernel = np.ones((self.config.light_wrap_width, self.config.light_wrap_width), np.uint8) |
| dilated_mask = cv2.dilate(mask, kernel, iterations=1) |
| edge_mask = dilated_mask - mask |
| |
| |
| blurred_bg = cv2.GaussianBlur(background, (21, 21), 10) |
| |
| |
| bg_light = blurred_bg * edge_mask[:, :, np.newaxis] |
| |
| |
| mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2) |
| wrapped = foreground + bg_light * self.config.light_wrap_intensity |
| |
| return np.clip(wrapped, 0, 255).astype(np.uint8) |
| |
| def add_shadow(self, image: np.ndarray, |
| mask: np.ndarray, |
| ground_plane: Optional[float] = None) -> np.ndarray: |
| """ |
| Add realistic shadow to composited image. |
| |
| Args: |
| image: Background image |
| mask: Object mask |
| ground_plane: Y-coordinate of ground plane |
| |
| Returns: |
| Image with shadow |
| """ |
| h, w = image.shape[:2] |
| |
| if ground_plane is None: |
| ground_plane = h * 0.9 |
| |
| |
| shadow_mask = mask.copy() |
| if len(shadow_mask.shape) == 3: |
| shadow_mask = shadow_mask[:, :, 0] |
| |
| |
| offset_x, offset_y = self.config.shadow_offset |
| |
| |
| src_points = np.float32([[0, 0], [w, 0], [0, h], [w, h]]) |
| dst_points = np.float32([ |
| [offset_x, offset_y], |
| [w + offset_x, offset_y], |
| [-offset_x * 2, h], |
| [w + offset_x * 2, h] |
| ]) |
| |
| matrix = cv2.getPerspectiveTransform(src_points, dst_points) |
| shadow_mask = cv2.warpPerspective(shadow_mask, matrix, (w, h)) |
| |
| |
| blur_size = int(self.config.shadow_blur) * 2 + 1 |
| shadow_mask = cv2.GaussianBlur(shadow_mask, (blur_size, blur_size), |
| self.config.shadow_blur) |
| |
| |
| shadow_mask[:int(ground_plane), :] = 0 |
| |
| |
| if shadow_mask.max() > 0: |
| shadow_mask = shadow_mask / shadow_mask.max() |
| shadow_mask *= self.config.shadow_opacity |
| |
| |
| shadow_color = np.array([0, 0, 0], dtype=np.float32) |
| shadow_mask_3ch = np.repeat(shadow_mask[:, :, np.newaxis], 3, axis=2) |
| |
| result = image * (1 - shadow_mask_3ch) + shadow_color * shadow_mask_3ch |
| |
| return np.clip(result, 0, 255).astype(np.uint8) |
| |
| def add_reflection(self, image: np.ndarray, |
| mask: np.ndarray, |
| reflection_strength: float = 0.3) -> np.ndarray: |
| """ |
| Add reflection effect for glossy surfaces. |
| |
| Args: |
| image: Input image |
| mask: Object mask |
| reflection_strength: Reflection opacity |
| |
| Returns: |
| Image with reflection |
| """ |
| h, w = image.shape[:2] |
| |
| |
| if len(mask.shape) == 2: |
| mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2) |
| else: |
| mask_3ch = mask |
| |
| if mask_3ch.max() > 1: |
| mask_3ch = mask_3ch / 255.0 |
| |
| object_only = image * mask_3ch |
| |
| |
| reflection = cv2.flip(object_only, 0) |
| |
| |
| gradient = np.linspace(reflection_strength, 0, h) |
| gradient = np.repeat(gradient[:, np.newaxis], w, axis=1) |
| gradient = np.repeat(gradient[:, :, np.newaxis], 3, axis=2) |
| |
| |
| reflection = reflection * gradient |
| |
| |
| reflection = cv2.GaussianBlur(reflection, (5, 5), 2) |
| |
| |
| result = image.copy() |
| result = result + reflection |
| |
| return np.clip(result, 0, 255).astype(np.uint8) |
| |
| def add_glow(self, image: np.ndarray, |
| mask: Optional[np.ndarray] = None, |
| color: Optional[Tuple[int, int, int]] = None) -> np.ndarray: |
| """ |
| Add glow effect to image or masked region. |
| |
| Args: |
| image: Input image |
| mask: Optional mask for selective glow |
| color: Glow color (BGR) |
| |
| Returns: |
| Image with glow effect |
| """ |
| if color is None: |
| color = (255, 255, 255) |
| |
| |
| if mask is not None: |
| if len(mask.shape) == 2: |
| glow_source = np.zeros_like(image) |
| for i in range(3): |
| glow_source[:, :, i] = mask * (color[i] / 255.0) |
| else: |
| glow_source = mask * np.array(color).reshape(1, 1, 3) / 255.0 |
| else: |
| |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
| _, bright_mask = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) |
| glow_source = cv2.cvtColor(bright_mask, cv2.COLOR_GRAY2BGR) |
| |
| |
| glow = np.zeros_like(image, dtype=np.float32) |
| |
| for i in range(1, 4): |
| blur_size = self.config.glow_radius * i |
| kernel_size = blur_size * 2 + 1 |
| |
| blurred = cv2.GaussianBlur(glow_source, (kernel_size, kernel_size), blur_size) |
| glow += blurred / (i * 2) |
| |
| |
| if glow.max() > 0: |
| glow = glow / glow.max() |
| glow *= self.config.glow_intensity * 255 |
| |
| |
| result = image.astype(np.float32) + glow |
| |
| return np.clip(result, 0, 255).astype(np.uint8) |
| |
| def chromatic_aberration(self, image: np.ndarray, |
| shift: Optional[float] = None) -> np.ndarray: |
| """ |
| Apply chromatic aberration effect. |
| |
| Args: |
| image: Input image |
| shift: Pixel shift amount |
| |
| Returns: |
| Image with chromatic aberration |
| """ |
| shift = shift or self.config.chromatic_shift |
| h, w = image.shape[:2] |
| |
| |
| b, g, r = cv2.split(image) |
| |
| |
| center_x, center_y = w // 2, h // 2 |
| |
| |
| M_r = np.float32([[1 + shift/w, 0, -shift], [0, 1 + shift/h, -shift]]) |
| r_shifted = cv2.warpAffine(r, M_r, (w, h)) |
| |
| |
| M_b = np.float32([[1 - shift/w, 0, shift], [0, 1 - shift/h, shift]]) |
| b_shifted = cv2.warpAffine(b, M_b, (w, h)) |
| |
| |
| result = cv2.merge([b_shifted, g, r_shifted]) |
| |
| return result |
| |
| def add_vignette(self, image: np.ndarray, |
| strength: Optional[float] = None) -> np.ndarray: |
| """ |
| Add vignette effect to image. |
| |
| Args: |
| image: Input image |
| strength: Vignette strength (0-1) |
| |
| Returns: |
| Image with vignette |
| """ |
| strength = strength or self.config.vignette_strength |
| h, w = image.shape[:2] |
| |
| |
| center_x, center_y = w // 2, h // 2 |
| Y, X = np.ogrid[:h, :w] |
| |
| |
| dist = np.sqrt((X - center_x)**2 + (Y - center_y)**2) |
| max_dist = np.sqrt(center_x**2 + center_y**2) |
| |
| |
| vignette = 1 - (dist / max_dist) * strength |
| vignette = np.clip(vignette, 0, 1) |
| |
| |
| vignette_3ch = np.repeat(vignette[:, :, np.newaxis], 3, axis=2) |
| result = image * vignette_3ch |
| |
| return np.clip(result, 0, 255).astype(np.uint8) |
| |
| def add_film_grain(self, image: np.ndarray, |
| intensity: Optional[float] = None) -> np.ndarray: |
| """ |
| Add film grain effect to image. |
| |
| Args: |
| image: Input image |
| intensity: Grain intensity |
| |
| Returns: |
| Image with film grain |
| """ |
| intensity = intensity or self.config.grain_intensity |
| |
| |
| h, w = image.shape[:2] |
| grain = np.random.randn(h, w, 3) * intensity * 255 |
| |
| |
| result = image.astype(np.float32) + grain |
| |
| return np.clip(result, 0, 255).astype(np.uint8) |
| |
| def motion_blur(self, image: np.ndarray, |
| angle: Optional[float] = None, |
| size: Optional[int] = None) -> np.ndarray: |
| """ |
| Apply directional motion blur. |
| |
| Args: |
| image: Input image |
| angle: Blur angle in degrees |
| size: Blur kernel size |
| |
| Returns: |
| Motion blurred image |
| """ |
| angle = angle or self.config.motion_blur_angle |
| size = size or self.config.motion_blur_size |
| |
| |
| kernel = np.zeros((size, size)) |
| kernel[int((size-1)/2), :] = np.ones(size) |
| kernel = kernel / size |
| |
| |
| M = cv2.getRotationMatrix2D((size/2, size/2), angle, 1) |
| kernel = cv2.warpAffine(kernel, M, (size, size)) |
| |
| |
| result = cv2.filter2D(image, -1, kernel) |
| |
| return result |
|
|
|
|
| class CompositeEffects: |
| """Advanced compositing effects.""" |
| |
| def __init__(self): |
| self.logger = setup_logger(f"{__name__}.CompositeEffects") |
| self.bg_effects = BackgroundEffects() |
| |
| def smart_composite(self, foreground: np.ndarray, |
| background: np.ndarray, |
| mask: np.ndarray, |
| effects: List[EffectType]) -> np.ndarray: |
| """ |
| Apply smart compositing with multiple effects. |
| |
| Args: |
| foreground: Foreground image |
| background: Background image |
| mask: Alpha mask |
| effects: List of effects to apply |
| |
| Returns: |
| Composited image with effects |
| """ |
| result = background.copy() |
| |
| |
| if len(mask.shape) == 2: |
| mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2) |
| else: |
| mask_3ch = mask |
| |
| if mask_3ch.max() > 1: |
| mask_3ch = mask_3ch / 255.0 |
| |
| |
| for effect in effects: |
| if effect == EffectType.BLUR: |
| result = self.bg_effects.apply_blur(result, mask=1-mask_3ch[:,:,0]) |
| elif effect == EffectType.BOKEH: |
| result = self.bg_effects.apply_bokeh(result) |
| elif effect == EffectType.VIGNETTE: |
| result = self.bg_effects.add_vignette(result) |
| |
| |
| if EffectType.LIGHT_WRAP in effects: |
| foreground = self.bg_effects.apply_light_wrap( |
| foreground, result, mask_3ch[:,:,0] |
| ) |
| |
| |
| result = result * (1 - mask_3ch) + foreground * mask_3ch |
| result = result.astype(np.uint8) |
| |
| |
| if EffectType.SHADOW in effects: |
| result = self.bg_effects.add_shadow(result, mask_3ch[:,:,0]) |
| |
| if EffectType.REFLECTION in effects: |
| result = self.bg_effects.add_reflection(result, mask_3ch[:,:,0]) |
| |
| if EffectType.GLOW in effects: |
| result = self.bg_effects.add_glow(result, mask_3ch[:,:,0]) |
| |
| |
| if EffectType.CHROMATIC_ABERRATION in effects: |
| result = self.bg_effects.chromatic_aberration(result) |
| |
| if EffectType.FILM_GRAIN in effects: |
| result = self.bg_effects.add_film_grain(result) |
| |
| return result |
| |
| def color_harmonization(self, foreground: np.ndarray, |
| background: np.ndarray, |
| mask: np.ndarray, |
| strength: float = 0.3) -> np.ndarray: |
| """ |
| Harmonize colors between foreground and background. |
| |
| Args: |
| foreground: Foreground image |
| background: Background image |
| mask: Foreground mask |
| strength: Harmonization strength |
| |
| Returns: |
| Color-harmonized foreground |
| """ |
| |
| bg_mean = np.mean(background, axis=(0, 1)) |
| bg_std = np.std(background, axis=(0, 1)) |
| |
| |
| fg_mean = np.mean(foreground, axis=(0, 1)) |
| fg_std = np.std(foreground, axis=(0, 1)) |
| |
| |
| result = foreground.astype(np.float32) |
| |
| for i in range(3): |
| |
| result[:, :, i] = (result[:, :, i] - fg_mean[i]) / (fg_std[i] + 1e-6) |
| |
| |
| result[:, :, i] = result[:, :, i] * (bg_std[i] * strength + fg_std[i] * (1 - strength)) |
| result[:, :, i] += bg_mean[i] * strength + fg_mean[i] * (1 - strength) |
| |
| return np.clip(result, 0, 255).astype(np.uint8) |