"""把位图生成的印章处理成 logo 资产:抠图、统一颜色、矢量化、小尺寸版本。 用法:.work/venv/bin/python scripts/process_seal.py <源图> <输出目录> [品牌红 HEX] """ import sys from pathlib import Path import numpy as np import potrace from PIL import Image from scipy import ndimage src, out_dir = Path(sys.argv[1]), Path(sys.argv[2]) brand = sys.argv[3] if len(sys.argv) > 3 else "#D9291C" out_dir.mkdir(parents=True, exist_ok=True) rgb = tuple(int(brand[i:i + 2], 16) for i in (1, 3, 5)) im = np.asarray(Image.open(src).convert("RGB")).astype(float) redness = (im[..., 0] - (im[..., 1] + im[..., 2]) / 2) / 255 # 印泥浓淡 → 透明度:保留颗粒和毛边,背景完全透明 alpha = np.clip((redness - 0.12) / (0.55 - 0.12), 0, 1) alpha = alpha * alpha * (3 - 2 * alpha) # smoothstep,让颗粒边缘柔和 ys, xs = np.where(redness > 0.35) y0, y1, x0, x1 = ys.min(), ys.max() + 1, xs.min(), xs.max() + 1 side = max(y1 - y0, x1 - x0) pad = round(side * 0.02) canvas = side + 2 * pad cy, cx = (y0 + y1) // 2, (x0 + x1) // 2 half = canvas // 2 def crop(a): out = np.zeros((canvas, canvas), dtype=a.dtype) sy0, sx0 = cy - half, cx - half ys_, xs_ = slice(max(sy0, 0), min(sy0 + canvas, a.shape[0])), slice(max(sx0, 0), min(sx0 + canvas, a.shape[1])) out[ys_.start - sy0:ys_.stop - sy0, xs_.start - sx0:xs_.stop - sx0] = a[ys_, xs_] return out alpha_c = crop(alpha) red_c = crop(redness) # 1. 质感主图(透明 PNG) rgba = np.zeros((canvas, canvas, 4), dtype=np.uint8) rgba[..., :3] = rgb rgba[..., 3] = (alpha_c * 255).round().astype(np.uint8) Image.fromarray(rgba).save(out_dir / "seal-textured.png") on_white = np.full((canvas, canvas, 3), 255.0) on_white = on_white * (1 - alpha_c[..., None]) + np.array(rgb) * alpha_c[..., None] Image.fromarray(on_white.round().astype(np.uint8)).save(out_dir / "seal-textured-on-white.png") def trace(mask, name, turdsize, alphamax=1.0, fill=brand): bm = potrace.Bitmap(~mask) # potracer 把 False 当作前景,这里取反 path = bm.trace(turdsize=turdsize, alphamax=alphamax, opticurve=True, opttolerance=0.2) parts = [] for curve in path: s = curve.start_point d = [f"M{s.x:.1f} {s.y:.1f}"] for seg in curve.segments: if seg.is_corner: d.append(f"L{seg.c.x:.1f} {seg.c.y:.1f}L{seg.end_point.x:.1f} {seg.end_point.y:.1f}") else: d.append(f"C{seg.c1.x:.1f} {seg.c1.y:.1f} {seg.c2.x:.1f} {seg.c2.y:.1f} {seg.end_point.x:.1f} {seg.end_point.y:.1f}") d.append("Z") parts.append("".join(d)) svg = (f'' f'') (out_dir / name).write_text(svg) return len(svg) # 2. 质感矢量版:放大 2 倍再描摹,保留颗粒 big = np.asarray(Image.fromarray((alpha_c * 255).astype(np.uint8)).resize((canvas * 2, canvas * 2), Image.LANCZOS)) / 255 n_tex = trace(big > 0.5, "seal-textured-2x.svg", turdsize=6) # 描摹坐标是 2 倍的,修正 viewBox p = out_dir / "seal-textured-2x.svg" p.write_text(p.read_text().replace(f'viewBox="0 0 {canvas} {canvas}"', f'viewBox="0 0 {canvas * 2} {canvas * 2}"')) p.rename(out_dir / "seal-textured.svg") # 3. 干净版:先模糊去掉颗粒,再二值化描摹,得到平滑的印面和字形 solid = ndimage.gaussian_filter((red_c > 0.35).astype(float), sigma=canvas * 0.006) > 0.5 solid = ndimage.binary_fill_holes(solid) & solid # 保留白字(它们与外部不连通时是「洞」) solid = ndimage.binary_opening(solid, iterations=2) n_clean = trace(solid, "seal-clean.svg", turdsize=canvas * 0.0005 * canvas, alphamax=1.1) # 4. 小尺寸版:白色笔画加粗(墨色腐蚀),保证 32px 下笔画不消失 thick = ndimage.binary_erosion(solid, iterations=max(2, round(canvas * 0.012))) n_small = trace(thick, "seal-small.svg", turdsize=canvas * 0.0008 * canvas, alphamax=1.1) # 5. 纸底:印面外轮廓(不含白字),向内收一点,深色背景上垫在印章下面,让白字和颗粒显示为纸色 paper = ndimage.binary_erosion(ndimage.binary_fill_holes(solid), iterations=max(2, round(canvas * 0.006))) trace(paper, "seal-paper.svg", turdsize=canvas * 0.001 * canvas, alphamax=1.2, fill="#FFFFFF") print(f"canvas={canvas}px brand={brand} textured.svg={n_tex // 1024}KB clean.svg={n_clean // 1024}KB small.svg={n_small // 1024}KB")