77 lines
2.1 KiB
Python
77 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageFilter
|
|
|
|
|
|
def clean_alpha(src: Path, dst: Path) -> tuple[int, int]:
|
|
img = Image.open(src).convert("RGBA")
|
|
w, h = img.size
|
|
px = img.load()
|
|
|
|
alpha = img.getchannel("A")
|
|
solid = alpha.point(lambda a: 255 if a > 8 else 0)
|
|
inner = solid.filter(ImageFilter.MinFilter(21))
|
|
edge = Image.new("L", img.size, 0)
|
|
edge.paste(solid)
|
|
edge_px = edge.load()
|
|
inner_px = inner.load()
|
|
|
|
removed = 0
|
|
softened = 0
|
|
for y in range(h):
|
|
upper_zone = y < int(h * 0.55)
|
|
for x in range(w):
|
|
r, g, b, a = px[x, y]
|
|
if a == 0:
|
|
continue
|
|
|
|
near_edge = edge_px[x, y] and not inner_px[x, y]
|
|
if not near_edge and a > 225:
|
|
continue
|
|
|
|
bright = (r + g + b) / 3
|
|
chroma = max(r, g, b) - min(r, g, b)
|
|
semi = a < 245
|
|
|
|
neutral_matte = semi and bright > 118 and chroma < 82
|
|
pink_or_cyan_matte = (
|
|
semi
|
|
and upper_zone
|
|
and bright > 130
|
|
and (
|
|
(r > 135 and b > 130 and g > 80)
|
|
or (g > 135 and b > 130 and r < 175)
|
|
)
|
|
and chroma < 135
|
|
)
|
|
|
|
if near_edge and (neutral_matte or pink_or_cyan_matte):
|
|
px[x, y] = (r, g, b, 0)
|
|
removed += 1
|
|
elif semi and neutral_matte:
|
|
px[x, y] = (r, g, b, int(a * 0.2))
|
|
softened += 1
|
|
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
img.save(dst)
|
|
return removed, softened
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("paths", nargs="+", type=Path)
|
|
parser.add_argument("--in-place", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
for path in args.paths:
|
|
dst = path if args.in_place else path.with_name(path.stem + "_clean.png")
|
|
removed, softened = clean_alpha(path, dst)
|
|
print(f"{path} -> {dst} removed={removed} softened={softened}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|