# -*- coding: utf-8 -*- """从授权书扫描件里抠出单位公章、法人印鉴与手写签名,做成透明 PNG。 扫描件底色偏黄且有网点,直接按「非白即墨」抠图会留下一圈脏边,故按通道差 分别处理:红章看 R 与 G/B 的差,签名看整体明度,再各自把弱信号压到全透明。 """ import io import os import shutil import sys import numpy as np from PIL import Image sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") HERE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(HERE, "_bid_assets", "seal_src", "p1_img1.png") OUT = os.path.join(HERE, "_bid_assets", "seal") os.makedirs(OUT, exist_ok=True) # 由 _tmp_seal_locate.py 定位得到(含少量留白) BOX = { "gongzhang": (843, 2235, 1310, 2739), # 圆形单位公章 "yinjian": (1578, 2347, 1821, 2603), # 方形法定代表人印鉴 "signature": (215, 2330, 520, 2520), # 手写签名「刘涛」 } # 扫描件是整幅 A4,但像素为 2140×3264,纵向被拉长了 7.8%(正圆公章的墨迹 # 框 440×479 与正方印鉴的 217×231 都印证了这一点)。按 A4 宽高比压回, # 公章才是正圆、印鉴才是正方,后面按毫米摆放时才不会变形。 SQUASH = 2140 * 297 / 210 / 3264 def stats(a, name): r, g, b = a[..., 0], a[..., 1], a[..., 2] print(f" {name}: 背景估计 R{np.percentile(r, 95):.0f} " f"G{np.percentile(g, 95):.0f} B{np.percentile(b, 95):.0f} " f"最暗 R{r.min()} G{g.min()} B{b.min()}") def red_seal(crop, name): """红章:以 G/B 通道的下沉量作为墨量,颜色统一成正红。""" a = np.asarray(crop, dtype=np.float32) stats(a, name) gb = (a[..., 1] + a[..., 2]) / 2 bg = np.percentile(gb, 96) # 纸张底色 ink = np.clip((bg - gb) / max(bg - 55.0, 1.0), 0, 1) ink[ink < 0.14] = 0 # 压掉网点与黄底 alpha = (ink ** 0.85 * 255).astype(np.uint8) rgb = np.zeros(a.shape, dtype=np.uint8) rgb[..., 0], rgb[..., 1], rgb[..., 2] = 200, 26, 26 out = Image.fromarray(np.dstack([rgb, alpha]), "RGBA") return out def ink_sign(crop, name): """签名:黑色笔迹,按明度取墨量。""" a = np.asarray(crop.convert("L"), dtype=np.float32) print(f" {name}: 明度 背景{np.percentile(a, 95):.0f} 最暗{a.min():.0f}") bg = np.percentile(a, 95) ink = np.clip((bg - a) / max(bg - 60.0, 1.0), 0, 1) ink[ink < 0.18] = 0 alpha = (ink ** 0.9 * 255).astype(np.uint8) rgb = np.zeros(a.shape + (3,), dtype=np.uint8) rgb[..., 0], rgb[..., 1], rgb[..., 2] = 20, 24, 40 # 蓝黑墨 return Image.fromarray(np.dstack([rgb, alpha]), "RGBA") def despeckle(im, min_px=40): """去掉扫描噪点:丢弃面积过小的连通块(用逐行并查集做 8 邻域标记)。""" a = np.asarray(im).copy() mask = a[..., 3] > 8 h, w = mask.shape lab = np.zeros((h, w), dtype=np.int32) parent = [0] def find(x): while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x def union(x, y): rx, ry = find(x), find(y) if rx != ry: parent[max(rx, ry)] = min(rx, ry) for y in range(h): for x in range(w): if not mask[y, x]: continue nb = [lab[y + dy, x + dx] for dy, dx in ((-1, -1), (-1, 0), (-1, 1), (0, -1)) if 0 <= y + dy < h and 0 <= x + dx < w and lab[y + dy, x + dx]] if nb: lab[y, x] = min(nb) for n in nb: union(lab[y, x], n) else: parent.append(len(parent)) lab[y, x] = len(parent) - 1 roots = np.zeros(len(parent), dtype=np.int32) for i in range(1, len(parent)): roots[i] = find(i) flat = roots[lab] ids, cnt = np.unique(flat[flat > 0], return_counts=True) drop = set(ids[cnt < min_px].tolist()) if drop: kill = np.isin(flat, list(drop)) a[..., 3][kill] = 0 print(f" 去斑:丢弃 {len(drop)} 个小块,共 {int(kill.sum())} 像素") return Image.fromarray(a, "RGBA") def trim(im, pad=6): a = np.asarray(im)[..., 3] ys, xs = np.nonzero(a > 8) if not len(ys): return im return im.crop((max(0, xs.min() - pad), max(0, ys.min() - pad), min(im.width, xs.max() + pad), min(im.height, ys.max() + pad))) src = Image.open(SRC).convert("RGB") print(f"源图 {src.size} 纵向压回系数 {SQUASH:.4f}") for name, box in BOX.items(): crop = src.crop(box) crop = crop.resize((crop.width, round(crop.height * SQUASH)), Image.LANCZOS) im = ink_sign(crop, name) if name == "signature" else red_seal(crop, name) im = trim(despeckle(im, 60 if name == "signature" else 30)) path = os.path.join(OUT, f"{name}.png") im.save(path) a = np.asarray(im)[..., 3] ys, xs = np.nonzero(a > 8) bw, bh = xs.max() - xs.min() + 1, ys.max() - ys.min() + 1 print(f" → {name}.png {im.size} 墨迹 {bw}×{bh}(长宽比 {bw / bh:.3f})" f" 非透明像素 {int((a > 8).sum()):,}") # 拼一张白底预览,便于确认抠图干净 prev = Image.new("RGB", (980, 420), "white") x = 20 for name in BOX: im = Image.open(os.path.join(OUT, f"{name}.png")) im.thumbnail((300, 300)) prev.paste(im, (x, 60), im) x += im.width + 30 prev.save(os.path.join(OUT, "_preview.png")) print(f"\n预览:{os.path.join(OUT, '_preview.png')}") # 另存一份到招投标材料目录,供另行取用 SHARE = os.path.join(r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目招投标", "相关准备材料", "印鉴素材(透明底)") CN = {"gongzhang": "单位公章", "yinjian": "法定代表人印鉴", "signature": "法定代表人签名"} if os.path.isdir(os.path.dirname(SHARE)): os.makedirs(SHARE, exist_ok=True) print(f"\n素材另存至 {SHARE}") for name, cn in CN.items(): shutil.copy2(os.path.join(OUT, f"{name}.png"), os.path.join(SHARE, f"{cn}-透明底.png")) print(f" {cn}-透明底.png") shutil.copy2(os.path.join(OUT, "_preview.png"), os.path.join(SHARE, "三枚印鉴预览.png")) print(" 三枚印鉴预览.png")