简介:面向机器学习与图像识别学习者,一套基于C++的手写签名真伪识别系统完整工程包,整合了图像去噪归一化、边缘轮廓增强、特征提取与模式匹配、支持向量机分类及演化计算优化等关键环节,可用于真假签名自动鉴定。压缩包共40个文件,以13个.h头文件和12个.cpp源文件为核心,构成可编译的Visual Studio工程;另附使用说明、README、图标及样例图片,整体仅1.08MB,结构紧凑便于查阅。目前已有84人学习浏览,适合正在开展课程设计或毕业设计的高校学生,以及想快速上手签名识别项目的开发者。包内模块划分明确,涵盖图像处理、目标匹配、Ada机器学习、MD5校验等功能,并留有特征向量分析与参数优化接口;读者可根据文档快速搭建环境,深入理解从预处理到分类的完整流水线,也能基于现有代码进行二次开发与实验扩展。
1. 手写签名真伪识别为什么难:一份自带完整脱敏源码的C++工程包
肉眼鉴别手写签名是个经验活,模仿笔迹和本人快写签名放在一起,连鉴定师都要反复比对笔画走势、起收笔力度才能下结论,普通人看走眼的概率并不低。机器学习做签名真伪识别,要解决的正是这种「看起来很像,但要给出一个可复用的判断依据」的问题:先把签名图像变成特征,再用分类器把真签名和伪签名分开。这里拆的是一套完整的C++ MFC工程,内置了图像预处理、特征提取、模式匹配、AdaBoost训练与判定全链路,还带样本管理和一份演化计算调参说明。适合正在做图像识别课程设计的人、想读一套原生C++图像处理代码的工程师,以及所有打算从零复现签名识别流程、又不希望依赖Python生态的从业者。
2. 工程拆包:从文件命名反推模块职责,以及先读哪份文档
打开工程包后,先别急着去点imgMatch.exe。这套包里文件比较多,其中既有 MFC 的 Doc/View 框架代码,也有独立的算法模块,还有几份文档和两个图标资源。先把文件按职能分好类,后面改代码时才不会在界面代码和算法代码之间来回迷路。
2.1 文件清单与模块映射
从压缩包里的文件名可以很清楚地反推每个文件在流水线里的位置,下面这张表是我拆包时整理的对应关系:
| 文件 / 目录 | 模块方向 | 在识别流程里的作用 |
|---|---|---|
| imgMatch.sln / imgMatch.dsp / imgMatch.vcxproj | 工程与编译入口 | 三套工程文件对应不同 VS 版本,sln 是当前主流入口 |
| imgMatch.cpp / imgMatchDoc.cpp / imgMatchView.cpp / MainFrm.cpp | MFC 程序框架 | Doc/View 架构,负责界面、菜单、消息分发 |
| ImgPro.cpp / ImgPro.h | 图像预处理 | 灰度化、二值化、去噪、归一化等核心算法 |
| targetMatch.cpp / targetMatch.h | 特征提取与匹配 | 把预处理后的签名图切成网格、统计密度,产出特征向量 |
| adaMachineLearning.cpp / adaMachineLearning.h | AdaBoost 训练与分类 | 训练弱分类器序列,输出真伪判定结果 |
| MyBitamp.cpp / MyBitamp.h | BMP 位图读写 | 直接操作位图缓冲区,绕开 GDI+ 的格式兼容限制 |
| md5.cpp / md5.h | 样本一致性校验 | 防止图像在多次实验中被无意修改后特征库失效 |
| AdvancedSet.cpp / AdvancedSet.h | 参数设置界面 | 调整预处理阈值、网格数等运行参数 |
| resource.h / imgMatch.rc / imgMatch.ico | 界面资源 | 菜单、图标等 MFC 资源定义 |
| bg3.bmp / bg4.bmp | 界面背景位图 | 非样本数据,运行时加载的界面素材 |
| 使用说明.txt / 说明文件.txt / 附赠资源.docx | 文档 | 部署步骤、代码结构、调参思路 |
| imgMatch.exe | 编译产物 | Release 编译出的可执行程序,可直接运行 |
这个工程与常见 Python 方案最大的区别在于:它没有依赖 OpenCV。MyBitamp.cpp直接用指针操作原始位图数据,这让整套代码可以脱离大型第三方库单独编译。代价是图像编解码的部分要自己处理,但反过来看,因为不依赖 OpenCV 版本,你在任意一台 Windows 机器上用 VS 打开都能编译,省去了配环境的麻烦。
模块边界也值得一提。ImgPro做图像前处理,targetMatch做特征匹配,adaMachineLearning做训练分类,MD5 模块做数据校验,这几块的职能是分开的。如果你想换成 SVM 或者移植到 Python,只需要替换分类器这一层,完全不用动预处理链路。
2.2 两份说明文档和一份附赠,阅读顺序别搞反
包里有两份文本文档:使用说明.txt和说明文件.txt,名字很接近,但内容定位不一样。按照这类工程包的惯例,使用说明.txt通常讲的是环境与启动流程,例如打开哪个工程文件、编译环境要求、样本目录放在哪里、点哪个菜单开始训练;说明文件.txt则偏代码结构,讲各文件之间的调用关系。正确的阅读顺序是先看使用说明.txt把程序跑起来,再回头看说明文件.txt理代码,否则你会被一堆 MFC 宏搞得一头雾水。
附赠资源.docx里面通常是演化计算调参的补充材料。项目正文里提到的演化计算优化,实际落地时主要用来做特征筛选和分类器参数寻优,这类文档一般会附几张「优化前后准确率对比」的图,以及特征子集选择的思路,适合在跑通代码之后再读,直接把它当 PDF 翻反而体会不到价值。
另外注意一个细节:MyBitamp.cpp的文件名拼写和通用写法不一致(一般是 MyBitmap),这是工程里常见的命名笔误,不影响编译,但你在 IDE 里搜索文件时别只记一个拼写。
3. 前处理链路:ImgPro 的图像预处理与特征向量提取实战
签名识别最容易被忽视、也最容易翻车的就是前处理。很多人拿到图像直接送分类器,结果训练集准确率很高,换一批图全跪。问题往往不在分类器,而在输入图像不一致:有的是彩色扫描件,有的是手机拍的白纸上黑字,还有的是灰度传真。ImgPro 模块做的事情就是把这些杂乱的图像统一成同一规格,再提取特征。
3.1 五个预处理步骤,顺序不能乱
第一是灰度化,用加权公式把 RGB 三通道合成单通道,目的是消除底色和色彩干扰;第二是二值化,把签名笔迹和背景分开,这一步在工程里用的是大津法自动求阈值,而不是固定阈值,因为扫描件亮度差异大,固定阈值很难覆盖所有样本;第三是中值滤波去噪,用 3×3 窗口去掉孤立噪点,同时保留笔锋的转折;第四是归一化,把签名图像缩放到统一尺寸,消除不同扫描仪的尺寸差异;第五是细化,这一步在部分版本里是可选的,它让笔画宽度统一,便于提取骨架类特征,但也会丢失压力信息,所以严格来说应该走两条特征路径,一条保留原始粗细,一条做骨架特征,再合并进向量。
顺序不能乱是有原因的。如果先缩放再做二值化,缩放的插值算法会在笔画边缘引入一层灰色过渡带,二值化时这层过渡带很容易被切成锯齿边缘;如果先做中值滤波再做灰度化,彩色图的滤波窗口会同时混合 RGB 三个通道的噪声,效果不如灰度化之后再滤。所以通用顺序是严格的灰度→二值→滤波→归一化→可选细化。
AdvancedSet.cpp这个模块就是给上述参数留的界面入口,你可以通过设置对话框调整滤波窗口大小、归一化目标尺寸和网格数,但一旦调了,就必须保证训练和识别时用同一组参数,这是后面避坑章节的重点。
3.2 网格密度特征提取:一段贴近工程实际的 C++ 实现
targetMatch.cpp里最核心的逻辑,是从预处理后的图像中计算特征向量。下面这段代码是这类工程最常用的简化写法,与包内ImgPro的处理思路一致,你可以对照源码看差异:
// targetMatch.cpp 特征提取核心流程 bool BuildFeatureVector(BYTE* src, int w, int h, vector<float>& feat) { // 1. 灰度化:三通道按人眼敏感度加权合并 vector<BYTE> gray(w * h); for (int i = 0; i < w * h; i++) { BYTE r = src[i * 3 + 0]; BYTE g = src[i * 3 + 1]; BYTE b = src[i * 3 + 2]; gray[i] = (BYTE)(0.299f * r + 0.587f * g + 0.114f * b); } // 2. 大津法自动求二值化阈值 int th = OtsuThreshold(gray.data(), w * h); vector<BYTE> bin(w * h); for (int i = 0; i < w * h; i++) { bin[i] = (gray[i] > th) ? 0 : 255; // 笔迹为0,背景为255 } // 3. 中值滤波去噪,窗口3x3 MedianFilter(bin.data(), w, h, 3); // 4. 尺寸归一化到统一规格,保证不同来源图片特征可比 const int NORM_W = 256, NORM_H = 128; ResizeImage(bin.data(), w, h, NORM_W, NORM_H); // 5. 切8x16网格,计算每个网格内笔迹像素占比 const int GRID_X = 8, GRID_Y = 16; feat.clear(); for (int gy = 0; gy < GRID_Y; gy++) { for (int gx = 0; gx < GRID_X; gx++) { int cnt = 0; int cellW = NORM_W / GRID_X; int cellH = NORM_H / GRID_Y; for (int yy = gy * cellH; yy < (gy + 1) * cellH; yy++) { for (int xx = gx * cellW; xx < (gx + 1) * cellW; xx++) { if (bin[yy * NORM_W + xx] == 0) cnt++; // 0表示笔迹点 } } feat.push_back((float)cnt / (float)(cellW * cellH)); } } // 6. 附加方向特征:按行统计笔迹点数、按列统计笔迹点数 for (int yy = 0; yy < NORM_H; yy++) { int rowCnt = 0; for (int xx = 0; xx < NORM_W; xx++) if (bin[yy * NORM_W + xx] == 0) rowCnt++; feat.push_back((float)rowCnt / NORM_W); } for (int xx = 0; xx < NORM_W; xx++) { int colCnt = 0; for (int yy = 0; yy < NORM_H; yy++) if (bin[yy * NORM_W + xx] == 0) colCnt++; feat.push_back((float)colCnt / NORM_H); } return true; }这段代码的逻辑分六个层次:灰度化用 0.299/0.587/0.114 三个固定权重,这是人眼对红绿蓝敏感度的标准系数,一般不需要调;大津法在灰度直方图上求阈值,避免手工指定;二值化时把笔迹置 0、背景置 255,是为了后面统计时bin[i] == 0的判断更直观;中值滤波窗口取 3×3,超过 5×5 就会把笔画转折处的细节抹平;归一化尺寸选 256×128 是综合考虑了特征维度和信息保留量。网格切 8×16 得到 128 维局部密度特征,按行按列统计又加入 128+256 维全局分布特征,最终特征维度在 512 左右。
参数要注意两个地方:网格数决定局部特征的粒度,8×16 对大尺度签名合适,如果样本里有很多小尺寸签名,可以改成 4×8 降维,但训练和识别必须保持一致;feat的长度由网格数和方向特征数共同决定,改动任何一个都会影响后续 AdaBoost 的输入维度。
3.3 特征向量与样本标注的对应关系
特征提取完成之后,每个签名就变成了一个 512 维的浮点向量。向量本身没有含义,必须和标签绑定才有训练价值。包里的DB_table.h正是承担样本管理的模块,它定义的样本表结构通常包含三个字段:样本图片路径、真伪标签(0 真签 / 1 伪签)、来源分组编号。来源分组编号很容易被忽略,但它在评估模型泛化能力时非常重要。
在组织训练集时,正确做法是按「人」分组而非按「图片」分组。也就是说,同一个人的全部真签名和对应的伪签名样本,要尽量集中放到同一个组里,训练集和测试集按组划分。如果随机打乱图像,同一个人的真签名和伪签名会同时出现在训练集和测试集里,分类器相当于开卷考试,测试准确率虚高。这个细节在后面避坑章节还会再遇到。
4. 分类器与训练评测:adaMachineLearning 里的 AdaBoost 调参逻辑
看到项目关键词里有「支持向量机分类」和「深度学习算法应用」,很多人会在源码里找 SVM 或 CNN 的痕迹,搜一圈后发现根本没有对应文件,不用慌。这个工程包里的分类器实际由adaMachineLearning.cpp承担,从文件名就能看出来是 AdaBoost,项目描述里提到的 SVM 和深度学习,是应用层面的技术方向,不是源代码层面的具体实现。这不是资源货不对板,而是「描述概念」与「工程落地」之间的常见错位。
4.1 为什么这里用 AdaBoost 而不是 SVM
AdaBoost 的核心思想是组合多个弱分类器,每个弱分类器只需比随机猜好一点,迭代多轮后综合成一个强分类器。每一轮训练都会把上一轮分错的样本权重调高,让下一轮弱分类器更关注难分样本。相比 SVM 需要求解二次规划问题,AdaBoost 在特征维度高、样本量不大时更轻量,训练速度快,对参数初始值也不那么敏感,这正是 C++ 工程里务实的选择。
具体到签名识别场景,真伪签名的边界往往不是一条干净的超平面,而是存在大量重叠区域,AdaBoost 的加权投票机制在这种噪声较多的数据上通常比单核 SVM 更稳。工程里弱分类器用的是单层决策树,也就是对单一特征维度取一个阈值,判断该维度的值是否超过阈值。单层决策树本身识别能力很弱,但几百个这样的弱分类器投票,就能构建出相当复杂的决策边界。
4.2 训练集与测试集的划分规则
包里的样本表一般有几十到几百个签名样本。训练测试划分建议按 3:1 起步,但有两个规则必须同时满足:第一是按人分组划分,保证同一个人的样本只出现在一侧;第二是伪签样本也要均匀分布在两组里,不能把全部伪签都放进测试集。
如果样本量超过 200,可以用 5 折交叉验证替代固定划分。如果样本总量只有几十个,留一验证更靠谱,但训练轮数会变成样本数次,训练速度可以接受的话也可以直接上。
4.3 AdaBoost 的迭代轮数与权重更新
训练循环的核心在adaMachineLearning.cpp,每一轮要做三件事:选出当前样本权重下错误率最低的弱分类器、计算这个弱分类器的投票权重、更新所有样本的权重。下面这段代码是 AdaBoost 训练单轮的标准逻辑:
// adaMachineLearning.cpp 单轮训练逻辑 void TrainOneRound(const vector<Sample>& samples, vector<float>& weights, vector<WeakRule>& rules) { // 1. 从弱分类器池中选择当前权重下错误率最低的规则 WeakRule best; float minErr = 1.0f; for (int t = 0; t < (int)rulePool.size(); t++) { float err = EvaluateRule(rulePool[t], samples, weights); if (err < minErr) { minErr = err; best = rulePool[t]; } } // 2. 计算该规则的权重系数 alpha,错误率越低权重越大 float alpha = 0.5f * log((1.0f - minErr) / max(minErr, 1e-6f)); // 3. 更新样本权重:分对的降权,分错的升权 for (int i = 0; i < (int)samples.size(); i++) { int pred = best.Predict(samples[i]); float factor = (pred == samples[i].label) ? exp(-alpha) : exp(alpha); weights[i] *= factor; } // 4. 权重归一化,保持概率分布 float sum = 0.0f; for (float w : weights) sum += w; for (float& w : weights) w /= sum; rules.push_back(best); }这段代码里的minErr存在一个边界情况:如果某个弱分类器在训练集上错误率为 0,alpha会变得非常大,下一轮权重更新时其他样本会被压得极低,最终导致过拟合。工程里的常见约束是给minErr设下限,比如不小于 0.01,或者对alpha做裁剪,超过一定值就按上限算。预测阶段则是把所有弱分类器的加权结果累加,累加值大于 0 判为真签,小于 0 判为伪签。
迭代轮数 T 的经验值在 50 到 200 之间。判断 T 是否合适的做法是边训练边记录训练集错误率,如果 T 增大到一定值后错误率不再下降,说明强分类器已经收敛;如果继续增大且测试集错误率回升,说明开始过拟合。另一个连带参数是学习率,工程里常取 0.5 到 1.0,T 大时适当调小学习率,相当于给每个弱分类器的投票权重打折,让整体收敛更平滑。
| 参数 | 常见取值 | 影响 | 调参注意 |
|---|---|---|---|
| 迭代轮数 T | 50~200 | 过小欠拟合,过大过拟合 | 观察错误率曲线平台 |
| 弱分类器类型 | 单层决策树 | 复杂模型容易蹭训练噪声 | 先以深度 1 起步 |
| 学习率 | 0.5~1.0 | 控制每轮权重收缩 | 与 T 联动,T 大则调低 |
| 特征维度 | 网格 128 + 方向 384 | 维度越高越看分类器鲁棒性 | 用演化计算做特征筛选 |
5. 避坑:复跑这套签名识别工程的五个高频问题
这套包整体结构不复杂,但由于是 MFC 工程,历史包袱比 Python 脚本多得多。以下五类问题是我在类似工程里反复踩过的,按频率从高到低排,每一条都给出现象、原因和解决路径,能省你不少查错时间。
5.1 字符集不一致导致界面假死和文件路径乱码
现象:工程编译通过,点菜单里的「打开签名图像」后,程序没反应或者直接崩掉,调试断点进到文件名处理那一步,字符串变量显示出来是乱码。 原因:工程的解决方案文件.sln存在多版本兼容问题,旧工程默认使用多字节字符集,而新版 VS 打开后默认按 Unicode 字符集编译,代码里如果用char*直接传给CString参数,运行时就出问题。 解决:在项目属性 → 配置属性 → 常规 → 字符集里,把整个工程的字符集改为「使用多字节字符集」;或者反方向,把代码里所有char*统一改成CStringA,彻底杜绝混用。改完要全量重编译,只重新编译一个文件往往还留着旧目标文件。
5.2 相对路径加载失败:bg3.bmp 找不到
现象:程序启动时提示找不到bg3.bmp或加载样本图片返回失败,但打开工程目录一看,文件明明就在那里。 原因:代码用的是相对路径。VS 调试时工作目录默认是工程文件所在目录,直接双击exe时工作目录变成exe所在目录,两者不一致,相对路径自然失效。 解决:把bg3.bmp、bg4.bmp以及样本数据目录全部放到输出目录下,或者把工程属性的调试工作目录改成$(ProjectDir)。更稳的做法是代码里用GetModuleFileName取到当前 exe 的绝对路径,再拼上资源目录,这样无论从哪个目录启动都不会错位。
5.3 特征维度不一致:训练集高准确率,识别全乱
现象:训练时准确率能达到 95% 以上,但单独识别某一张训练图都频繁出错,或者识别结果在真伪之间反复横跳。 原因:训练和识别走了不同的预处理入口。训练时图像归一化到 256×128,识别时直接用了原图尺寸;训练时网格是 8×16,识别时改成 4×8。特征向量的维度变了,AdaBoost 拿到的输入每维含义都错位,投票结果自然全乱。 解决:在BuildFeatureVector入口加一行断言,检查feat.size()是否等于预设的维度常量,不等就抛异常。同时把归一化尺寸、网格数定义成全局常量,只允许从AdvancedSet.cpp界面里改,不要在两处各写一个数字。
5.4 正负样本不均衡导致假签全部漏判
现象:训练过程的错误率下降得很快,但测试结果里真签几乎全对,假签几乎全错。把被误判的样本拿出来看,你会发现很多假签其实模仿得很粗糙。 原因:签名样本的采集天然存在不均衡,真签可能上百份,伪签只有二十份左右。AdaBoost 初始阶段按样本数量分配权重,真签样本量占大头的类别话语权过强,少数类被压制。 解决:对伪签样本做数据增强,例如旋转 ±3 度、缩放 ±5%、轻微加噪,把伪签扩充到与真签接近的数量;或者加权采样,给每个伪签样本更高的初始权重。如果条件允许,确保正负样本比不超过 5:1,模型判断才有意义。
5.5 双击 imgMatch.exe 闪退
现象:编译好的imgMatch.exe拷贝到另一台机器双击,直接闪退或报「应用程序无法正常启动」,报错代码类似 0xc000007b。 原因:imgMatch.exe是 Release 模式下编译的动态链接版本,目标机器上没有对应的 MFC 和 VC 运行库;另外,MyBitamp.cpp对位图格式有要求,如果加载的 BMP 不是 24 位彩色格式,位图解析那一步就可能段错误闪退。 解决:先在 IDE 里用 Debug 模式跑通全部流程,确认代码本身没问题;再在目标机器安装对应版本的运行库;样本图片统一转成 24 位 BMP 再放入样本目录,避免用 32 位带 Alpha 通道的格式。
6. 再往上走一步:用演化计算做特征筛选,再用留一验证确认模型真的能打
跑通基础流程之后,会发现影响最终效果的不只是分类器参数,还有特征向量的质量。512 维特征里有大量冗余维度,很多是网格密度之间的相关性导致的重复信息,留着只会增加过拟合风险。
演化计算在这里的价值,是把「选哪些特征维度」变成一个搜索问题。常见做法是把每个特征维度编码成二进制基因位,1 表示保留,0 表示丢弃;个体适应度直接定义为交叉验证准确率,再加上一个惩罚项,特征保留数越多惩罚越大。每一代做选择、交叉、变异,迭代几十代就能收敛到一个相对精简的特征子集。这个思路在附赠文档里有对照图表,但你在自己的数据上跑一遍会更有体会。
替换分类器也是值得做的实验。在adaMachineLearning的预测接口不变的前提下,换一个线性 SVM 或随机森林进这个 C++ 框架并不难,关键是要先确认特征向量对后续算法是通用的。我的建议是先用网格密度特征跑通基线,再在同一个特征文件上对比不同分类器,而不是边换特征边换模型,那样出了问题根本不知道是哪一层引起的。
验证阶段做一个留一交叉验证会比直接看测试集准确率更严谨。每个样本轮流拿出来当测试,其余全部做训练,最终取平均准确率。代价是训练次数等于样本数,几十个样本时完全可接受。我实际跑这类实验时出现过训练集 96%、测试集 62% 的情况,查了两天发现就是归一化尺寸不一致,后来每次搭图像机器学习链路,都强制在特征提取入口断言一遍维度,再谈分类器调参。这个工程包的 C++ 代码链改起来不如 Python 顺手,但它把全部过程摊在你面前,值得亲手跑一遍再替换任何模块。希望帮到你。
本文还有配套的精品资源,点击获取