165 lines
8.3 KiB
Python
165 lines
8.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
# 2026-10-01 PD 지시 — mobaGameEquipment_v10 결과 36장 중 "쓸 만한 것"은 3장(초록 마법 단검 · 은색 갑옷 · 갈색 부츠)뿐.
|
|
# 합격 기준(PD 가 고른 3장의 공통점): 물건 하나가 따로 떨어져 있다 · 원형 바탕/후광/딸린 조각이 없다 · 전체가 여백 안에 들어온다.
|
|
# 방법: 대상마다 여러 장을 뽑고(장당 약 7초), 투명도(알파)로 자동 채점해 기준에 맞는 것만 추린다 → 추린 것을 사람이 본다.
|
|
# python comfy_moba_batch.py <출력폴더> <round 이름> <대상당 장수> [시작] [끝]
|
|
# 자동 채점(배경 제거 뒤 알파 기준):
|
|
# · 불투명 비율 6~42% (원형 바탕이 붙으면 50% 를 넘는다) · 가장자리 닿음 1.5% 이하(잘림) · 가장 큰 덩어리가 불투명의 88% 이상(딸린 조각 없음)
|
|
import io, json, os, sys, time, uuid, urllib.request, urllib.parse
|
|
from collections import deque
|
|
from PIL import Image, ImageDraw
|
|
|
|
HOST = "http://127.0.0.1:8000"
|
|
CKPT = "mobaGameEquipment_v10.safetensors"
|
|
PREFIX = "(masterpiece:1.2), (best quality:1.2), moba game icon"
|
|
NEG = ("lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, "
|
|
"1girl, 1boy, person, human, character, face, hair, breasts, body, wearing, mannequin, bust, "
|
|
"circle, round frame, border, badge, emblem, aura, halo, light rays, glowing background, explosion, fire background, "
|
|
"multiple objects, two weapons, crossed weapons, duplicate, out of frame")
|
|
|
|
# 1 · 2차에서 형태가 약했던 대상은 낱말을 구체적으로 바꿨다(활 · 벨트 · 목걸이 · 화살통 · 반지 · 둔기 · 투척)
|
|
SUBJECTS = [
|
|
("sword", "longsword"),
|
|
("blunt", "one-handed war hammer"),
|
|
("bow", "longbow, wooden bow with bowstring"),
|
|
("dagger", "dagger"),
|
|
("staff", "magic staff with crystal"),
|
|
("magic_dagger", "magic dagger, glowing blade"),
|
|
("armor", "plate armor, breastplate"),
|
|
("belt", "leather belt with metal buckle"),
|
|
("gloves", "gauntlets"),
|
|
("boots", "leather boots"),
|
|
("ring", "gold ring with gemstone"),
|
|
("necklace", "amulet, pendant with gem on chain"),
|
|
("shield", "shield"),
|
|
("quiver", "arrow quiver, leather tube holding arrows"),
|
|
("spellbook", "closed spellbook, thick book"),
|
|
("throwing", "throwing knife"),
|
|
("orb", "crystal orb"),
|
|
("offhand_sword", "short sword"),
|
|
]
|
|
|
|
|
|
def post(path, data):
|
|
req = urllib.request.Request(HOST + path, data=json.dumps(data).encode("utf-8"), headers={"Content-Type": "application/json"})
|
|
return json.loads(urllib.request.urlopen(req, timeout=60).read())
|
|
|
|
|
|
def get(path):
|
|
return urllib.request.urlopen(HOST + path, timeout=300).read()
|
|
|
|
|
|
def workflow(prompt, seed):
|
|
return {
|
|
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CKPT}},
|
|
"3": {"class_type": "CLIPSetLastLayer", "inputs": {"clip": ["1", 1], "stop_at_clip_layer": -2}},
|
|
"4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["3", 0], "text": prompt}},
|
|
"5": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["3", 0], "text": NEG}},
|
|
"6": {"class_type": "EmptyLatentImage", "inputs": {"width": 512, "height": 512, "batch_size": 1}},
|
|
"7": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0], "seed": seed, "steps": 30, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": 1.0}},
|
|
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["7", 0], "vae": ["1", 2]}},
|
|
"12": {"class_type": "InspyrenetRembg", "inputs": {"image": ["8", 0], "torchscript_jit": "default"}},
|
|
"9": {"class_type": "SaveImage", "inputs": {"images": ["12", 0], "filename_prefix": "moba_batch"}},
|
|
}
|
|
|
|
|
|
def run(prompt, seed):
|
|
pid = post("/prompt", {"prompt": workflow(prompt, seed), "client_id": str(uuid.uuid4())})["prompt_id"]
|
|
t0 = time.time()
|
|
while time.time() - t0 < 600:
|
|
hist = json.loads(get("/history/" + pid))
|
|
if pid in hist:
|
|
st = hist[pid].get("status", {})
|
|
if st.get("status_str") == "error":
|
|
raise RuntimeError(json.dumps(st)[:900])
|
|
for node in hist[pid].get("outputs", {}).values():
|
|
for im in node.get("images", []):
|
|
q = urllib.parse.urlencode({"filename": im["filename"], "subfolder": im.get("subfolder", ""), "type": im.get("type", "output")})
|
|
return Image.open(io.BytesIO(get("/view?" + q))).convert("RGBA")
|
|
time.sleep(1.0)
|
|
raise TimeoutError("comfy timeout")
|
|
|
|
|
|
def score(im):
|
|
"""알파로 채점 — (합격 여부, 불투명 비율, 가장자리 닿음 비율, 가장 큰 덩어리 비율)"""
|
|
n = 96
|
|
a = im.getchannel("A").resize((n, n), Image.BILINEAR)
|
|
px = a.load()
|
|
solid = [[px[x, y] > 40 for x in range(n)] for y in range(n)]
|
|
total = sum(sum(1 for v in row if v) for row in solid)
|
|
if total == 0:
|
|
return False, 0.0, 0.0, 0.0
|
|
opaque = total / float(n * n)
|
|
edge = sum(1 for i in range(n) if solid[0][i]) + sum(1 for i in range(n) if solid[n - 1][i]) + sum(1 for i in range(n) if solid[i][0]) + sum(1 for i in range(n) if solid[i][n - 1])
|
|
edge_ratio = edge / float(4 * n)
|
|
seen = [[False] * n for _ in range(n)]
|
|
best = 0
|
|
for y in range(n):
|
|
for x in range(n):
|
|
if solid[y][x] and not seen[y][x]:
|
|
q = deque([(x, y)]); seen[y][x] = True; size = 0
|
|
while q:
|
|
cx, cy = q.popleft(); size += 1
|
|
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
|
nx, ny = cx + dx, cy + dy
|
|
if 0 <= nx < n and 0 <= ny < n and solid[ny][nx] and not seen[ny][nx]:
|
|
seen[ny][nx] = True; q.append((nx, ny))
|
|
best = max(best, size)
|
|
main = best / float(total)
|
|
ok = 0.06 <= opaque <= 0.42 and edge_ratio <= 0.015 and main >= 0.88
|
|
return ok, opaque, edge_ratio, main
|
|
|
|
|
|
def checker(size, n=16):
|
|
im = Image.new("RGB", (size, size), (58, 58, 58))
|
|
d = ImageDraw.Draw(im)
|
|
for y in range(0, size, n):
|
|
for x in range(0, size, n):
|
|
if (x // n + y // n) % 2:
|
|
d.rectangle([x, y, x + n - 1, y + n - 1], fill=(74, 74, 74))
|
|
return im
|
|
|
|
|
|
def main():
|
|
out, tag, count = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
lo = int(sys.argv[4]) if len(sys.argv) > 4 else 0
|
|
hi = int(sys.argv[5]) if len(sys.argv) > 5 else len(SUBJECTS)
|
|
os.makedirs(out, exist_ok=True)
|
|
subs = SUBJECTS[lo:hi]
|
|
show = 6
|
|
cell, pad, label = 170, 4, 16
|
|
sheet = Image.new("RGB", (pad + 110 + show * (cell + pad), len(subs) * (cell + pad) + pad), (40, 40, 40))
|
|
draw = ImageDraw.Draw(sheet)
|
|
report = []
|
|
for i, (name, body) in enumerate(subs):
|
|
prompt = "%s, (%s:1.35), no humans, single item, isolated, centered, full object in frame, simple background, still life" % (PREFIX, body)
|
|
passed = []
|
|
t0 = time.time()
|
|
for j in range(count):
|
|
im = run(prompt, 12000 + (lo + i) * 29 + j * 503)
|
|
ok, opaque, edge, mainc = score(im)
|
|
fn = "%s_%s_%02d%s.png" % (tag, name, j, "" if ok else "_x")
|
|
im.save(os.path.join(out, fn))
|
|
if ok:
|
|
passed.append((mainc - abs(opaque - 0.24), fn, im))
|
|
passed.sort(key=lambda t: -t[0])
|
|
report.append((name, len(passed), count))
|
|
print("%s %s 합격 %d/%d (%.0f초)" % (tag, name, len(passed), count, time.time() - t0)); sys.stdout.flush()
|
|
y = pad + i * (cell + pad)
|
|
draw.text((pad, y + 4), name, fill=(240, 220, 160))
|
|
draw.text((pad, y + 22), "%d/%d" % (len(passed), count), fill=(200, 200, 200))
|
|
for k, (_, fn, im) in enumerate(passed[:show]):
|
|
bg = checker(cell)
|
|
small = im.resize((cell, cell), Image.LANCZOS)
|
|
bg.paste(small, (0, 0), small)
|
|
sheet.paste(bg, (pad + 110 + k * (cell + pad), y))
|
|
draw.text((pad + 112 + k * (cell + pad), y + 2), fn[len(tag) + len(name) + 2:len(tag) + len(name) + 4], fill=(255, 255, 255))
|
|
path = os.path.join(out, "%s_pass_sheet_%d_%d.png" % (tag, lo, hi))
|
|
sheet.save(path)
|
|
print("sheet", path)
|
|
print("요약", ", ".join("%s %d/%d" % r for r in report))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|