335 lines
13 KiB
Python
335 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Master Asset Processor - Mrtva Dolina
|
|
Processes all game assets:
|
|
- Remove green screen (#00FF00) → transparent
|
|
- Separate multi-object images
|
|
- Organize into clean structure
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
import numpy as np
|
|
from scipy import ndimage
|
|
import shutil
|
|
|
|
class MrtvaDolina_AssetProcessor:
|
|
def __init__(self, base_dir="/Users/davidkotnik/Desktop/novafarma"):
|
|
self.base_dir = Path(base_dir)
|
|
self.output_dir = self.base_dir / "mrtva_dolina"
|
|
|
|
# Green screen settings
|
|
self.green_color = np.array([0, 255, 0])
|
|
self.tolerance = 30
|
|
self.min_object_size = 400 # minimum 20x20 pixels
|
|
|
|
# Source directories (prioritize green screen sources)
|
|
self.sources_greenscreen = [
|
|
self.base_dir / "Slike_za_Tiled", # Has green screen
|
|
]
|
|
|
|
self.sources_transparent = [
|
|
self.base_dir / "assets" / "narezano_loceno", # Already transparent
|
|
]
|
|
|
|
# Category mapping (keywords → category)
|
|
self.categories = {
|
|
"characters/players": ["kai_character", "ana_character", "player"],
|
|
"characters/npcs": ["npc", "ivan", "marija", "trader", "blacksmith", "merchant", "baker", "farmer", "doctor", "romance"],
|
|
"characters/zombies": ["zombie"],
|
|
"creatures/animals": ["animal", "cow", "chicken", "dog", "livestock", "donkey", "horse"],
|
|
"creatures/monsters": ["troll", "creature", "anomalous", "cryptid", "slime"],
|
|
"creatures/bosses": ["grok", "werewolf", "boss"],
|
|
"buildings/houses": ["house", "farmhouse", "barn"],
|
|
"buildings/ruins": ["ruin", "buildings_ruins"],
|
|
"buildings/structures": ["building", "town_buildings", "structure", "blacksmith_workshop", "bridge", "wall", "city", "portal"],
|
|
"terrain/ground": ["grass", "soil", "tileset", "ground", "terrain"],
|
|
"terrain/water": ["water", "ocean", "pond", "river"],
|
|
"terrain/special": ["mine_tileset", "mine_interior", "mine_entrance"],
|
|
"vegetation/trees": ["tree", "sequoia", "birch", "vegetation"],
|
|
"vegetation/plants": ["flower", "plant", "leaf"],
|
|
"vegetation/crops": ["crop", "seed", "wheat", "vegetable", "fruit"],
|
|
"objects/farming": ["sprinkler", "tool", "farming"],
|
|
"objects/items": ["items_pack", "weapon", "bow", "arrow", "backpack", "chest", "grave"],
|
|
"objects/furniture": ["furniture", "bathroom", "bedroom", "kitchen", "living_room"],
|
|
}
|
|
|
|
# Statistics
|
|
self.stats = {
|
|
"processed": 0,
|
|
"separated": 0,
|
|
"skipped": 0,
|
|
"errors": 0
|
|
}
|
|
|
|
# Metadata collection
|
|
self.metadata = []
|
|
|
|
def create_output_structure(self):
|
|
"""Create all category directories"""
|
|
print("🗂️ Creating directory structure...")
|
|
for category in self.categories.keys():
|
|
category_path = self.output_dir / category
|
|
category_path.mkdir(parents=True, exist_ok=True)
|
|
print(f" ✓ {category}")
|
|
|
|
# Also create misc category
|
|
(self.output_dir / "misc").mkdir(parents=True, exist_ok=True)
|
|
print(f" ✓ misc")
|
|
|
|
def is_green_screen(self, img_array):
|
|
"""Check if image has green screen background"""
|
|
if img_array.shape[2] < 3:
|
|
return False
|
|
|
|
# Check for green pixels
|
|
green_mask = np.all(np.abs(img_array[:, :, :3] - self.green_color) < self.tolerance, axis=2)
|
|
green_percentage = np.sum(green_mask) / (img_array.shape[0] * img_array.shape[1])
|
|
|
|
return green_percentage > 0.1 # At least 10% green
|
|
|
|
def remove_green_screen(self, img):
|
|
"""Remove green screen and make transparent"""
|
|
img_array = np.array(img.convert("RGBA"))
|
|
|
|
# Create mask for green pixels
|
|
green_mask = np.all(np.abs(img_array[:, :, :3] - self.green_color) < self.tolerance, axis=2)
|
|
|
|
# Set alpha to 0 for green pixels
|
|
img_array[green_mask, 3] = 0
|
|
|
|
return Image.fromarray(img_array)
|
|
|
|
def separate_objects(self, img):
|
|
"""Detect and separate individual objects"""
|
|
img_array = np.array(img)
|
|
|
|
# Get alpha channel (transparency)
|
|
if img_array.shape[2] == 4:
|
|
alpha = img_array[:, :, 3]
|
|
else:
|
|
# No alpha channel, use luminance threshold
|
|
gray = np.mean(img_array[:, :, :3], axis=2)
|
|
alpha = (gray > 10).astype(np.uint8) * 255
|
|
|
|
# Label connected components
|
|
labels, num_objects = ndimage.label(alpha > 128)
|
|
|
|
if num_objects <= 1:
|
|
return [img] # Single object or empty
|
|
|
|
# Extract each object
|
|
objects = []
|
|
for i in range(1, num_objects + 1):
|
|
mask = (labels == i)
|
|
|
|
# Check object size
|
|
if np.sum(mask) < self.min_object_size:
|
|
continue
|
|
|
|
# Find bounding box
|
|
rows = np.any(mask, axis=1)
|
|
cols = np.any(mask, axis=0)
|
|
|
|
if not np.any(rows) or not np.any(cols):
|
|
continue
|
|
|
|
y_min, y_max = np.where(rows)[0][[0, -1]]
|
|
x_min, x_max = np.where(cols)[0][[0, -1]]
|
|
|
|
# Crop object
|
|
cropped = img_array[y_min:y_max+1, x_min:x_max+1]
|
|
objects.append(Image.fromarray(cropped))
|
|
|
|
return objects if objects else [img]
|
|
|
|
def categorize_filename(self, filename):
|
|
"""Determine category from filename"""
|
|
filename_lower = filename.lower()
|
|
|
|
for category, keywords in self.categories.items():
|
|
if any(keyword in filename_lower for keyword in keywords):
|
|
return category
|
|
|
|
return "misc"
|
|
|
|
def process_image(self, image_path, test_mode=False, has_greenscreen=True):
|
|
"""Process a single image"""
|
|
try:
|
|
# Load image
|
|
img = Image.open(image_path)
|
|
|
|
if has_greenscreen:
|
|
# Check and remove green screen
|
|
img_array = np.array(img.convert("RGBA"))
|
|
if not self.is_green_screen(img_array):
|
|
if test_mode:
|
|
print(f" ⊘ {image_path.name} - No green screen, skipping")
|
|
self.stats["skipped"] += 1
|
|
return
|
|
|
|
# Remove green screen
|
|
img_transparent = self.remove_green_screen(img)
|
|
else:
|
|
# Already transparent, just convert to RGBA
|
|
img_transparent = img.convert("RGBA")
|
|
|
|
# Separate objects
|
|
objects = self.separate_objects(img_transparent)
|
|
|
|
# Categorize
|
|
category = self.categorize_filename(image_path.stem)
|
|
|
|
# Save objects
|
|
if len(objects) == 1:
|
|
# Single object
|
|
output_path = self.output_dir / category / f"{image_path.stem}.png"
|
|
objects[0].save(output_path, "PNG")
|
|
|
|
if test_mode:
|
|
print(f" ✓ {image_path.name} → {category}/{output_path.name}")
|
|
|
|
self.metadata.append({
|
|
"filename": output_path.name,
|
|
"category": category,
|
|
"source": str(image_path),
|
|
"objects": 1
|
|
})
|
|
|
|
else:
|
|
# Multiple objects
|
|
for idx, obj in enumerate(objects, 1):
|
|
output_path = self.output_dir / category / f"{image_path.stem}_obj{idx:02d}.png"
|
|
obj.save(output_path, "PNG")
|
|
|
|
if test_mode:
|
|
print(f" ✓ {image_path.name} → {category}/ ({len(objects)} objects)")
|
|
|
|
self.metadata.append({
|
|
"filename": f"{image_path.stem}_obj*.png",
|
|
"category": category,
|
|
"source": str(image_path),
|
|
"objects": len(objects)
|
|
})
|
|
|
|
self.stats["separated"] += len(objects)
|
|
|
|
self.stats["processed"] += 1
|
|
|
|
except Exception as e:
|
|
print(f" ✗ ERROR: {image_path.name} - {str(e)}")
|
|
self.stats["errors"] += 1
|
|
|
|
def test_run(self, num_samples=10):
|
|
"""Test processing on a few images"""
|
|
print(f"\n🧪 TEST RUN - Processing {num_samples} sample images\n")
|
|
|
|
# Collect sample images from GREENSCREEN sources
|
|
samples = []
|
|
for source in self.sources_greenscreen:
|
|
if source.exists():
|
|
png_files = list(source.glob("**/*.png"))[:num_samples]
|
|
samples.extend(png_files)
|
|
|
|
samples = samples[:num_samples]
|
|
|
|
print(f"Found {len(samples)} test images from green screen sources\n")
|
|
|
|
# Process samples
|
|
for img_path in samples:
|
|
self.process_image(img_path, test_mode=True, has_greenscreen=True)
|
|
|
|
# Show stats
|
|
print(f"\n📊 Test Results:")
|
|
print(f" Processed: {self.stats['processed']}")
|
|
print(f" Separated: {self.stats['separated']}")
|
|
print(f" Skipped: {self.stats['skipped']}")
|
|
print(f" Errors: {self.stats['errors']}")
|
|
|
|
return self.stats["errors"] == 0
|
|
|
|
def process_all(self):
|
|
"""Process all images"""
|
|
print("\n🚀 PROCESSING ALL IMAGES\n")
|
|
|
|
# Reset stats
|
|
self.stats = {"processed": 0, "separated": 0, "skipped": 0, "errors": 0}
|
|
|
|
# Process green screen images
|
|
print("📗 Processing GREEN SCREEN images (Slike_za_Tiled)...")
|
|
greenscreen_images = []
|
|
for source in self.sources_greenscreen:
|
|
if source.exists():
|
|
png_files = list(source.glob("**/*.png"))
|
|
greenscreen_images.extend(png_files)
|
|
print(f" Found {len(png_files)} images in {source.name}")
|
|
|
|
for idx, img_path in enumerate(greenscreen_images, 1):
|
|
if idx % 100 == 0:
|
|
print(f" Progress: {idx}/{len(greenscreen_images)} ({idx/len(greenscreen_images)*100:.1f}%)")
|
|
self.process_image(img_path, has_greenscreen=True)
|
|
|
|
print(f" ✓ Processed {len(greenscreen_images)} green screen images\n")
|
|
|
|
# Process transparent images (copy & categorize)
|
|
print("📘 Processing TRANSPARENT images (narezano_loceno)...")
|
|
transparent_images = []
|
|
for source in self.sources_transparent:
|
|
if source.exists():
|
|
png_files = list(source.glob("**/*.png"))
|
|
transparent_images.extend(png_files)
|
|
print(f" Found {len(png_files)} images in {source.name}")
|
|
|
|
for idx, img_path in enumerate(transparent_images, 1):
|
|
if idx % 100 == 0:
|
|
print(f" Progress: {idx}/{len(transparent_images)} ({idx/len(transparent_images)*100:.1f}%)")
|
|
self.process_image(img_path, has_greenscreen=False)
|
|
|
|
print(f" ✓ Processed {len(transparent_images)} transparent images\n")
|
|
|
|
total = len(greenscreen_images) + len(transparent_images)
|
|
print(f"\n✅ DONE! Processed {total} images")
|
|
print(f"\n📊 Final Statistics:")
|
|
print(f" Processed: {self.stats['processed']}")
|
|
print(f" Separated: {self.stats['separated']}")
|
|
print(f" Skipped: {self.stats['skipped']}")
|
|
print(f" Errors: {self.stats['errors']}")
|
|
|
|
# Save metadata
|
|
metadata_path = self.output_dir / "metadata.json"
|
|
with open(metadata_path, 'w') as f:
|
|
json.dump(self.metadata, f, indent=2)
|
|
|
|
print(f"\n💾 Metadata saved to: {metadata_path}")
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print(" MASTER ASSET PROCESSOR - MRTVA DOLINA")
|
|
print("=" * 60)
|
|
|
|
processor = MrtvaDolina_AssetProcessor()
|
|
|
|
# Create directory structure
|
|
processor.create_output_structure()
|
|
|
|
# Test run first
|
|
print("\n" + "=" * 60)
|
|
success = processor.test_run(num_samples=10)
|
|
|
|
if success:
|
|
print("\n✅ Test run successful!")
|
|
print("\n" + "=" * 60)
|
|
print("STARTING FULL PROCESSING...")
|
|
print("=" * 60)
|
|
|
|
# Run full processing
|
|
processor.process_all()
|
|
|
|
print("\n" + "=" * 60)
|
|
print("🎉 ALL DONE!")
|
|
print("=" * 60)
|
|
else:
|
|
print("\n❌ Test run had errors. Please fix before continuing.")
|