Files
SkinPress/scripts/relearn/learn_maps.py
M1rsem ec62941845 Initial release: SkinPress — Minecraft skin to Bambu 3MF
ModVer full-body and MakerWorld chibi customizers with tests and templates.
2026-07-20 10:02:33 +03:00

330 lines
10 KiB
Python

"""Learn body-part pixel identity (by part name suffix) -> UV from examples."""
from __future__ import annotations
import json
import re
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
SKINS = ROOT / "tests" / "fixtures" / "skins"
EXTRACT = ROOT / "3mf_extract"
SETTINGS = (EXTRACT / "Metadata" / "model_settings.config").read_text(
encoding="utf-8", errors="replace"
)
EXAMPLES = {
"jay": {"skin": "jay.png", "oids": list(range(1312, 1318)), "arms": "classic"},
"foresta": {"skin": "foresta.png", "oids": list(range(1306, 1312)), "arms": "slim"},
"jem": {"skin": "jem.png", "oids": list(range(1288, 1294)), "arms": "slim"},
"smile": {"skin": "smile.png", "oids": list(range(1294, 1300)), "arms": "classic"},
}
TEMPLATE_CLASSIC = {
"head": 305,
"torso": 293,
"leg_r": 478,
"leg_l": 650,
"arm_r": 838,
"arm_l": 840,
}
TEMPLATE_SLIM = {
"head": 479,
"torso": 294,
"leg_r": 839,
"leg_l": 844,
"arm_r": 1006,
"arm_l": 1165,
}
OVERLAY = {
"classic": {
"head": [
("top", 40, 0, 8, 8),
("bottom", 48, 0, 8, 8),
("right", 32, 8, 8, 8),
("front", 40, 8, 8, 8),
("left", 48, 8, 8, 8),
("back", 56, 8, 8, 8),
],
"torso": [
("top", 20, 32, 8, 4),
("bottom", 28, 32, 8, 4),
("right", 16, 36, 4, 12),
("front", 20, 36, 8, 12),
("left", 28, 36, 4, 12),
("back", 32, 36, 8, 12),
],
"arm_r": [
("top", 44, 32, 4, 4),
("bottom", 48, 32, 4, 4),
("right", 40, 36, 4, 12),
("front", 44, 36, 4, 12),
("left", 48, 36, 4, 12),
("back", 52, 36, 4, 12),
],
"arm_l": [
("top", 52, 48, 4, 4),
("bottom", 56, 48, 4, 4),
("right", 48, 52, 4, 12),
("front", 52, 52, 4, 12),
("left", 56, 52, 4, 12),
("back", 60, 52, 4, 12),
],
"leg_r": [
("top", 4, 32, 4, 4),
("bottom", 8, 32, 4, 4),
("right", 0, 36, 4, 12),
("front", 4, 36, 4, 12),
("left", 8, 36, 4, 12),
("back", 12, 36, 4, 12),
],
"leg_l": [
("top", 4, 48, 4, 4),
("bottom", 8, 48, 4, 4),
("right", 0, 52, 4, 12),
("front", 4, 52, 4, 12),
("left", 8, 52, 4, 12),
("back", 12, 52, 4, 12),
],
}
}
OVERLAY["slim"] = {
**OVERLAY["classic"],
"arm_r": [
("top", 44, 32, 3, 4),
("bottom", 47, 32, 3, 4),
("right", 40, 36, 4, 12),
("front", 44, 36, 3, 12),
("left", 47, 36, 4, 12),
("back", 51, 36, 3, 12),
],
"arm_l": [
("top", 52, 48, 3, 4),
("bottom", 55, 48, 3, 4),
("right", 48, 52, 4, 12),
("front", 52, 52, 3, 12),
("left", 55, 52, 4, 12),
("back", 59, 52, 3, 12),
],
}
def object_name(oid: int) -> str:
m = re.search(
rf'<object id="{oid}">\s*<metadata key="name" value="([^"]+)"', SETTINGS
)
return (m.group(1) if m else "").lower()
def get_pixel_parts(oid: int) -> list[dict]:
m = re.search(rf'<object id="{oid}">(.*?)</object>', SETTINGS, re.S)
if not m:
return []
out = []
for idx, pm in enumerate(
re.finditer(r'<part id="(\d+)"[^>]*>(.*?)</part>', m.group(1), re.S)
):
pb = pm.group(2)
name_m = re.search(r'key="name" value="([^"]+)"', pb)
svol_m = re.search(r'key="source_volume_id" value="([^"]+)"', pb)
name = name_m.group(1) if name_m else ""
if "ixel" not in name.lower() and "pixel" not in name.lower():
continue
# identity: prefer unique svol if not all zeros; else name suffix; else index
svol = svol_m.group(1) if svol_m else None
suffix_m = re.search(r"(\d+)$", name.replace(".", " "))
# better: Hat_Item_Pixel.067 -> 67
suffix_m = re.search(r"\.(\d+)$", name)
if not suffix_m:
suffix_m = re.search(r"(\d+)$", name)
suffix = suffix_m.group(1) if suffix_m else str(idx)
out.append(
{
"part_id": pm.group(1),
"name": name,
"svol": svol,
"suffix": suffix,
"index": idx,
}
)
return out
def pixel_keys(oid: int, mode: str) -> set[str]:
"""Return set of identity keys for kept pixels."""
parts = get_pixel_parts(oid)
if not parts:
return set()
svols = [p["svol"] for p in parts]
unique_svol = len(set(svols)) > 1 and not all(s in (None, "0") for s in svols)
if mode == "svol" or (mode == "auto" and unique_svol):
return {p["svol"] for p in parts if p["svol"] and p["svol"].isdigit()}
return {p["suffix"] for p in parts}
def classify_plate_parts(eoids: list[int]) -> dict[str, int]:
head = torso = None
arms, legs = [], []
for oid in eoids:
n = object_name(oid)
if "hat" in n or ("head" in n and "joint" not in n):
head = oid
elif "tors" in n:
torso = oid
elif "arm" in n:
arms.append(oid)
elif "leg" in n:
legs.append(oid)
out = {}
if head:
out["head"] = head
if torso:
out["torso"] = torso
if len(arms) >= 1:
out["arm_r"] = arms[0]
if len(arms) >= 2:
out["arm_l"] = arms[1]
if len(legs) >= 1:
out["leg_r"] = legs[0]
if len(legs) >= 2:
out["leg_l"] = legs[1]
return out
def all_texels(regions):
for face, x0, y0, w, h in regions:
for j in range(h):
for i in range(w):
yield face, i, j, x0 + i, y0 + j
def learn_part(part_key, template_oid, example_oids: dict[str, int], regions, skins, key_mode):
tmpl_parts = get_pixel_parts(template_oid)
if not tmpl_parts:
return {}, key_mode
# Determine key mode from template
svols = [p["svol"] for p in tmpl_parts]
unique_svol = len(set(svols)) > 1 and not all(s in (None, "0") for s in svols)
mode = "svol" if (key_mode == "auto" and unique_svol) or key_mode == "svol" else "suffix"
def key_of(p):
return p["svol"] if mode == "svol" else p["suffix"]
tmpl_keys = [key_of(p) for p in tmpl_parts]
example_kept = {
ename: pixel_keys(oid, mode) for ename, oid in example_oids.items()
}
for ename, kept in example_kept.items():
print(f" {ename}.{part_key}: kept_keys={len(kept)} mode={mode}")
candidates = list(all_texels(regions))
mapping = {}
for key in sorted(set(tmpl_keys), key=lambda x: int(x) if str(x).isdigit() else x):
pattern = {ename: (key in kept) for ename, kept in example_kept.items()}
best = None
best_score = -1
for face, i, j, u, v in candidates:
score = sum(
1
for ename, should_keep in pattern.items()
if (skins[ename].getpixel((u, v))[3] > 10) == should_keep
)
if score > best_score:
best_score = score
best = {
"face": face,
"i": i,
"j": j,
"u": u,
"v": v,
"score": score,
"total": len(pattern),
"key_mode": mode,
}
if best and best_score == best["total"]:
mapping[str(key)] = best
print(f" {part_key}: {len(mapping)}/{len(set(tmpl_keys))} perfect (mode={mode})")
return mapping, mode
def main():
skins = {
e: Image.open(SKINS / m["skin"]).convert("RGBA") for e, m in EXAMPLES.items()
}
classic_plate = {
e: classify_plate_parts(m["oids"])
for e, m in EXAMPLES.items()
if m["arms"] == "classic"
}
slim_plate = {
e: classify_plate_parts(m["oids"])
for e, m in EXAMPLES.items()
if m["arms"] == "slim"
}
result = {"classic": {}, "slim": {}, "key_modes": {"classic": {}, "slim": {}}}
print("=== CLASSIC ===")
for part_key, oid in TEMPLATE_CLASSIC.items():
ex = {e: p[part_key] for e, p in classic_plate.items() if part_key in p}
if part_key == "head":
for e, p in slim_plate.items():
if "head" in p:
ex[e] = p["head"]
mapping, mode = learn_part(
part_key, oid, ex, OVERLAY["classic"][part_key], skins, "auto"
)
result["classic"][part_key] = mapping
result["key_modes"]["classic"][part_key] = mode
print("\n=== SLIM ===")
for part_key, oid in TEMPLATE_SLIM.items():
ex = {e: p[part_key] for e, p in slim_plate.items() if part_key in p}
regions = OVERLAY["slim"][part_key]
if part_key == "head":
for e, p in classic_plate.items():
if "head" in p:
ex[e] = p["head"]
regions = OVERLAY["classic"]["head"]
mapping, mode = learn_part(part_key, oid, ex, regions, skins, "auto")
result["slim"][part_key] = mapping
result["key_modes"]["slim"][part_key] = mode
out = ROOT / "skin_figurine" / "data" / "pixel_uv_maps.json"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(result, indent=2), encoding="utf-8")
print(f"\nWrote {out}")
# Validate
for style, plates, tmpl in [
("classic", classic_plate, TEMPLATE_CLASSIC),
("slim", slim_plate, TEMPLATE_SLIM),
]:
print(f"\nValidation {style}:")
for ename, parts in plates.items():
skin = skins[ename]
for part_key, eoid in parts.items():
amap = result[style][part_key]
mode = result["key_modes"][style][part_key]
kept = pixel_keys(eoid, mode)
pred = {
k
for k, info in amap.items()
if skin.getpixel((info["u"], info["v"]))[3] > 10
}
domain = set(amap)
kept_d = kept & domain
pred_d = pred & domain
print(
f" {ename}.{part_key}: kept={len(kept_d)} pred={len(pred_d)} "
f"inter={len(kept_d & pred_d)} miss={len(kept_d - pred_d)} extra={len(pred_d - kept_d)}"
)
if __name__ == "__main__":
main()