尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python与OpenCV笔迹识别系统:图像预处理与轮廓特征提取实战

Python与OpenCV笔迹识别系统:图像预处理与轮廓特征提取实战 简介基于PythonOpenCV的笔迹识别系统项目源码是面向计算机视觉与图像识别方向学习者的高质量课程设计资源。该项目在导师指导下完成以97分高分通过代码完整、可直接运行涵盖图像预处理、文字区域定位、笔迹特征提取与分类识别等核心环节并提供了清晰的目录结构方便快速定位与二次开发。资源包大小约38.22MB内含项目工程文件、Python源码及配套说明能够支撑读者快速搭建笔迹识别实验环境理解算法流程与参数调优思路。目前已有956人学习下载适合用于课程设计、毕业设计或OpenCV实战入门。通过复现该项目读者可掌握基于传统图像处理与机器学习相结合的笔迹识别方法学习到图像二值化、轮廓检测、特征向量构建等经典操作同时积累项目模块划分、注释规范与结果评估等工程化细节整体具备很强的参考与复用价值。1. 基于 Python 与 OpenCV 的笔迹识别系统课程大作业的完整落地拆解笔迹识别听起来玄学但用 OpenCV 做出来的这套课程级笔迹识别系统本质上只干两件事先把字迹图像洗干净再把笔迹变成一串可比较的数字特征。我第一次跑通这套基于 Python 和 OpenCV 图像识别的笔迹识别系统源码时印象最深的是它没有调任何深度学习框架全靠 OpenCV 传统的轮廓分析与特征匹配就在几十张样本上拿到了很稳的区分度。这个项目很适合两类人一是正在做 OpenCV 课程大作业、需要一套完整可跑代码的学生二是想搞懂传统图像特征提取到底怎么落地、而不只是会调模型接口的开发。这套源码的完整程度足够让你从一张带字迹的照片或扫描图直接走到打分结果而且运行环境要求很低装好 OpenCV 就能跑。2. 系统整体流程从一张字迹图到相似度分数的四步管线2.1 管线拆解预处理、特征、比对、判决各管一段打开源码后你会发现整个笔迹识别系统并没有把「识别」做成一个黑匣子而是拆成了四个可以单独调试的模块图像预处理、特征提取、相似度计算和结果判定。这种拆法的好处是如果最终识别率不理想你可以逐段打印中间结果定位问题而不是对着一个模型干瞪眼。从工程角度看这也是课程大作业拿高分的一个关键原因——导师看的是你清不清楚每一步在干什么。第一段是图像预处理。笔迹识别对图像质量极敏感同一张纸用手机拍和用扫描仪扫出来的效果天差地别。源码里默认的处理链是读取图像 → 转灰度 → 高斯滤波去噪 → 二值化 → 形态学操作清理噪声 → 找到字迹区域并裁剪。这段管线处理完你就得到一张只有黑白笔画、背景干净的图像后续所有特征提取都基于这张「干净图」。第二段是特征提取。源码里同时实现了两类特征轮廓特征和形状描述子。具体来说它先通过cv2.findContours拿到所有笔画的轮廓再计算每个轮廓的几何属性面积、周长、外接矩形宽高比最后用cv2.HuMoments算出 Hu 矩作为笔画的形状特征。Hu 矩的妙处在于它对平移、缩放和旋转不敏感也就是说哪怕你把样本纸转了个角度再拍特征依然稳定。第三段是相似度计算。源码提供两种比对方式一种是用cv2.matchShapes直接比对两幅轮廓图的形状相似度另一种是把 Hu 矩特征向量拉平后算欧氏距离。实际跑下来前者更适合笔画结构差异大的样本后者更适合同一人字迹的细微匹配。第四段是判决。拿到相似度分数后源码设了一个阈值——大于阈值判为同一人笔迹小于则判为不同。这个阈值不是写死的源码里留了参数接口建议你按自己的样本集去调。2.2 预处理阶段的关键代码灰度、去噪、二值化一步到位预处理是整条管线里最影响结果的一步这部分源码值得逐行看。下面是从源码里抽出来的核心预处理流程我加了注释和参数说明import cv2 import numpy as np def preprocess_image(image_path): # 读取图像第二个参数 0 表示直接以灰度模式读入 # 常见做法是先按彩色读再做灰度转换但这里直接灰度读更省事 img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # 高斯滤波去噪核大小取 5x5标准差自动计算 # 字迹扫描件常带纸张纹理噪声和传感器噪点不滤掉会影响后续二值化 blurred cv2.GaussianBlur(img, (5, 5), 0) # Otsu 自动阈值二值化不需要手工设阈值算法自己找最佳分割点 # THRESH_BINARY_INV 让笔画变白、背景变黑因为后面找轮廓默认找白色区域 _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 形态学开运算先腐蚀再膨胀用来去掉孤立噪点和细小的笔画毛刺 # 核大小取 3x3太大会把真正的笔画细节也抹掉这点后面踩坑章节会细说 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) cleaned cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return cleaned这段代码的核心逻辑是灰度化减少计算量、高斯滤波压低噪声、Otsu 二值化代替手工调阈值、开运算清理毛刺。其中cv2.threshold的THRESH_BINARY_INV方向一定不能搞反——如果你输出的是白底黑字后面findContours会把整个背景当成一个巨型轮廓特征直接废掉。这是个非常经典的翻车点我自己的代码里第一次就栽在这。参数层面的建议是手写扫描件的GaussianBlur核大小取 3 到 5 就够了取 7 以上笔画边缘会明显变虚morphologyEx的核不要超过 5x5否则「口」这类闭合结构的内部空间会被填掉。如果你拍的样本光照不均可以在二值化之前加一步cv2.adaptiveThreshold自适应阈值但注意它比 Otsu 慢不少样本量大时要斟酌。2.3 从预处理到特征提取之间轮廓查找的边界条件预处理完成后下一步是找轮廓。这步看起来就一行cv2.findContours但里面对后续特征质量的影响非常大。源码里查找轮廓的实现是这样的# 轮廓查找RETR_EXTERNAL 只取最外层轮廓避免嵌套轮廓重复计算 # CHAIN_APPROX_SIMPLE 压缩水平、垂直、对角方向的冗余点减少内存 contours, hierarchy cv2.findContours( cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) # 过滤掉面积过小的噪点轮廓 # 比如 10x10 像素以下的零散黑点很可能是纸张污渍而不是笔画 min_area 100 valid_contours [c for c in contours if cv2.contourArea(c) min_area]RETR_EXTERNAL这个参数值得多说一句笔迹里经常出现「O」「口」这类带内孔的笔画如果你用RETR_TREE或RETR_CCOMP把内孔轮廓也提取出来单个字就会多出好几个轮廓特征向量里会出现大量冗余。实际做的时候RETR_EXTERNAL对于单层笔迹识别是够用的但如果你识别的是连笔签名签名内部的笔画交叉会造成很多封闭区域这时候可以试试RETR_CCOMP配合层级判断只取最外层。面积过滤阈值min_area需要根据你的图像分辨率调整如果输入图是 300 DPI 的扫描件100 这个值偏保守可以放大到 300如果是手机拍的且在预处理时做了缩放那 50 到 100 更合适。我倾向于把min_area写成参数放到配置文件里而不是写死在代码中因为不同采集设备出来的图噪点尺度差很多。3. 特征提取与相似度计算Hu 矩、轮廓比对和阈值判定的具体实现3.1 Hu 矩特征向量为什么它能抵抗平移、缩放和旋转干扰这一章是整个笔迹识别系统的核心所在也是对 OpenCV 传统图像识别掌握程度的试金石。Hu 矩是 OpenCV 中经典到不能再经典的形状描述子基于图像的几何矩构造出 7 个不变矩。它的理论基础是一幅二值图像的pq阶几何矩定义为所有像素坐标的加权和而 Hu 在这基础上构造出的 7 个组合量对平移、缩放和旋转都有不变性。对于笔迹识别来说这个性质非常宝贵。因为实际采集字迹样本时你根本不可能保证两次拍摄的笔迹在图像中的位置、大小、角度完全一致。如果有人直接把原始像素逐点相减做比对换支笔、换张纸、挪个位置结果就全变了而 Hu 矩提取的是笔画整体形状的统计特性这些干扰因素对它几乎无效。源码中对每个有效轮廓都计算了 Hu 矩并把这些矩拼接成特征向量def extract_shape_features(contours): # 对每个有效轮廓计算 Hu 矩并拼成一个一维特征向量 # 注意 HuMoments 返回的是 7 个值的二维列向量需要 flatten 成一行 hu_list [] for contour in contours: moments cv2.moments(contour) # 先算几何矩 hu cv2.HuMoments(moments).flatten() # 再转 Hu 矩并展平 # 对 Hu 矩取对数因为 Hu 矩的值跨度非常大直接比较数值会被大数主导 # 常见做法是 hu -sign(h) * log10(abs(h))幅度差异大的分量被拉平 hu -np.sign(hu) * np.log10(np.abs(hu)) hu_list.append(hu) # 把所有轮廓的 Hu 矩拼接成一个大特征向量 # 如果轮廓数量不等这里可以先对轮廓按面积排序取前 N 个保证维度一致 if len(hu_list) 0: feature_vector np.concatenate(hu_list) else: feature_vector np.zeros((7,)) # 空图像返回全零向量防止后续崩溃 return feature_vector这里最关键的一行是对 Hu 矩取对数。因为 Hu 矩的计算涉及像素坐标的幂次加和所以不同阶次的矩数值可能相差几十个数量级。如果不取对数后续计算欧氏距离时大数值的维度会完全盖过小数值的维度相当于小维度特征形同虚设。用-sign(hu) * log10(abs(hu))的好处是既压了量级又保留了正负号和相对大小关系。3.2 两张图的相似度怎么算matchShapes 与特征向量距离的双路方案特征向量是提取出来了但「识别」还需要一个把特征变成结论的比较手段。源码实现了两种比对方案分别对应 OpenCV 里两套不同的 API这里建议你都跑一遍看看效果差异def compare_handwriting(features_a, features_b, contours_a, contours_b): # 方案一基于轮廓的 matchShapes直接比对两幅图的轮廓形状 # 返回的是归一化的相似度数值值越小说明形状越接近 # 注意 matchShapes 需要的是原始轮廓点集不是 Hu 矩 scores [] for ca in contours_a: for cb in contours_b: score cv2.matchShapes(ca, cb, cv2.CONTOURS_MATCH_I2, 0) scores.append(score) # 取所有轮廓比对分数的最小值代表最相似的笔画对的匹配程度 shape_score min(scores) if scores else float(inf) # 方案二基于 Hu 矩特征向量的欧氏距离 # 先统一特征向量长度截断或补零再算欧氏距离 max_len max(len(features_a), len(features_b)) fa np.resize(features_a, max_len) fb np.resize(features_b, max_len) distance np.linalg.norm(fa - fb) return shape_score, distance两种方案的差别在于matchShapes直接操纵轮廓点集对不同数目的轮廓逐对匹配对局部笔画形变更敏感而特征向量距离把所有轮廓的 Hu 矩拼成一个全局向量丢失了「这个字有哪几个组成部分」的信息。实际跑下来的经验是判别签名真伪时matchShapes更合适因为签名的整体结构特征重要判别书写的汉字笔迹时特征向量距离更稳因为汉字结构复杂逐对匹配容易错配。源码里最终的判决逻辑是把两个分数相加并归一化然后和阈值similarity_threshold比较。阈值放到了配置区域默认值我印象中是 0.3 到 0.5 之间但强烈建议你用自己收集的样本重新标定。3.3 把比对结果汇总成判决阈值怎么定才不翻车阈值定是整个系统里最「玄学」但又最体现工程经验的部分。定太高同一人的笔迹会被误判为不同定太低不同人的笔迹会混进来。源码里给的是一个固定的similarity_threshold但我见过的合格做法是做一个简单的阈值标定脚本准备同一人笔迹 20 对、不同人笔迹 20 对分别算分数然后取两类分数分布的分界线中间值作为阈值。# 阈值标定的简化逻辑跑完样本后打印分数分布手动定阈值 # 同一人笔迹的分数通常集中在低值区不同人的分散在高值区 def calibration_loop(sample_pairs): same_scores [] diff_scores [] for pair in sample_pairs: score compute_total_score(pair) if pair.is_same_person: same_scores.append(score) else: diff_scores.append(score) print(同一人分数均值:, np.mean(same_scores)) print(不同人分数均值:, np.mean(diff_scores)) print(建议阈值:, (np.mean(same_scores) np.mean(diff_scores)) / 2)从工程角度说这种简单标定比拍脑袋定阈值靠谱得多。尤其注意如果同人分数和不同人分数的分布有重叠区域说明特征提取环节还有问题而不是单纯调阈值能解决的。这时候回头检查预处理看看是不是二值化把笔画细节弄丢了。4. 笔迹识别避坑指南五个让识别率骤降的经典问题4.1 二值化方向反转导致轮廓全乱现象预处理输出的图像看起来笔画清晰但findContours找出的轮廓只有一个巨大的方块特征向量数值全部异常识别率直接崩盘。原因cv2.threshold用了THRESH_BINARY而不是THRESH_BINARY_INV导致背景是白色255笔画是黑色0。OpenCV 的findContours默认寻找白色区域于是整个背景变成一个轮廓。解决统一使用THRESH_BINARY_INV让笔画变白或者找轮廓时把RETR_EXTERNAL换成别的模式也不行最根本的解法是在二值化后立刻cv2.bitwise_not反转。我在源码里看到它用的是前一种建议保持不要改。4.2 图像分辨率不一致导致特征漂移现象同一支笔写的同一句话扫描仪扫出来的样本之间识别正常但手机拍的和扫描仪互相比对分数差异巨大。原因手机拍照的分辨率、透视形变、光照阴影都跟扫描件差别很大。Hu 矩对旋转缩放不敏感但对非均匀形变透视和阴影造成的灰度不均很敏感。解决统一采集条件这是最优先的手段。如果做不到统一就在预处理阶段做透视校正——用cv2.findContours找到纸张边缘再用cv2.getPerspectiveTransform做投影变换把纸面拉正。这个步骤源码里没有但属于笔迹识别的加分项。4.3 形态学核大小破坏笔画细节现象识别率不稳定有时同一人笔迹分数很低有时又高得离谱排查半天特征和阈值都没问题。原因morphologyEx的核开的太大比如用了 7x7 甚至 11x11导致笔画细部如横折的拐角、提笔的细尖被腐蚀掉特征向量里丢失了关键形状信息。解决核从 3x3 开始调起配合可视化输出把处理后的图像存到磁盘上肉眼对比。我一般习惯每个处理阶段都cv2.imwrite一张中间图排查时直接看哪一步丢了信息。4.4 空轮廓或轮廓数不一致导致特征向量长度对齐失败现象某张图笔迹特别淡二值化后笔画断裂成几百个小碎块另一张图笔迹很浓只有几个大轮廓。两边特征向量长度差了几十倍np.resize把数据搞乱。原因轮廓数量不稳定而特征向量按轮廓拼接后长度等于轮廓数 × 7数量不一时直接在拼接那步就维度崩溃或者被resize强行对齐后产生垃圾特征。解决统一取面积最大的前 N 个轮廓参与特征提取比如 N20不够的补零多了的截断。这个逻辑源码里没有完全做全需要你自己补一下属于最值得动手改进的一处。这里顺便说一句轮廓过滤的min_area也直接影响这个问题的严重程度。4.5 用彩色噪声图直接跑识别预处理形同虚设现象直接拿手机原图跑灰度化后阴影和彩色背景混在笔迹里二值化后出现大片黑块。原因预处理流程被跳过或缩水——有人为了省时间直接把 RGB 图缩放到固定尺寸就丢进二值化结果背景阴影全变成黑色区域这些区域面积巨大轮廓特征被完全污染。解决严格按「灰度 → 高斯滤波 → Otsu → 形态学」的管线走完不要跳步。如果光照阴影确实严重先加cv2.divide做背景归一化再用 Otsu。这是我自己的血泪经验跳过预处理的结果就是特征向量里全是阴影的形状笔迹本身的特征反而成了次要成分。5. 进阶玩法用你自己的笔迹样本做一个小型识别验证并加上可视化中间结果这套源码的价值不止于拿到 97 分大作业更在于它是一套可以扩展的实验框架。我给你指一个价值和性价比最高的扩展方向采集你自己的笔迹样本建立一个人级比对实验。具体做法是准备 A4 白纸写 10 次同样的字比如「签名测试」四个字隔一段时间写尽量保持自然书写状态再请另一个人写同样的内容 10 次。扫描进系统后前 80% 做特征库后 20% 做验证。比对时给你一个小提示建议绘出相似度分数的分布直方图同一人分数和不同人分数两个峰拉得越开说明特征选得越好。如果想把识别结果可视化可以在源码之上加一段把轮廓画出来的逻辑这样你能直接看到系统认为是笔画的部分到底对不对# 可视化在原图上把判定为有效笔画的轮廓画出来 # 绿框代表参与特征提取的轮廓红框代表被过滤掉的噪点 def visualize_contours(image_path, valid_contours, invalid_contours): img_color cv2.imread(image_path) cv2.drawContours(img_color, valid_contours, -1, (0, 255, 0), 2) cv2.drawContours(img_color, invalid_contours, -1, (0, 0, 255), 1) cv2.imwrite(contour_visualization.png, img_color) print(轮廓可视化结果已保存为 contour_visualization.png) # 注意如果画完之后发现绿色轮廓明显比笔迹小一大圈 # 说明预处理阶段把笔画腐蚀掉了回头调形态学核大小 # 如果红色噪点密密麻麻说明 min_area 阈值设太低另一个值得做的验证是用你手头已有的样本把阈值扫描一遍画出准确率随阈值变化的曲线。具体方法是让阈值从 0 到 1 以 0.05 为步长扫一遍每步计算一次准确率判断正确的样本对数除以总样本对数你会看到准确率曲线通常在某个区间有一段平台那段平台对应的阈值就是稳健的取值区域。这个验证过程花费的时间不超过二十分钟但对理解整个笔迹识别系统的行为帮助很大。这类笔迹识别系统天然有个边界要认清它把笔迹变成特征向量做距离比较只适合做「同源判断」——回答两幅字迹是否出自同一人之手而且依赖比对样本的书写内容尽量一致。如果书写内容完全不同轮廓的几何特征差异会增大误判概率显著上升。这是传统图像识别路线的固有限制课程设计层面够用但如果往深度学习方向扩展可以考虑用 Siamese 网络替代手工特征不过那对数据量和算力的要求都高出一个量级。不过即便只是把现有源码里的阈值标定和轮廓数对齐这两个点改掉你的系统识别稳定性就已经超过大部分课程作业水平了。最后说个我自己的习惯从那以后我每次跑完比对都会强制把中间图像、特征向量和分数都存一份到本地目录存档名字带上参数信息比如preprocess_ksize5_morph3_area100_personA_vs_personB_score0.31.jpg。这样三天后你再回来看结果还能精确复现当时的每一步而不是看着一行打印的分数干瞪眼。这套项目源码的骨架是好的把这两个习惯加上去它就不再是一个交完就忘的作业而是一个可以继续长出新功能的小系统。希望帮到你。本文还有配套的精品资源点击获取
返回列表