""" Camera Forensic Realism Engine - Core Processing Pipeline v2 Complete rewrite focused on accurate iPhone ISP color science emulation. Instead of cheap "filter" effects, this implements the actual processing stages of Apple's Image Signal Processor: 1. iPhone Tone Mapping (S-curve with highlight rolloff + shadow lift) 2. Display P3 Color Rendering (wider gamut → natural rich colors) 3. Smart HDR Local Tone Mapping (adaptive micro-contrast) 4. Real Tone Skin Rendering (warm, natural skin preservation) 5. ISP Sharpening (unsharp mask like Apple's ISP) 6. Subtle Sensor Characteristics (minimal, tasteful noise + vignette) All processing operates on numpy float32 arrays in [0, 1] range. """ import numpy as np from typing import Optional # ============================================================================ # UTILITY: Color Space Conversions # ============================================================================ def _srgb_to_linear(img: np.ndarray) -> np.ndarray: """Decode sRGB gamma to linear light (IEC 61966-2-1).""" out = np.where( img <= 0.04045, img / 12.92, np.power(np.clip((img + 0.055) / 1.055, 0, None), 2.4) ) return out.astype(np.float32) def _linear_to_srgb(img: np.ndarray) -> np.ndarray: """Encode linear light to sRGB gamma.""" out = np.where( img <= 0.0031308, img * 12.92, 1.055 * np.power(np.clip(img, 0, None), 1.0 / 2.4) - 0.055 ) return np.clip(out, 0.0, 1.0).astype(np.float32) def _rgb_to_hsl(img: np.ndarray) -> np.ndarray: """Convert RGB [0,1] to HSL [0,1]. Vectorized.""" r, g, b = img[:, :, 0], img[:, :, 1], img[:, :, 2] cmax = np.maximum(np.maximum(r, g), b) cmin = np.minimum(np.minimum(r, g), b) delta = cmax - cmin # Lightness l = (cmax + cmin) / 2.0 # Saturation s = np.where(delta < 1e-7, 0.0, delta / (1.0 - np.abs(2.0 * l - 1.0) + 1e-7)) # Hue h = np.zeros_like(l) mask_r = (cmax == r) & (delta > 1e-7) mask_g = (cmax == g) & (delta > 1e-7) mask_b = (cmax == b) & (delta > 1e-7) h[mask_r] = ((g[mask_r] - b[mask_r]) / delta[mask_r]) % 6 h[mask_g] = ((b[mask_g] - r[mask_g]) / delta[mask_g]) + 2 h[mask_b] = ((r[mask_b] - g[mask_b]) / delta[mask_b]) + 4 h = h / 6.0 return np.stack([h, s, l], axis=-1).astype(np.float32) def _hsl_to_rgb(hsl: np.ndarray) -> np.ndarray: """Convert HSL [0,1] to RGB [0,1]. Vectorized.""" h, s, l = hsl[:, :, 0], hsl[:, :, 1], hsl[:, :, 2] c = (1.0 - np.abs(2.0 * l - 1.0)) * s x = c * (1.0 - np.abs((h * 6.0) % 2 - 1.0)) m = l - c / 2.0 h6 = h * 6.0 r = np.zeros_like(h) g = np.zeros_like(h) b = np.zeros_like(h) # Sector 0: [0, 1) mask = (h6 >= 0) & (h6 < 1) r[mask] = c[mask]; g[mask] = x[mask] # Sector 1: [1, 2) mask = (h6 >= 1) & (h6 < 2) r[mask] = x[mask]; g[mask] = c[mask] # Sector 2: [2, 3) mask = (h6 >= 2) & (h6 < 3) g[mask] = c[mask]; b[mask] = x[mask] # Sector 3: [3, 4) mask = (h6 >= 3) & (h6 < 4) g[mask] = x[mask]; b[mask] = c[mask] # Sector 4: [4, 5) mask = (h6 >= 4) & (h6 < 5) r[mask] = x[mask]; b[mask] = c[mask] # Sector 5: [5, 6) mask = (h6 >= 5) & (h6 < 6) r[mask] = c[mask]; b[mask] = x[mask] result = np.stack([r + m, g + m, b + m], axis=-1) return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 1: iPhone Tone Mapping # ============================================================================ def apply_iphone_tone_curve(image: np.ndarray, strength: float = 0.7, highlight_rolloff: float = 0.6, shadow_lift: float = 0.4, contrast: float = 0.5) -> np.ndarray: """Apply Apple-style S-curve tone mapping. iPhone ISP characteristics: - Smooth highlight compression (prevents clipping, preserves sky detail) - Shadow lifting (brightens dark areas for visibility without looking flat) - Mid-tone contrast boost (punchy but not harsh) - Filmic rolloff in highlights (smooth transition to white) All adjustable. The defaults emulate the iPhone 15 Pro "Standard" style. """ if strength <= 0: return image # Work in linear light for physically correct processing linear = _srgb_to_linear(image) # --- S-Curve via rational function (smoother than sigmoid) --- # This approximates Apple's proprietary tone curve: # - Lifts shadows gently (iPhone never crushes blacks) # - Boosts mid-tones for "punchy" look # - Rolls off highlights smoothly (filmic, not digital clip) # Shadow lift: raise the floor of the curve shadow_offset = shadow_lift * 0.03 * strength linear = linear + shadow_offset # Contrast S-curve using a smooth power function # Pivot around mid-gray (0.18 in linear = middle gray) pivot = 0.18 contrast_amount = 1.0 + contrast * 0.6 * strength # Apply per-channel contrast around pivot result = pivot * np.power(linear / (pivot + 1e-6), contrast_amount) # Highlight rolloff: compress highlights smoothly # Uses a soft-knee compression curve knee = 1.0 - highlight_rolloff * 0.3 * strength above_knee = result > knee if np.any(above_knee): excess = result[above_knee] - knee # Soft compression: maps excess through a log curve compressed = knee + (1.0 - knee) * (1.0 - np.exp(-excess / ((1.0 - knee) + 1e-6))) result[above_knee] = compressed # Convert back to sRGB result = _linear_to_srgb(np.clip(result, 0.0, 1.0)) # Blend with original output = image * (1.0 - strength) + result * strength return np.clip(output, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 2: Display P3 Color Rendering # ============================================================================ # Transform matrices (CIE XYZ as intermediate) # sRGB -> XYZ (D65) _SRGB_TO_XYZ = np.array([ [0.4124564, 0.3575761, 0.1804375], [0.2126729, 0.7151522, 0.0721750], [0.0193339, 0.1191920, 0.9503041], ], dtype=np.float32) # XYZ -> Display P3 (D65) _XYZ_TO_P3 = np.array([ [ 2.4934969, -0.9313836, -0.4027108], [-0.8294890, 1.7626641, 0.0236247], [ 0.0358458, -0.0761724, 0.9568845], ], dtype=np.float32) # Display P3 -> XYZ (D65) _P3_TO_XYZ = np.array([ [0.4865709, 0.2656677, 0.1982173], [0.2289746, 0.6917385, 0.0792869], [0.0000000, 0.0451134, 1.0439444], ], dtype=np.float32) # XYZ -> sRGB (D65) _XYZ_TO_SRGB = np.array([ [ 3.2404542, -1.5371385, -0.4985314], [-0.9692660, 1.8760108, 0.0415560], [ 0.0556434, -0.2040259, 1.0572252], ], dtype=np.float32) def apply_p3_color_rendering(image: np.ndarray, strength: float = 0.5, saturation_boost: float = 0.3, warmth: float = 0.3) -> np.ndarray: """Simulate iPhone's Display P3 wider gamut color rendering. iPhones capture and display in Display P3, which has ~25% more colors than sRGB, especially in reds and greens. This makes photos look richer and more vibrant without being garish. This stage: 1. Converts to linear light 2. Transforms from sRGB gamut to P3 gamut (expanding color range) 3. Applies subtle saturation and warmth adjustments 4. Converts back to sRGB with soft gamut clipping The result: richer reds, deeper greens, more vibrant skin tones — the signature "iPhone color pop" without oversaturation. """ if strength <= 0: return image # Convert to linear linear = _srgb_to_linear(image) h, w, _ = linear.shape pixels = linear.reshape(-1, 3) # sRGB -> XYZ -> P3 xyz = (pixels @ _SRGB_TO_XYZ.T) p3 = (xyz @ _XYZ_TO_P3.T) # In P3 space, apply subtle saturation boost # (This simulates the wider gamut's richer color reproduction) p3_mean = p3.mean(axis=1, keepdims=True) sat_factor = 1.0 + saturation_boost * 0.8 * strength p3_saturated = p3_mean + (p3 - p3_mean) * sat_factor # Apply warmth shift (iPhone's warm bias) # Slightly boost red, slightly reduce blue warmth_factor = warmth * strength p3_saturated[:, 0] *= (1.0 + 0.15 * warmth_factor) # Red warmer p3_saturated[:, 1] *= (1.0 + 0.04 * warmth_factor) # Green slight warm p3_saturated[:, 2] *= (1.0 - 0.12 * warmth_factor) # Blue cooler # P3 -> XYZ -> sRGB (with soft gamut clipping) xyz_back = (p3_saturated @ _P3_TO_XYZ.T) srgb_linear = (xyz_back @ _XYZ_TO_SRGB.T) # Soft gamut clip: instead of hard [0,1] clip, use smooth compression # This prevents color shifts at gamut boundaries srgb_linear = np.clip(srgb_linear, 0, None) over_mask = srgb_linear > 1.0 if np.any(over_mask): excess = srgb_linear[over_mask] - 1.0 srgb_linear[over_mask] = 1.0 - np.exp(-excess) * 0.05 srgb_linear = np.clip(srgb_linear, 0.0, 1.0).reshape(h, w, 3) # Back to sRGB gamma result = _linear_to_srgb(srgb_linear) output = image * (1.0 - strength) + result * strength return np.clip(output, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 3: Smart HDR Local Tone Mapping # ============================================================================ def _box_blur(img: np.ndarray, radius: int) -> np.ndarray: """Fast box blur using cumulative sum. Works on 2D arrays.""" if radius <= 0: return img # Horizontal pass cs = np.cumsum(img, axis=1) cs = np.pad(cs, ((0, 0), (radius, 0)), mode='constant') blurred = (cs[:, radius:] - cs[:, :-radius]) / radius # Vertical pass cs = np.cumsum(blurred, axis=0) cs = np.pad(cs, ((radius, 0), (0, 0)), mode='constant') blurred = (cs[radius:, :] - cs[:-radius, :]) / radius # Handle shape mismatch from integer division h, w = img.shape blurred = blurred[:h, :w] return blurred.astype(np.float32) def apply_local_tone_mapping(image: np.ndarray, strength: float = 0.4, detail_boost: float = 0.5, radius_pct: float = 0.08) -> np.ndarray: """Apply Smart HDR-inspired local tone mapping. Apple's Smart HDR captures multiple exposures and fuses them. We simulate the visual result: - Local contrast enhancement (makes textures pop) - Brings out detail in both shadows and highlights - Creates that "HDR but natural" iPhone look - Adds micro-contrast that makes the image feel sharp and alive Uses a fast approximation of unsharp-mask local tone mapping. """ if strength <= 0: return image h, w = image.shape[:2] radius = max(3, int(min(h, w) * radius_pct)) # Convert to luminance for local adaptation lum = 0.2126 * image[:, :, 0] + 0.7152 * image[:, :, 1] + 0.0722 * image[:, :, 2] # Compute local mean luminance (what Smart HDR adapts to) local_mean = _box_blur(lum, radius) # Local detail = difference between pixel and local average # This is the micro-contrast/texture information detail = lum - local_mean # Enhance detail (boost local contrast) detail_factor = 1.0 + detail_boost * strength * 2.5 enhanced_detail = detail * detail_factor # Build luminance correction map # Where local_mean is dark -> lighten (shadow lift) # Where local_mean is bright -> darken slightly (highlight recovery) target_mean = 0.5 # Compress toward middle gray correction = (target_mean - local_mean) * 0.7 * strength # Apply luminance correction per channel (preserves color ratios) lum_ratio = np.clip(lum, 1e-6, None) # New luminance = corrected local mean + enhanced detail new_lum = local_mean + correction + enhanced_detail new_lum = np.clip(new_lum, 0.0, 1.0) # Scale RGB channels by luminance change ratio scale = (new_lum / lum_ratio)[:, :, np.newaxis] scale = np.clip(scale, 0.5, 2.0) # Prevent extreme scaling result = image * scale # Blend with original output = image * (1.0 - strength) + result * strength return np.clip(output, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 4: Real Tone Skin Rendering # ============================================================================ def apply_skin_tone_rendering(image: np.ndarray, strength: float = 0.5, warmth: float = 0.5, smoothness: float = 0.3) -> np.ndarray: """Apply Apple's Real Tone skin rendering. Apple specifically optimizes skin tone reproduction: - Detects skin-tone hue ranges (orange-ish, 15-45 degree hue) - Applies warmth while preventing oversaturation - Slightly reduces chroma in very saturated skin to prevent "orange look" - Preserves natural variation across skin tones This works in HSL space for precise skin tone targeting. """ if strength <= 0: return image hsl = _rgb_to_hsl(image) h, s, l = hsl[:, :, 0], hsl[:, :, 1], hsl[:, :, 2] # Skin tone detection mask (hue range ~15-50 degrees = 0.04-0.14 in [0,1]) # with saturation > 0.15 and luminance in reasonable range skin_hue_center = 0.08 # ~30 degrees (warm skin tone center) skin_hue_width = 0.12 # ~43 degrees on each side hue_dist = np.minimum(np.abs(h - skin_hue_center), np.abs(h - skin_hue_center + 1.0)) hue_dist = np.minimum(hue_dist, np.abs(h - skin_hue_center - 1.0)) # Smooth falloff skin mask (0 = not skin, 1 = definitely skin) skin_mask = np.exp(-0.5 * (hue_dist / (skin_hue_width + 1e-6)) ** 2) skin_mask *= np.clip(s * 4.0, 0, 1) # Need some saturation skin_mask *= np.clip(1.0 - np.abs(l - 0.5) * 3.0, 0, 1) # Not too dark/bright skin_mask = skin_mask.astype(np.float32) # Apply effects ONLY to skin regions effect_strength = skin_mask * strength # 1. Warmth: shift skin hue slightly toward warm (lower hue = more red-orange) h_shift = -0.06 * warmth # Subtle warmth push h_new = h + h_shift * effect_strength # 2. Saturation control: prevent oversaturated orange skin # Reduce saturation for overly-saturated skin tones s_target = np.clip(s, 0, 0.45) # iPhone caps skin saturation to prevent "orange face" s_new = s + (s_target - s) * effect_strength * 0.7 # 3. Luminance: slight brighten for skin (Real Tone lifts skin slightly) l_boost = 0.12 * strength l_new = l + l_boost * effect_strength # Recombine hsl_new = np.stack([h_new % 1.0, np.clip(s_new, 0, 1), np.clip(l_new, 0, 1)], axis=-1).astype(np.float32) result = _hsl_to_rgb(hsl_new) return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 5: Deep Fusion Texture Crunch (Edge-Preserving Frequency Separation) # ============================================================================ def apply_deep_fusion_crunch(image: np.ndarray, strength: float = 0.5, texture_frequency: float = 0.5) -> np.ndarray: """Simulate Apple's Deep Fusion algorithm. Rather than a basic unsharp mask which creates halos around high-contrast edges (like a tree against a sky) and amplifies noise in flat areas, Deep Fusion isolates mid-to-high frequency *textures* (fabric, skin pores) and massively amplifies them, using an edge-preserving variance mask. """ if strength <= 0: return image h, w = image.shape[:2] # Extract luminance lum = 0.2126 * image[:, :, 0] + 0.7152 * image[:, :, 1] + 0.0722 * image[:, :, 2] # 1. Fast Edge-Preserving Low-Pass Filter (Guided/Bilateral Approximation) # We compute local variance to identify hard edges vs textures vs flat areas. radius = max(2, int(min(h, w) * 0.015 * texture_frequency)) # Base structure radius # E[X] and E[X^2] for variance mean_lum = _box_blur(lum, radius) mean_lum_sq = _box_blur(lum * lum, radius) # Local Variance: V(X) = E[X^2] - (E[X])^2 var_lum = np.clip(mean_lum_sq - mean_lum * mean_lum, 0.0, None) # Scale variance to use as a mask # Flat areas have var ~0. Hard edges have high var. # Textures exist in the middle variance band. edge_threshold = 0.05 edge_mask = np.clip(var_lum / edge_threshold, 0.0, 1.0) # The detail layer (high frequencies) detail = lum - mean_lum # We want to amplify details where it is NOT a hard edge (where edge_mask is low) # but we also don't want to amplify pure flat noise. # So we enhance based on an inverse edge mask. texture_mask = 1.0 - edge_mask # Deep Fusion is aggressive. At max strength we're multiplying texture by 3x. crunch_factor = strength * 2.5 amplified_detail = detail * (1.0 + crunch_factor * texture_mask) # Recombine luminance new_lum = mean_lum + amplified_detail new_lum = np.clip(new_lum, 0.0, 1.0) # Scale RGB by luminance ratio lum_safe = np.clip(lum, 1e-6, None) scale = (new_lum / lum_safe)[:, :, np.newaxis] scale = np.clip(scale, 0.5, 2.0) result = image * scale return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 6: ISP Sharpening (Unsharp Mask) # ============================================================================ def apply_isp_sharpening(image: np.ndarray, strength: float = 0.3, radius_pct: float = 0.005) -> np.ndarray: """Apply iPhone ISP-style unsharp masking. Apple's ISP applies subtle, intelligent sharpening: - Operates primarily on luminance (avoids color artifacts) - Small radius (captures fine detail like skin pores, hair) - Moderate strength (visible but not crunchy) - Slightly halos permitted (signature iPhone "crispness") """ if strength <= 0: return image h, w = image.shape[:2] radius = max(1, int(min(h, w) * radius_pct)) # Extract luminance lum = 0.2126 * image[:, :, 0] + 0.7152 * image[:, :, 1] + 0.0722 * image[:, :, 2] # Blur for unsharp mask blurred = _box_blur(lum, radius) # High-frequency detail detail = lum - blurred # Apply sharpening via luminance scaling sharp_amount = strength * 1.5 sharpened_lum = lum + detail * sharp_amount sharpened_lum = np.clip(sharpened_lum, 0.0, 1.0) # Scale RGB channels by luminance change lum_safe = np.clip(lum, 1e-6, None) scale = (sharpened_lum / lum_safe)[:, :, np.newaxis] scale = np.clip(scale, 0.7, 1.5) # Prevent extreme scaling result = image * scale return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 7: Subtle Sensor Characteristics # ============================================================================ def apply_sensor_character(image: np.ndarray, strength: float = 0.3, noise_amount: float = 0.3, vignette_amount: float = 0.4, seed: int = 42) -> np.ndarray: """Apply subtle real-camera sensor characteristics. Minimal, tasteful, not a filter: - Very subtle luminance-only noise (like iPhone at ISO 200-400) - Gentle vignette matching iPhone's f/1.78 lens - Not the main effect — just finishing touches """ if strength <= 0: return image h, w = image.shape[:2] result = image.copy() rng = np.random.default_rng(seed) # --- Subtle luminance noise (iPhone-like, barely visible) --- if noise_amount > 0: # iPhone noise is very fine-grained, mostly in shadows lum = 0.2126 * image[:, :, 0] + 0.7152 * image[:, :, 1] + 0.0722 * image[:, :, 2] # More noise in shadows, less in highlights (like real sensors) noise_scale = (1.0 - lum) * 0.015 * noise_amount * strength noise = rng.normal(0, 1, (h, w)).astype(np.float32) * noise_scale # Apply noise to all channels equally (luminance-style noise) result = result + noise[:, :, np.newaxis] # --- Gentle vignette (iPhone f/1.78 lens) --- if vignette_amount > 0: y = np.linspace(-1, 1, h, dtype=np.float32) x = np.linspace(-1, 1, w, dtype=np.float32) xx, yy = np.meshgrid(x, y) r = np.sqrt(xx ** 2 + yy ** 2) r_norm = r / np.sqrt(2.0) # iPhone vignette is very gentle, almost imperceptible vig = 1.0 - vignette_amount * 0.12 * strength * np.power(r_norm, 2.5) vig = np.clip(vig, 0.0, 1.0).astype(np.float32) result = result * vig[:, :, np.newaxis] return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 8: Color Temperature & White Balance # ============================================================================ def apply_white_balance(image: np.ndarray, strength: float = 0.5, temperature: float = 0.3, tint: float = 0.0) -> np.ndarray: """Apply iPhone-style automatic white balance correction. iPhone AWB tends toward: - Slightly warm (daylight bias) - Neutral tint (no green/magenta shift) - Accurate gray point targeting Temperature: -1 (cool/blue) to +1 (warm/orange). Default 0.3 = iPhone warm. Tint: -1 (green) to +1 (magenta). Default 0 = neutral. """ if strength <= 0: return image result = image.copy() # Temperature: warm = boost red/yellow, cool = boost blue temp_effect = temperature * 0.25 * strength result[:, :, 0] = result[:, :, 0] * (1.0 + temp_effect * 1.0) # Red result[:, :, 1] = result[:, :, 1] * (1.0 + temp_effect * 0.3) # Green (slight) result[:, :, 2] = result[:, :, 2] * (1.0 - temp_effect * 0.8) # Blue # Tint: green-magenta axis tint_effect = tint * 0.2 * strength result[:, :, 1] = result[:, :, 1] * (1.0 + tint_effect) # Green return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 9: iPhone Blue Shadow Tint (HDR Artifact) # ============================================================================ def apply_blue_shadow_tint(image: np.ndarray, strength: float = 0.4, shadow_threshold: float = 0.35, blue_intensity: float = 0.5) -> np.ndarray: """Apply iPhone's characteristic blue-ish tint in dark areas. iPhones are known for lifting shadows with a subtle blue/purple cast. This comes from HDR processing: when the ISP lifts shadow detail, it shifts the blacks toward blue instead of pure neutral. This is a very recognizable iPhone trait — dark clothes, hair, and shadows all have this subtle blue tint in iPhone photos. Uses luminance-based masking with smooth cubic falloff so only genuinely dark areas receive the tint. """ if strength <= 0: return image # Compute luminance lum = 0.2126 * image[:, :, 0] + 0.7152 * image[:, :, 1] + 0.0722 * image[:, :, 2] # Shadow mask: 1.0 in deepest shadows, smooth falloff to 0.0 # Uses cubic falloff for natural transition shadow_mask = np.clip((shadow_threshold - lum) / (shadow_threshold + 1e-6), 0.0, 1.0) shadow_mask = shadow_mask ** 2 # Smooth cubic-like falloff shadow_mask = (shadow_mask * strength)[:, :, np.newaxis].astype(np.float32) # Blue tint color shift: # Slightly reduce red + green in shadows, boost blue # This creates that characteristic iPhone blue-black look tint = np.zeros_like(image) tint[:, :, 0] = -0.15 * blue_intensity # Reduce red in shadows tint[:, :, 1] = -0.05 * blue_intensity # Slightly reduce green tint[:, :, 2] = +0.25 * blue_intensity # Boost blue in shadows result = image + tint * shadow_mask return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # STAGE 10: Highlight Warmth (iPhone golden highlights) # ============================================================================ def apply_highlight_warmth(image: np.ndarray, strength: float = 0.3, highlight_threshold: float = 0.7, warmth: float = 0.5) -> np.ndarray: """Apply warm golden tint to highlights (iPhone characteristic). iPhone photos have subtly warm highlights — sunlit areas, bright skin, and light sources all have a gentle golden quality. This complements the blue shadows to create the full iPhone color signature: warm highlights + cool/blue shadows = the iPhone look. """ if strength <= 0: return image lum = 0.2126 * image[:, :, 0] + 0.7152 * image[:, :, 1] + 0.0722 * image[:, :, 2] # Highlight mask: smooth ramp in bright areas hl_mask = np.clip((lum - highlight_threshold) / (1.0 - highlight_threshold + 1e-6), 0.0, 1.0) hl_mask = (hl_mask * strength)[:, :, np.newaxis].astype(np.float32) # Golden warmth: boost red/yellow in highlights tint = np.zeros_like(image) tint[:, :, 0] = +0.18 * warmth # Warm red tint[:, :, 1] = +0.06 * warmth # Slight green-yellow tint[:, :, 2] = -0.12 * warmth # Reduce blue in highlights result = image + tint * hl_mask return np.clip(result, 0.0, 1.0).astype(np.float32) # ============================================================================ # MASTER PIPELINE # ============================================================================ def process_iphone_realism(image: np.ndarray, master_strength: float = 0.7, # Tone mapping enable_tone_mapping: bool = True, tone_strength: float = 0.6, highlight_rolloff: float = 0.5, shadow_lift: float = 0.4, contrast: float = 0.5, # P3 Color enable_p3_color: bool = True, color_strength: float = 0.5, color_saturation: float = 0.3, color_warmth: float = 0.3, # Local tone mapping enable_local_tone: bool = True, local_tone_strength: float = 0.35, detail_boost: float = 0.4, # Skin rendering enable_skin_rendering: bool = True, skin_strength: float = 0.5, skin_warmth: float = 0.4, # Deep Fusion Texture Crunch enable_deep_fusion: bool = True, fusion_strength: float = 0.6, fusion_texture_freq: float = 0.5, # White balance enable_white_balance: bool = True, wb_strength: float = 0.5, wb_temperature: float = 0.25, wb_tint: float = 0.0, # Color grading (blue shadows + warm highlights) enable_color_grading: bool = True, blue_shadows: float = 0.4, warm_highlights: float = 0.3, # Sharpening enable_sharpening: bool = True, sharpen_strength: float = 0.3, # Sensor characteristics enable_sensor: bool = True, sensor_strength: float = 0.25, sensor_noise: float = 0.3, sensor_vignette: float = 0.4, seed: int = 42) -> np.ndarray: """Run the full iPhone ISP Realism pipeline. Processing order matches Apple's actual ISP pipeline: 1. White balance correction (first in real ISP) 2. Tone mapping (exposure + S-curve) 3. Display P3 color rendering (gamut + color science) 4. Smart HDR local tone mapping (multi-frame fusion emulation) 5. Real Tone skin rendering (skin-specific optimization) 6. Deep Fusion texture crunch (edge-preserving high-frequency boost) 7. Color grading (blue shadows + warm highlights) 8. ISP sharpening (crispness) 9. Sensor characteristics (subtle noise + vignette) """ result = image.copy() # Stage 1: White Balance if enable_white_balance: result = apply_white_balance(result, wb_strength * master_strength, wb_temperature, wb_tint) # Stage 2: Tone Mapping if enable_tone_mapping: result = apply_iphone_tone_curve(result, tone_strength * master_strength, highlight_rolloff, shadow_lift, contrast) # Stage 3: P3 Color Rendering if enable_p3_color: result = apply_p3_color_rendering(result, color_strength * master_strength, color_saturation, color_warmth) # Stage 4: Local Tone Mapping if enable_local_tone: result = apply_local_tone_mapping(result, local_tone_strength * master_strength, detail_boost) # Stage 5: Skin Rendering if enable_skin_rendering: result = apply_skin_tone_rendering(result, skin_strength * master_strength, skin_warmth) # Stage 6: Deep Fusion Texture Crunch if enable_deep_fusion: result = apply_deep_fusion_crunch(result, fusion_strength * master_strength, fusion_texture_freq) # Stage 7: Color Grading (iPhone signature: blue shadows + warm highlights) if enable_color_grading: result = apply_blue_shadow_tint(result, blue_shadows * master_strength) result = apply_highlight_warmth(result, warm_highlights * master_strength) # Stage 8: Sharpening if enable_sharpening: result = apply_isp_sharpening(result, sharpen_strength * master_strength) # Stage 9: Sensor Character if enable_sensor: result = apply_sensor_character(result, sensor_strength * master_strength, sensor_noise, sensor_vignette, seed) return np.clip(result, 0.0, 1.0).astype(np.float32)