from __future__ import annotations from pathlib import Path import numpy as np from PIL import Image, ImageFilter ROOT = Path(r"D:\Work_AI\Dansori") PUPPET = ROOT / "DansoriEQ" / "src" / "DansoriEQ.App" / "Assets" / "Characters" / "Puppets" / "LeeSoriV2" SOURCE = PUPPET / "leesori_v2_source.png" BACKUP = PUPPET / "leesori_v2_source_pre_waist_fix.png" QA = PUPPET / "qa_source_waist_patch_black.png" AI_PATCH = Path( r"C:\Users\eKeerar\.codex\generated_images\019f277b-18ad-7800-8b99-e77ae92de4bd" r"\ig_0f4c5e4720cfe26e016a47e80d6f9c8191ba3b82a1c165cf39.png" ) def alpha_bbox(im: Image.Image) -> tuple[int, int, int, int]: return im.getchannel("A").getbbox() or (0, 0, im.width, im.height) def subject_bbox_on_checker(im: Image.Image) -> tuple[int, int, int, int]: arr = np.array(im.convert("RGB")) mask = arr.max(axis=2) < 225 ys, xs = np.where(mask) return int(xs.min()), int(ys.min()), int(xs.max() + 1), int(ys.max() + 1) def map_box( src_box: tuple[int, int, int, int], src_bbox: tuple[int, int, int, int], ref_bbox: tuple[int, int, int, int], ) -> tuple[int, int, int, int]: sx0, sy0, sx1, sy1 = src_bbox rx0, ry0, rx1, ry1 = ref_bbox sw, sh = sx1 - sx0, sy1 - sy0 rw, rh = rx1 - rx0, ry1 - ry0 x0, y0, x1, y1 = src_box return ( int(rx0 + ((x0 - sx0) / sw) * rw), int(ry0 + ((y0 - sy0) / sh) * rh), int(rx0 + ((x1 - sx0) / sw) * rw), int(ry0 + ((y1 - sy0) / sh) * rh), ) def main() -> None: base = Image.open(BACKUP if BACKUP.exists() else SOURCE).convert("RGBA") ref = Image.open(AI_PATCH).convert("RGBA") src_bbox = alpha_bbox(base) ref_bbox = subject_bbox_on_checker(ref) # Torso-only region: below crop-top chest, through waist and top waistband. src_patch_box = (86, 306, 282, 505) ref_patch_box = map_box(src_patch_box, src_bbox, ref_bbox) patch = ref.crop(ref_patch_box).resize( (src_patch_box[2] - src_patch_box[0], src_patch_box[3] - src_patch_box[1]), Image.Resampling.LANCZOS, ) mask = Image.new("L", patch.size, 0) px = mask.load() w, h = patch.size for y in range(h): for x in range(w): nx = abs((x / max(1, w - 1)) * 2 - 1) ny = abs((y / max(1, h - 1)) * 2 - 1) edge = max(nx, ny) value = int(max(0.0, min(1.0, (1.0 - edge) / 0.36)) * 210) # Keep the upper chest and far lower pants transition conservative. if y < 28: value = int(value * (y / 28)) if y > h - 28: value = int(value * ((h - y) / 28)) px[x, y] = value mask = mask.filter(ImageFilter.GaussianBlur(5)) out = base.copy() out.alpha_composite(Image.composite(patch, out.crop(src_patch_box), mask), (src_patch_box[0], src_patch_box[1])) out.save(SOURCE) black = Image.new("RGBA", out.size, (16, 16, 18, 255)) black.alpha_composite(out) black.convert("RGB").save(QA) if __name__ == "__main__": main()