naari-avatar / preprocess.py
GaneshGowri's picture
v3.1.0 - Add background artifact fix for ponytail issue
abe5aa4 verified
Raw
History Blame Contribute Delete
10.6 kB
"""
Naari Studio VTON - Preprocessing Module
=========================================
Shared image preprocessing utilities for all engines.
v3.1.0 - Enhanced with background artifact fixes
Author: AnahataSri (Naari Studio)
License: MIT
"""
import logging
from typing import Tuple, Optional
from PIL import Image, ImageStat, ImageFilter
import numpy as np
from config import MAX_IMAGE_SIZE, DIFFUSION_SIZE
logger = logging.getLogger(__name__)
# =============================================================================
# IMAGE VALIDATION
# =============================================================================
def validate_image(image: Image.Image, name: str = "Image") -> Tuple[bool, str]:
"""
Validate an input image.
Args:
image: PIL Image to validate
name: Name for error messages
Returns:
Tuple of (is_valid, error_message)
"""
if image is None:
return False, f"❌ {name} is required"
# Check if it's a valid PIL Image
try:
width, height = image.size
except:
return False, f"❌ {name} is not a valid image"
# Check minimum size
if width < 64 or height < 64:
return False, f"❌ {name} is too small (minimum 64x64)"
# Check maximum size
if width > 4096 or height > 4096:
return False, f"❌ {name} is too large (maximum 4096x4096)"
return True, ""
def validate_inputs(
person_image: Image.Image,
garment_image: Image.Image
) -> Tuple[bool, str]:
"""
Validate both person and garment images.
Args:
person_image: PIL Image of person
garment_image: PIL Image of garment
Returns:
Tuple of (is_valid, error_message)
"""
is_valid, msg = validate_image(person_image, "Person image")
if not is_valid:
return False, msg
is_valid, msg = validate_image(garment_image, "Garment image")
if not is_valid:
return False, msg
return True, ""
# =============================================================================
# BACKGROUND ARTIFACT FIX
# =============================================================================
def detect_dark_background(image: Image.Image, threshold: int = 60) -> bool:
"""
Detect if image has a predominantly dark background.
Dark backgrounds can be misinterpreted as hair by the model.
Args:
image: PIL Image to analyze
threshold: Brightness threshold (0-255). Below this is considered dark.
Returns:
True if background appears dark
"""
# Sample edges to check background
width, height = image.size
# Sample strips from edges (10% of width/height)
edge_width = max(10, width // 10)
edge_height = max(10, height // 10)
# Get edge regions
top = image.crop((0, 0, width, edge_height))
bottom = image.crop((0, height - edge_height, width, height))
left = image.crop((0, 0, edge_width, height))
right = image.crop((width - edge_width, 0, width, height))
# Calculate average brightness of edges
edge_brightnesses = []
for edge in [top, bottom, left, right]:
stat = ImageStat.Stat(edge.convert('L'))
edge_brightnesses.append(stat.mean[0])
avg_edge_brightness = sum(edge_brightnesses) / len(edge_brightnesses)
logger.info(f"Edge brightness: {avg_edge_brightness:.1f}")
return avg_edge_brightness < threshold
def replace_dark_background(
image: Image.Image,
target_color: Tuple[int, int, int] = (230, 230, 230),
edge_threshold: int = 60
) -> Image.Image:
"""
Replace dark background with a neutral light color.
This prevents the model from misinterpreting dark backgrounds as hair.
Args:
image: PIL Image to process
target_color: RGB color to use for background (default: light gray)
edge_threshold: Brightness threshold for edge detection
Returns:
Image with replaced background
"""
# Convert to RGB if needed
if image.mode != 'RGB':
image = image.convert('RGB')
# Create a simple edge-based mask
# This is a lightweight approach that doesn't require heavy models
img_array = np.array(image)
# Convert to grayscale for brightness detection
gray = np.array(image.convert('L'))
# Create mask where pixels are darker than threshold
dark_mask = gray < edge_threshold
# Dilate edges slightly to ensure full coverage
from scipy import ndimage
dark_mask = ndimage.binary_dilation(dark_mask, iterations=2)
# Only replace pixels that are likely background (edges + dark)
width, height = image.size
edge_margin = min(width, height) // 8
# Create edge mask
edge_mask = np.zeros_like(dark_mask)
edge_mask[:edge_margin, :] = True # Top
edge_mask[-edge_margin:, :] = True # Bottom
edge_mask[:, :edge_margin] = True # Left
edge_mask[:, -edge_margin:] = True # Right
# Combine: only replace dark pixels at edges
replace_mask = dark_mask & edge_mask
# Replace background
result = img_array.copy()
result[replace_mask] = target_color
logger.info(f"Replaced {replace_mask.sum()} background pixels")
return Image.fromarray(result)
# =============================================================================
# IMAGE NORMALIZATION
# =============================================================================
def normalize_image(
image: Image.Image,
max_size: int = None,
target_mode: str = "RGB",
fix_background: bool = True
) -> Image.Image:
"""
Normalize an image for processing.
- Converts to target color mode
- Resizes if larger than max_size while maintaining aspect ratio
- Optionally fixes dark background artifacts
Args:
image: PIL Image to normalize
max_size: Maximum dimension (uses config default if None)
target_mode: Target color mode (default RGB)
fix_background: Whether to fix dark background artifacts
Returns:
Normalized PIL Image
"""
max_size = max_size or MAX_IMAGE_SIZE
# Convert color mode
if image.mode != target_mode:
image = image.convert(target_mode)
# Fix dark background if enabled
if fix_background:
if detect_dark_background(image):
logger.info("Dark background detected, applying fix")
try:
image = replace_dark_background(image)
except Exception as e:
logger.warning(f"Background fix failed: {e}, continuing without fix")
# Resize if too large
width, height = image.size
if max(width, height) > max_size:
ratio = max_size / max(width, height)
new_width = int(width * ratio)
new_height = int(height * ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
logger.info(f"Resized image from {width}x{height} to {new_width}x{new_height}")
return image
def resize_for_diffusion(
image: Image.Image,
target_size: int = None
) -> Image.Image:
"""
Resize image for diffusion models.
Ensures dimensions are multiples of 8 for VAE compatibility.
Args:
image: PIL Image to resize
target_size: Target max dimension (uses config default if None)
Returns:
Resized PIL Image with dimensions divisible by 8
"""
target_size = target_size or DIFFUSION_SIZE
width, height = image.size
# Calculate new size maintaining aspect ratio
if max(width, height) > target_size:
ratio = target_size / max(width, height)
width = int(width * ratio)
height = int(height * ratio)
# Round to nearest multiple of 8
width = (width // 8) * 8
height = (height // 8) * 8
# Ensure minimum size
width = max(width, 64)
height = max(height, 64)
return image.resize((width, height), Image.LANCZOS)
# =============================================================================
# INPUT PREPROCESSING
# =============================================================================
def preprocess_inputs(
person_image: Image.Image,
garment_image: Image.Image,
fix_background: bool = True
) -> Tuple[Image.Image, Image.Image, str]:
"""
Preprocess person and garment images for try-on.
This is the main preprocessing function that:
1. Validates both images
2. Normalizes color modes
3. Fixes dark background artifacts (person image only)
4. Resizes to appropriate dimensions
Args:
person_image: PIL Image of person
garment_image: PIL Image of garment
fix_background: Whether to apply background artifact fixes
Returns:
Tuple of (processed_person, processed_garment, status_message)
If validation fails, returns (None, None, error_message)
"""
# Validate
is_valid, error_msg = validate_inputs(person_image, garment_image)
if not is_valid:
return None, None, error_msg
# Normalize person image with background fix
person_processed = normalize_image(person_image, fix_background=fix_background)
# Normalize garment image without background fix (not needed)
garment_processed = normalize_image(garment_image, fix_background=False)
logger.info(f"Preprocessed: person={person_processed.size}, garment={garment_processed.size}")
return person_processed, garment_processed, "✅ Images preprocessed"
# =============================================================================
# NUMPY CONVERSION UTILITIES
# =============================================================================
def pil_to_numpy(image: Image.Image) -> np.ndarray:
"""Convert PIL Image to numpy array."""
return np.array(image)
def numpy_to_pil(array: np.ndarray) -> Image.Image:
"""Convert numpy array to PIL Image."""
return Image.fromarray(array.astype(np.uint8))
def ensure_pil(image) -> Optional[Image.Image]:
"""
Ensure input is a PIL Image.
Handles numpy arrays and existing PIL Images.
Args:
image: Input (numpy array or PIL Image)
Returns:
PIL Image or None if conversion fails
"""
if image is None:
return None
if isinstance(image, Image.Image):
return image
if isinstance(image, np.ndarray):
return numpy_to_pil(image)
logger.warning(f"Unknown image type: {type(image)}")
return None