| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- """dechecker.py —— 兜底用:把 AI 图里画死的"透明棋盘格/背景"抠掉,恢复真透明。
- 重要:现在生图管线已直接向接口请求真·透明 PNG(providers + pipeline),
- 正常情况下根本不需要本模块。只有当上游模型仍把背景画成棋盘格时,才作为
- 最后兜底自动运行。
- 关键改进(避免戳洞):只移除"与图像边缘相连"的背景(四周的棋盘格 + 透明边),
- 角色内部的白色高光/亮点因为被角色包住、不与边缘相连,会被完整保留——
- 不会再像以前那样把白色切成缺口。
- """
- import os
- import sys
- import numpy as np
- from PIL import Image, ImageFilter
- def _border_outside(bg):
- """返回与图像四边相连的背景区域(布尔)。优先用 cv2,没有则用 numpy 形态学重建。"""
- try:
- import cv2
- n, labels = cv2.connectedComponents(bg.astype(np.uint8), connectivity=4)
- border = set(labels[0, :]).union(labels[-1, :], labels[:, 0], labels[:, -1])
- border.discard(0)
- return np.isin(labels, list(border))
- except Exception:
- # numpy 兜底:从四边种子在 bg 内反复膨胀直到稳定(形态学重建)
- seed = np.zeros_like(bg)
- seed[0, :] = bg[0, :]; seed[-1, :] = bg[-1, :]
- seed[:, 0] = bg[:, 0]; seed[:, -1] = bg[:, -1]
- for _ in range(4000):
- grown = seed | np.roll(seed, 1, 0) | np.roll(seed, -1, 0) \
- | np.roll(seed, 1, 1) | np.roll(seed, -1, 1)
- grown &= bg
- if np.array_equal(grown, seed):
- break
- seed = grown
- return seed
- def clean(im):
- """返回 (新图RGBA, 抹掉的像素数)。无棋盘/已透明的图基本无操作。"""
- img = im.convert("RGBA")
- arr = np.asarray(img).copy()
- al = arr[:, :, 3]
- rgb = arr[:, :, :3].astype(np.int16)
- bright = rgb.max(2)
- sat = bright - rgb.min(2)
- light = (al >= 200) & (bright > 185) & (sat < 28) # 棋盘格的白/浅灰方块
- trans = al < 40
- if light.mean() < 0.04: # 几乎没有浅色块 → 不是棋盘
- return img, 0
- bg = light | trans
- outside = _border_outside(bg)
- # 只在"边缘相连的浅色块"占比可观时才动手(避免误伤纯色背景插画)
- if (outside & light).mean() < 0.03:
- return img, 0
- # 向内吃 2px 去掉灰边
- m = outside
- for _ in range(2):
- m = (m | np.roll(m, 1, 0) | np.roll(m, -1, 0)
- | np.roll(m, 1, 1) | np.roll(m, -1, 1))
- removed = int(((al > 10) & m).sum())
- a = al.copy(); a[m] = 0
- arr[:, :, 3] = a
- arr[m, 0] = 0; arr[m, 1] = 0; arr[m, 2] = 0
- out = Image.fromarray(arr.astype(np.uint8), "RGBA")
- out.putalpha(out.getchannel("A").filter(ImageFilter.GaussianBlur(1.0))) # 柔边
- return out, removed
- def declutter_img(img, feather=False):
- out, removed = clean(img)
- return out.convert("RGBA"), removed > 0
- def declutter_file(src, dst=None):
- out, removed = clean(Image.open(src))
- if removed > 0:
- out.save(dst or src)
- return removed > 0
- def _walk(paths):
- for p in paths:
- if os.path.isdir(p):
- for root, _d, files in os.walk(p):
- for f in files:
- if f.lower().endswith(".png"):
- yield os.path.join(root, f)
- elif p.lower().endswith(".png"):
- yield p
- if __name__ == "__main__":
- n = 0
- for f in _walk(sys.argv[1:] or ["."]):
- try:
- if declutter_file(f):
- n += 1
- print("cleaned:", f)
- except Exception as e:
- print("skip", f, e)
- print(f"done. {n} file(s) cleaned.")
|