Soul_Thread / engine /mapper.py
3rdaiOhpinFully's picture
Update engine/mapper.py
c914720 verified
Raw
History Blame Contribute Delete
10.4 kB
import os
from PIL import Image, ImageStat
Image.MAX_IMAGE_PIXELS = 200_000_000
from engine.hoodie_rules import AOP_HOODIE_MAPPING_RULES
from engine.hf_processors import run_optional_ai_processors
def norm(name):
return name.lower().replace(" ", "_")
def pct_box(w, h, x1, y1, x2, y2):
return (int(w * x1), int(h * y1), int(w * x2), int(h * y2))
def region_score(img):
stat = ImageStat.Stat(img.convert("RGB"))
contrast = sum(stat.stddev) / 3
saturation_hint = max(stat.mean) - min(stat.mean)
brightness = sum(stat.mean) / 3
return contrast + saturation_hint + (255 - abs(140 - brightness)) * 0.12
def analyze_regions(image, prompt, ai_results=None):
ai_results = ai_results or {}
w, h = image.size
p = (prompt or "").lower()
base = [
("PRIMARY_FOCAL_REGION", pct_box(w, h, 0.18, 0.10, 0.82, 0.82)),
("SECONDARY_FOCAL_REGION", pct_box(w, h, 0.04, 0.00, 0.75, 0.50)),
("ENVIRONMENT_REGION", pct_box(w, h, 0.00, 0.00, 1.00, 1.00)),
("LOWER_ANCHOR_REGION", pct_box(w, h, 0.05, 0.58, 0.95, 1.00)),
("LEFT_EDGE_FLOW_REGION", pct_box(w, h, 0.00, 0.04, 0.38, 0.96)),
("RIGHT_EDGE_FLOW_REGION", pct_box(w, h, 0.62, 0.04, 1.00, 0.96)),
]
regions = []
for region_type, box in base:
crop = image.crop(box)
score = region_score(crop)
if region_type == "PRIMARY_FOCAL_REGION":
if any(k in p for k in ["face", "head", "person", "character", "dj", "main", "center", "figure", "subject"]):
score += 45
if region_type == "SECONDARY_FOCAL_REGION":
if any(k in p for k in ["eye", "monster", "upper", "teeth", "dramatic", "background"]):
score += 35
if region_type == "LOWER_ANCHOR_REGION":
if any(k in p for k in ["pocket", "deck", "table", "lower", "base", "ground", "smoke"]):
score += 30
if region_type == "ENVIRONMENT_REGION":
if any(k in p for k in ["pattern", "repeat", "tile", "smoke", "flame", "abstract", "texture"]):
score += 30
if region_type == "LEFT_EDGE_FLOW_REGION":
if any(k in p for k in ["left sleeve", "left edge", "flame", "smoke"]):
score += 20
if region_type == "RIGHT_EDGE_FLOW_REGION":
if any(k in p for k in ["right sleeve", "right edge", "purple", "smoke"]):
score += 20
regions.append({
"type": region_type,
"box": box,
"score": round(score, 2),
"source": "local_region_analysis",
})
for category, result in ai_results.items():
if not result.get("ok"):
continue
for det in result.get("detections", []):
det_type = str(det.get("type", category)).lower()
box = det.get("box")
if not box or len(box) != 4:
continue
region_type = None
if det_type in ["face", "person", "object", "subject"]:
region_type = "PRIMARY_FOCAL_REGION"
elif det_type in ["text", "ocr"]:
region_type = "TEXT_REGION"
elif det_type == "logo":
region_type = "LOGO_REGION"
if region_type:
regions.append({
"type": region_type,
"box": tuple(map(int, box)),
"score": float(det.get("score", 85)),
"source": "hf_optional_ai",
})
return sorted(regions, key=lambda r: r["score"], reverse=True)
def cover_resize_no_padding(img, target_w, target_h):
src_w, src_h = img.size
scale = max(target_w / src_w, target_h / src_h)
new_w, new_h = int(src_w * scale), int(src_h * scale)
resized = img.resize((new_w, new_h), Image.LANCZOS)
left = max(0, (new_w - target_w) // 2)
top = max(0, (new_h - target_h) // 2)
return resized.crop((left, top, left + target_w, top + target_h))
def contain_resize_with_transparency(img, target_w, target_h):
src_w, src_h = img.size
scale = min(target_w / src_w, target_h / src_h)
new_w, new_h = int(src_w * scale), int(src_h * scale)
resized = img.resize((new_w, new_h), Image.LANCZOS)
canvas = Image.new("RGBA", (target_w, target_h), (0, 0, 0, 0))
canvas.paste(resized, ((target_w - new_w) // 2, (target_h - new_h) // 2))
return canvas
def tile_texture(img, target_w, target_h):
tile_size = max(300, min(900, target_w, target_h))
tile = cover_resize_no_padding(img, tile_size, tile_size)
out = Image.new("RGBA", (target_w, target_h))
for x in range(0, target_w, tile.width):
for y in range(0, target_h, tile.height):
out.paste(tile, (x, y))
return out.crop((0, 0, target_w, target_h))
def get_mask_path(template_id, piece_name):
candidates = [
f"template_assets/{template_id}/masks/{piece_name}.png",
f"template_assets/{template_id}/masks/{norm(piece_name)}.png",
f"template_assets/{template_id}/{piece_name}.png",
f"template_assets/{template_id}/{norm(piece_name)}.png",
]
for path in candidates:
if os.path.exists(path):
return path
return None
def apply_piece_mask(mapped_img, template_id, piece_name):
mask_path = get_mask_path(template_id, piece_name)
if not mask_path:
return mapped_img, None
mask_img = Image.open(mask_path).convert("RGBA").resize(mapped_img.size, Image.LANCZOS)
alpha = mask_img.getchannel("A")
out = mapped_img.convert("RGBA")
out.putalpha(alpha)
return out, mask_path
def choose_region(piece_name, regions):
rule = AOP_HOODIE_MAPPING_RULES.get(norm(piece_name))
if not rule:
return regions[0], "safe_cover"
for preferred in rule["preferred"]:
for region in regions:
if region["type"] == preferred and region["type"] not in rule["forbidden"]:
return region, rule["strategy"]
for region in regions:
if region["type"] not in rule["forbidden"]:
return region, rule["strategy"]
env = next((r for r in regions if r["type"] == "ENVIRONMENT_REGION"), regions[0])
return env, "fallback_texture"
def apply_strategy(image, region, strategy, target_w, target_h):
crop = image.crop(region["box"]).convert("RGBA")
if strategy == "texture_band":
return tile_texture(crop, target_w, target_h)
if strategy == "vertical_texture":
return cover_resize_no_padding(crop, target_w, target_h)
if strategy == "dark_texture":
return cover_resize_no_padding(crop, target_w, target_h)
if strategy == "fallback_texture":
return cover_resize_no_padding(crop, target_w, target_h)
if strategy == "centered_focal":
return cover_resize_no_padding(crop, target_w, target_h)
if strategy == "wide_dramatic":
return cover_resize_no_padding(crop, target_w, target_h)
if strategy == "lower_anchor":
return cover_resize_no_padding(crop, target_w, target_h)
return cover_resize_no_padding(crop, target_w, target_h)
def map_all(image_path, template_id, pieces, prompt, use_optional_ai=False):
image = Image.open(image_path).convert("RGBA")
ai_results = run_optional_ai_processors(image_path, prompt) if use_optional_ai else {}
regions = analyze_regions(image, prompt, ai_results)
outputs = {}
decisions = []
warnings = []
used_boxes = {}
mask_usage = {}
for piece_name, size in pieces.items():
target_w, target_h = size
region, strategy = choose_region(piece_name, regions)
mapped = apply_strategy(image, region, strategy, target_w, target_h)
mapped, mask_path = apply_piece_mask(mapped, template_id, piece_name)
outputs[piece_name] = mapped
decision = {
"piece": piece_name,
"region": region["type"],
"strategy": strategy,
"box": region["box"],
"score": region["score"],
"output_size": [target_w, target_h],
"mask_applied": bool(mask_path),
"mask_path": mask_path,
}
decisions.append(decision)
key = str(region["box"])
used_boxes[key] = used_boxes.get(key, 0) + 1
mask_usage[piece_name] = bool(mask_path)
risky_small = norm(piece_name) in [
"left_sleeve",
"right_sleeve",
"collar",
"left_cuff",
"right_cuff",
"waistband",
"kangaroo_pocket",
]
if risky_small and region["type"] in [
"TEXT_REGION",
"LOGO_REGION",
"PRIMARY_FOCAL_REGION",
]:
warnings.append(
f"{piece_name}: readable text, logo, or primary focal content may be on a risky small panel."
)
if not mask_path:
warnings.append(
f"{piece_name}: no piece mask found, output is rectangular. Add template_assets/{template_id}/masks/{piece_name}.png for true shaped AOP output."
)
front_decisions = [
d for d in decisions if norm(d["piece"]) in ["front", "front_body"]
]
if front_decisions and front_decisions[0]["region"] != "PRIMARY_FOCAL_REGION":
warnings.append("Main focal point was not placed on the front panel.")
if used_boxes and max(used_boxes.values()) > 3:
warnings.append("Too many panels are using the same crop. Design may look repetitive.")
if not any(mask_usage.values()):
warnings.append(
"No template masks were applied. Current outputs are rectangular crops, not true garment-shaped production pieces."
)
return outputs, {
"prompt": prompt,
"template_id": template_id,
"plain_engine_logic": [
"Big panels get meaning.",
"Small panels get texture.",
"Risky seams get atmosphere.",
"Pockets get lower anchors.",
"Cuffs get color bands.",
"Hoods get continuation, not the whole story.",
"True AOP piece shape requires alpha masks from template PNGs.",
],
"regions": regions,
"decisions": decisions,
"warnings": warnings,
"optional_ai_processors": ai_results,
}