| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- # -*- 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")
|