
简介这是一套面向高校计算机相关专业学生的毕业设计完整资料围绕OpenCV图像识别技术实现笔迹识别系统适合正在准备毕设或需要图像处理实战项目的学习者。资源包共29个文件包含2个Python源码文件、9个PNG与4个JPG测试素材、9个DOCX及DOC文档、1个PPTX演示文稿、2个Markdown说明和1个TXT日志压缩包约43.92MB覆盖从代码到文档的全流程。源码部分实现了图像灰度化、二值化、边缘检测、最大外接矩形筛选以及基于余弦距离与直方图相似度的笔迹比对逻辑并将运行结果输出至TXT文件界面采用PyQt5搭建支持文件选取、图片展示与运行触发。文档方面提供需求分析、概要设计、详细设计、数据库设计、测试文档、安装部署及软件使用说明等可直接参考撰写论文与答辩材料。目前已有147人学习适合需要完整项目方案与文档模板的毕业生。1. 笔迹识别系统拆包一份能跑通的 OpenCV 毕业设计长什么样很多做计算机视觉方向毕业设计的同学卡点从来不是「不会调 OpenCV」而是手里没有一份结构完整、能跑通、文档齐全的参考工程。网上搜到的代码要么只有单个main.py要么依赖一堆没写进 requirements 的库跑起来直接ModuleNotFoundError: No module named opencv。这份基于 OpenCV 图像识别的笔迹识别系统恰好补上了这个缺口——它不只是一个脚本而是一整套带源码、测试素材、13 份设计文档和演示 PPT 的完整工程包。它的核心逻辑是把两张手写笔迹图片做灰度化、二值化、边缘检测提取最大外接矩形并筛选再用余弦距离加直方图相似度做两级比对最终输出一个相似度数值。界面用 PyQt5 搭能选文件、能展示图片、能触发识别。适合谁正在做图像识别方向毕业设计、需要一份可复现参考工程的同学以及想快速理解「传统 CV 做笔迹比对」完整链路的开发者。下面我按拆包顺序把这份资源从结构到跑通、从参数到坑一层层讲清楚。2. 工程结构与运行环境先看清包里有什么再动手2.1 目录拆解与文件职责拿到bijishibie-master.zip之后别急着双击运行先解压看结构。这个包的组织方式很典型源码、素材、文档三块分得很清楚路径/文件类型作用main.py源码程序主入口PyQt5 界面与事件绑定distinguish.py源码图像处理与相似度比对核心逻辑1.jpg/2.jpg素材界面图标或默认展示图测试素材/素材叶青、李国富、云烟、秦晴各两张笔迹图全部文档/文档需求、概要、详细设计、测试、部署等 13 份README.md/README.en.md文档中英文说明工作日志.txt文档开发过程记录icon.png素材窗口图标测试素材的命名规律值得注意每个人两张图如叶青1.png、叶青2.png这正好对应「同一人笔迹相似度应该高、不同人应该低」的验证需求。你拿到手第一件事就是用这四组素材跑一遍看同人比对和跨人比对的结果差异是否明显。2.2 环境依赖与安装这套工程依赖的是 Python OpenCV PyQt5 NumPy 这条经典组合。常见做法是建一个独立虚拟环境避免和你机器上已有的包打架# 创建并激活虚拟环境Windows 用 venv\Scripts\activate python -m venv venv source venv/bin/activate # 安装核心依赖版本不必锁死但建议 opencv 用 4.x pip install opencv-python numpy pyqt5这里有个高频翻车点很多人装的是opencv-python但代码里import cv2却报ModuleNotFoundError。原因通常是装到了全局环境、而运行时用的是虚拟环境或者反过来。判断方法很简单在报错的那个解释器里执行pip show opencv-python看路径是否和python -c import sys; print(sys.executable)输出一致。不一致就说明装错环境了。提示如果你的系统里同时有 Python 2 和 Python 3务必确认pip和python指向同一个版本否则会出现「明明装了却找不到 cv2」的玄学问题。2.3 首次运行与界面验证依赖装好后直接运行主程序python main.py正常情况下会弹出一个 PyQt5 窗口包含文件选择按钮、图片展示区和运行按钮。点「打开文件」选一张测试素材再选第二张点运行界面会展示处理结果并输出相似度。如果窗口一闪而过多半是main.py里某个 import 失败用命令行运行而不是双击就能看到完整报错栈。这一步的验证目标是界面能起、能选图、能出结果。三个都过了说明环境和代码基本匹配可以进入下一章看核心算法到底怎么算的。3. 图像预处理链路灰度、二值、边缘检测与外接矩形3.1 灰度化与二值化的参数选择笔迹识别的第一步是把彩色图变成计算机好处理的形式。distinguish.py里的处理顺序是读图 → 灰度 → 二值 → 边缘检测 → 找轮廓 → 生成最大外接矩形 → 筛选。先看灰度加二值这段import cv2 import numpy as np def preprocess(img_path): # 读取图片第二个参数 0 表示直接以灰度模式读入 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f图片读取失败检查路径: {img_path}) # 高斯模糊降噪核大小 (5,5) 是经验值笔迹图噪声不大时够用 blurred cv2.GaussianBlur(img, (5, 5), 0) # 二值化THRESH_OTSU 自动选阈值避免手动调参 _, binary cv2.threshold( blurred, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU ) return img, binary逻辑说明IMREAD_GRAYSCALE直接读成单通道省去手动cvtColor。高斯模糊的核大小决定降噪强度(5,5)对扫描件级别的笔迹图足够核太大会把细笔画糊掉。二值化这里用了THRESH_BINARY_INV是因为笔迹通常是深色字、浅色底反转后笔画变成白色前景方便后续轮廓提取THRESH_OTSU让 OpenCV 自己算阈值比手写一个 127 稳得多。参数怎么改如果素材是手机拍的、光照不均先加一步自适应阈值cv2.adaptiveThreshold效果更好如果背景本身很干净高斯模糊可以省掉但省掉后边缘检测可能多出噪点轮廓。3.2 边缘检测与外接矩形筛选二值图之后是边缘检测和轮廓处理这一步决定了「哪些区域会被拿去比对」def extract_rects(binary): # Canny 边缘检测双阈值 50/150 是常用起点 edges cv2.Canny(binary, 50, 150) # 找轮廓RETR_EXTERNAL 只取最外层避免嵌套轮廓干扰 contours, _ cv2.findContours( edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) rects [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 筛选过滤太小的噪点框和过大的整图框 if w * h 500 or w * h 0.9 * binary.shape[0] * binary.shape[1]: continue rects.append((x, y, w, h)) return rects逻辑说明Canny的双阈值里低阈值管「弱边缘连不连」高阈值管「强边缘起不起」。50/150 是 OpenCV 官方示例的经典比例笔迹图一般够用。RETR_EXTERNAL只取外层轮廓因为笔迹的笔画轮廓不需要层级关系。筛选条件里w*h 500是去掉噪点小框 0.9 整图面积是去掉把整张图框住的无效矩形——这两个阈值就是「筛选不符合条件矩形」的具体落地。参数怎么改如果笔迹笔画很细、Canny 漏检把低阈值降到 30如果噪点多、框太碎把低阈值提到 80。筛选的面积下限 500 是像素单位图片分辨率高时要相应调大否则会保留一堆小噪点框。3.3 为什么用外接矩形而不是直接整图比对这里有个设计取舍值得说清楚。直接拿两张整图做相似度会受纸张大小、拍摄角度、留白多少影响同一人两次写的字可能因为位置偏移就被判成不相似。先提取笔画区域的外接矩形相当于把「有效内容」抠出来再比抗平移能力更强。但代价是如果矩形提取不准比如把两行字框成一个大矩形或者把一个字拆成多个小矩形后续比对就会错位。所以矩形筛选的阈值不是随便设的它直接决定比对的基本单元。我一般会先把中间结果画出来看一眼# 调试用把提取到的矩形画在原图上保存 debug cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) for (x, y, w, h) in rects: cv2.rectangle(debug, (x, y), (x w, y h), (0, 0, 255), 2) cv2.imwrite(debug_rects.png, debug)跑一遍测试素材打开debug_rects.png如果红框正好套住每个笔画区域说明参数合适如果框得乱七八糟先调这里别急着往下走。这一步是后面所有相似度计算的地基。4. 相似度判断余弦距离加直方图的两级比对4.1 第一级矩形区域的余弦距离粗筛相似度判断分两步走第一步用余弦距离做粗筛把明显不像的矩形对先排除掉def cosine_similarity(vec_a, vec_b): # 展平成一维向量并转 float避免整型溢出 a vec_a.flatten().astype(np.float32) b vec_b.flatten().astype(np.float32) # 余弦相似度 点积 / (模长乘积)加 1e-8 防除零 denom (np.linalg.norm(a) * np.linalg.norm(b)) 1e-8 return float(np.dot(a, b) / denom)逻辑说明余弦距离衡量的是两个向量方向的一致性对亮度整体偏移不敏感适合做粗筛。这里把矩形区域像素展平当向量用是最朴素的用法。1e-8是防止某个矩形全黑导致模长为零的后悔药不加的话会出nan或除零警告。参数怎么改余弦相似度的阈值一般设在 0.60.8 之间做粗筛。设太低粗筛没起到过滤作用设太高可能把真正相似的矩形也筛掉。这个值需要拿测试素材试出来没有万能值。4.2 第二级直方图相似度精判粗筛留下的矩形对再用直方图相似度做精判这一步才是决定「算不算相同」的关键def histogram_similarity(rect_a, rect_b): # 对两个矩形区域分别算灰度直方图256 个 bin hist_a cv2.calcHist([rect_a], [0], None, [256], [0, 256]) hist_b cv2.calcHist([rect_b], [0], None, [256], [0, 256]) # 归一化消除区域大小差异的影响 cv2.normalize(hist_a, hist_a, 0, 1, cv2.NORM_MINMAX) cv2.normalize(hist_b, hist_b, 0, 1, cv2.NORM_MINMAX) # 相关性比较返回值越接近 1 越相似 return cv2.compareHist(hist_a, hist_b, cv2.HISTCMP_CORREL)逻辑说明直方图描述的是像素灰度分布对笔画的粗细、浓淡变化比较敏感正好补上余弦距离只看方向的短板。HISTCMP_CORREL返回的是相关性系数范围大致 -1 到 1越接近 1 越像。归一化那步不能省否则区域大的直方图数值天然大比较会失真。参数怎么改判定「相同」的阈值摘要里写的是「相似度高于一定数值」这个数值通常落在 0.70.9。设低了误判率高不同人也判相同设高了漏判率高同一人也判不同。建议拿四组测试素材各跑一遍画一条阈值-准确率的曲线来定。4.3 最终相似度的计算与输出两级比对跑完最终结果按摘要里的公式来相同数量除以所有比较次数。def final_similarity(rects_a, rects_b, cos_thresh0.7, hist_thresh0.8): same_count 0 total_count 0 for ra in rects_a: for rb in rects_b: total_count 1 # 第一级余弦粗筛 if cosine_similarity(ra, rb) cos_thresh: continue # 第二级直方图精判 if histogram_similarity(ra, rb) hist_thresh: same_count 1 # 防除零没有任何比较时返回 0 return same_count / total_count if total_count else 0.0逻辑说明双重循环把 A 图每个矩形和 B 图每个矩形都比一遍total_count记录总比较次数same_count记录两级都通过的次数。最终比值就是整体相似度。这个设计的好处是简单直观缺点是计算量随矩形数量平方增长矩形多的时候会慢。参数怎么改cos_thresh和hist_thresh是两个核心旋钮建议先固定一个调另一个。另外如果矩形数量差异很大比如一张图 5 个框、另一张 50 个框这个比值会被拉低可以考虑先对矩形数量做归一化或者只取面积最大的前 N 个矩形参与比对。4.4 结果输出到 txt程序会把运行结果落盘输出项包括当前时间、运行结果、程序运行状态import datetime def save_result(similarity, status, pathresult.txt): with open(path, a, encodingutf-8) as f: now datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) f.write(f时间: {now}\n) f.write(f相似度: {similarity:.4f}\n) f.write(f状态: {status}\n) f.write(- * 30 \n)逻辑说明用追加模式a而不是覆盖模式w这样多次运行的结果会累积方便对比不同参数下的表现。encodingutf-8是为了避免中文在 Windows 默认 GBK 下写乱码。状态字段可以填「成功」「图片读取失败」「无有效矩形」等排查问题时一眼能看出卡在哪一步。5. 避坑与常见问题排查5.1 装了 opencv 却报 No module named cv2现象pip install opencv-python显示成功运行main.py却报ModuleNotFoundError: No module named opencv或找不到cv2。原因最常见的是 pip 和 python 不在同一个环境。系统里可能有多个 Pythonpip 装到了 A运行时用的是 B。其次是虚拟环境没激活就装了包。解决先跑python -c import sys; print(sys.executable)确认当前解释器路径再跑pip show opencv-python看安装位置两者对不上就重新在正确环境里装。养成习惯装包前先确认虚拟环境已激活。5.2 图片读取返回 None 导致后续全崩现象程序跑到灰度化那步报cv2.error或者img.shape报NoneType没有属性。原因cv2.imread读不到文件时不会抛异常而是静默返回None。路径里有中文、路径拼错、文件格式不支持都会触发。解决在读图后立刻加if img is None判断并抛出明确错误见 3.1 的代码。另外 OpenCV 对中文路径支持不好路径里尽量别带中文或者用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_GRAYSCALE)绕过。5.3 矩形提取过多或过少现象调试图里红框要么密密麻麻全是小框要么一个大框套住整张图导致相似度结果忽高忽低。原因Canny 双阈值和面积筛选阈值不匹配当前图片分辨率。分辨率高时500 像素的面积下限太小噪点全被保留分辨率低时0.9 整图面积的上限又太宽松。解决先看debug_rects.png再针对性调。噪点多就提高 Canny 低阈值或加大面积下限框太大就降低面积上限比例。记住这两个阈值是像素单位换一批素材就要重新校准。5.4 相似度结果不稳定、同人比对也偏低现象同一人的两张笔迹相似度只有 0.3 左右明显不合理。原因多半是矩形没有对齐。同一人两次写的字位置不同矩形提取出来的区域内容不对应余弦和直方图自然比不中。另外阈值设太高也会压低结果。解决先确认矩形提取是否稳定看调试图再适当降低cos_thresh和hist_thresh。如果位置偏移是主因可以考虑在比对前对矩形按位置排序或者引入简单的模板对齐。这是传统 CV 方法的固有边界心里要有数。5.5 PyQt5 界面按钮点了没反应现象窗口能弹出但点「打开文件」或「运行」没任何反应。原因信号与槽没绑定成功或者槽函数里抛了异常被吞掉。PyQt5 里槽函数报错有时不会直接崩而是静默失败。解决在槽函数入口加print或日志确认是否被调用。如果被调用但中途失败把槽函数体用try/except包起来打印异常。另外确认按钮的clicked.connect写在了正确的初始化位置。6. 进阶玩法把相似度阈值调优做成可复现的实验跑通只是起点这份工程真正有价值的地方在于它给你留了调参空间。我一般会做一件事把四组测试素材叶青、李国富、云烟、秦晴各两张组织成一个小的评测集同人两张算正样本跨人组合算负样本然后扫一遍阈值看准确率怎么变。import itertools # 测试素材按人分组 groups { 叶青: [测试素材/叶青1.png, 测试素材/叶青2.png], 李国富: [测试素材/李国富1.png, 测试素材/李国富2.png], 云烟: [测试素材/云烟1.png, 测试素材/云烟2.png], 秦晴: [测试素材/秦晴1.png, 测试素材/秦晴2.png], } def evaluate(cos_thresh, hist_thresh): correct, total 0, 0 names list(groups.keys()) # 正样本同人两张 for name in names: a, b groups[name] sim run_pipeline(a, b, cos_thresh, hist_thresh) total 1 if sim 0.5: # 同人判定阈值 correct 1 # 负样本跨人组合 for n1, n2 in itertools.combinations(names, 2): sim run_pipeline(groups[n1][0], groups[n2][0], cos_thresh, hist_thresh) total 1 if sim 0.5: correct 1 return correct / total # 网格搜索 for ct in [0.6, 0.7, 0.8]: for ht in [0.7, 0.8, 0.9]: acc evaluate(ct, ht) print(fcos{ct}, hist{ht}, acc{acc:.2f})逻辑说明run_pipeline是你把前面预处理、矩形提取、两级比对串起来的封装函数。正样本期望相似度高负样本期望相似度低用 0.5 作为判定分界。网格搜索把两个阈值各取三档跑九组组合看哪组准确率最高。这套流程的价值在于它把「阈值怎么设」从拍脑袋变成了可复现的实验答辩时也能拿数据说话。参数怎么改判定分界 0.5 可以按你的数据分布调整阈值档位可以加密到 0.05 步长。如果正负样本区分度不够说明特征本身不够强可以考虑在直方图之外再加一个笔画宽度或投影特征。注意四组素材样本量很小网格搜索出来的最优阈值只能作为参考别当成通用结论。样本越多结论越稳。从那以后我每次拿到这类传统 CV 工程都强制先跑一遍中间结果可视化再动任何阈值——因为血泪经验告诉我跳过可视化直接调参最后一定是在错误的地基上盖楼。希望这份拆解能帮你少走几个弯路把这份资源真正用起来。本文还有配套的精品资源点击获取