ArchEnhancer / backend /services /postprocess.py
Aguilar Elizondo
Initial commit: Architecture AI Enhancer v1.0.0
6bfa765
Raw
History Blame Contribute Delete
7.95 kB
"""
Post-Processing Service
Applies subtle photographic enhancements to the generated images:
- Local contrast enhancement (CLAHE)
- Film grain simulation
- Subtle vignette effect
- Color grading adjustments
"""
import logging
import numpy as np
from PIL import Image, ImageEnhance, ImageFilter
import cv2
logger = logging.getLogger(__name__)
def apply_clahe(image: Image.Image, clip_limit: float = 2.0) -> Image.Image:
"""
Apply Contrast Limited Adaptive Histogram Equalization (CLAHE)
This enhances local contrast without over-amplifying noise.
Args:
image: Input PIL Image
clip_limit: Threshold for contrast limiting (higher = more contrast)
Returns:
Enhanced PIL Image
"""
# Convert to numpy array
img_np = np.array(image)
# Convert to LAB color space
lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB)
# Split channels
l, a, b = cv2.split(lab)
# Apply CLAHE to L channel
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(8, 8))
l_clahe = clahe.apply(l)
# Merge channels
lab_clahe = cv2.merge([l_clahe, a, b])
# Convert back to RGB
rgb = cv2.cvtColor(lab_clahe, cv2.COLOR_LAB2RGB)
return Image.fromarray(rgb)
def add_film_grain(
image: Image.Image,
intensity: float = 0.02,
grain_size: float = 1.0
) -> Image.Image:
"""
Add subtle film grain for a more organic look
Args:
image: Input PIL Image
intensity: Strength of the grain effect (0.01-0.05 recommended)
grain_size: Size of grain particles
Returns:
Image with film grain
"""
img_np = np.array(image).astype(np.float32) / 255.0
# Generate noise
noise = np.random.normal(0, intensity, img_np.shape)
# Optional: blur noise for larger grain
if grain_size > 1.0:
noise = cv2.GaussianBlur(noise, (0, 0), grain_size)
# Add noise to image
noisy = img_np + noise
noisy = np.clip(noisy, 0, 1)
# Convert back to uint8
result = (noisy * 255).astype(np.uint8)
return Image.fromarray(result)
def apply_vignette(
image: Image.Image,
strength: float = 0.3,
radius: float = 0.8
) -> Image.Image:
"""
Apply a subtle vignette effect
Darkens the corners and edges of the image to draw focus to the center.
Args:
image: Input PIL Image
strength: Vignette intensity (0-1)
radius: Radius of the unaffected center area (0-1)
Returns:
Image with vignette
"""
width, height = image.size
img_np = np.array(image).astype(np.float32)
# Create coordinate grids
x = np.linspace(-1, 1, width)
y = np.linspace(-1, 1, height)
X, Y = np.meshgrid(x, y)
# Calculate distance from center
distance = np.sqrt(X**2 + Y**2)
# Create vignette mask
vignette = 1 - np.clip((distance - radius) / (1 - radius), 0, 1) * strength
vignette = vignette[:, :, np.newaxis] # Add channel dimension
# Apply vignette
result = img_np * vignette
result = np.clip(result, 0, 255).astype(np.uint8)
return Image.fromarray(result)
def enhance_colors(
image: Image.Image,
saturation: float = 1.1,
contrast: float = 1.05,
brightness: float = 1.0
) -> Image.Image:
"""
Apply subtle color grading adjustments
Args:
image: Input PIL Image
saturation: Saturation multiplier (1.0 = no change)
contrast: Contrast multiplier (1.0 = no change)
brightness: Brightness multiplier (1.0 = no change)
Returns:
Color-graded image
"""
# Adjust saturation
if saturation != 1.0:
enhancer = ImageEnhance.Color(image)
image = enhancer.enhance(saturation)
# Adjust contrast
if contrast != 1.0:
enhancer = ImageEnhance.Contrast(image)
image = enhancer.enhance(contrast)
# Adjust brightness
if brightness != 1.0:
enhancer = ImageEnhance.Brightness(image)
image = enhancer.enhance(brightness)
return image
def sharpen_image(image: Image.Image, strength: float = 1.0) -> Image.Image:
"""
Apply subtle sharpening
Args:
image: Input PIL Image
strength: Sharpening strength (0-2 recommended)
Returns:
Sharpened image
"""
if strength <= 0:
return image
# Use UnsharpMask for better control
from PIL import ImageFilter
# Blend between original and sharpened
sharpened = image.filter(ImageFilter.UnsharpMask(radius=1, percent=150, threshold=3))
if strength < 1.0:
# Blend with original
return Image.blend(image, sharpened, strength)
else:
return sharpened
def postprocess_image(
image: Image.Image,
apply_contrast: bool = True,
apply_grain: bool = True,
apply_vignette_effect: bool = True,
apply_color_grading: bool = True,
apply_sharpening: bool = True
) -> Image.Image:
"""
Apply complete post-processing pipeline
This function orchestrates all post-processing effects in the optimal order:
1. Local contrast enhancement (CLAHE)
2. Color grading
3. Sharpening
4. Film grain
5. Vignette
Args:
image: Input PIL Image
apply_contrast: Enable local contrast enhancement
apply_grain: Enable film grain
apply_vignette_effect: Enable vignette
apply_color_grading: Enable color adjustments
apply_sharpening: Enable sharpening
Returns:
Post-processed PIL Image
"""
logger.info("Starting post-processing")
try:
# Step 1: Local contrast
if apply_contrast:
logger.debug("Applying CLAHE")
image = apply_clahe(image, clip_limit=2.0)
# Step 2: Color grading
if apply_color_grading:
logger.debug("Applying color grading")
image = enhance_colors(
image,
saturation=1.08, # Slightly more saturated
contrast=1.03, # Slightly more contrast
brightness=1.0 # No brightness change
)
# Step 3: Sharpening
if apply_sharpening:
logger.debug("Applying sharpening")
image = sharpen_image(image, strength=0.6)
# Step 4: Film grain
if apply_grain:
logger.debug("Adding film grain")
image = add_film_grain(image, intensity=0.015, grain_size=1.2)
# Step 5: Vignette
if apply_vignette_effect:
logger.debug("Applying vignette")
image = apply_vignette(image, strength=0.2, radius=0.85)
logger.info("Post-processing completed")
return image
except Exception as e:
logger.error(f"Error in post-processing: {e}", exc_info=True)
logger.warning("Returning original image")
return image
def create_comparison(
original: Image.Image,
processed: Image.Image,
padding: int = 10
) -> Image.Image:
"""
Create a side-by-side comparison image
Useful for visualizing before/after results.
Args:
original: Original image
processed: Processed image
padding: Space between images in pixels
Returns:
Combined comparison image
"""
# Ensure both images are the same size
if original.size != processed.size:
processed = processed.resize(original.size, Image.LANCZOS)
width, height = original.size
# Create new image with space for both
comparison = Image.new('RGB', (width * 2 + padding, height), color='white')
# Paste images
comparison.paste(original, (0, 0))
comparison.paste(processed, (width + padding, 0))
return comparison