Files
workbuddy_skills/draw-ui/scripts/prepare_image_asset.py
T
admin 237a09a5b0 修:draw-ui / oil-motion 原被当子模块指针收录 ⇒ 改为正常文件入库(两份内容原先对别人是空的)
一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。

二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。

三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
2026-10-08 22:29:52 +08:00

170 lines
6.1 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
from PIL import Image
def foreground_mask(img: Image.Image, *, threshold: int) -> Image.Image:
rgb = img.convert("RGB")
return Image.eval(rgb, lambda px: 0 if px >= threshold else 255).convert("L")
def trim(img: Image.Image, *, threshold: int, padding: int) -> Image.Image:
mask = foreground_mask(img, threshold=threshold)
bbox = mask.getbbox()
if bbox is None:
return img
left, top, right, bottom = bbox
left = max(0, left - padding)
top = max(0, top - padding)
right = min(img.width, right + padding)
bottom = min(img.height, bottom + padding)
return img.crop((left, top, right, bottom))
def trim_alpha(img: Image.Image, *, padding: int) -> Image.Image:
rgba = img.convert("RGBA")
alpha = rgba.getchannel("A")
bbox = alpha.getbbox()
if bbox is None:
return rgba
left, top, right, bottom = bbox
left = max(0, left - padding)
top = max(0, top - padding)
right = min(rgba.width, right + padding)
bottom = min(rgba.height, bottom + padding)
return rgba.crop((left, top, right, bottom))
def white_to_alpha(img: Image.Image, *, threshold: int, feather: int) -> Image.Image:
rgba = img.convert("RGBA")
pixels = rgba.load()
for y in range(rgba.height):
for x in range(rgba.width):
red, green, blue, alpha = pixels[x, y]
brightness = max(red, green, blue)
if red >= threshold and green >= threshold and blue >= threshold:
pixels[x, y] = (255, 255, 255, 0)
continue
if feather > 0 and brightness >= threshold - feather:
new_alpha = int(alpha * (threshold - brightness + feather) / feather)
pixels[x, y] = (red, green, blue, max(0, min(alpha, new_alpha)))
return rgba
def parse_hex_color(value: str) -> tuple[int, int, int]:
raw = value.strip().lstrip("#")
if len(raw) != 6:
raise ValueError("key color must be a 6-digit hex color, for example #00ff00")
return int(raw[0:2], 16), int(raw[2:4], 16), int(raw[4:6], 16)
def key_color_to_alpha(
img: Image.Image,
*,
key_color: tuple[int, int, int],
threshold: int,
feather: int,
despill: bool,
edge_contract: int,
) -> Image.Image:
rgba = img.convert("RGBA")
pixels = rgba.load()
kr, kg, kb = key_color
for y in range(rgba.height):
for x in range(rgba.width):
red, green, blue, alpha = pixels[x, y]
distance = ((red - kr) ** 2 + (green - kg) ** 2 + (blue - kb) ** 2) ** 0.5
if distance <= threshold:
pixels[x, y] = (red, green, blue, 0)
continue
if feather > 0 and distance <= threshold + feather:
new_alpha = int(alpha * (distance - threshold) / feather)
pixels[x, y] = (red, green, blue, max(0, min(alpha, new_alpha)))
if despill:
pixels = rgba.load()
for y in range(rgba.height):
for x in range(rgba.width):
red, green, blue, alpha = pixels[x, y]
if alpha == 0:
continue
# Remove green spill on semi-transparent and edge pixels.
green_excess = green - max(red, blue)
if green_excess <= 0:
continue
edge_factor = 1.0 - (alpha / 255.0)
reduction = int(green_excess * (0.72 + 0.28 * edge_factor))
green = max(max(red, blue), green - reduction)
pixels[x, y] = (red, green, blue, alpha)
if edge_contract > 0:
rgba = contract_alpha_edge(rgba, pixels=edge_contract)
return rgba
def contract_alpha_edge(img: Image.Image, *, pixels: int) -> Image.Image:
rgba = img.convert("RGBA")
alpha = rgba.getchannel("A")
try:
from PIL import ImageFilter
for _ in range(pixels):
alpha = alpha.filter(ImageFilter.MinFilter(3))
rgba.putalpha(alpha)
except Exception:
pass
return rgba
def main() -> int:
parser = argparse.ArgumentParser(
description="Prepare AI-generated UI assets by trimming white padding and optionally making white background transparent."
)
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--no-trim", action="store_true", help="Keep original canvas size.")
parser.add_argument("--alpha", action="store_true", help="Convert near-white background to transparent alpha.")
parser.add_argument("--key-color", help="Convert a chroma key color such as #00ff00 to transparent alpha.")
parser.add_argument("--threshold", type=int, default=248, help="Near-white threshold, 0-255.")
parser.add_argument("--key-threshold", type=int, default=28, help="Chroma key distance threshold.")
parser.add_argument("--feather", type=int, default=12, help="Soft alpha range below threshold.")
parser.add_argument("--despill", action="store_true", help="Reduce green spill on chroma-keyed edges.")
parser.add_argument("--edge-contract", type=int, default=0, help="Shrink alpha edge by this many pixels after keying.")
parser.add_argument("--padding", type=int, default=24, help="Padding kept around trimmed foreground.")
args = parser.parse_args()
img = Image.open(args.input).convert("RGBA")
if args.key_color:
img = key_color_to_alpha(
img,
key_color=parse_hex_color(args.key_color),
threshold=args.key_threshold,
feather=args.feather,
despill=args.despill,
edge_contract=args.edge_contract,
)
if not args.no_trim:
img = trim_alpha(img, padding=args.padding)
elif not args.no_trim:
img = trim(img, threshold=args.threshold, padding=args.padding)
if args.alpha:
img = white_to_alpha(img, threshold=args.threshold, feather=args.feather)
args.output.parent.mkdir(parents=True, exist_ok=True)
img.save(args.output)
print(f"wrote {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())