ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenCV笔迹识别全流程:图像预处理、特征提取与匹配实战

OpenCV笔迹识别全流程:图像预处理、特征提取与匹配实战 简介面向计算机视觉与图像识别学习者这份基于PythonOpenCV的笔迹识别系统项目源码是一套经过完整验证的高分课程大作业工程。项目在导师指导下完成获得97分评价代码可直接下载运行适合作为课程设计、毕业设计或竞赛项目的参考骨架。内容围绕笔迹识别任务覆盖图像预处理、特征提取、分类器构建与识别结果输出等核心流程能够帮助读者直观理解OpenCV在模式识别中的实际运用。压缩包整体约38.22MB文件结构清晰源码工程完整解压后即可按说明使用目前已有956人学习下载属于口碑较好的实战型资源。从项目结构到关键算法实现均有清晰脉络读者可据此梳理图像识别系统搭建思路并迁移到其他笔迹、字体或形状识别场景对快速上手图像识别实战、完善课程报告或扩展二次开发能力颇具价值。1. 基于 OpenCV 的笔迹识别系统这门 97 分课设到底做了什么笔迹识别是 OpenCV 图像识别课程设计里出镜率很高的题目但这门拿到 97 分的课设没有上深度学习而是用经典图像处理的路数把整条链路跑通了读入 → 预处理 → 特征提取 → 匹配 → 出结果。拿到这份基于 Python OpenCV 的源码包你会发现它不是一个只会对着固定样本比对的玩具而是一套能替换样本、能调阈值、能输出置信度的完整工程。它的核心价值在于解决了三件事一是把深浅不一、带噪点的扫描稿变成干净的骨架图二是把图像转成网格密度、投影和 Hu 矩这些能相互比较的特征三是用模板匹配和特征匹配两套方案给出识别结果与可信度。适合的人群很明确正在做图像识别课设的学生、需要快速搭一个 OpenCV 图像识别 Demo 的开发者、以及想从源码里学预处理和特征工程套路的人。如果你是这三类里的任何一类这份源码值得在动手前先完整过一遍它能把「笔迹识别」这件事从黑匣子变成你能逐行改的代码。2. 预处理管线把扫描稿变成电脑能算的骨架图预处理是笔迹识别里最影响准确率的一段。我见过不少同学跳过这一步直接拿原图去匹配结果识别率惨不忍睹回头还以为是算法不行。这一章把灰度化、二值化、去噪、骨架提取四步拆开讲每一步都给代码和参数说明。2.1 灰度化与二值化识别准确率的第一道分水岭笔迹识别的输入大多是扫描件或者手机拍的白纸颜色信息除了干扰基本没别的用处。纸张发黄、红笔批注、网格线这些都会在匹配阶段变成噪音。所以第一步就是把三通道彩色图压成单通道灰度图再进一步压成只有黑和白二值的图。二值化这一步尤其关键图片最终只保留「有没有笔画」这个信息后面所有特征统计都建立在这一步的结果上。import cv2 import numpy as np def preprocess(img_path): # 读图OpenCV 默认按 BGR 三通道读入 img cv2.imread(img_path) if img is None: raise FileNotFoundError(f读不到图片: {img_path}) # 灰度化BGR - Gray把颜色信息扔掉 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊压制扫描件上的细碎噪点 # 核 (5, 5) 是经验值太小没效果太大会把笔画边缘糊掉 blurred cv2.GaussianBlur(gray, (5, 5), 0) # Otsu 自动阈值不用手调阈值适合笔画深浅不一的作业纸 # THRESH_BINARY_INV 让笔迹变成白色(255)、背景变成黑色(0) _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) return binary这段代码的逻辑是把彩色图先降维再用 Otsu 自动找一个能把前景和背景分开的阈值。参数说明里有两个点值得记cv2.threshold的第二个参数传 0 是因为 Otsu 模式会忽略它真正的阈值由算法自己算THRESH_BINARY_INV反色之后白色像素代表笔画后面统计白色像素占比时语义就顺了。如果你用的是普通固定阈值比如恒为 127换一张光线不同的扫描件就会翻车所以源码里走 Otsu 是更稳的选择。遇到纸张亮度不均、一半亮一半暗的情况固定阈值和 Otsu 都会在暗区出错。我一般在这种情况下改用自适应阈值binary cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 5)blockSize31表示每个像素参照周围 31×31 邻域计算局部阈值必须是奇数C5表示在邻域均值基础上减 5 作为阈值C 越大越容易把浅笔画滤掉。这个参数组合对网格纸、暗角照片都挺能扛代价是计算慢一些。2.2 去噪、形态学操作与骨架提取二值化之后的图仍然不干净扫描仪的灰尘点、纸张纤维都会变成小白块。普通高斯模糊对这种椒盐式的孤立噪点效果有限更合适的是中值滤波加开运算。做完这两步之后再做骨架提取也就是教程里常说的「细化」把笔画压成单像素宽的骨架这样特征的形状信息会更纯粹。# 中值滤波对椒盐噪点比高斯滤波更有效 denoised cv2.medianBlur(binary, 3) # 开运算先腐蚀后膨胀去掉孤立小点同时保持笔画粗细 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) opened cv2.morphologyEx(denoised, cv2.MORPH_OPEN, kernel) # 骨架提取把笔画压成单像素宽方便后续做形状特征 try: import cv2.ximgproc as xip skeleton xip.thinning(opened) except ImportError: # 没装 opencv-contrib-python 时的备用实现 skeleton zhang_suen_thinning(opened)开运算里的kernel(3, 3)是常用配置太大会把细笔画的头尾削掉。骨架提取这里有个环境注意点cv2.ximgproc.thinning需要安装opencv-contrib-python才有如果只装了opencv-pythonimport 会直接报错。源码包里一般会带一个手写的 Zhang-Suen 细化函数作为兜底它的原理是反复执行两步子迭代把边缘像素按八个邻域的拓扑关系一层层剥掉直到只剩单像素骨架。Zhang-Suen 的实现思路不算复杂核心是每次迭代标记出满足删除条件的边界点统一处理后再进入下一轮直到没有像素可删。手写版本在纯 Python 循环下会比较慢但对课设规模的小图完全够用。骨架提取的价值在于同一个字正常写和用力写笔画粗细可能差一倍但骨架结构基本一致后续特征匹配就少吃「粗细」这个变量的亏。2.3 中间结果可视化不保存调试图就没法调参预处理有没有做对光看最终识别率是很难定位的。我习惯把每一步的中间结果拼在一张图里存下来哪一步出了问题一眼就能看见。这个习惯在调笔迹识别这类图像任务时特别管用因为很多问题的根源就藏在灰度、二值化或骨架某一步里。def debug_visualize(original, binary, skeleton): # 三个图横向拼接方便对比 # 灰度图要先用 cvtColor 转回三通道否则 hstack 会因为通道数不一致报错 parts [ original, cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR), cv2.cvtColor(skeleton, cv2.COLOR_GRAY2BGR), ] canvas np.hstack(parts) cv2.imwrite(debug_pipeline.png, canvas)np.hstack要求所有图的高度一致所以二值图和骨架图都要先转回三通道。看调试图的时候我按这个顺序排查如果二值化图里笔画断裂说明阈值参数太严或模糊过度如果骨架图在笔画交叉处断开说明细化输入里就有断点要回头调形态学的开闭运算如果骨架图里全是毛刺说明去噪不充分。这条排查路径基本能把预处理阶段 80% 的问题定位掉。把这一步固定写进主流程每次换样本都自动生成一张调试图比临时写cv2.imshow去弹窗高效得多。3. 特征提取与比对笔迹凭什么被认出来预处理做完得到的是一张干净的骨架图。但图本身不能直接拿去比较因为像素级比较对位置、大小、倾斜都太敏感。这一章讲清楚特征向量怎么设计、两套比对方案怎么选、以及结果怎么输出才可信。3.1 特征向量设计与尺寸归一化把图像转成特征向量本质上是在做「压缩」把几万个像素压缩成几十个有判别力的数字。笔迹识别里常用的三件套是网格密度、投影直方图和 Hu 矩。网格密度统计每个局部区域里笔画占多少能反映字的整体分布投影直方图分别统计每行、每列的笔画总量能刻画字的胖瘦和伸展方向Hu 矩是七个对平移、旋转、缩放不敏感的全局形状描述子正好应对同一人不同写字姿势的差异。def extract_feature(binary_img): h, w binary_img.shape # 统一缩放到 64x64避免样本大小不同导致特征不可比 resized cv2.resize(binary_img, (64, 64), interpolationcv2.INTER_AREA) # 网格密度把图分成 4x4 共 16 个格子统计每个格子的白色像素占比 grid_feat [] for i in range(4): for j in range(4): cell resized[i * 16:(i 1) * 16, j * 16:(j 1) * 16] grid_feat.append(np.count_nonzero(cell) / 256.0) # 投影特征每行、每列分别统计白色像素数再归一化到 [0,1] row_proj np.count_nonzero(resized, axis1).astype(float) / 64.0 col_proj np.count_nonzero(resized, axis0).astype(float) / 64.0 # Hu 矩7 个值数值跨度极大取 log 压缩 moments cv2.HuMoments(cv2.moments(resized)).flatten() hu_feat -np.sign(moments) * np.log10(np.abs(moments) 1e-10) # 拼接成完整特征向量16 64 64 7 151 维 feat np.hstack([grid_feat, row_proj, col_proj, hu_feat]) return feat.astype(np.float32)尺寸归一化放在最前面是为了消除扫描时字号不同带来的影响INTER_AREA在缩小图像时比默认的双线性插值更抗锯齿。网格密度用了 4×4 的划分格子里白色像素占比的计算用np.count_nonzero配合格子面积 256 做归一化这样特征值落在 0 到 1 之间不同格子之间可比。Hu 矩的数值范围可能差十几个数量级直接拿去算相似度时大数会淹没小数所以用 log 变换压缩一下符号保留原样这是这类特征处理里的常规操作。3.2 模板匹配与特征匹配两套方案的取舍比对方案决定了整个识别系统的性格。模板匹配用cv2.matchTemplate逻辑是把查询图当模板在目标图里滑窗找到相似度最高的位置。它适合印刷体、票据这类位置和大小都很稳定的场景但对笔迹这种同一个人两次写出来都不同、位置大小全在抖的输入它特别容易误判。特征匹配则先把每张图都压缩成特征向量再算向量之间的余弦相似度或欧氏距离对位置偏移和轻微形变的容忍度高得多所以笔迹识别项目里我更推荐以后者为主。def match_template_score(target_img, template_img): # TM_CCOEFF_NORMED 对亮度变化不敏感结果范围 [-1, 1] res cv2.matchTemplate(target_img, template_img, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(res) return max_valdef match_feature(feat_query, feat_db): scores [] for name, feat in feat_db.items(): # 余弦相似度比较两个特征向量的方向一致性 dot np.dot(feat_query, feat) denom np.linalg.norm(feat_query) * np.linalg.norm(feat) 1e-10 scores.append((name, float(dot / denom))) scores.sort(keylambda x: x[1], reverseTrue) return scores模板匹配的TM_CCOEFF_NORMED是归一化相关系数它对整体亮度不敏感值越接近 1 说明越像。特征匹配里的余弦相似度则把每个特征向量当成空间里的一个点方向越接近说明笔画结构越像。倒排之后取最大值就是系统认为最可能的笔迹主人。两套方案我会同时保留在工程里模板匹配留给「待识别图像和库图像是同源扫描件」这种极端对齐的场景做快速粗筛特征匹配作为主识别器。源码包里两个函数都齐了跑的时候可以用同一个样本分别试两种方案对比结果就知道各自适合什么输入。3.3 阈值、Top-K 与识别结果输出识别不是简单回答「是谁」而是要回答「有多大的把握是谁」。如果来一个陌生人特征匹配也可能硬选出一个相似度最高的所以必须设置信度阈值低于阈值就判定为「未知笔迹」。输出方面保留前三名而不是只给第一名能让你看到召回结果里是否出现了「同字不同人」的干扰项。def recognize(binary, feat_db, top_k3, threshold0.75): feat extract_feature(binary) ranking match_feature(feat, feat_db) # 最高相似度低于阈值直接判为未知不给硬凑的结果 if ranking[0][1] threshold: return unknown, ranking[:top_k] return ranking[0][0], ranking[:top_k]阈值 0.75 不是拍脑袋定的它是拿一批「本人样本」和「他人样本」分别跑相似度之后选的一个能把两类分开的经验值。不同笔迹库的分布不一样这个值必须重新标定。具体做法是把所有人两两组合算一遍相似度画出本人相似度和他人相似度的分布取两者交叠区域中间的数当阈值。Top-K 的意义在于当第一名和第二名的分数接近时大概率是这两个人的字形确实相近这时候只看第一名就会掩盖这个信息。输出格式可以是控制台打印也可以在工程里直接改成 JSON方便后续接界面。4. 主程序与模块拆分把识别流程串成能复用的工程预处理和特征匹配单独都能跑但没有一个清晰的主程序把它们串起来换样本、调参数就会变得很痛苦。这一章看的是工程组织方式文件怎么分、参数放哪、命令行怎么接。4.1 文件结构一眼看懂这个工程怎么组织拿到源码包解压之后先别急着运行花两分钟看一遍目录结构。笔迹识别这类 OpenCV 项目一般会按处理阶段拆成独立模块这样每一步都能单独测试。下面是这份源码里常见的一层结构也是我自己在搭同类工程时习惯用的组织方式文件 / 目录职责main.py入口负责串流程、解析命令行参数preprocessing.py灰度、二值化、去噪、骨架提取features.py特征提取与向量归一化matching.py模板匹配、特征匹配、Top-K 排序config.py集中管理阈值、尺寸、路径等全局参数samples/待识别的样本图片db/笔迹库原始图与特征向量缓存模块拆分的标准是「每一步能不能单独 import 测试」。preprocessing.py里放一个preprocess()features.py里放extract_feature()matching.py里放match_feature()各自都能拿一张图独立验证输出这样的工程你改任何一环都不用把整条链路跑一遍。config.py的存在尤其重要把散落在代码里的魔法数字都收进来调参的时候只改一个文件就行。4.2 核心调用流程与参数集中管理主程序的核心就是一条流水线读样本 → 预处理 → 提特征 → 和库里每个样本比对 → 排序输出。参数集中到config.py之后主流程的代码会变得非常短而且每个参数都有明确的注释换数据集的时候知道该动哪里。# config.py RESIZE_SIZE 64 # 特征提取前的统一尺寸 GRID_N 4 # 网格密度划分的格数 GAUSSIAN_K (5, 5) # 高斯模糊核 THRESH 0.75 # 置信度阈值低于此值判为 unknown TOP_K 3 # 输出前几个候选 DB_PATH db/features.npy # 特征库保存路径# main.py import argparse import cv2 import numpy as np from config import THRESH, TOP_K from preprocessing import preprocess from features import extract_feature from matching import match_feature def main(): parser argparse.ArgumentParser(description笔迹识别) parser.add_argument(--query, requiredTrue, help待识别图片路径) parser.add_argument(--db, defaultdb/features.npy, help特征库文件) args parser.parse_args() # 1. 读入并预处理 binary preprocess(args.query) # 2. 提取查询样本的特征 query_feat extract_feature(binary) # 3. 加载特征库并比对 feat_db np.load(args.db, allow_pickleTrue).item() ranking match_feature(query_feat, feat_db) # 4. 判断置信度并输出 if ranking[0][1] THRESH: print(识别结果: unknown最高相似度 {:.3f}.format(ranking[0][1])) else: print(识别结果: {}相似度 {:.3f}.format(ranking[0][0], ranking[0][1])) for name, score in ranking[:TOP_K]: print( candidate: {}, score: {:.3f}.format(name, score)) if __name__ __main__: main()np.load(..., allow_pickleTrue).item()把特征库读成一个字典键是样本名值是特征向量这是 OpenCV 项目里轻量持久化的常见做法不需要引入数据库。整个主流程只有十个左右的调用点每一行都在说清楚「现在做到哪一步了」。如果识别效果不对先用 debug 图确认预处理再单独打印特征向量看是不是有 NaN最后才怀疑匹配逻辑——这个排错顺序能省下大量时间。4.3 命令行运行与调参入口工程跑起来之后调参就变成了高频操作。把查询图片路径、候选数量、阈值全部提到命令行每次实验不用改代码。这是让这个课设从「能跑」变成「好用」的一步答辩的时候演示换样本、换阈值比贴代码更有说服力。# 基本用法识别一张样本 python main.py --query samples/01.png # 指定特征库和输出候选数 python main.py --query samples/02.png --db db/features.npy --top-k 5 # 调低置信度阈值观察低相似度样本的排序变化 python main.py --query samples/03.png --threshold 0.6--top-k和--threshold覆盖了config.py里的同名参数命令行传参优先级更高。调参的时候我一般会固定一个查询样本改变一个参数记录识别结果和所有候选分数这样做上十组哪个参数对结果影响最大就有数了。源码包里如果已经内置了样本和特征库先原样跑一遍得到基线结果再改成你的笔迹样本这样每一步改动都有对照不会出现「改了一堆参数但不知道是哪一步让识别率变差」的情况。5. 避坑指南OpenCV 笔迹识别项目里五个高频翻车点这一章是我自己跑 OpenCV 图像识别项目时真实踩过的坑每条都按「现象 → 原因 → 解决」来写。这五个问题在课程设计里出现的频率极高提前看一眼能省下大半天的排错时间。5.1 装了 OpenCV 还是报 No module named cv2现象在终端里执行python main.py第一行就抛出ModuleNotFoundError: No module named cv2但明明已经用pip install opencv-python装过了。原因绝大多数情况是把包装进了另一个 Python 环境。系统自带的 Python 和虚拟环境用的是不同的 site-packages 目录pip命令指向的环境不一定是你运行脚本的环境。还有一种可能是只装了opencv-python却没装opencv-contrib-python导致需要cv2.ximgproc的代码 import 失败。解决先确认运行环境再在该环境内安装# 确认当前 pip 属于哪个 Python python -m pip --version # 在当前 python 环境内安装两个包contrib 提供 ximgproc 等扩展模块 python -m pip install opencv-python opencv-contrib-python # 安装后验证 python -c import cv2; print(cv2.__version__)用python -m pip install而不是裸pip install能保证装进当前这个 Python 解释器对应的环境。如果项目用的是 conda 环境记得先conda activate再安装。版本验证打印出的版本号如果是 4.x就说明 OpenCV 4 的 API 已经就绪。5.2 findContours 返回值变化与 contourArea 未定义现象从网上复制的轮廓代码在自己环境里跑contour, hierarchy cv2.findContours(...)直接报错提示返回值数量不对或者调用cv2.contourArea()时 IDE 提示未定义标识符。原因OpenCV 3.x 和 4.x 之间findContours的返回值结构变了。3.x 返回三个值(image, contours, hierarchy)4.x 返回两个值(contours, hierarchy)直接把旧代码的赋值方式搬过来就会崩。contourArea未定义通常是 IDE 的静态检查没识别到运行时其实能用但返回值解包的问题在运行时确实会崩。解决统一按 OpenCV 4 的写法解包并且不依赖第一个返回值# OpenCV 4.x 写法第一个返回值丢弃 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 过滤太小的噪声轮廓 contours [c for c in contours if cv2.contourArea(c) 50]RETR_EXTERNAL只取最外层轮廓CHAIN_APPROX_SIMPLE压缩水平、垂直、对角方向的线段端点能大幅减少轮廓点数。面积阈值 50 是经验值具体要看图的尺寸如果图缩放过这个阈值也要按比例缩放。笔迹识别里轮廓一般用在「检测笔迹区域」或「用 matchShapes 做形状比对」所以拿到轮廓后的第一步通常是面积过滤把灰尘点产生的微型轮廓直接清掉。5.3 中文路径让 imread 读不出图现象代码逻辑完全没错图片放到项目根目录也能读出来但一旦路径里带中文目录或中文文件名cv2.imread返回的就是None。原因OpenCV 的imread底层用的是 C 的文件接口对中文等非 ASCII 路径支持一直不好Windows 环境下尤其明显。这个问题和 Python 本身没关系纯是 OpenCV 的历史兼容问题。解决用np.fromfile配合cv2.imdecode绕过它这是图像处理里处理中文路径的标准做法def imread_unicode(path): # 用 numpy 以字节方式读文件再交给 imdecode 解码 data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)np.fromfile读入的是原始字节流cv2.imdecode负责按图像格式解码这个组合对中文路径和绝对路径都稳定。注意cv2.imread返回None时不会抛异常只会静默失败所以读图之后一定检查是否为空。我在预处理函数里加了一个if img is None: raise的判断就是为了一开始在入口就把这个问题暴露出来而不是让它在后面变成维度不匹配的诡异报错。5.4 骨架提取慢到像死循环现象调用细化函数后程序长时间没反应CPU 占满或者跑一张几百万像素的大图要好几分钟等得人想直接关掉终端。原因手写的 Zhang-Suen 细化如果逐像素用 Python 循环加多次迭代复杂度会很高。扫描件原图往往有几百万像素而细化要求的输入是二值图大部分区域是背景逐像素检查邻域条件的开销会非常大。解决先缩小再细化缩小用我们前面特征提取那步的 resize 思路同时把输入限制在笔迹区域而不是整张纸。细化之前还可以把所有笔画白色像素的连通域求出来大块的区域才值得细化孤立噪点直接丢弃。如果你用的是cv2.ximgproc.thinning它本身是 C 实现的速度尚可但输入图过大时同样建议先缩放到统一尺寸再做比如 64×64 或 128×128。骨架提取对识别精度的影响不敏感到必须以原图分辨率进行缩小后反而能让特征更稳定。5.5 二值化后笔画断成一截一截现象预处理调试图里字的横折钩在转角处断掉撇捺中间出现空洞导致后面特征统计完全不靠谱。原因阈值选得太严或者纸张有底色、光照不均。固定阈值 127 在这种场景下会把较浅的笔画直接滤掉Otsu 处理全局不均的光照时亮区暗区的分割标准不一致也会让笔画断裂。解决优先换自适应阈值把blockSize调小一些比如从 31 改到 21C从 5 改到 2让判断更贴近局部亮度。如果断裂还是存在在形态学那一层开运算之后补一个闭运算用cv2.MORPH_CLOSE把细小的断口接上。闭运算的参数kernel同样取(3, 3)太大会把本来分开的两个笔画黏在一起。调这一层参数时每次只改一个值对照调试图看效果不要同时动三个参数那样你根本不知道是哪个改动起了作用。6. 进阶用形状描述子 留一验证把识别率再往上顶基础流程跑通之后识别率还是不够的话通常有两个改进方向换更强的形状描述子以及用严谨的验证方法找到真正的瓶颈。这一章给出两个可以直接落地的技巧。6.1 用 matchShapes 补一刀缓解字迹抖动特征向量虽然对位置和大小有一定容忍度但同一人写字时笔画的角度、弧度变化仍然是误差来源。cv2.matchShapes是基于轮廓的 Hu 矩比对它对轮廓的整体形状做归一化比较正好适合用手写轮廓做二次确认。做法是在特征匹配选出前两名之后把查询图的轮廓和这两名的库图轮廓再算一次matchShapes用它的结果修正最终排序。def refine_with_shapes(binary_query, db_contours, ranking): # 提取查询图最外层轮廓 contours, _ cv2.findContours(binary_query, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: return ranking query_contour max(contours, keycv2.contourArea) refined [] for name, score in ranking: # matchShapes 返回的是距离越小越相似 dist cv2.matchShapes(query_contour, db_contours[name], cv2.CONTOURS_MATCH_I2, 0) refined.append((name, score - dist * 0.1)) refined.sort(keylambda x: x[1], reverseTrue) return refinedCONTOURS_MATCH_I2是三种匹配模式里对形变最敏感的一种距离值越小说明轮廓越像。我在最终分数里减去dist * 0.1相当于给形状距离一个较小的惩罚权重让它去微调而不是覆盖原有的特征匹配排序。这个权重就是调参入口如果发现形状距离太激进导致误判就把权重降到 0.05如果感觉微调没起作用再往上加。这样等于给识别系统加了第二个视角两个视角结论一致时答案基本是稳的。6.2 留一验证法给识别率一个可信的数字改进之后到底提升了多少不能靠「试了几张感觉还行」要跑一遍留一验证法。做法很简单每个人的笔迹样本里轮流拿一张当查询剩下全部当库统计命中率。这样每个样本都被检过一次识别率的数字是完整算出来的答辩时也能直接拿出来讲。def leave_one_out(samples_by_person): correct 0 total 0 for person, imgs in samples_by_person.items(): for i in range(len(imgs)): # 当前样本当查询其余样本为库 query imgs[i] db {} for p, ims in samples_by_person.items(): for j, im in enumerate(ims): if p person and j i: continue db[f{p}_{j}] extract_feature(im) pred, _ recognize(query, db) correct (pred person) total 1 print(留一验证识别率: {:.1f}%.format(100.0 * correct / total))每个人的样本至少要有三张以上这个验证才有统计意义。跑完之后如果识别率卡在 90% 上下去看哪些样本被判错了它们往往是字形本身和他人极度相似的那几张。这个验证方法也暴露了一个真相笔迹识别的上限不只是算法决定的还取决于库内样本之间的区分度。从那以后我每次做完一个识别项目都会强制走一遍留一验证把误判样本单独存一个目录反复看而不是只盯着总正确率。这种方式逼着你去面对具体失败的案例比盲目调参有用得多。希望这份基于 Python OpenCV 的笔迹识别源码和这些调参思路能帮你在课设或练习里少走几步弯路把精力留给真正影响结果的那几个参数。本文还有配套的精品资源点击获取
返回列表