cn-signature-verifier
一个轻量级的中文手写签名验证工具。 基于“骨架重合度”算法,无需后端 AI 模型,纯前端实现。可以有效防止用户乱涂乱画。
安装
npm install cn-signature-verifier
运行示例 (Run Examples)
本项目提供了三个平台的完整示例,位于 examples/ 目录下。
Web / H5: 直接用浏览器打开
examples/web-h5/index.html。微信小程序: 使用微信开发者工具导入
examples/wechat-native目录。UniApp: 使用 HBuilderX 导入
examples/uniapp目录运行。
这份文档详细介绍了核心算法函数 verifyPixels 的用法、参数配置以及调优指南。
此函数是整个库的“大脑”,它是纯 JavaScript 实现的,不依赖 DOM,因此既可以在 浏览器/小程序 中使用,也可以在 Node.js 后端环境中使用。
verifyPixels API 使用文档
verifyPixels 是核心校验函数,用于对比“用户手写笔迹”与“标准参考字体”的像素重合度。
1. 函数签名
import { verifyPixels } from 'cn-signature-verifier/src/core.js';
const result = verifyPixels(userPixels, refPixels, options);
2. 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userPixels | Uint8ClampedArray |
是 | 用户手写画布的像素数据。格式为 [r, g, b, a, ...] 的一维数组。 |
| refPixels | Uint8ClampedArray |
是 | 标准参考字体画布的像素数据。格式同上。长度必须与 userPixels 一致。 |
| options | Object |
否 | 配置项对象,用于调整验证的严格程度(详见下表)。 |
options 配置项详解
| 属性名 | 类型 | 默认值 | 描述 | 调优建议 |
|---|---|---|---|---|
thresholdCoverage |
Number | 0.12 |
覆盖率及格线 (0~1)。 计算公式: 重合像素 / 参考字像素。表示用户笔迹覆盖了背景字的多少比例。 |
太严(难通过):调低 (如 0.08)。 太松(空心字也过):调高 (如 0.2)。 注意:如果背景字很粗,建议调低此值。 |
thresholdMess |
Number | 0.65 |
越界率容忍上限 (0~1)。 计算公式: 越界像素 / 用户总像素。表示用户的笔画有多少比例画在了字外面。 |
太严(连笔挂掉):调高 (如 0.75)。 太松(乱画也能过):调低 (如 0.5)。 |
minInkRatio |
Number | 0.005 |
最小笔墨量。 防止用户只点了一个点就提交。 |
通常保持默认即可。 |
3. 返回值说明
函数返回一个对象 Result:
{
pass: Boolean, // 验证是否通过
message: String, // 提示信息 (例如 "验证通过" 或 "请勿乱涂乱画")
score: {
coverage: Number, // 实际覆盖率 (例如 0.15 代表 15%)
mess: Number, // 实际越界率 (例如 0.40 代表 40%)
ink: Number // 用户绘制的总像素点数
}
}
4. 算法原理 (简述)
该算法基于 RGBA 像素分析:
- 识别参考区:遍历
refPixels,Alpha 值 > 20 的区域被视为“标准答案区域”。 - 识别笔迹:遍历
userPixels,识别深色像素(R < 150 且 Alpha > 50)为“用户笔迹”。 - 对比计算:
- 重合点 (Overlap):用户笔迹 落在 标准答案区域内的点。
- 越界点 (Mess):用户笔迹 落在 标准答案区域外的点。
- 判定:
- 如果
Overlop / 参考总面积>thresholdCoverage - 并且
Mess / 用户总笔迹<thresholdMess - 则判定为 通过。
- 如果
5. 使用示例
场景 A:在浏览器中使用 (原生 Canvas)
import { verifyPixels } from 'cn-signature-verifier/src/core.js';
// 1. 获取用户 Canvas 数据
const userCanvas = document.getElementById('userCanvas');
const userCtx = userCanvas.getContext('2d');
const userData = userCtx.getImageData(0, 0, 300, 150).data; // Uint8ClampedArray
// 2. 生成参考数据 (在内存中创建一个 Canvas)
const refCanvas = document.createElement('canvas');
refCanvas.width = 300;
refCanvas.height = 150;
const refCtx = refCanvas.getContext('2d');
// 绘制标准字
refCtx.font = 'bold 100px sans-serif';
refCtx.fillStyle = 'black';
refCtx.fillText('张三', 150, 75);
const refData = refCtx.getImageData(0, 0, 300, 150).data;
// 3. 调用验证
const result = verifyPixels(userData, refData, {
thresholdCoverage: 0.1, // 稍微宽松一点
thresholdMess: 0.6
});
if (result.pass) {
console.log("验证成功!覆盖率:", result.score.coverage);
} else {
console.error(result.message);
}
场景 B:在 Node.js 后端使用
如果你想在服务器端验证签名(例如用户上传了签名的 base64 图片),你需要使用 canvas 库。
npm install canvas
const { createCanvas, loadImage } = require('canvas');
const { verifyPixels } = require('cn-signature-verifier/src/core.js');
async function verifyOnServer(userImageBuffer, targetName) {
const width = 300;
const height = 150;
// 1. 加载用户上传的图片
const userCanvas = createCanvas(width, height);
const userCtx = userCanvas.getContext('2d');
const img = await loadImage(userImageBuffer);
userCtx.drawImage(img, 0, 0, width, height);
const userPixels = userCtx.getImageData(0, 0, width, height).data;
// 2. 生成标准字图片
const refCanvas = createCanvas(width, height);
const refCtx = refCanvas.getContext('2d');
refCtx.font = 'bold 100px "SimHei", sans-serif'; // 注意服务器要有对应字体
refCtx.fillStyle = '#000';
refCtx.textAlign = 'center';
refCtx.textBaseline = 'middle';
// 使用 strokeText 加粗,提高服务器端验证的容错率
refCtx.lineWidth = 15;
refCtx.strokeText(targetName, width / 2, height / 2);
refCtx.fillText(targetName, width / 2, height / 2);
const refPixels = refCtx.getImageData(0, 0, width, height).data;
// 3. 验证
// 注意:如果使用了加粗(lineWidth),建议降低 thresholdCoverage
return verifyPixels(userPixels, refPixels, {
thresholdCoverage: 0.08
});
}
6. 调优指南 (Troubleshooting)
当验证结果不符合预期时,请根据返回的 score 进行调整:
Q1: 用户写得很工整,但提示“请沿着背景字书写” (验证失败)
- 原因:用户的笔画太细,或者背景字太细,导致重合面积不够;或者
thresholdCoverage设得太高。 - 解决:
- 在 UI 上将背景字加粗 (
lineWidth+strokeText)。 - 将
options.thresholdCoverage从默认的0.12降低到0.08。
- 在 UI 上将背景字加粗 (
Q2: 用户在旁边乱画圈,居然通过了?
- 原因:
thresholdMess设得太高,允许了过多的杂乱笔画。 - 解决:将
options.thresholdMess从默认的0.65降低到0.5或更低。
Q3: 用户还没写完(只写了一半),提示通过了?
- 原因:用户写的那一半完美覆盖了背景字,导致 mess 很低,且 coverage 刚好过了及格线。
- 解决:提高
options.thresholdCoverage。
Q4: 我把背景字变得非常非常粗,结果很难通过?
- 原因:分母(背景字面积)变大了,分子(重合面积)增加幅度不如分母大,导致覆盖率数值下降。
- 解决:字越粗,
thresholdCoverage应该越低。建议设为0.06-0.08。
7. 移动端体验优化指南
在手机端手写时,用户常反馈“明明写对了,但验证不通过”或“很难完全覆盖背景字”。这通常是因为:
- 手指遮挡:用户看不清笔尖落点。
- 背景字太细:标准字体的线条很细,用户稍微写偏一点就算“越界”或“未重合”。
解决方案:加粗字体 + 调整阈值
通过 Canvas 的 描边 (Stroke) 功能人为加粗背景字,并配合参数调整,可以显著提高通过率。
步骤 1:修改 UI 显示(让用户看到的字变粗)
在你的绘图初始化逻辑中(例如 init 或 drawBg 函数),使用 strokeText 来加粗文字。
// 1. 设置颜色
ctx.fillStyle = '#e0e0e0';
ctx.strokeStyle = '#e0e0e0'; // 描边颜色需与填充一致
// 2. 设置字体
ctx.font = 'bold 100px sans-serif'; // 使用粗体
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 3. 【关键】设置描边宽度 (加粗程度)
// 推荐 15-20px,数值越大字越粗,容错率越高
ctx.lineWidth = 15;
ctx.lineJoin = 'round'; // 圆润转角
// 4. 同时绘制描边和填充
ctx.strokeText(targetName, x, y); // 先描边
ctx.fillText(targetName, x, y); // 后填充
步骤 2:修改验证逻辑(让程序比对的字也变粗)
注意: 验证时生成的“参考图”必须与用户看到的“背景图”粗细一致。
如果你使用的是封装好的 verifyWeb 或 verifyUniApp,可能需要修改适配器源码,或者确保传入的 font 足够粗。如果需要极高的容错率,建议直接调整核心校验参数。
步骤 3:调整覆盖率阈值 (thresholdCoverage)
这是最重要的一步。 当字体变粗后,字体的总像素面积(分母)会大幅增加。如果用户的笔画粗细不变,计算出的覆盖率数值会自然下降。
- 未加粗时:及格线通常设为
0.12(12%)。 - 加粗后 (
lineWidth=15):建议将及格线降低至0.06~0.08。
完整优化配置示例
const result = verifyPixels(userPixels, refPixels, {
// 降低覆盖率要求,因为分母(背景字)变大了
thresholdCoverage: 0.08,
// 越界率保持不变,或者稍微放宽
thresholdMess: 0.65,
// 必须确保 refPixels 生成时也使用了 strokeText 加粗,
// 否则用户画在描边区域会被算作“越界”。
});
附:常见粗细设置参考
| 场景 | lineWidth 设置 | 推荐 thresholdCoverage | 体验描述 |
|---|---|---|---|
| PC 鼠标签字 | 0 (默认) |
0.12 |
精准,要求高。 |
| 手机 - 适中 | 10 |
0.10 |
适合电容笔书写。 |
| 手机 - 宽松 | 15 |
0.08 |
推荐。手指书写体验最佳,不容易误判。 |
| 极度宽松 | 25 |
0.05 |
字非常肥大,用户几乎随便画画都能碰到字。 |