BurningTimesAi/공유/기획/FirstRising/아이템/comfy_moba_repaint_hq.py

150 lines
9.6 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
# 2026-10-02 PD 지적 — 1차 다시 칠하기(comfy_moba_repaint.py · 512) 결과에 "퀄리티 좀 아쉬운데?".
# 원인(직접 본 것): 512 로 줄여 칠해 1024 원본의 세부가 뭉개졌고, 세기를 낮춰(0.5~0.65) MOBA 풍의 질감이 덜 올라왔다.
# 고품질 방식(이 스크립트):
# 1) 원본에서 윤곽선을 뽑아 ControlNet(선화 또는 캐니)으로 형태를 붙잡는다 → 세기를 높여(0.75~0.9) 질감 · 빛을 MOBA 풍으로 새로 칠해도 형태가 남는다.
# 2) 768(모델 권장 최대)에서 칠한 뒤 4x-UltraSharp 로 키우고 1024 로 맞춰, 약한 세기(0.3)로 한 번 더 다듬는다(세부 선명도).
# 3) 배경 제거(Inspyrenet) → 1024 투명 PNG. 채점은 1차와 같다(원본 실루엣 겹침 · 한 덩어리 · 가장자리).
# python comfy_moba_repaint_hq.py <출력폴더> <round 이름> <번호들 "1015,1253" 또는 all> [윤곽 "lineart,canny"] [세기 "0.75,0.9"] [시드 수=1] [윤곽 강도=0.8]
import io, json, os, sys, time, uuid, urllib.parse
from PIL import Image, ImageDraw
import comfy_moba_repaint as base # 올리기 · 채점 · 캡션 표는 1차 스크립트의 것을 그대로 쓴다
CKPT = base.CKPT
PREFIX = "(masterpiece:1.2), (best quality:1.2), moba game icon"
STYLE = "highly detailed, intricate details, sharp focus, polished metal, strong specular highlights, rim light, rich colors, hand painted game equipment icon"
# 시험(h1)에서 갑옷에 소매 · 팔이 덧붙어 몸 관련 낱말을 더했다
NEG = base.NEG + ", flat color, sketch, monochrome, washed out, sleeves, arms, torso, body, mannequin"
RES1, RES2 = 768, 1024 # 1차로 칠하는 크기 · 최종 크기
CN = {"lineart": "control_v11p_sd15_lineart_fp16.safetensors", "canny": "control_v11p_sd15_canny_fp16.safetensors"}
UPSCALER = "4x-UltraSharp.pth"
REFINE = 0.30 # 2차 다듬기 세기
def prepare(path, res):
"""원본 아이콘을 res 캔버스 가운데에 여백을 두고 놓는다 — (어두운 바탕 RGB, 투명 RGBA)"""
im = Image.open(path).convert("RGBA")
box = im.getchannel("A").getbbox()
if box:
im = im.crop(box)
scale = res * base.FIT / float(max(im.size))
im = im.resize((max(1, int(im.width * scale)), max(1, int(im.height * scale))), Image.LANCZOS)
rgba = Image.new("RGBA", (res, res), (0, 0, 0, 0))
rgba.paste(im, ((res - im.width) // 2, (res - im.height) // 2), im)
rgb = Image.new("RGB", (res, res), base.BG)
rgb.paste(rgba, (0, 0), rgba)
return rgb, rgba
def workflow(src768, src1024, prompt, seed, denoise, kind, cn_strength):
"""768 칠하기(ControlNet) → 4배 확대 → 1024 → 약하게 다듬기(ControlNet) → 배경 제거"""
wf = {
"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}},
"20": {"class_type": "LoadImage", "inputs": {"image": src768}},
"22": {"class_type": "LoadImage", "inputs": {"image": src1024}},
"30": {"class_type": "ControlNetLoader", "inputs": {"control_net_name": CN[kind]}},
# 윤곽은 1024 원본에서 뽑는다(가는 선까지 남긴다)
"31": ({"class_type": "LineArtPreprocessor", "inputs": {"image": ["22", 0], "coarse": "disable", "resolution": 1024}} if kind == "lineart"
else {"class_type": "Canny", "inputs": {"image": ["22", 0], "low_threshold": 0.15, "high_threshold": 0.45}}),
"32": {"class_type": "ControlNetApplyAdvanced", "inputs": {"positive": ["4", 0], "negative": ["5", 0], "control_net": ["30", 0], "image": ["31", 0],
"strength": cn_strength, "start_percent": 0.0, "end_percent": 0.85, "vae": ["1", 2]}},
"21": {"class_type": "VAEEncode", "inputs": {"pixels": ["20", 0], "vae": ["1", 2]}},
"7": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["32", 0], "negative": ["32", 1], "latent_image": ["21", 0], "seed": seed,
"steps": 30, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": denoise}},
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["7", 0], "vae": ["1", 2]}},
"40": {"class_type": "UpscaleModelLoader", "inputs": {"model_name": UPSCALER}},
"41": {"class_type": "ImageUpscaleWithModel", "inputs": {"upscale_model": ["40", 0], "image": ["8", 0]}},
"42": {"class_type": "ImageScale", "inputs": {"image": ["41", 0], "upscale_method": "lanczos", "width": RES2, "height": RES2, "crop": "disabled"}},
"43": {"class_type": "VAEEncode", "inputs": {"pixels": ["42", 0], "vae": ["1", 2]}},
"44": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["32", 0], "negative": ["32", 1], "latent_image": ["43", 0], "seed": seed + 1,
"steps": 24, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": REFINE}},
"45": {"class_type": "VAEDecode", "inputs": {"samples": ["44", 0], "vae": ["1", 2]}},
"12": {"class_type": "InspyrenetRembg", "inputs": {"image": ["45", 0], "torchscript_jit": "default"}},
"9": {"class_type": "SaveImage", "inputs": {"images": ["12", 0], "filename_prefix": "moba_hq"}},
}
return wf
def run(wf):
pid = base.post("/prompt", {"prompt": wf, "client_id": str(uuid.uuid4())})["prompt_id"]
t0 = time.time()
while time.time() - t0 < 900:
hist = json.loads(base.get("/history/" + pid))
if pid in hist:
st = hist[pid].get("status", {})
if st.get("status_str") == "error":
raise RuntimeError(json.dumps(st)[:1500])
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(base.get("/view?" + q))).convert("RGBA")
time.sleep(1.0)
raise TimeoutError("comfy timeout")
def main():
out, tag = sys.argv[1], sys.argv[2]
cap = base.load_captions()
ids = sorted(cap.keys()) if sys.argv[3] == "all" else [int(v) for v in sys.argv[3].split(",")]
kinds = (sys.argv[4] if len(sys.argv) > 4 else "lineart,canny").split(",")
denoises = [float(v) for v in (sys.argv[5] if len(sys.argv) > 5 else "0.75,0.9").split(",")]
seeds = int(sys.argv[6]) if len(sys.argv) > 6 else 1
cn_strength = float(sys.argv[7]) if len(sys.argv) > 7 else 0.8
old_dir = sys.argv[8] if len(sys.argv) > 8 else "" # 1차 결과(고른 것) 폴더 — 있으면 비교 칸으로 넣는다
os.makedirs(out, exist_ok=True)
show = len(kinds) * len(denoises) * seeds
extra = 2 if old_dir else 1
cell, pad = 256, 4
sheet = Image.new("RGB", (pad + (show + extra) * (cell + pad), len(ids) * (cell + pad) + pad), (40, 40, 40))
draw = ImageDraw.Draw(sheet)
scores = {}
def put(im, col, y, text):
bg = base.checker(cell); small = im.resize((cell, cell), Image.LANCZOS); bg.paste(small, (0, 0), small)
sheet.paste(bg, (pad + col * (cell + pad), y))
draw.text((pad + col * (cell + pad) + 3, y + 2), text, fill=(255, 255, 255))
for i, gid in enumerate(ids):
path = os.path.join(base.SRC, "%d.png" % gid)
rgb1, _ = prepare(path, RES1)
rgb2, rgba2 = prepare(path, RES2)
n1 = base.upload(rgb1, "bt_moba_hq768_%d.png" % gid)
n2 = base.upload(rgb2, "bt_moba_hq1024_%d.png" % gid)
src_mask = base.mask(rgba2)
prompt = "%s, (%s:1.2), %s, no humans, single item, isolated, dark background" % (PREFIX, cap[gid], STYLE)
y = pad + i * (cell + pad)
put(rgba2, 0, y, "%d 원본" % gid)
col = 1
if old_dir:
old = os.path.join(old_dir, "%d_0.png" % gid)
if os.path.exists(old):
put(Image.open(old).convert("RGBA"), 1, y, "1차(512)")
col = 2
results = []
t0 = time.time()
for kind in kinds:
for d in denoises:
for j in range(seeds):
fn = "%s_%d_%s_d%02d_%d.png" % (tag, gid, kind, int(d * 100), j)
# 이미 만든 그림은 다시 만들지 않는다(긴 작업이 끊겨도 이어서 돌린다)
if os.path.exists(os.path.join(out, fn)):
im = Image.open(os.path.join(out, fn)).convert("RGBA")
else:
im = run(workflow(n1, n2, prompt, 40000 + gid * 7 + j * 911 + int(d * 100), d, kind, cn_strength))
im.save(os.path.join(out, fn))
s, iou, mainc, edge = base.score(im, src_mask)
results.append(dict(file=fn, score=round(s, 3), iou=round(iou, 3), main=round(mainc, 3), edge=round(edge, 3), denoise=d, kind=kind))
put(im, col, y, "%s d%.2f #%d IoU%.2f" % (kind, d, j, iou)); col += 1
scores[gid] = results
print("%s %d · %d장 · %.0f초" % (tag, gid, len(results), time.time() - t0)); sys.stdout.flush()
sheet.save(os.path.join(out, "%s_sheet.png" % tag))
json.dump(scores, io.open(os.path.join(out, "%s_scores.json" % tag), "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print("sheet", os.path.join(out, "%s_sheet.png" % tag))
if __name__ == "__main__":
main()