简介面向深度学习入门者以及需要完成课程作业、实验设计或小型毕设的学生这份资源提供了一套基于卷积神经网络CNN的笔迹鉴定完整实现。项目将数据预处理、模型搭建、损失函数定义、训练与指标记录整合为清晰的模块化流程使用者按README说明即可运行无需深厚编程基础。压缩包共13个文件文件类型以7个Python脚本为主覆盖预处理、训练、网络结构和损失计算等核心环节另有4个编译缓存文件用于加速调用1个Markdown文档提供项目讲解与运行指南1个TensorFlow事件文件则记录训练过程中的标量指标便于可视化和监控模型表现。整个压缩包仅9KB轻量且易于迁移到自己的数据集。目前已有463人学习下载说明其实用性和可复现性已得到初步验证。通过研读代码可以完整掌握笔迹图像从加载、增强到特征提取、分类比对的落地流程。对于想将CNN应用于模式识别任务的学生和开发者这份资源既是可直接运行的实验项目也是开展小型毕设的可靠起点。 笔迹这个东西看着玄乎实际上是有迹可循的。干了这么多年图像处理和模式识别我一直觉得鉴定笔迹本质上就是一个“特征提取 相似度比对”的问题跟人脸识别、指纹识别的思路是一脉相承的。今天我就把这套完整代码的复现过程摊开讲一遍从预处理开始到特征提取、相似度计算再到结果判定环节全都有代码也全给出照着抄就能跑通一版基础的笔迹鉴定工具。这篇内容适合三类人一是刚入门的图像处理开发想看看传统特征工程怎么做二是做文档检验相关工作的朋友需要快速验证一个想法三是纯粹对“机器怎么分辨手写字迹”感兴趣的技术爱好者。我尽量把每一步为什么这么做讲清楚避免大家只能跑通代码但不懂原理那样换个场景就抓瞎了。1. 项目整体设计与思路拆解笔迹鉴定入门的第一个门槛不是算法而是搞清楚“用什么特征代表一个笔迹”。字体结构、笔画走向、运笔力度、连笔习惯、字间距分布这些在文件检验领域是人工看重的核心维度。落到代码里就要把这些主观经验转成可计算的数值指标。我采用的方案是“传统图像特征为主、几何结构特征为辅”。主干使用 HOG方向梯度直方图描述笔画方向分布搭配 LBP局部二值模式描述纹理细节再叠加一组手写的几何特征——比如单字宽高比、重心偏移、笔画交叉点数量。这个组合是我在多个小样本数据集上对比过的综合区分度比较高而且相比深度学习模型不需要大量标注数据几十份样本就能看到可用的效果。为什么不用 CNN 或者 Transformer不是说深度学习不行而是笔迹鉴定在实际业务里经常面临“小样本”困境——嫌疑人和历史样本往往只有几页纸训练一个大模型数据量根本不够。传统特征方法在这个场景下反而更稳健而且解释性强。你把一张字迹图和另一张比对最后能明确指出“这两个笔迹在笔画倾角上差了 12 度”这种结论深度学习很难给你这种可解释的输出。技术选型上使用 Python OpenCV原因很简单OpenCV 的图像读取、轮廓查找、形态学操作接口成熟处理文档扫描件的效率也非常高。整个工程结构非常扁平所有代码可以放在一个文件里跑通降低复现门槛。2. 核心特征提取把“笔迹风格”翻译成数字笔迹鉴定算法的核心竞争力就在特征这一层。特征选得漂不漂亮直接决定后面的比对准不准。我拆成三块来讲每一块解决一类问题。2.1 HOG 特征刻画笔画的走向与气势HOG 的全称是方向梯度直方图核心思想是统计图像局部区域的梯度方向分布。在笔迹里不同人写“横”的手势不一样有的人喜欢写平直有的人习惯带一点上翘的弧度这在梯度直方图里就会体现为不同方向上的能量差异。我在实现时把单字图像统一缩放为 128x128 像素然后设置 cell 为 16x16block 为 2x2方向 bin 数量取 9。这样每个单字能产出一个固定维度的向量方便后续做相似度计算。实测下来这个参数组合对笔画粗细变化和轻微旋转不太敏感稳定性相对最好。如果你要复现建议先在这组参数上跑通再试着调 block 大小观察效果。2.2 LBP 特征抓取运笔的纹理细节HOG 负责“大方向”LBP 负责“小纹理”。落笔轻重不一致、某些人喜欢“拖笔”导致笔画边缘粗糙、还有运笔时产生的微小颗粒感这些局部纹理特征都是 LBP 的强项。我使用的 LBP 设定为半径 2、邻域点 16 个忽略图像边界区域最后生成统一长度的直方图向量并对直方图归一化。这里有个容易踩的坑LBP 直方图一定要归一化否则不同写字面积的样本直接比对会失真。小字和大字写同一个字LBP 直方图的总能量差很多归一化之后就只看纹理分布形态不看总量比对才公平。2.3 几何统计特征补充人工经验机器特征有时候会忽略肉眼最直观的差异所以我用几何特征做补充。具体包括五组字体的外接矩形宽高比反映写字“偏扁”还是“偏瘦”笔迹重心的水平和垂直偏移量反映落笔习惯在方格内的区域笔画交叉点数量反映连笔和字迹的复杂程度主要笔画方向倾斜角估算方式是基于轮廓点做直线拟合连通区域数量反映笔画的断裂程度很多人写字一笔一画断得很开这个特征非常明显。计算量不大但每个特征都有明确含义组合使用后能有效提高区分度。3. 实操过程与核心环节实现进入代码环节。我尽量把每一步拆得细一点方便直接对照着跑。开发环境为本机 Python 3.9 OpenCV 4.8完整依赖只有一个 opencv-python 包。3.1 工程目录与样本准备先建一个干净的项目目录handwriting_forensics/ ├── samples/ │ ├── person_a/ │ │ ├── a_1.jpg │ │ ├── a_2.jpg │ │ └── a_3.jpg │ ├── person_b/ │ │ ├── b_1.jpg │ │ └── b_2.jpg │ └── unknown/ │ └── q_1.jpg └── hw_forensics.py样本建议拍摄或扫描时保持纸面平整、光线均匀笔画和背景之间要有明显对比度。不要用有格线的作业纸直接做测试格线会在预处理阶段造成大量干扰。如果必须用有格线的纸可以在预处理里加一步颜色过滤处理这个后面会细说。3.2 预处理模块噪声去除与二值化预处理是整个流程的地基。我见过太多人把这步跳过结果后面全乱套。import cv2 import numpy as np def preprocess(image_path, target_size128): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 使用自适应阈值避免光照不均导致二值化失败 binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 15 ) # 形态学开运算去除孤立噪点 kernel np.ones((3, 3), np.uint8) cleaned cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 找到最大轮廓作为主体区域并裁剪外围空白 contours, _ cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None max_contour max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(max_contour) cropped cleaned[y:y h, x:x w] # 缩放并填充到正方形 h_, w_ cropped.shape if h_ w_: pad (h_ - w_) // 2 cropped cv2.copyMakeBorder(cropped, 0, 0, pad, pad, cv2.BORDER_CONSTANT, value0) else: pad (w_ - h_) // 2 cropped cv2.copyMakeBorder(cropped, pad, pad, 0, 0, cv2.BORDER_CONSTANT, value0) resized cv2.resize(cropped, (target_size, target_size), interpolationcv2.INTER_AREA) return resized这里有几个非常关键的细节。自适应阈值比全局阈值更抗光照变化这是处理手机拍摄样本的关键。开运算的核如果取太大会把笔画的细梢消掉影响后续特征提取取太小又去不掉噪点。我实测 3x3 是通用的折中值。3.3 特征提取模块的完整实现def extract_hog(gray_img, cell_size16, block_size2, bins9): from skimage.feature import hog hog_feat hog( gray_img, orientationsbins, pixels_per_cell(cell_size, cell_size), cells_per_block(block_size, block_size), block_normL2-Hys ) return hog_feat def extract_lbp(gray_img, radius2, n_points16): from skimage.feature import local_binary_pattern lbp local_binary_pattern(gray_img, n_points, radius, methoduniform) hist, _ np.histogram(lbp.ravel(), binsn_points 2, range(0, n_points 2)) hist hist.astype(np.float32) hist / (hist.sum() 1e-7) return hist def extract_geometry_features(binary_img): # 基于二值图计算一组几何特征 contours, _ cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) full_contour np.vstack(contours) if contours else np.array([[0, 0], [1, 1]]) x, y, w, h cv2.boundingRect(full_contour) aspect_ratio w / max(h, 1) moments cv2.moments(full_contour) if moments[m00] ! 0: cx moments[m10] / moments[m00] cy moments[m01] / moments[m00] else: cx, cy 0, 0 center_x_offset cx - (x w / 2) center_y_offset cy - (y h / 2) # 估算笔画倾斜角 angle np.rad2deg(cv2.minAreaRect(full_contour)[2]) # 交叉点检测对细化后的骨架做邻域分析 ske cv2.ximgproc.thinning(binary_img) cross_count 0 for i in range(1, ske.shape[0] - 1): for j in range(1, ske.shape[1] - 1): if ske[i, j] 0: continue nb [ske[i-1, j-1], ske[i-1, j], ske[i-1, j1], ske[i, j-1], ske[i, j1], ske[i1, j-1], ske[i1, j], ske[i1, j1]] transitions sum(1 for k in range(8) if nb[k] 0 and nb[(k1) % 8] 255) if transitions 4: cross_count 1 return np.array([aspect_ratio, center_x_offset, center_y_offset, angle, cross_count], dtypenp.float32) def extract_features(image_path): processed preprocess(image_path) if processed is None: return None binary (processed 0).astype(np.float32) hog_f extract_hog(processed) lbp_f extract_lbp(processed) geo_f extract_geometry_features((processed 128).astype(np.uint8) * 255) return np.concatenate([hog_f, lbp_f, geo_f])关于交叉点检测我必须提个醒这段代码用的是细化和邻域跳变判断这个方法对参数敏感如果你的图像上存在大量毛刺交叉点会误报得非常离谱。稳妥的做法是先用膨胀操作去除毛刺或者直接用 cv2.ximgproc.thinning 自带选项。另外cv2.ximgproc 在 pip 安装的 opencv-python 里不带需要安装 opencv-contrib-python 才能用。为了避免额外依赖你也可以把交叉点检测放到调试环节确认有效再启用。3.4 相似度比对与判定特征提取完成后剩下的就是计算距离。def cosine_similarity(vec_a, vec_b): norm_a np.linalg.norm(vec_a) norm_b np.linalg.norm(vec_b) if norm_a 0 or norm_b 0: return 0.0 return float(np.dot(vec_a, vec_b) / (norm_a * norm_b)) def compare_samples(feat_a, feat_b): cos cosine_similarity(feat_a, feat_b) euclid np.linalg.norm(feat_a - feat_b) return cos, euclid def build_reference(folder_path): feats [] for fname in os.listdir(folder_path): if fname.lower().endswith((.jpg, .jpeg, .png)): feat extract_features(os.path.join(folder_path, fname)) if feat is not None: feats.append(feat) if feats: return np.mean(feats, axis0) return None def identify_unknown(unknown_path, person_folders): unknown_feat extract_features(unknown_path) if unknown_feat is None: return unknown, 0.0 best_person None best_score -1 for person_folder in person_folders: ref_feat build_reference(person_folder) if ref_feat is None: continue score, _ compare_samples(unknown_feat, ref_feat) if score best_score: best_score score best_person person_folder return best_person, best_score相似度分数范围一般在 0.6 到 0.99 之间我简单说下判定经验。得分超过 0.85可以初步认定是同一人笔迹低于 0.75基本可以排除介于中间的是灰色地带政策上不能下明确结论只能标“待复核”。当然实际使用时要根据样本的情况调整阈值不同笔、不同纸张导致的特征漂移差异很大不要一刀切。3.5 多字综合投票提升准确率单字比对的波动很大。一次手误写歪了、一个笔画下笔重了都会显著影响特征。我们这边做得比较保守不仅仅比较单个字而是取多个字各自提取特征两两对比做投票。举个例子预测写“鉴定”两个字就先分别提取“鉴”的特征和“定”的特征再把未知样本的两个字分别和参考样本的两个字做两轮比对两次都得高分才算锁定。如果其中一次判定“灰色地带”整体结果就会标记为“存疑”不会给出误导性结论。4. 常见问题与排查技巧实录这里把我在复现过程中实际遇到的高频问题整理成表格你在跑代码时如果卡住按这个排查基本能解决大部分问题。问题现象可能原因处理办法二值化后笔画严重断裂原图对比度低或光照不均先做直方图均衡化再自适应阈值特征提取速度很慢图像尺寸太大提前缩放到 256 像素以下LBP 直方图比对结果异常未归一化或字写太大/太小检查归一化代码是否存在HOG 特征浮动过大图像旋转角度差异大用 minAreaRect 做旋转校正交叉点数量爆表毛刺太多细化前先做中值滤波或膨胀待测样本有格线干扰扫描件带作业格线用颜色通道过滤或形态学背景去除不同来源字体差异大不同人写字特征方差较大第一轮先粗筛再对重点候选做二次比对还有一个很常见的坑把“相似度高”直接等同于“是同一人”。这个逻辑一定要警惕。前提是待测样本和参考样本已经排除了并非同一人书写但只是风格模仿的情况。职业模仿者能轻松骗过传统特征算法因为他们的运笔习惯本身带有欺骗性。我见过不少新手拿笔画特征去鉴定结果被刻意模仿的样本坑得毫无还手之力。这个行业里没有绝对可靠的工具任何自动鉴定结果都需要专业检验人员复核这是底线。5. 优化方向与后续扩展如果你跑通了整条流水线会发现这套传统方案的短板主要集中在旋转敏感性和样本数量敏感两个方面。下面三个方向是我自己实际验证过或者正在试的升级方案你可以按需扩展。5.1 自动倾斜校正扫描时纸放歪了字迹整体旋转 5 度以上HOG 特征就会有明显的偏移。可以在预处理步骤里增加一步对最大外接矩形做 minAreaRect 计算旋转角度然后执行逆旋转矫正。这里要注意旋转后的空白区域用背景色填充不要填黑色否则二值化之后会出现大面积伪笔画干扰后续特征提取。5.2 针对多个字符的融合比对单字模型弄完建议再上一个多字融合。选词的时候记得挑“笔画复杂、结构差异大”的汉字比如“馨”“赢”“攀”。这类字信息量大特征更丰富。不要只拿“一”“二”“三”这种极简字做样本特征不够鲜明区分度和鲁棒性会很差。5.3 深度学习方案结合传统特征跑通之后如果想继续冲精度可以把特征向量接入一个轻量级 Siamese 网络。输入不用原始图像而是前面提取好的 HOGLBP几何特征。这样既能保持小样本下的稳定性又能借助神经网络的非线性映射提升表达力。实测下来在 20 人左右的样本集上比纯特征方法能提升 5 到 8 个百分点的分类准确率不过代价是训练和调参的时间成本。我在实际使用这个工程时还有一个体会笔迹鉴定代码再好也只是工具真正决定结论可靠性的是样本采集的规范性和鉴定者的经验。代码能帮你量化和检索异常特征但偶然书写条件造成的假差异、长期书写练习导致的特征漂移都还需要判断的人去把握。把工具定位成“辅助分析系统”你写出来的东西才能真正在业务里站得住脚。如果后面还有精力可以做两件事一是扩展不同书写工具的数据集圆珠笔、钢笔、铅笔的纹理特征分布差异很大二是把数据库从单页扫描扩展到连续多页手写文档检测。这两块才是笔迹鉴定工程化落地的真正门槛。本文还有配套的精品资源点击获取