255 lines
8.8 KiB
Python
255 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
2.5D Asset Inspector
|
|
Usage: python scripts/inspect-assets.py image1.png image2.jpg ...
|
|
or: python scripts/inspect-assets.py path/to/folder/
|
|
|
|
Checks each image and reports:
|
|
- Format and mode
|
|
- Whether it has a real transparent background
|
|
- Background type if not transparent (dark, light, complex)
|
|
- Recommended depth level based on image characteristics
|
|
- Whether the background is likely a problem (product shot vs scene/artwork)
|
|
|
|
The AI reads this output and uses it to inform the user.
|
|
The script NEVER modifies images — inspect only.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
print("PIL not found. Install with: pip install Pillow")
|
|
sys.exit(1)
|
|
|
|
|
|
def analyse_image(path):
|
|
result = {
|
|
"path": path,
|
|
"filename": os.path.basename(path),
|
|
"status": None,
|
|
"format": None,
|
|
"mode": None,
|
|
"size": None,
|
|
"bg_type": None,
|
|
"bg_colour": None,
|
|
"likely_needs_removal": None,
|
|
"notes": [],
|
|
}
|
|
|
|
try:
|
|
img = Image.open(path)
|
|
result["format"] = img.format or os.path.splitext(path)[1].upper().strip(".")
|
|
result["mode"] = img.mode
|
|
result["size"] = img.size
|
|
w, h = img.size
|
|
|
|
except Exception as e:
|
|
result["status"] = "ERROR"
|
|
result["notes"].append(f"Could not open: {e}")
|
|
return result
|
|
|
|
# --- Alpha / transparency check ---
|
|
if img.mode == "RGBA":
|
|
extrema = img.getextrema()
|
|
alpha_min = extrema[3][0] # 0 = has real transparency, 255 = fully opaque
|
|
if alpha_min == 0:
|
|
result["status"] = "CLEAN"
|
|
result["bg_type"] = "transparent"
|
|
result["notes"].append("Real alpha channel with transparent pixels — clean cutout")
|
|
result["likely_needs_removal"] = False
|
|
return result
|
|
else:
|
|
result["notes"].append("RGBA mode but alpha is fully opaque — background was never removed")
|
|
img = img.convert("RGB") # treat as solid for analysis below
|
|
|
|
if img.mode not in ("RGB", "L"):
|
|
img = img.convert("RGB")
|
|
|
|
# --- Sample corners and edges to detect background colour ---
|
|
pixels = img.load()
|
|
sample_points = [
|
|
(0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1), # corners
|
|
(w // 2, 0), (w // 2, h - 1), # top/bottom center
|
|
(0, h // 2), (w - 1, h // 2), # left/right center
|
|
]
|
|
|
|
samples = []
|
|
for x, y in sample_points:
|
|
try:
|
|
px = pixels[x, y]
|
|
if isinstance(px, int):
|
|
px = (px, px, px)
|
|
samples.append(px[:3])
|
|
except Exception:
|
|
pass
|
|
|
|
if not samples:
|
|
result["status"] = "UNKNOWN"
|
|
result["notes"].append("Could not sample pixels")
|
|
return result
|
|
|
|
# --- Classify background ---
|
|
avg_r = sum(s[0] for s in samples) / len(samples)
|
|
avg_g = sum(s[1] for s in samples) / len(samples)
|
|
avg_b = sum(s[2] for s in samples) / len(samples)
|
|
avg_brightness = (avg_r + avg_g + avg_b) / 3
|
|
|
|
# Check colour consistency (low variance = solid bg, high variance = scene/complex bg)
|
|
max_r = max(s[0] for s in samples)
|
|
max_g = max(s[1] for s in samples)
|
|
max_b = max(s[2] for s in samples)
|
|
min_r = min(s[0] for s in samples)
|
|
min_g = min(s[1] for s in samples)
|
|
min_b = min(s[2] for s in samples)
|
|
variance = max(max_r - min_r, max_g - min_g, max_b - min_b)
|
|
|
|
result["bg_colour"] = (int(avg_r), int(avg_g), int(avg_b))
|
|
|
|
if variance > 80:
|
|
result["status"] = "COMPLEX_BG"
|
|
result["bg_type"] = "complex or scene"
|
|
result["notes"].append(
|
|
"Background varies significantly across edges — likely a scene, "
|
|
"photograph, or artwork background rather than a solid colour"
|
|
)
|
|
result["likely_needs_removal"] = False # complex bg = probably intentional content
|
|
result["notes"].append(
|
|
"JUDGMENT: Complex backgrounds usually mean this image IS the content "
|
|
"(site screenshot, artwork, section bg). Background likely should be KEPT."
|
|
)
|
|
|
|
elif avg_brightness < 40:
|
|
result["status"] = "DARK_BG"
|
|
result["bg_type"] = "solid dark/black"
|
|
result["notes"].append(
|
|
f"Solid dark background detected — average edge brightness: {avg_brightness:.0f}/255"
|
|
)
|
|
result["likely_needs_removal"] = True
|
|
result["notes"].append(
|
|
"JUDGMENT: Dark studio backgrounds on product shots typically need removal. "
|
|
"BUT if this is a screenshot, artwork, or intentionally dark composition, keep it."
|
|
)
|
|
|
|
elif avg_brightness > 210:
|
|
result["status"] = "LIGHT_BG"
|
|
result["bg_type"] = "solid white/light"
|
|
result["notes"].append(
|
|
f"Solid light background detected — average edge brightness: {avg_brightness:.0f}/255"
|
|
)
|
|
result["likely_needs_removal"] = True
|
|
result["notes"].append(
|
|
"JUDGMENT: White studio backgrounds on product shots typically need removal. "
|
|
"BUT if this is a screenshot, UI mockup, or document, keep it."
|
|
)
|
|
|
|
else:
|
|
result["status"] = "MIDTONE_BG"
|
|
result["bg_type"] = "solid mid-tone colour"
|
|
result["notes"].append(
|
|
f"Solid mid-tone background detected — avg colour: RGB{result['bg_colour']}"
|
|
)
|
|
result["likely_needs_removal"] = None # ambiguous — let AI judge
|
|
result["notes"].append(
|
|
"JUDGMENT: Ambiguous — could be a branded background (keep) or a "
|
|
"studio colour backdrop (remove). AI must judge based on context."
|
|
)
|
|
|
|
# --- JPEG format warning ---
|
|
if result["format"] in ("JPEG", "JPG"):
|
|
result["notes"].append(
|
|
"JPEG format — cannot store transparency. "
|
|
"If bg removal is needed, user must provide a PNG version or approve CSS workaround."
|
|
)
|
|
|
|
# --- Size note ---
|
|
if w > 2000 or h > 2000:
|
|
result["notes"].append(
|
|
f"Large image ({w}x{h}px) — resize before embedding. "
|
|
"See references/asset-pipeline.md Step 3 for depth-appropriate targets."
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def print_report(results):
|
|
print("\n" + "═" * 55)
|
|
print(" 2.5D Asset Inspector Report")
|
|
print("═" * 55)
|
|
|
|
for r in results:
|
|
print(f"\n📁 {r['filename']}")
|
|
print(f" Format : {r['format']} | Mode: {r['mode']} | Size: {r['size']}")
|
|
|
|
status_icons = {
|
|
"CLEAN": "✅",
|
|
"DARK_BG": "⚠️ ",
|
|
"LIGHT_BG": "⚠️ ",
|
|
"COMPLEX_BG": "🔵",
|
|
"MIDTONE_BG": "❓",
|
|
"UNKNOWN": "❓",
|
|
"ERROR": "❌",
|
|
}
|
|
icon = status_icons.get(r["status"], "❓")
|
|
print(f" Status : {icon} {r['status']}")
|
|
|
|
if r["bg_type"]:
|
|
print(f" Bg type: {r['bg_type']}")
|
|
|
|
if r["likely_needs_removal"] is True:
|
|
print(" Removal: Likely needed (product/object shot)")
|
|
elif r["likely_needs_removal"] is False:
|
|
print(" Removal: Likely NOT needed (scene/artwork/content image)")
|
|
else:
|
|
print(" Removal: Ambiguous — AI must judge from context")
|
|
|
|
for note in r["notes"]:
|
|
print(f" → {note}")
|
|
|
|
print("\n" + "═" * 55)
|
|
clean = sum(1 for r in results if r["status"] == "CLEAN")
|
|
flagged = sum(1 for r in results if r["status"] in ("DARK_BG", "LIGHT_BG", "MIDTONE_BG"))
|
|
complex_bg = sum(1 for r in results if r["status"] == "COMPLEX_BG")
|
|
errors = sum(1 for r in results if r["status"] == "ERROR")
|
|
|
|
print(f" Clean: {clean} | Flagged: {flagged} | Complex/Scene: {complex_bg} | Errors: {errors}")
|
|
print("═" * 55)
|
|
print("\nNext step: Read JUDGMENT notes above and inform the user.")
|
|
print("See references/asset-pipeline.md for the exact notification format.\n")
|
|
|
|
|
|
def collect_paths(args):
|
|
paths = []
|
|
for arg in args:
|
|
if os.path.isdir(arg):
|
|
for f in os.listdir(arg):
|
|
if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".avif")):
|
|
paths.append(os.path.join(arg, f))
|
|
elif os.path.isfile(arg):
|
|
paths.append(arg)
|
|
else:
|
|
print(f"⚠️ Not found: {arg}")
|
|
return paths
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help'):
|
|
print("\nUsage:")
|
|
print(" python scripts/inspect-assets.py image.png")
|
|
print(" python scripts/inspect-assets.py image1.jpg image2.png")
|
|
print(" python scripts/inspect-assets.py path/to/folder/\n")
|
|
if len(sys.argv) < 2:
|
|
sys.exit(1)
|
|
else:
|
|
sys.exit(0)
|
|
|
|
paths = collect_paths(sys.argv[1:])
|
|
if not paths:
|
|
print("No valid image files found.")
|
|
sys.exit(1)
|
|
|
|
results = [analyse_image(p) for p in paths]
|
|
print_report(results)
|