| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- """Lightweight image quality checks for generated game assets."""
- from collections import deque
- def _as_rgba_image(image_or_path):
- from PIL import Image
- if isinstance(image_or_path, str):
- return Image.open(image_or_path).convert("RGBA")
- return image_or_path.convert("RGBA")
- def alpha_component_report(image_or_path, max_size=320, threshold=24):
- """Return connected-component stats for non-transparent pixels."""
- img = _as_rgba_image(image_or_path)
- if max(img.size) > max_size:
- scale = max_size / max(img.size)
- img = img.resize((max(1, int(img.width * scale)), max(1, int(img.height * scale))))
- alpha = img.getchannel("A")
- data = bytearray(1 if v > threshold else 0 for v in alpha.getdata())
- w, h = img.size
- opaque = sum(data)
- if opaque == 0:
- return {
- "ok": False,
- "reason": "empty_alpha",
- "size": [w, h],
- "opaque": 0,
- "components": [],
- "significantComponents": [],
- "largestShare": 0,
- }
- visited = bytearray(w * h)
- components = []
- for start, is_opaque in enumerate(data):
- if not is_opaque or visited[start]:
- continue
- q = deque([start])
- visited[start] = 1
- count = 0
- min_x = w
- min_y = h
- max_x = 0
- max_y = 0
- while q:
- idx = q.popleft()
- y, x = divmod(idx, w)
- count += 1
- min_x = min(min_x, x)
- min_y = min(min_y, y)
- max_x = max(max_x, x)
- max_y = max(max_y, y)
- for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
- if nx < 0 or ny < 0 or nx >= w or ny >= h:
- continue
- nidx = ny * w + nx
- if data[nidx] and not visited[nidx]:
- visited[nidx] = 1
- q.append(nidx)
- components.append({
- "pixels": count,
- "share": round(count / max(1, opaque), 4),
- "bbox": [min_x, min_y, max_x + 1, max_y + 1],
- })
- components.sort(key=lambda c: c["pixels"], reverse=True)
- significant = [
- c for c in components
- if c["pixels"] >= max(60, int(opaque * 0.06))
- ]
- return {
- "ok": True,
- "size": [w, h],
- "opaque": opaque,
- "opaqueRatio": round(opaque / max(1, w * h), 4),
- "components": components[:12],
- "significantComponents": significant,
- "largestShare": round(components[0]["pixels"] / max(1, opaque), 4),
- "secondShare": round(components[1]["pixels"] / max(1, opaque), 4) if len(components) > 1 else 0,
- }
- def boss_preview_quality(image_or_path):
- """Check whether a boss preview reads as one assembled character."""
- report = alpha_component_report(image_or_path)
- if not report.get("ok"):
- return False, "预览图没有有效 Alpha 主体。", report
- sig_count = len(report["significantComponents"])
- largest = report["largestShare"]
- second = report["secondShare"]
- opaque_ratio = report["opaqueRatio"]
- if opaque_ratio < 0.035:
- return False, "关主体量太小,预览图主体不清晰。", report
- if sig_count >= 3:
- return False, f"关主被拆成 {sig_count} 个明显分离的大块,无法识别为完整角色。", report
- if sig_count >= 2 and second >= 0.12:
- return False, "关主存在明显分离的大部件,像拆件表而不是完整角色。", report
- if largest < 0.74:
- return False, "关主最大主体占比过低,整体轮廓不完整。", report
- return True, "关主预览主体连贯。", report
|