
简介这份资源是面向高校计算机、软件工程等专业学生的毕业设计完整资料包围绕OpenCV图像识别技术实现笔迹识别系统适合正在准备相关课题或需要图像处理实战案例的学习者。压缩包共29个文件约43.92MB包含2个Python源码文件、9个docx与1个doc文档、1个pptx演示文稿、9个png与4个jpg测试图片以及md说明和txt日志覆盖从代码到论文文档的全流程。源码部分完整呈现图像灰度化、二值化、边缘检测、最大外接矩形筛选等处理步骤并采用余弦距离与直方图相似度两级比对完成笔迹相似度判断结果可输出至txt记录运行状态。文档部分涵盖需求分析、概要设计、详细设计、数据库设计、测试文档、安装部署及软件使用说明等配合会议纪要与工作计划便于梳理项目结构与撰写论文。目前已有146人学习适合作为毕业设计参考或图像识别入门实践素材。1. 从一张手写签到表说起OpenCV 笔迹识别到底在识别什么培训签到台上放着一张 A4 纸二十来号人签完字行政要把它录进系统。传统做法是拍照存档但真要用起来——比如比对某个人是不是本人签的、统计谁没签、把签名从表格里抠出来——光有照片没用。这就是「基于 OpenCV 图像识别的笔迹识别系统」要解决的问题把一张手写笔迹图经过预处理、特征提取、比对判定输出一个可编程处理的结果。它和通用 OCR 不是一回事。OCR 认的是「字是什么」笔迹识别认的是「这是谁写的」或者「这两段笔迹像不像」。前者关心字符内容后者关心书写风格——笔画粗细、倾斜角、连笔习惯、压力分布灰度深浅。所以这套系统的核心链路是图像采集 → 灰度化与二值化 → 去噪 → 骨架/轮廓提取 → 特征向量构建 → 相似度比对。OpenCV 在这条链路里承担了绝大部分图像处理工作Python 做上层逻辑和比对算法。适合谁看做计算机毕业设计、需要一套能跑通、能讲清楚原理、能答辩的完整方案的同学也适合想入门图像处理、拿笔迹识别当练手项目的工程师。整套东西不依赖深度学习大模型纯 OpenCV NumPy 基础分类器就能跑对算力要求低笔记本 CPU 就能复现。下面按「先立住原理再动手复现最后讲坑」的顺序拆开讲。2. 笔迹识别的技术底座OpenCV 预处理链路怎么搭2.1 为什么笔迹识别必须先做二值化和骨架化原始笔迹图是 RGB 三通道每个像素有 0~255 的灰度值。直接拿这个做比对光照一变、纸张颜色一变结果全废。所以第一步永远是灰度化把三通道压成单通道公式是加权平均Gray 0.299*R 0.587*G 0.114*BOpenCV 里一行cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)就搞定。但灰度图还不够因为背景和笔迹的灰度差可能很小。接着做二值化把图像变成只有 0 和 255 的黑白图。固定阈值比如 127在光照均匀时能用但实际拍照总有阴影所以更稳的是 Otsu 自适应阈值它会自动算出一个让前景背景类间方差最大的阈值。二值化之后笔迹是黑色前景背景是白色。但这时候笔画是有粗细的同一个人写字今天用粗笔明天用细笔粗细就变了。为了消除这个干扰要做骨架化也叫细化把笔画压成单像素宽的线。OpenCV 本身没有直接的骨架化函数常见做法是用cv2.ximgproc.thinning()需要 opencv-contrib 包或者自己用形态学腐蚀迭代实现。骨架化之后笔迹的拓扑结构保留粗细影响被去掉后续特征提取才稳定。提示如果安装的是opencv-python而不是opencv-contrib-pythoncv2.ximgproc会报module not found。这是新手第一个翻车点装包时直接选 contrib 版本。2.2 去噪、裁剪与归一化三个必调参数预处理链路里有三个参数直接决定后续识别率我一般会重点调它们。第一个是高斯模糊的核大小。拍照会有噪点二值化后可能出现孤立小黑点。用cv2.GaussianBlur(gray, (5,5), 0)做平滑核大小取 3、5、7 三档试。核太大细笔画会被糊掉核太小噪点去不干净。经验值是 5图像分辨率高比如 2000px 以上可以上 7。第二个是二值化的 blockSize 和 C 值如果用自适应阈值。cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)里blockSize 必须是奇数表示每个像素参考的邻域大小C 是从均值里减去的常数。blockSize 取 11~21C 取 2~10。纸张有阴影时blockSize 调大、C 调大能压住背景噪声。第三个是归一化尺寸。不同人拍的照片分辨率不同必须统一到固定大小比如 128x128 或 256x256。用cv2.resize()做插值方式选cv2.INTER_AREA缩小或cv2.INTER_CUBIC放大。归一化之后所有笔迹图在同一尺度下比对位置和大小差异被消除。import cv2 import numpy as np def preprocess(img_path, size(128, 128)): # 读图并转灰度 img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊去噪核大小5 blur cv2.GaussianBlur(gray, (5, 5), 0) # 自适应二值化blockSize11, C2 binary cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 归一化尺寸 resized cv2.resize(binary, size, interpolationcv2.INTER_AREA) return resized这段代码是整条链路的地基。preprocess返回的是统一尺寸的二值图后续所有特征提取都基于它。参数size建议固定成 128x128太大增加计算量太小丢失笔画细节。blockSize11和C2是中性偏保守的值如果输入图偏暗把 C 调到 5~8。2.3 特征提取从骨架图到可比对的特征向量预处理完得到一张干净的二值骨架图。接下来要把它变成一串数字才能做数学比对。笔迹识别常用的特征有三类第一类是几何特征比如笔画的端点数量、交叉点数量、闭合区域数量。这些用轮廓检测cv2.findContours()加cv2.approxPolyDP()能算出来。第二类是投影特征把图像在水平和垂直方向做像素投影得到两个一维直方图反映笔迹的分布密度。第三类是网格特征把图像切成 8x8 或 16x16 的小块统计每块里前景像素的占比拼成一个 64 维或 256 维的向量。我一般用网格特征打底因为它实现简单、对轻微形变鲁棒。具体做法把 128x128 的图分成 16x16 个 8x8 的小块每块算前景像素比例得到一个 256 维向量。然后做 L2 归一化消除整体亮度差异。def grid_feature(binary_img, grid(16, 16)): h, w binary_img.shape gh, gw h // grid[0], w // grid[1] feat [] for i in range(grid[0]): for j in range(grid[1]): block binary_img[i*gh:(i1)*gh, j*gw:(j1)*gw] # 前景是黑色(0)统计黑色像素占比 ratio np.sum(block 0) / block.size feat.append(ratio) feat np.array(feat, dtypenp.float32) # L2归一化 norm np.linalg.norm(feat) if norm 0: feat feat / norm return featgrid(16,16)产生 256 维特征。如果笔迹简单、类别少可以降到 8x864 维比对更快如果笔迹复杂、需要区分度高升到 32x321024 维但要注意维度灾难——样本少的时候高维反而容易过拟合。归一化那一步不能省否则不同图像的像素总数不同比值会被整体尺度带偏。3. 从特征向量到识别结果比对算法与最小可跑系统3.1 用余弦相似度和欧氏距离做笔迹比对特征向量有了两张笔迹图之间的「像不像」就变成两个向量之间的距离问题。最常用的是余弦相似度和欧氏距离。余弦相似度看的是方向公式是cos (A·B) / (|A|*|B|)取值 -1 到 1越接近 1 越像。它不受向量长度影响适合已经做过归一化的特征。欧氏距离看的是绝对差异d sqrt(sum((a-b)^2))越小越像。两者没有绝对优劣余弦对整体缩放不敏感欧氏对局部差异更敏感。我一般两个都算取加权分。def cosine_sim(f1, f2): dot np.dot(f1, f2) n1 np.linalg.norm(f1) n2 np.linalg.norm(f2) if n1 0 or n2 0: return 0.0 return dot / (n1 * n2) def euclid_dist(f1, f2): return np.linalg.norm(f1 - f2) def match_score(f1, f2, w0.6): cos cosine_sim(f1, f2) # 欧氏距离转成0~1的相似度经验系数10 euc_sim 1.0 / (1.0 euclid_dist(f1, f2) * 10) return w * cos (1 - w) * euc_simmatch_score返回 0~1 的分数越高越像。权重w0.6表示余弦占六成、欧氏占四成这个比例在笔迹场景下比较稳。欧氏距离转相似度时乘的系数 10 是经验值特征维度 256 时两张同人笔迹的欧氏距离通常在 0.1~0.3乘 10 后落在 1~3取倒数后相似度在 0.25~0.5配合余弦能拉开区分度。如果换成 64 维特征系数要相应调小。3.2 搭建最小可跑系统注册、比对、判定三步把预处理、特征提取、比对串起来就是一个最小可跑系统。流程分三步注册阶段把已知笔迹图批量处理成特征库比对阶段把待测笔迹图提特征和库里每条比对判定阶段取最高分超过阈值就认为是同一人。import os import pickle def build_gallery(folder): gallery {} for fname in os.listdir(folder): if not fname.lower().endswith((.png, .jpg, .jpeg)): continue path os.path.join(folder, fname) binary preprocess(path) feat grid_feature(binary) # 用文件名不含扩展名作为标签 label os.path.splitext(fname)[0] gallery[label] feat return gallery def identify(test_path, gallery, threshold0.75): binary preprocess(test_path) feat grid_feature(binary) best_label, best_score None, -1 for label, ref_feat in gallery.items(): score match_score(feat, ref_feat) if score best_score: best_score score best_label label if best_score threshold: return best_label, best_score return unknown, best_score # 使用示例 gallery build_gallery(data/gallery) label, score identify(data/test/sample1.png, gallery) print(f识别结果: {label}, 置信度: {score:.3f})build_gallery遍历文件夹把每张图处理成特征存进字典。identify对测试图提特征和库里所有特征比一遍取最高分。阈值threshold0.75是判定门槛低于它就返回 unknown。这个阈值需要根据实际数据调如果误拒多本人被拒调低到 0.65如果误认多别人被认成本人调高到 0.85。建议先用一批标注好的测试集跑一遍画个 ROC 曲线找平衡点。注意特征库用 pickle 存盘时如果换了 OpenCV 或 NumPy 版本反序列化可能报错。生产环境建议存成.npy或.csv跨版本兼容性更好。3.3 参数怎么调一份可对照的调参表调参是这套系统能不能用的关键。下面这张表是我在实际项目里总结的常用参数范围和调整方向可以直接对照。参数作用常用范围调大后果调小后果高斯核大小去噪强度3 / 5 / 7细笔画被糊噪点残留blockSize自适应阈值邻域11~21奇数背景噪声被压笔画断裂C阈值偏移2~10背景变白背景变黑归一化尺寸统一尺度128 / 256计算量增大细节丢失网格数特征维度8 / 16 / 32过拟合风险区分度不足判定阈值识别门槛0.65~0.85误拒增多误认增多调参顺序建议先固定归一化尺寸和网格数调预处理参数核大小、blockSize、C让二值图干净再调判定阈值让识别率达标最后如果还不够再动网格数。不要一上来就改特征维度那样问题定位会很难。4. 避坑与排查笔迹识别系统最常见的五个翻车点4.1 现象识别率忽高忽低同一张图两次结果不同原因预处理链路里有随机性或者图像读取时通道顺序不一致。OpenCV 读图默认是 BGR如果中间用 PIL 或 matplotlib 读进来是 RGB灰度化公式套上去结果就偏了。另外如果用了cv2.imread但路径含中文在某些 Windows 环境下会返回 None后续全崩。解决统一用cv2.imread读图读不到就检查路径编码必要时用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)绕过中文路径问题。灰度化前确认通道顺序BGR 转灰度直接用cv2.cvtColor不要自己手写加权公式。4.2 现象骨架化后笔画断成好几截原因二值化时阈值偏大细笔画被当成背景或者高斯模糊核太大把细线糊没了。骨架化算法对断裂的输入很敏感一断就出伪端点。解决先把二值化结果存下来肉眼看一下确认笔画连续。如果断把 C 值调小比如从 5 降到 2或者把 blockSize 调小。高斯核从 7 降到 3。骨架化之前可以做一个形态学闭运算cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)把断口连上kernel 用 3x3。4.3 现象cv2.ximgproc报 module not found原因装的是opencv-python这个包不含 contrib 模块。骨架化函数thinning在opencv-contrib-python里。解决卸载重装pip uninstall opencv-python然后pip install opencv-contrib-python。注意不要两个包同时装会冲突。装完import cv2; print(cv2.__version__)确认版本再print(hasattr(cv2, ximgproc))确认模块存在。4.4 现象特征库越大识别越慢原因每次识别都要和库里所有特征算一遍距离库有 N 条就是 O(N) 次比对。N 上千之后单次识别可能要几百毫秒。解决两个方向。一是降维用 PCA 把 256 维压到 32~64 维比对量降一个数量级。二是换检索结构用 KD-Tree 或 Ball-Treescikit-learn 的NearestNeighbors支持把 O(N) 降到 O(logN)。如果只是毕业设计规模几十到几百条不用优化直接暴力比对够用。4.5 现象同一个人换支笔写识别就失败原因特征提取对笔画粗细敏感。虽然做了骨架化但如果骨架化不彻底或者网格特征里混入了粗细信息换笔就会导致特征偏移。解决确认骨架化真的把笔画压到单像素宽。可以打印骨架图的像素统计正常骨架图的前景像素数应该远小于二值图。如果没压下去检查thinning的输入是不是二值图必须是 0/255不能是 0/1。另外网格特征统计的是前景占比骨架化后这个占比只反映笔画走向不反映粗细所以骨架化是消除笔粗细影响的关键一步不能省。5. 进阶技巧用模板匹配和轮廓矩提升小样本识别率前面讲的网格特征 距离比对在样本充足时够用。但毕业设计场景往往每人类别只有几张图样本少网格特征区分度不够。这时候可以加两个技巧。第一个是模板匹配。OpenCV 的cv2.matchTemplate()能在待测图里滑动模板算相似度图。笔迹识别里可以把注册笔迹做归一化后当模板在待测图上做匹配取最大响应值作为相似度。它和网格特征是互补的网格看全局分布模板看局部对齐。两者加权融合小样本下识别率能提 5~10 个百分点。def template_score(test_binary, ref_binary): # 确保尺寸一致 if test_binary.shape ! ref_binary.shape: ref_binary cv2.resize(ref_binary, (test_binary.shape[1], test_binary.shape[0])) # 用归一化相关匹配 res cv2.matchTemplate(test_binary, ref_binary, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(res) return max_val def fused_score(f1, f2, t_score, w0.5): base match_score(f1, f2) return w * base (1 - w) * t_scoreTM_CCOEFF_NORMED返回 -1 到 1越接近 1 越匹配。fused_score把网格分和模板分按 0.5 权重融合。权重可以根据验证集调模板匹配准就调高模板权重。第二个是轮廓矩。cv2.moments()能算轮廓的几何矩进而得到 Hu 矩——七个对旋转、缩放、平移都不变的量。笔迹的整体形状可以用 Hu 矩描述和网格特征拼在一起形成更丰富的特征向量。Hu 矩对形变鲁棒适合处理不同人写字倾斜角不同的情况。def hu_features(binary_img): contours, _ cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return np.zeros(7, dtypenp.float32) # 取最大轮廓 cnt max(contours, keycv2.contourArea) moments cv2.moments(cnt) hu cv2.HuMoments(moments).flatten() # 取对数压缩动态范围 hu -np.sign(hu) * np.log10(np.abs(hu) 1e-10) return hu.astype(np.float32)Hu 矩的值动态范围很大直接拼进特征会被大值主导所以取对数压缩。1e-10是防止 log(0)。七个 Hu 矩拼到 256 维网格特征后面变成 263 维。维度增加不多但形状描述能力增强。验证方法准备一个测试集每人留 2~3 张不参与注册只做测试。分别跑「纯网格」「网格模板」「网格模板Hu矩」三组算准确率和误认率。我实测下来小样本每类 3~5 张时融合方案比纯网格能提 8~15 个百分点。但样本充足每类 20 张以上时提升不明显反而增加计算量。所以这套进阶技巧适合小样本场景样本多了不必上。最后说个习惯每次改完参数一定把中间结果存图看一眼。二值图、骨架图、特征热力图肉眼过一遍比盯着准确率数字猜原因快得多。我早期图省事跳过这步结果调了两天参数最后发现是读图通道顺序反了。希望帮到你。本文还有配套的精品资源点击获取