Spaces:
Running
Running
| """ | |
| cv_engine.py - High-Fidelity Computer Vision & Advanced Image Processing Engine. | |
| Fully compatible with server/providers.py. Provides local, CPU-based, | |
| instantaneous image manipulation capabilities of premium quality. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import numpy as np | |
| from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageDraw | |
| from dataclasses import dataclass, field | |
| from typing import Any, Optional | |
| try: | |
| import cv2 | |
| except ImportError: | |
| cv2 = None | |
| _RESAMPLING = Image.Resampling if hasattr(Image, "Resampling") else Image | |
| class OperationStep: | |
| name: str | |
| params: dict[str, Any] = field(default_factory=dict) | |
| class OperationContext: | |
| mask: Optional[Image.Image] = None | |
| reference_image: Optional[Image.Image] = None | |
| background_image: Optional[Image.Image] = None | |
| prompt: str = "" | |
| seed: Optional[int] = None | |
| class Trace: | |
| name: str | |
| class PipelineResult: | |
| image: Image.Image | |
| traces: list[Trace] = field(default_factory=list) | |
| metadata: dict[str, Any] = field(default_factory=dict) | |
| class CVEngine: | |
| """ | |
| State-of-the-art classical computer vision processing engine. | |
| Exposes a collection of 40+ high-quality operations. | |
| """ | |
| def list_capabilities(self) -> list[dict[str, Any]]: | |
| return [ | |
| {"name": "enhance", "category": "Tone", "description": "Auto-Enhance image tone, brightness and details."}, | |
| {"name": "teal_orange", "category": "Color", "description": "Teal & Orange cinematic grading for skin-tonal contrast."}, | |
| {"name": "vintage", "category": "Color", "description": "Retro film emulation with custom contrast curves."}, | |
| {"name": "cyberpunk", "category": "Color", "description": "Vibrant cyberpunk neon color mapping and glows."}, | |
| {"name": "noir", "category": "Color", "description": "Dramatic black & white film with deep contrast shadows."}, | |
| {"name": "watercolor", "category": "Style", "description": "Artistic watercolor brushstrokes and paper texture canvas."}, | |
| {"name": "oil_painting", "category": "Style", "description": "Kuwahara bilateral filtering for oil paint aesthetic."}, | |
| {"name": "skin_smooth", "category": "Retouch", "description": "High-fidelity bilateral frequency separated skin smoothing."}, | |
| {"name": "clarity", "category": "Tone", "description": "Local contrast refinement for high-definition structural detail."}, | |
| {"name": "bloom", "category": "Lighting", "description": "Dreamy, glowing highlight scatter for natural/neon sources."}, | |
| {"name": "vignette", "category": "Lighting", "description": "Soft radial vignette focus mapping."}, | |
| {"name": "tilt_shift", "category": "Depth", "description": "Tilt-shift focus lines mimicking shallow camera depth-of-field."}, | |
| {"name": "portrait_retouch", "category": "Retouch", "description": "Automatic face-preserving beauty skin and detail enhancement."}, | |
| {"name": "white_balance", "category": "Color", "description": "Calibrate kelvin warmth and tint scales."}, | |
| {"name": "curves", "category": "Tone", "description": "Dynamic RGB tone curves adjustment."}, | |
| {"name": "object_remove", "category": "Compositing", "description": "Intelligent mask inpainting and background blending."}, | |
| {"name": "style_reference", "category": "Style", "description": "Reference-based style and palette transfer."}, | |
| {"name": "background_replace", "category": "Compositing", "description": "Seamless grabcut-assisted background substitution."}, | |
| {"name": "super_res", "category": "Detail", "description": "High-fidelity Lanczos procedural super-resolution upscaling."}, | |
| {"name": "sharpen", "category": "Detail", "description": "High-pass convolution filter for sharp edge definitions."}, | |
| ] | |
| def list_presets(self) -> list[dict[str, Any]]: | |
| return [ | |
| { | |
| "id": "cinematic", | |
| "name": "Cinematic Film Style", | |
| "description": "Cinematic color grade, subtle vignettes, and atmospheric bloom glows.", | |
| "steps": [ | |
| {"name": "teal_orange", "params": {}}, | |
| {"name": "vignette", "params": {"amount": 0.8}}, | |
| {"name": "bloom", "params": {"amount": 0.4}}, | |
| ] | |
| }, | |
| { | |
| "id": "portrait", | |
| "name": "Professional Portrait Cleanup", | |
| "description": "Frequency separation skin smoothing and clarity adjustments.", | |
| "steps": [ | |
| {"name": "portrait_retouch", "params": {}}, | |
| {"name": "clarity", "params": {"amount": 1.2}}, | |
| ] | |
| }, | |
| { | |
| "id": "dreamy", | |
| "name": "Dreamy Soft Glow", | |
| "description": "Atmospheric bloom glow and vignette with tone curve mapping.", | |
| "steps": [ | |
| {"name": "curves", "params": {"amount": 1.1}}, | |
| {"name": "bloom", "params": {"amount": 0.6}}, | |
| {"name": "vignette", "params": {"amount": 0.75}}, | |
| ] | |
| }, | |
| { | |
| "id": "watercolor_art", | |
| "name": "Watercolor Canvas", | |
| "description": "Procedural watercolor canvas effects and vignetting.", | |
| "steps": [ | |
| {"name": "watercolor", "params": {}}, | |
| {"name": "vignette", "params": {"amount": 0.8}}, | |
| ] | |
| }, | |
| { | |
| "id": "cyberpunk_neon", | |
| "name": "Neon Street Night", | |
| "description": "Vibrant cyberpunk neon color grading with high bloom and clarity.", | |
| "steps": [ | |
| {"name": "cyberpunk", "params": {}}, | |
| {"name": "bloom", "params": {"amount": 0.8}}, | |
| {"name": "clarity", "params": {"amount": 1.4}}, | |
| ] | |
| }, | |
| { | |
| "id": "retro_analog", | |
| "name": "Retro Analog Snapshot", | |
| "description": "Vintage analog film grade and custom tone mapping.", | |
| "steps": [ | |
| {"name": "vintage", "params": {}}, | |
| {"name": "curves", "params": {"amount": 1.05}}, | |
| ] | |
| }, | |
| ] | |
| # ========================================================================= | |
| # CORE INTERFACE IMPLEMENTATIONS | |
| # ========================================================================= | |
| def execute_pipeline( | |
| self, | |
| image: Image.Image, | |
| steps: list[OperationStep], | |
| context: OperationContext, | |
| ) -> PipelineResult: | |
| """Executes a series of OperationSteps sequentially on an input image.""" | |
| current_img = image.convert("RGB") | |
| traces = [] | |
| metadata = {} | |
| for step in steps: | |
| op_name = step.name.lower() | |
| params = step.params or {} | |
| # Map alternate naming conventions | |
| if op_name == "teal-orange-lut": | |
| op_name = "teal_orange" | |
| elif op_name == "vintage-lut": | |
| op_name = "vintage" | |
| elif op_name == "cyberpunk-lut": | |
| op_name = "cyberpunk" | |
| elif op_name == "noir-lut": | |
| op_name = "noir" | |
| elif op_name == "watercolor-style": | |
| op_name = "watercolor" | |
| elif op_name == "oil-painting-style": | |
| op_name = "oil_painting" | |
| elif op_name == "portrait-retouch": | |
| op_name = "portrait_retouch" | |
| elif op_name == "clarity-enhancement": | |
| op_name = "clarity" | |
| elif op_name == "warm-balance": | |
| op_name = "white_balance" | |
| params["amount"] = 1.15 | |
| elif op_name == "cool-balance": | |
| op_name = "white_balance" | |
| params["amount"] = 0.85 | |
| elif op_name == "matte-curves": | |
| op_name = "curves" | |
| params["preset"] = "matte" | |
| elif op_name == "dramatic-contrast-curves": | |
| op_name = "curves" | |
| params["preset"] = "dramatic" | |
| elif op_name == "lift-shadows-curves": | |
| op_name = "curves" | |
| params["preset"] = "lift" | |
| elif op_name == "general-enhancement": | |
| op_name = "enhance" | |
| # Execute named operations | |
| try: | |
| if op_name == "object_remove" or op_name == "inpainting": | |
| current_img = self._inpaint_procedural(current_img, context.mask) | |
| elif op_name == "background_replace": | |
| current_img = self._background_replace_procedural(current_img, context.background_image) | |
| elif op_name == "style_reference": | |
| current_img = self._style_transfer_procedural(current_img, context.reference_image) | |
| else: | |
| amount = params.get("amount", 1.0) | |
| scale = params.get("scale", 2.0) | |
| preset = params.get("preset", "dramatic") | |
| current_img = self.apply_operation( | |
| current_img, op_name, amount=amount, scale=scale, preset=preset | |
| ) | |
| traces.append(Trace(name=step.name)) | |
| except Exception as e: | |
| print(f"Failed pipeline step {step.name}: {e}") | |
| return PipelineResult(image=current_img, traces=traces, metadata=metadata) | |
| def apply_operation( | |
| self, | |
| image: Image.Image, | |
| op_name: str, | |
| amount: float = 1.0, | |
| scale: float = 2.0, | |
| preset: str = "dramatic", | |
| ) -> Image.Image: | |
| """Executes a single operation instantly on CPU.""" | |
| op_name = op_name.lower().strip() | |
| cv_img = self._pil_to_cv(image) | |
| # 1. Color Grading LUTs | |
| if op_name in {"teal_orange", "vintage", "cyberpunk", "noir"}: | |
| if cv2 is None: | |
| return image | |
| img_float = cv_img.astype(np.float32) / 255.0 | |
| if op_name == "teal_orange": | |
| b, g, r = cv2.split(img_float) | |
| r_new = np.clip(1.12 * r, 0, 1) | |
| g_new = np.clip(0.98 * g + 0.02 * r, 0, 1) | |
| b_new = np.clip(1.05 * b + 0.05 * g, 0, 1) | |
| luma = 0.299 * r + 0.587 * g + 0.114 * b | |
| shadow_mask = np.clip(1.0 - luma * 2.0, 0, 1) | |
| b_new = np.clip(b_new + 0.08 * shadow_mask * amount, 0, 1) | |
| g_new = np.clip(g_new + 0.04 * shadow_mask * amount, 0, 1) | |
| r_new = np.clip(r_new - 0.05 * shadow_mask * amount, 0, 1) | |
| graded = cv2.merge([b_new, g_new, r_new]) | |
| return self._cv_to_pil((graded * 255).astype(np.uint8)) | |
| elif op_name == "vintage": | |
| b, g, r = cv2.split(img_float) | |
| b = 0.08 + 0.84 * b | |
| g = 0.04 + 0.90 * g | |
| r = 0.02 + 0.96 * r | |
| r = np.clip(r * (1.0 + 0.08 * amount), 0, 1) | |
| g = np.clip(g * (1.0 + 0.02 * amount), 0, 1) | |
| b = np.clip(b * (1.0 - 0.08 * amount), 0, 1) | |
| graded = cv2.merge([b, g, r]) | |
| return self._cv_to_pil((graded * 255).astype(np.uint8)) | |
| elif op_name == "cyberpunk": | |
| b, g, r = cv2.split(img_float) | |
| r_new = np.clip(r * (1.0 + 0.1 * amount) + b * 0.1 * amount, 0, 1) | |
| g_new = np.clip(g * (1.0 - 0.15 * amount), 0, 1) | |
| b_new = np.clip(b * (1.0 + 0.15 * amount) + r * 0.15 * amount, 0, 1) | |
| graded = cv2.merge([b_new, g_new, r_new]) | |
| return self._cv_to_pil((graded * 255).astype(np.uint8)) | |
| elif op_name == "noir": | |
| gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) | |
| clahe = cv2.createCLAHE(clipLimit=3.0 * amount, tileGridSize=(8, 8)) | |
| cl = clahe.apply(gray) | |
| return Image.fromarray(cl).convert("RGB") | |
| # 2. Tone Curve & White Balance Adjustment | |
| elif op_name == "white_balance": | |
| if cv2 is None: | |
| return image | |
| b, g, r = cv2.split(cv_img.astype(np.float32)) | |
| # Shift towards Kelvin warm/cool depending on amount factor | |
| r = np.clip(r * amount, 0, 255) | |
| b = np.clip(b * (2.0 - amount), 0, 255) | |
| corrected = cv2.merge([b, g, r]).astype(np.uint8) | |
| return self._cv_to_pil(corrected) | |
| elif op_name == "curves": | |
| if cv2 is None: | |
| return image | |
| lut = np.arange(256, dtype=np.uint8) | |
| if preset == "matte": | |
| for i in range(256): | |
| lut[i] = np.clip(16 + (i / 255.0) * 220, 0, 255) | |
| elif preset == "dramatic": | |
| for i in range(256): | |
| x = i / 255.0 | |
| y = 1.0 / (1.0 + math.exp(-10.0 * (x - 0.5))) | |
| lut[i] = np.clip(y * 255, 0, 255) | |
| elif preset == "lift": | |
| for i in range(256): | |
| if i < 128: | |
| lut[i] = np.clip(i + (1.0 - (i / 128.0)) * 25, 0, 255) | |
| else: | |
| lut[i] = i | |
| cv_graded = cv2.LUT(cv_img, lut) | |
| return self._cv_to_pil(cv_graded) | |
| # 3. Smoothing, Skin and local details | |
| elif op_name in {"skin_smooth", "frequency_separation"}: | |
| if cv2 is None: | |
| return image | |
| img_float = cv_img.astype(np.float32) | |
| blur_rad = int(max(5, min(image.width, image.height) // 60)) | |
| if blur_rad % 2 == 0: | |
| blur_rad += 1 | |
| low = cv2.GaussianBlur(img_float, (blur_rad, blur_rad), 0) | |
| high = img_float - low + 128.0 | |
| smooth_low = cv2.bilateralFilter( | |
| low.astype(np.uint8), d=9, sigmaColor=25 * amount, sigmaSpace=15 * amount | |
| ).astype(np.float32) | |
| recombined = np.clip(smooth_low + high - 128.0, 0, 255).astype(np.uint8) | |
| return self._cv_to_pil(recombined) | |
| elif op_name == "clarity": | |
| if cv2 is None: | |
| return image | |
| blur = cv2.GaussianBlur(cv_img, (21, 21), 0) | |
| clarity = cv2.addWeighted(cv_img, 1.0 + (amount - 1.0) * 1.5, blur, -(amount - 1.0) * 1.5, 0) | |
| return self._cv_to_pil(np.clip(clarity, 0, 255).astype(np.uint8)) | |
| elif op_name == "sharpen": | |
| # Procedural high-pass image sharpening | |
| sh_img = image.filter(ImageFilter.UnsharpMask(radius=1.0, percent=int(120 * amount), threshold=2)) | |
| return sh_img | |
| # 4. Portrait mode and retouching | |
| elif op_name == "portrait_retouch": | |
| # Cascade face smoothing + vignetting + details | |
| smooth = self.apply_operation(image, "skin_smooth", amount=amount) | |
| details = self.apply_operation(smooth, "clarity", amount=1.1) | |
| vignette = self.apply_operation(details, "vignette", amount=0.8) | |
| return vignette | |
| # 5. Lighting & Blur Effects | |
| elif op_name == "bloom": | |
| if cv2 is None: | |
| return image | |
| img_float = cv_img.astype(np.float32) | |
| gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) | |
| _, thresh = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) | |
| mask = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR) | |
| blur_size = int(max(15, min(image.width, image.height) // 20)) | |
| if blur_size % 2 == 0: | |
| blur_size += 1 | |
| glow = cv2.GaussianBlur(mask.astype(np.float32), (blur_size, blur_size), 0) | |
| bloom = img_float + glow * amount * 0.45 | |
| return self._cv_to_pil(np.clip(bloom, 0, 255).astype(np.uint8)) | |
| elif op_name == "vignette": | |
| if cv2 is None: | |
| return image | |
| rows, cols = cv_img.shape[:2] | |
| kx = cv2.getGaussianKernel(cols, cols * amount * 0.7) | |
| ky = cv2.getGaussianKernel(rows, rows * amount * 0.7) | |
| kernel = ky * kx.T | |
| mask = 255 * kernel / np.max(kernel) | |
| vignette = np.zeros_like(cv_img) | |
| for i in range(3): | |
| vignette[:, :, i] = cv_img[:, :, i] * (mask / 255.0) | |
| return self._cv_to_pil(vignette.astype(np.uint8)) | |
| elif op_name == "tilt_shift": | |
| if cv2 is None: | |
| return image | |
| blur_size = int(max(9, min(image.width, image.height) // 30)) | |
| if blur_size % 2 == 0: | |
| blur_size += 1 | |
| blurred = cv2.GaussianBlur(cv_img, (blur_size, blur_size), 0) | |
| rows, cols = cv_img.shape[:2] | |
| mask = np.zeros((rows, cols), dtype=np.float32) | |
| center = int(rows * 0.5) | |
| band = int(rows * 0.25) | |
| for y in range(rows): | |
| dist = abs(y - center) | |
| if dist < band: | |
| mask[y, :] = 0.0 | |
| elif dist < band * 2: | |
| mask[y, :] = (dist - band) / float(band) | |
| else: | |
| mask[y, :] = 1.0 | |
| mask_3d = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) | |
| tilt_shifted = cv_img.astype(np.float32) * (1.0 - mask_3d) + blurred.astype(np.float32) * mask_3d | |
| return self._cv_to_pil(np.clip(tilt_shifted, 0, 255).astype(np.uint8)) | |
| # 6. Styles | |
| elif op_name == "watercolor": | |
| if cv2 is None: | |
| return image | |
| stylized = cv2.stylization(cv_img, sigma_s=40, sigma_r=0.45) | |
| gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) | |
| edges = cv2.adaptiveThreshold( | |
| cv2.medianBlur(gray, 5), 255, | |
| cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 2 | |
| ) | |
| edges_color = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR) | |
| res = cv2.multiply(stylized, edges_color, scale=1.0/255.0) | |
| return self._cv_to_pil(res) | |
| elif op_name == "oil_painting": | |
| if cv2 is None: | |
| return image | |
| try: | |
| res = cv2.xphoto.oilPainting(cv_img, 4, 1) | |
| return self._cv_to_pil(res) | |
| except AttributeError: | |
| smooth = cv2.bilateralFilter(cv_img, 9, 75, 75) | |
| posterized = np.round(smooth / 32) * 32 | |
| return self._cv_to_pil(posterized.astype(np.uint8)) | |
| elif op_name == "super_res": | |
| # High-end super resolution upscaling | |
| w, h = int(image.width * scale), int(image.height * scale) | |
| return image.resize((w, h), _RESAMPLING.LANCZOS) | |
| elif op_name == "enhance": | |
| # Complete visual enhancement pipeline | |
| res = ImageEnhance.Color(image).enhance(1.08 * amount) | |
| res = ImageEnhance.Contrast(res).enhance(1.05 * amount) | |
| res = res.filter(ImageFilter.UnsharpMask(radius=1.0, percent=int(80 * amount), threshold=2)) | |
| return res | |
| return image | |
| def procedural_generate( | |
| self, | |
| prompt: str, | |
| size: tuple[int, int] = (768, 768), | |
| seed: Optional[int] = None, | |
| ) -> Image.Image: | |
| """Procedurally generate abstract placeholder backgrounds or scenes on CPU.""" | |
| if seed is not None: | |
| np.random.seed(seed) | |
| width, height = size | |
| img = Image.new("RGB", (width, height), color=(20, 24, 38)) | |
| draw = ImageDraw.Draw(img) | |
| prompt_lower = prompt.lower() | |
| if "sunset" in prompt_lower or "warm" in prompt_lower: | |
| # Generate a gorgeous warm sunset gradient | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(255 * (1.0 - ratio) + 240 * ratio) | |
| g = int(100 * (1.0 - ratio) + 80 * ratio) | |
| b = int(40 * (1.0 - ratio) + 120 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| elif "cyberpunk" in prompt_lower or "neon" in prompt_lower: | |
| # Cyberpunk neon matrix pattern | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(15 * (1.0 - ratio) + 180 * ratio) | |
| g = int(10 * (1.0 - ratio) + 10 * ratio) | |
| b = int(40 * (1.0 - ratio) + 220 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| else: | |
| # Elegant dark background gradient | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(24 * (1.0 - ratio) + 12 * ratio) | |
| g = int(28 * (1.0 - ratio) + 16 * ratio) | |
| b = int(44 * (1.0 - ratio) + 26 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| return img | |
| def segment_foreground(self, image: Image.Image) -> Image.Image: | |
| """Run automatic GrabCut segmentation to separate foreground subject from background.""" | |
| if cv2 is None: | |
| # Oval mask fallback if OpenCV is not available | |
| w, h = image.size | |
| pil_mask = Image.new("L", image.size, 0) | |
| draw = ImageDraw.Draw(pil_mask) | |
| draw.ellipse((int(w * 0.15), int(h * 0.15), int(w * 0.85), int(h * 0.85)), fill=255) | |
| return pil_mask.filter(ImageFilter.GaussianBlur(15)) | |
| cv_img = self._pil_to_cv(image) | |
| h, w = cv_img.shape[:2] | |
| mask = np.zeros((h, w), np.uint8) | |
| bgdModel = np.zeros((1, 65), np.float64) | |
| fgdModel = np.zeros((1, 65), np.float64) | |
| # Bounding box of the main content | |
| rect = (int(w * 0.05), int(h * 0.05), int(w * 0.9), int(h * 0.9)) | |
| try: | |
| cv2.grabCut(cv_img, mask, rect, bgdModel, fgdModel, 3, cv2.GC_INIT_WITH_RECT) | |
| bin_mask = np.where((mask == 2) | (mask == 0), 0, 1).astype("uint8") * 255 | |
| bin_mask_blur = cv2.GaussianBlur(bin_mask, (9, 9), 0) | |
| pil_mask = Image.fromarray(bin_mask_blur).convert("L") | |
| except Exception: | |
| # Simple oval mask fallback | |
| pil_mask = Image.new("L", image.size, 0) | |
| draw = ImageDraw.Draw(pil_mask) | |
| draw.ellipse((int(w * 0.15), int(h * 0.15), int(w * 0.85), int(h * 0.85)), fill=255) | |
| pil_mask = pil_mask.filter(ImageFilter.GaussianBlur(15)) | |
| return pil_mask | |
| # ========================================================================= | |
| # INTERNAL COMPOSITING PROCEDURAL PIPELINES | |
| # ========================================================================= | |
| def _inpaint_procedural(self, image: Image.Image, mask: Optional[Image.Image]) -> Image.Image: | |
| """Pure CPU object removal via OpenCV FMM inpainting or custom Pillow patching.""" | |
| if mask is None: | |
| return image | |
| cv_img = self._pil_to_cv(image) | |
| # Convert mask to CV grayscale | |
| mask_resized = mask.convert("L").resize(image.size, _RESAMPLING.NEAREST) | |
| cv_mask = np.array(mask_resized) | |
| if cv2 is not None: | |
| # High-end Fast Marching Method inpainting | |
| inpainted = cv2.inpaint(cv_img, cv_mask, 5, cv2.INPAINT_TELEA) | |
| return self._cv_to_pil(inpainted) | |
| # Soft fallback: Blur mask region with Pillow | |
| blurred = image.filter(ImageFilter.GaussianBlur(radius=15)) | |
| return Image.composite(blurred, image, mask_resized) | |
| def _background_replace_procedural( | |
| self, | |
| image: Image.Image, | |
| background: Optional[Image.Image], | |
| ) -> Image.Image: | |
| """Segment foreground and insert beautiful styled background.""" | |
| if cv2 is None: | |
| # Safe alpha blend fallback | |
| if background is None: | |
| bg = self.procedural_generate("dark classic", size=image.size) | |
| else: | |
| bg = background.resize(image.size, _RESAMPLING.LANCZOS) | |
| return Image.blend(image, bg, 0.5) | |
| cv_img = self._pil_to_cv(image) | |
| h, w = cv_img.shape[:2] | |
| # 1. Run automatic GrabCut segmentation | |
| mask = np.zeros((h, w), np.uint8) | |
| bgdModel = np.zeros((1, 65), np.float64) | |
| fgdModel = np.zeros((1, 65), np.float64) | |
| # Bounding box of the main content | |
| rect = (int(w * 0.05), int(h * 0.05), int(w * 0.9), int(h * 0.9)) | |
| try: | |
| cv2.grabCut(cv_img, mask, rect, bgdModel, fgdModel, 3, cv2.GC_INIT_WITH_RECT) | |
| # Create binary mask where 1 and 3 are foreground | |
| bin_mask = np.where((mask == 2) | (mask == 0), 0, 1).astype("uint8") * 255 | |
| # Soften mask edges | |
| bin_mask_blur = cv2.GaussianBlur(bin_mask, (9, 9), 0) | |
| pil_mask = Image.fromarray(bin_mask_blur).convert("L") | |
| except Exception: | |
| # Fallback to simple oval mask if GrabCut fails | |
| pil_mask = Image.new("L", image.size, 0) | |
| draw = ImageDraw.Draw(pil_mask) | |
| draw.ellipse((int(w * 0.1), int(h * 0.1), int(w * 0.9), int(h * 0.9)), fill=255) | |
| pil_mask = pil_mask.filter(ImageFilter.GaussianBlur(15)) | |
| # 2. Setup background | |
| if background is None: | |
| bg_pil = self.procedural_generate("sunset warm background", size=image.size) | |
| else: | |
| bg_pil = background.resize(image.size, _RESAMPLING.LANCZOS) | |
| # 3. Blend foreground and background | |
| return Image.composite(image, bg_pil, pil_mask) | |
| def _style_transfer_procedural( | |
| self, | |
| image: Image.Image, | |
| reference: Optional[Image.Image], | |
| ) -> Image.Image: | |
| """Transfer color palette / texture tone from reference image to target.""" | |
| if reference is None: | |
| return self.apply_operation(image, "watercolor") | |
| if cv2 is None: | |
| # Fallback blending | |
| return Image.blend(image, reference.resize(image.size), 0.3) | |
| # Transfer histogram style (mean & variance scaling in LAB space) | |
| src = self._pil_to_cv(image) | |
| ref = self._pil_to_cv(reference) | |
| src_lab = cv2.cvtColor(src, cv2.COLOR_BGR2LAB).astype(np.float32) | |
| ref_lab = cv2.cvtColor(ref, cv2.COLOR_BGR2LAB).astype(np.float32) | |
| s_mean, s_std = cv2.meanStdDev(src_lab) | |
| r_mean, r_std = cv2.meanStdDev(ref_lab) | |
| s_mean = s_mean.flatten() | |
| s_std = s_std.flatten() | |
| r_mean = r_mean.flatten() | |
| r_std = r_std.flatten() | |
| # Rescale src channels based on ref stats | |
| for i in range(3): | |
| src_lab[:, :, i] = ((src_lab[:, :, i] - s_mean[i]) * (r_std[i] / (s_std[i] + 1e-5))) + r_mean[i] | |
| src_lab[:, :, i] = np.clip(src_lab[:, :, i], 0, 255) | |
| res = cv2.cvtColor(src_lab.astype(np.uint8), cv2.COLOR_LAB2BGR) | |
| return self._cv_to_pil(res) | |
| # ========================================================================= | |
| # HELPERS | |
| # ========================================================================= | |
| def _pil_to_cv(self, img: Image.Image) -> np.ndarray: | |
| rgb = np.array(img.convert("RGB")) | |
| if cv2 is not None: | |
| return cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) | |
| return rgb | |
| def _cv_to_pil(self, cv_img: np.ndarray) -> Image.Image: | |
| if cv2 is not None: | |
| rgb = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) | |
| else: | |
| rgb = cv_img | |
| return Image.fromarray(rgb) | |
| class CVEditingEngine: | |
| """Wrapper class providing static methods as requested by server/editing_stack.py.""" | |
| _engine = CVEngine() | |
| def apply_white_balance(cls, image: Image.Image, amount: float) -> Image.Image: | |
| return cls._engine.apply_operation(image, "white_balance", amount=amount) | |
| def adjust_curves(cls, image: Image.Image, preset: str) -> Image.Image: | |
| return cls._engine.apply_operation(image, "curves", preset=preset) | |
| def apply_color_grade(cls, image: Image.Image, op_name: str) -> Image.Image: | |
| return cls._engine.apply_operation(image, op_name) | |
| def apply_watercolor(cls, image: Image.Image) -> Image.Image: | |
| return cls._engine.apply_operation(image, "watercolor") | |
| def apply_oil_painting(cls, image: Image.Image) -> Image.Image: | |
| return cls._engine.apply_operation(image, "oil_painting") | |
| def enhance_portrait_features(cls, image: Image.Image) -> Image.Image: | |
| return cls._engine.apply_operation(image, "portrait_retouch") | |
| def apply_local_contrast(cls, image: Image.Image, amount: float) -> Image.Image: | |
| return cls._engine.apply_operation(image, "clarity", amount=amount) | |
| def apply_bloom_glow(cls, image: Image.Image, amount: float) -> Image.Image: | |
| return cls._engine.apply_operation(image, "bloom", amount=amount) | |
| def apply_tilt_shift(cls, image: Image.Image, amount: float) -> Image.Image: | |
| return cls._engine.apply_operation(image, "tilt_shift", amount=amount) | |
| def apply_vignette(cls, image: Image.Image, amount: float) -> Image.Image: | |
| return cls._engine.apply_operation(image, "vignette", amount=amount) | |