手写签名抠图

把手机拍的手写签名照,变成透明底、能直接插进 Word / PDF 的 300dpi PNG。零参数:阈值自己定,不用你调。

这是可操作的演示,下面跑的是真实系统的同一套算法。内置的那张签名是用贝塞尔曲线现画的 合成手写图,不是任何真人的签名。你也可以拖自己的签名照进来 —— 图片不上传服务器,读文件、计算、导出全在这一页里完成,断网也照跑。

1 · 这套系统解决什么问题

需求听起来只有一句话:「把签名照的白纸去掉」。真正难的是下面这些 ——它们决定了这活是「五分钟画图软件魔术棒」还是一套要认真写的东西。

难点现实是什么样这里怎么解
光照不均 手机对着纸拍,同一张纸的纸底灰度从 90 到 230 都有(手挡出的阴影、桌面反光)。 一个固定阈值必然一头糊成块、另一头断成虚线,手调 level 每张都要试三轮。 先做背景归一化:用大核取局部最亮当作「纸」,把原图除掉它;再让 Otsu 在归一化后的「墨 / 纸」双峰上自己定阈值。默认零参数。
签名栏横线 签在有横线的签名栏上,笔画穿过横线 —— 两者本来就是同一个连通域, 整域的长宽比一点也不扁,「按细长判直线」那条路实测删除量为 0 改用水平结构元开运算:只有连续水平长度 ≥ 35% 图宽的像素能活下来, 提出来的就是那条线本身。删完再纵向闭运算,把被切断的竖笔桥接回去。
纸纹与污点 拍照/扫描一定带一堆比笔画小得多的黑点,混进透明图印出来是脏的。 连通域面积过滤:小于主笔画面积 0.4% 的孤立域直接清零。剥线之后再扫一遍, 清掉被切断的横线残段。
插进 Word 尺寸失控 同样像素数的 PNG,插进 Word 可能落成 3cm 宽,也可能占满一页 —— 取决于文件里 有没有写 dpi,而浏览器导出的 PNG 默认不写。 导出时手工往 PNG 里插一个 pHYs 块写死 300dpi,并在卡片上直接告诉你 「按 300dpi 落下来有多宽(cm)」。
抠飞了看不出来 阈值判飞时,输出要么是一张几乎全空的图、要么整片黑,而程序退出码照样是 0。 这种错最贵:它会一路跟到盖章的文件上。 fail-closed 闸:墨迹占比不在 0.3%–45% 之间就拒绝出图,并说明「这张可能不是 深墨浅纸的签名照」。宁可不给,不给一张假的。
签名是敏感件 一枚手写签名在流程上约等于一枚章。传上任何服务器都是白白多一处风险面。 纯前端:整条链路(读文件 → 计算 → 导出)都在浏览器里。没有上传接口, 也就没有「服务器上还留着一份」这个问题。
一套算法两个实现 批量走命令行(进文档流水线,一次几十张),交互走浏览器。两边各写一遍必然漂, 而漂了不会报错 —— 只是两边给出的图不一样。 同一批基准图跑数值对账:两边的 Otsu 阈值差 ≤ 3、墨迹占比差 ≤ 0.6% 才算过门,不过门不许发布。

下面演示区里这些规则都在真的跑,不是画的示意图。

2 · 上手点一点

换一张源图
把签名照拖到这里(也可以直接 ⌘V 粘贴)—— 图片留在你的浏览器里,不会离开这台设备
0
源图(合成 / 你上传的)
抠完 · 灰格 = 透明

3 · 技术上怎么做的

环节做法
背景归一化 核宽取图短边的 12%(大到能吞掉笔画宽度)做灰度膨胀,再均值滤波平滑,得到「这张纸本身 在每个位置有多亮」的估计;原图除以它 × 255。拍照的光照梯度、阴影、反光在这一步被压平。
Otsu 在归一化后的直方图上取类间方差最大的分割点。「墨 / 纸」是标准的双峰分布,Otsu 是这类问题的 教科书解 —— 关键是喂给它的必须是归一化之后的图,直接对原图做 Otsu 一样会被阴影带偏。
软 alpha 阈值不做硬切:T×0.55 以下全不透明,T×1.06 以上全透明,中间线性过渡。 笔锋的抗锯齿因此被保留,放大 4 倍后边缘不会出现台阶。
形态学 膨胀 / 腐蚀 / 开 / 闭全部落在一个单调双端队列的一维滑动窗极值上,复杂度 O(n) 且与核宽无关,横竖各扫一遍即可分离实现。全程 Float32Array,没有第三方图像库。
剥横线 水平开运算提出直线 → 从 alpha 里减掉 → 在删除区做纵向闭运算桥接:只有该列在横线上下 都还有墨时竖笔才会被接回,纯横线区上下无墨不会复活。 并且变换生不生效必须拿前后差分作证 —— 没检出横线就明说「这次是空操作」, 不许静默通过(静默 no-op 是最难发现的假绿)。
300 dpi 浏览器的 canvas.toBlob 不写 pHYs 块。导出时在 IHDR 之后手工插入一个 9 字节 pHYs(11811 像素/米 = 300dpi)并补上 CRC32。插进 Word 就按物理尺寸落地。
机器门 墨迹占比闸(0.3%–45%)、输出非空断言、批量时输入枚举为空即非零退出 —— 一律 fail-closed。 哑掉的守卫和它要防的 bug 是同一类东西。
两实现对账 命令行版(批量、进文档流水线)与浏览器版是同一套算法的两个实现,没有共享代码。 靠一批基准图的数值对账(Otsu ±3、墨占比 ±0.6%)+ 一条 headless 端到端 UI 用例 守住一致性,两门全绿才准发布。
这两道门各抓到过一个「页面正常渲染、控制台零报错」的真 bug:①滑动窗队列按核宽而不是按行长 分配,定长数组越界写被静默丢弃,同一张图两边墨占比 1.51% vs 15.91%,丢了十倍笔画; ②算法文件顶层函数挂到全局,消费方重复声明触发 SyntaxError,整个界面脚本一行都没执行 (点什么都没反应),而此时数值对账那道门是全绿的。所以门要有两道,且必须测不同的东西。
演示中的签名为合成手写图(贝塞尔曲线绘制),不涉及任何真人签名。 你上传的图片仅在本页面内处理,不会被发送到任何服务器;本页无任何外部请求。