Files
novafarma/scripts/remove_bg_advanced.py
David Kotnik d0043ec390 Advanced background removal tool with auto-detection and edge preservation
- Auto-detects white/black/colored backgrounds
- Preserves cartoon outlines using gradient edge detection
- Aggressive dark gray removal (tolerance=60 for black bg)
- Smooth feathering to avoid jagged edges
- Test files show clean removal without outline damage
2025-12-31 00:13:16 +01:00

282 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""
Advanced background removal for game assets:
- Detects background color (white, black, or other)
- Preserves bold cartoon outlines
- Creates clean transparent PNGs
- Works with dual-style system
"""
import os
from pathlib import Path
from PIL import Image, ImageChops
import numpy as np
def detect_background_color(img):
"""
Detect dominant background color by sampling corners
Returns: 'white', 'black', 'color', or None
"""
width, height = img.size
# Sample corner pixels (10x10 from each corner)
corners = [
img.crop((0, 0, 10, 10)), # Top-left
img.crop((width-10, 0, width, 10)), # Top-right
img.crop((0, height-10, 10, height)), # Bottom-left
img.crop((width-10, height-10, width, height)) # Bottom-right
]
# Get average color from corners
corner_colors = []
for corner in corners:
corner_array = np.array(corner)
avg_color = corner_array.mean(axis=(0, 1))
corner_colors.append(avg_color)
avg_bg = np.mean(corner_colors, axis=0)
# Determine background type
if avg_bg[:3].mean() > 240: # Very light
return 'white'
elif avg_bg[:3].mean() < 15: # Very dark
return 'black'
else:
return 'color'
def remove_white_background(img, tolerance=30):
"""
Remove white/light background while preserving cartoon outlines
"""
# Convert to RGBA
img = img.convert('RGBA')
data = np.array(img)
r, g, b, a = data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3]
# Create mask: white areas (all channels high)
white_mask = (r > 255 - tolerance) & (g > 255 - tolerance) & (b > 255 - tolerance)
# Create mask: light gray areas
light_gray_mask = (
(r > 220) & (g > 220) & (b > 220) &
(np.abs(r.astype(int) - g.astype(int)) < tolerance) &
(np.abs(r.astype(int) - b.astype(int)) < tolerance) &
(np.abs(g.astype(int) - b.astype(int)) < tolerance)
)
# Combine masks
bg_mask = white_mask | light_gray_mask
# Set alpha to 0 where background detected
data[bg_mask, 3] = 0
return Image.fromarray(data, mode='RGBA')
def remove_black_background(img, tolerance=60):
"""
Remove black/dark background while preserving black outlines
More aggressive version to catch dark gray artifacts
"""
# Convert to RGBA
img = img.convert('RGBA')
data = np.array(img)
r, g, b, a = data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3]
# Detect edges (where color changes rapidly = outlines!)
from scipy import ndimage
# Calculate gradients (more sensitive)
grad_r = ndimage.sobel(r)
grad_g = ndimage.sobel(g)
grad_b = ndimage.sobel(b)
# Magnitude of gradient
magnitude = np.sqrt(grad_r**2 + grad_g**2 + grad_b**2)
# Areas with high gradient = edges (keep these!)
# Lowered threshold to preserve more edges
is_edge = magnitude > 30
# Create mask: dark areas (all channels low) AND not an edge
# More aggressive: also catch dark gray (60 instead of 30)
black_mask = (
(r < tolerance) &
(g < tolerance) &
(b < tolerance) &
~is_edge # NOT an edge!
)
# Set alpha to 0 where background detected
data[black_mask, 3] = 0
return Image.fromarray(data, mode='RGBA')
def remove_color_background(img, sample_size=10, tolerance=30):
"""
Remove colored background by sampling corners
"""
# Convert to RGBA
img = img.convert('RGBA')
data = np.array(img)
width, height = img.size
# Sample corner to get BG color
corner = img.crop((0, 0, sample_size, sample_size))
bg_color = np.array(corner).mean(axis=(0, 1))[:3]
r, g, b, a = data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3]
# Create mask: colors similar to background
color_diff = np.sqrt(
(r - bg_color[0])**2 +
(g - bg_color[1])**2 +
(b - bg_color[2])**2
)
bg_mask = color_diff < tolerance
# Set alpha to 0 where background detected
data[bg_mask, 3] = 0
return Image.fromarray(data, mode='RGBA')
def cleanup_edges(img):
"""
Clean up semi-transparent edge pixels (anti-aliasing artifacts)
+ Add subtle feathering for smoother edges
"""
from scipy import ndimage
data = np.array(img)
alpha = data[:,:,3].astype(float)
# Slightly blur the alpha channel for smooth edges
alpha_blurred = ndimage.gaussian_filter(alpha, sigma=0.5)
# Remove very transparent pixels (< 10% opacity)
alpha_blurred[alpha_blurred < 25] = 0
# Make mostly opaque pixels fully opaque (> 90%)
alpha_blurred[alpha_blurred > 230] = 255
data[:,:,3] = alpha_blurred.astype(np.uint8)
return Image.fromarray(data, mode='RGBA')
def remove_background_auto(img_path: Path, output_path: Path = None, dry_run: bool = False):
"""
Automatically detect and remove background
"""
try:
# Load image
img = Image.open(img_path)
# Skip if already has transparency
if img.mode == 'RGBA':
data = np.array(img)
alpha = data[:,:,3]
# If already has transparent pixels, skip
if (alpha < 255).sum() > (alpha.size * 0.1): # >10% transparent
print(f" ⏭️ Already has transparency: {img_path.name}")
return False
# Detect background type
bg_type = detect_background_color(img)
if bg_type is None:
print(f" ⚠️ Could not detect background: {img_path.name}")
return False
print(f" 🎨 Detected {bg_type} background: {img_path.name}")
# Remove background based on type
if bg_type == 'white':
result = remove_white_background(img, tolerance=30)
elif bg_type == 'black':
result = remove_black_background(img, tolerance=60) # More aggressive!
else:
result = remove_color_background(img, tolerance=30)
# Cleanup edges
result = cleanup_edges(result)
# Save
if output_path is None:
output_path = img_path
if not dry_run:
result.save(output_path, 'PNG', optimize=True)
print(f" ✅ Removed {bg_type} background: {img_path.name}")
return True
except Exception as e:
print(f" ❌ Error processing {img_path.name}: {e}")
return False
def process_directory(directory: Path, dry_run: bool = False, recursive: bool = True):
"""
Process all PNG files in directory
"""
print(f"\n{'='*70}")
print(f"📂 Processing: {directory}")
print(f"{'='*70}")
if recursive:
png_files = list(directory.rglob('*.png'))
else:
png_files = list(directory.glob('*.png'))
if not png_files:
print(" No PNG files found")
return 0
count = 0
for png_file in png_files:
if remove_background_auto(png_file, dry_run=dry_run):
count += 1
return count
def main():
import argparse
parser = argparse.ArgumentParser(description='Advanced background removal for game assets')
parser.add_argument('path', type=str, help='File or directory to process')
parser.add_argument('--dry-run', action='store_true', help='Show what would be done')
parser.add_argument('--no-recursive', action='store_true', help='Do not process subdirectories')
args = parser.parse_args()
print("=" * 70)
print("🧹 ADVANCED BACKGROUND REMOVAL")
print("=" * 70)
if args.dry_run:
print("\n⚠️ DRY RUN MODE - No changes will be made\n")
print("\nFeatures:")
print(" • Auto-detects white, black, or colored backgrounds")
print(" • Preserves cartoon outlines and edges")
print(" • Creates clean transparent PNGs")
print("=" * 70)
path = Path(args.path)
if path.is_file():
remove_background_auto(path, dry_run=args.dry_run)
elif path.is_dir():
total = process_directory(path, dry_run=args.dry_run, recursive=not args.no_recursive)
print(f"\n{'='*70}")
if not args.dry_run:
print(f"✅ COMPLETE! Processed {total} images")
else:
print(f"✅ DRY RUN COMPLETE! Would process {total} images")
print("=" * 70)
else:
print(f"❌ Error: {path} is not a valid file or directory")
if __name__ == "__main__":
main()