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

资讯详情

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

OpenCV图像识别实战:从ROI截取到SVM与CNN模型训练部署

OpenCV图像识别实战:从ROI截取到SVM与CNN模型训练部署 简介这份面向OpenCV图像识别场景的成熟版实战项目针对具备Python基础的计算机视觉学习者和开发者围绕字母识别的完整流程系统展示了从图像采集、ROI区域提取、特征提取到模型训练与实时识别的工程实现。压缩包共包含22个文件以Python脚本为主另有预训练模型、测试图片及说明文档包体整体约13.6MB。项目中覆盖模板匹配、Haar级联分类器、卷积神经网络CNN等多种识别方案并提供传统机器学习与深度学习两套训练入口用户可根据实际场景灵活选型。同时配套摄像头实时识别脚本、自定义数据集处理工具与参数配置脚本便于二次开发与参数调优。目前已有438人学习浏览适合希望快速上手OpenCV字母识别并需要完整可运行代码框架的读者。1. 图像识别不是调个 opencv 函数就完事这套工程到底在教你什么很多朋友学 opencv 图像识别往往从cv2.findContours开始调出一个轮廓就以为识别到头了。真到项目里会发现识别是“采集数据 → 处理图像 → 训练模型 → 测试部署”这条完整链路任何一环缺失效果都会打对折。这套工程就是为这条链路准备的它包含get_ROI.py的样本截取、my_dataset.py的数据集管理、learn_train.py与learn_cnn.py两套训练路线、以及已经训练好的model1.pkl。你只需要有 Python 3.7 环境和摄像头就能从零把字母识别跑通。适合想把常规的图像识别项目从头到尾走通的工程师也适合学校课程设计拿来做底座改造。2. 先看流程再动手这套代码的模块分工与数据链路2.1 先分清哪些是入口、哪些是辅助模块拿到压缩包解开后直接能看到十几个文件和__pycache__目录。当年我第一次打开时也懵了后来按“入口 / 依赖 / 数据文件”三类归了一下使用顺序就清楚很多。换你拿到手我也建议先做这个动作别急着双击运行。下表是我整理后的模块分工按这个去读源码读起来会顺很多。文件角色使用方式get_ROI.py从图片或摄像头画面中框选字母区域保存为训练样本可单独运行my_dataset.py数据集读取类负责遍历文件夹、读图、缩放、生成样本与标签被训练脚本导入Selete_train_data.py把数据集划分为训练集 / 验证集 / 测试集可单独运行learn_setting.py集中管理批大小、学习率、图片尺寸等超参数被训练脚本导入func.py通用的图像预处理与工具函数被各脚本导入learn_encoding.py把字母标签编码成数字训练和预测都用它被训练脚本导入learn_train.py传统机器学习路线训练后产出model1.pkl入口脚本learn_cnn.py卷积神经网络路线适合有一定数据量时使用入口脚本ceshi_zhuangyong.py加载训练好的模型跑摄像头或图片测试入口脚本exemaker.py用 PyInstaller 把测试脚本打包成 exe可选入口ceshi1.jpg一张测试图片用来快速验证模型数据文件model1.pkl训练好的模型pickle 序列化保存数据文件Readme.txt工程说明文档文档入口判断其实很简单看if __name__ __main__:在哪个文件里。像my_dataset.py、func.py这类没有入口块的文件就是给别人当依赖用的。learn_train.py、learn_cnn.py、ceshi_zhuangyong.py这些能独立执行的才需要按顺序跑。提示__pycache__里那一堆.cpython-37.pyc是 Python 3.7 运行时的字节码缓存不是项目自己写的文件。后面避坑章节我会专门说它现在先不碰。2.2 一条数据从图像到分类结果要经过哪几步这个工程的完整数据链路是这样的原始图像 → 灰度化 / 二值化 → ROI 截取 → 尺寸归一 → 特征提取 → 分类器训练或预测 → 输出字母标签。OpenCV 在这条链路上主要做前三件事读图、抠 ROI、预处理。get_ROI.py负责把字母从复杂背景里框出来生成干净的训练素材my_dataset.py在做尺寸归一和灰度化func.py里一般还会放一些二值化、膨胀、直方图均衡之类的工具函数。这些都属于“特征提取”的准备阶段。分类环节由model1.pkl承担。这个文件其实是训练完成后的分类器对象用 pickle 序列化存在磁盘上。预测的时候只要pickle.load把它读回内存再对预处理好的特征向量调用predict就行。它的好处是模型训练一次之后任何时候都能直接复用不用每次重新跑训练。搞清楚了这条链路你再看learn_train.py和learn_cnn.py的区别就很容易前者走的是“人工特征 传统分类器SVM / KNN”后者走的是“卷积神经网络自动提取特征”两条路最终产出的都是model1.pkl。后面第四部分我会详细对比。2.3 环境准备Python 版本、OpenCV 与依赖安装从.cpython-37.pyc可以判断这工程是在 Python 3.7 下跑过的。我一般会用 conda 单独建一个干净环境避免和日常开发环境里的 OpenCV 版本互相干扰。依赖其实就两个核心包OpenCV 和 NumPy大多数情况下再加个 scikit-learn 用于传统分类器。conda create -n letter_env python3.7 conda activate letter_env pip install opencv-python numpy scikit-learn装完以后用这行命令验证一下 OpenCV 是否能正常导入。python -c import cv2; print(cv2.__version__)如果输出 4.x 的版本号说明环境没问题。注意别用pip install opencv-contrib-python和opencv-python同时装两个包都装了以后命名空间会打架后面cv2.selectROI这类函数经常莫名其妙报 AttributeError。提示如果机器是树莓派或者 ARM 板子pip install opencv-python不一定有预编译的 wheel通常得用apt install python3-opencv或者自己编译。编译步骤多普通项目先试 apt省时间。3. 把训练数据做扎实ROI 截取、数据集划分与预处理细节3.1 get_ROI.py用 selectROI 把字母从图像里框出来做图像识别最容易被忽略的就是数据质量。模型再好训练集里全是歪的、糊的、带背景噪点的字母图片最后效果一定翻车。这个工程里get_ROI.py要解决的就是这个问题从一张大图里把每个字母区域单独裁出来存成干净的训练图。常见做法是用cv2.selectROI打开一个窗口让你用鼠标框选区域框完按空格确认、按 ESC 取消。下面是我平时写这段代码的习惯写法import cv2 def grab_roi(image_path, save_dir, label): img cv2.imread(image_path) if img is None: print(图片读取失败检查路径) return roi cv2.selectROI(select roi, img, showCrosshairTrue) if roi (0, 0, 0, 0): print(没有选中区域) return x, y, w, h roi crop img[y:y h, x:x w] save_path f{save_dir}/{label}.png cv2.imwrite(save_path, crop) print(保存到:, save_path) cv2.destroyAllWindows()这段代码的逻辑很直接selectROI返回一个四元组(x, y, w, h)其中x和y是选区左上角坐标w和h是选区的宽和高。切片img[y:yh, x:xw]把这块区域截出来再按类别名保存。showCrosshairTrue会在选区内显示十字辅助线框选小字母时定位更准。用cv2.imwrite保存时建议文件名统一按类别命名比如A_01.png、B_02.png这样后面my_dataset.py遍历文件夹时能直接从文件名推断标签省去额外维护标注文件的麻烦。如果你用的是 OpenCV 3.4 之前的版本selectROI这个函数还不存在那就得自己做鼠标回调——cv2.setMouseCallback配合cv2.EVENT_LBUTTONDOWN和cv2.EVENT_LBUTTONUP记录两个角点手动画矩形。这一点在第 5 章的避坑清单里还会再讲。3.2 my_dataset.py数据集读取与标签映射训练脚本不能一张一张去手动读图所以需要一个数据集类把“图片路径 → 像素矩阵 → 标签”这套流程封装好。这套工程里my_dataset.py干的就是这件事。它的核心逻辑是遍历一个根目录下不同的子文件夹子文件夹的名字就是类别标签。假设你的训练数据目录长这样dataset/ A/ A_01.png A_02.png B/ B_01.png那么读取数据的典型写法如下import os import cv2 import numpy as np class LetterDataset: def __init__(self, root_dir, img_size(32, 32)): self.samples [] self.labels [] for label_name in sorted(os.listdir(root_dir)): label_dir os.path.join(root_dir, label_name) if not os.path.isdir(label_dir): continue for file_name in os.listdir(label_dir): path os.path.join(label_dir, file_name) img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: continue img cv2.resize(img, img_size) self.samples.append(img.flatten() / 255.0) self.labels.append(label_name) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.samples[idx], self.labels[idx]这里有几个点需要解释一下cv2.imread(path, cv2.IMREAD_GRAYSCALE)是直接以灰度模式读图省掉手动cv2.cvtColor一步cv2.resize(img, img_size)把所有图片统一到32 x 32因为送入分类器时特征维度必须一致img.flatten() / 255.0把二维矩阵拉成一维向量并归一化到[0, 1]这一步很关键不归一化的话SVM 和 CNN 的收敛效果都会有明显差异。标签这里直接用文件夹名字符串A、B这种。真正训练时learn_encoding.py会把字符串映射成数字A - 0、B - 1这步叫标签编码模型输出的是数字预测完再反向映射回字母显示。3.3 Selete_train_data.py训练集、验证集、测试集怎么切数据集切分看似简单实际上有很多细节。如果切分不科学模型在验证集上的得分高得离谱一上摄像头就露馅。我一般用scikit-learn的train_test_split来切写法如下from sklearn.model_selection import train_test_split X np.array(dataset.samples) # 形状 (样本数, 1024) y np.array(dataset.labels) X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.3, random_state42, stratifyy ) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, random_state42, stratifyy_temp )test_size0.3表示先把 30% 的数据留出来作为临时集再从临时集里对半分出验证集和测试集最终比例大约是训练集 70%、验证集 15%、测试集 15%。random_state42固定随机种子保证每次运行切分结果一致这个习惯强烈建议保留否则换一台机器结果就不一样没法对比实验。stratifyy是容易被新手忽略的参数它表示按标签比例分层抽样。如果你的数据集里 A 有 100 张、B 只有 20 张不分层的话不小心就可能把 A 的样本全分到训练集、把 B 的样本全分到测试集模型直接废掉。做字母识别时每个类别样本数量很难完全平衡stratify是必选项。划分完以后model1.pkl是用训练集得到的模型验证集用来调超参测试集是最后评估用的。我见过不少工程把测试集反复拿来调参这相当于考试前先看了答案最终评估分数没有参考价值。这套工程里Selete_train_data.py把三步逻辑拆开就是为了避免这个坑。4. 模型训练从 learn_setting 超参到 learn_train 与 learn_cnn 的取舍4.1 learn_setting.py为什么把超参数集中管理训练模型时批大小、学习率、迭代轮数、图片尺寸这些参数会反复调整。如果每个参数都散落在各个脚本里调参的时候就得满工程找赋值语句改一处漏一处是常有的事。这个工程把超参数集中放到learn_setting.py这是很明智的做法。典型的learn_setting.py内容大概长这样# 训练超参数 batch_size 32 epochs 50 learning_rate 0.001 # 图像参数 image_size (32, 32) num_classes 26 # 路径参数 train_data_dir dataset model_save_path model1.pkl # 固定随机种子保证可复现 random_seed 42batch_size是一次喂给模型多少张样本。批大小太小的话训练不稳定太大的话显存或内存占用高而且容易收敛到比较差的局部最优。做字母识别这种小任务32 是个很稳妥的起点。learning_rate对 CNN 来说一般从 0.001 起步传统 SVM 不用管它。epochs是全部数据遍历几遍我习惯先设 50观察验证集损失不再下降就手动停。把超参数放在一个文件里的好处是训练时只需要专注于改这一个文件learn_train.py和learn_cnn.py通过from learn_setting import *拿到所有配置。这样出错时排查范围也小。4.2 learn_train.py用传统分类器把“特征提取 分类”走通传统机器学习的图像识别思路是先人工提取特征再把特征交给分类器。这套工程里learn_train.py走的就是这条路。图片已经由数据集类处理成 1024 维32 x 32的特征向量了训练部分就很直白。常见实现是用 SVM。下面是一个可以直接套用的训练代码框架from sklearn.svm import SVC from learn_setting import model_save_path clf SVC(kernellinear, C1.0, probabilityTrue) clf.fit(X_train, y_train) train_acc clf.score(X_train, y_train) val_acc clf.score(X_val, y_val) print(train acc:, train_acc, val acc:, val_acc) import pickle with open(model_save_path, wb) as f: pickle.dump(clf, f)C是 SVM 的正则化参数C越大模型越倾向于把所有训练样本都分对容易过拟合C越小模型越简单但可能欠拟合。字母识别样本量不大我一般先试C1.0再看验证集准确率调整如果训练集 99% 而验证集只有 80%基本可以判断过拟合了这时候把C调小到 0.1 再试。kernellinear对高维特征1024 维通常够用训练也快。probabilityTrue是为后续如果想输出置信度做准备不需要的话可以去掉训练速度会快一点。4.3 learn_cnn.py三层卷积的轻量网络结构当训练数据到几百张以上、字母背景差异比较大的时候传统特征就不太够用了。learn_cnn.py换成了卷积神经网络让网络自己去学特征。以 PyTorch 为例这套工程里常见的轻量网络结构如下import torch.nn as nn class LetterCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool nn.MaxPool2d(2) self.fc1 nn.Linear(64 * 8 * 8, 128) self.out nn.Linear(128, num_classes) def forward(self, x): x self.pool(torch.relu(self.conv1(x))) x self.pool(torch.relu(self.conv2(x))) x x.view(x.size(0), -1) x torch.relu(self.fc1(x)) return self.out(x)输入是32 x 32的单通道灰度图。第一次卷积后变成32 x 32的 32 通道特征图池化后16 x 16第二次卷积后 64 通道再池化变成8 x 8。所以全连接层输入维度是64 * 8 * 8 4096。view(x.size(0), -1)是把三维特征图拉平成二维矩阵交给后面的全连接层。激活函数用的是relu它的优势是计算简单能缓解梯度消失对这个小网络来说完全够用。训练时配合交叉熵损失和 Adam 优化器工程里默认配置就行。要注意 CNN 对数据量更敏感如果每个字母只有十来张图CNN 很容易过拟合这时候learn_train.py的 SVM 反而更稳。4.4 两套路线的选择依据到底用哪一套主要看三件事数据量、硬件条件、对实时性的要求。对比项learn_train.pySVMlearn_cnn.pyCNN每类最少样本量1020 张可用50 张以上较稳训练耗时秒级分钟级CPU 能跑硬件要求无 GPU 也能轻松跑无 GPU 也能训练但慢识别速度极快快过拟合风险低数据少时较高部署体积模型文件很小模型文件稍大我的习惯是先跑通learn_train.py把整个流程验证没问题了再把数据量攒到每类 50 张以上尝试learn_cnn.py。两个模型都训出来以后用测试集分别打分哪个高就用哪个别盲目迷信深度学习。在工程里稳定易维护往往比刷高一个百分点更重要。5. 图像识别避坑指南我复现这套工程时翻车的五个地方当初我在复现这套工程的路上一共翻过五个车每一条都是实打实花时间排查出来的。你提前看完至少能省半天功夫。坑一运行时报 ModuleNotFoundError 找不到 cv2现象执行python get_ROI.py第一行就报ModuleNotFoundError: No module named cv2。原因最常见是 OpenCV 压根没装或者 pip 装到了另一个 Python 环境。这个工程用的是 Python 3.7如果你电脑上有多个 Pythonpip默认装的那个环境和执行脚本的环境不是同一个。解决先确认当前环境里 python 的位置再重新安装python --version pip install opencv-python numpy scikit-learn python -c import cv2; print(cv2.__version__)如果用的是 conda先把环境激活再执行上面的命令。切记不要同时装opencv-python和opencv-contrib-python两个包混装会让cv2模块变得不稳定。坑二cv2.selectROI 报 AttributeError提示模块没有这个属性现象get_ROI.py调用cv2.selectROI报错AttributeError: module cv2 has no attribute selectROI。原因selectROI是 OpenCV 3.4.2 版本才加入的功能如果装的是老版本或者某些精简版 OpenCV没有编译这个函数。解决最省事的办法是升级 OpenCV 版本pip install --upgrade opencv-python升级后如果还不行就得自己写鼠标回调函数实现框选方法是用cv2.setMouseCallback注册事件在EVENT_LBUTTONDOWN记录起点、EVENT_LBUTTONUP记录终点然后cv2.rectangle画框。代码量不大但对 OpenCV 的事件机制要求清楚一些。坑三pickle 加载 model1.pkl 时出现 UnicodeDecodeError 或 EOFError现象执行with open(model1.pkl, rb) as f: clf pickle.load(f)报UnicodeDecodeError或者EOFError。原因UnicodeDecodeError通常是因为用文本模式打开二进制文件或者用 Python 2 保存的模型用 Python 3 读EOFError多半是文件没读完或者文件本身损坏。解决加载模型必须用二进制读模式import pickle with open(model1.pkl, rb) as f: clf pickle.load(f)如果这是 Python 2 时代留下的模型加载时加一个编码参数clf pickle.load(f, encodinglatin1)另外model1.pkl和脚本必须在同一个目录下。用相对路径加载时工作目录不对也会报文件找不到这类问题多用绝对路径能排查出来。坑四改完 func.py 后重新运行效果却没变化现象修改了func.py里的预处理逻辑重新运行ceshi_zhuangyong.py识别结果和之前一模一样。原因项目里有__pycache__目录里面存放着第一次运行生成的.pyc字节码。Python 判断源码文件没有变化时会直接加载.pyc缓存。有时候源码改了但时间戳精度问题让 Python 误判或者干脆是 IDE 的缓存机制在作怪。解决删掉__pycache__目录再重新运行rm -rf __pycache__这一步看似简单实际非常管用。从那以后我每次改完源码都会习惯性清理一遍缓存目录再重新跑入口脚本省了很多“改了没反应”的困惑。坑五contourArea 报错或者在摄像头画面里找不到字母区域现象图像轮廓处理时代码报contourArea() is not defined或者摄像头测试时字母区域没有正确框出来。原因前者大概率是拿到了空轮廓列表还继续调用面积计算或者是 OpenCV 版本之间 C 接口与 Python 接口混用了后者多半是背景太复杂固定阈值分割把字母和背景混在一起了。解决调用contourArea之前先判断轮廓列表是否为空contours, hierarchy cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: if len(cnt) 4: continue area cv2.contourArea(cnt)len(cnt) 4用来过滤掉点数太少的轮廓因为计算面积至少需要三个点。摄像头里找不到字母通常是二值化阈值选得不对。可以先用cv2.threshold配合cv2.Trackbar动态调整阈值找到合适的值以后再写死到代码里比拍脑袋设一个固定值可靠得多。OpenCV 的adaptiveThreshold对光线变化更友好在摄像头场景下强烈建议优先考虑。6. 把 model1.pkl 真正用起来摄像头测试与打包 exe 的两个实战技巧6.1 让摄像头测试脚本稳定输出识别结果ceshi_zhuangyong.py的作用是加载训练好的模型连接摄像头做实时识别。核心注意点只有一个预测时的输入特征必须和训练时完全一致。训练时图片是32 x 32灰度图、flatten()成一维、再除以 255 归一化。预测时少了任何一步predict都会报维度错误或者结果完全不对。下面是一个可以参考的实时识别框架import cv2 import pickle import numpy as np with open(model1.pkl, rb) as f: clf pickle.load(f) cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break # 先用固定窗口截取画面中央区域避复杂背景 x, y, w, h 80, 60, 200, 200 roi frame[y:y h, x:x w] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (32, 32)) feat gray.flatten() / 255.0 label clf.predict([feat])[0] print(识别结果:, label) cv2.rectangle(frame, (x, y), (x w, y h), (0, 0, 255), 2) cv2.imshow(result, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里最容易被忽略的就是clf.predict([feat])[0]。feat是一维数组但 sklearn 要求传入二维矩阵所以必须用[feat]包一层表示只预测一行样本。predict返回的是数组[0]取出第一行的预测结果。如果你加载的是 CNN 模型输入格式会是(1, 1, 32, 32)的四维张量那要在预测前先reshape这一步搞错了模型会直接报 shape mismatch。6.2 用 exemaker.py 打包 exe 时别把 model1.pkl 漏在外面工程里的exemaker.py负责把测试程序打包成 exe方便在没有 Python 环境的机器上直接运行。用 PyInstaller 打包时最常见的坑是命令行只打包了主脚本model1.pkl这个模型文件没有一起打进去结果程序在别的电脑上双击运行直接提示找不到文件。正确的做法是用--add-data把模型文件带进包内。exemaker.py里可以这样写import PyInstaller.__main__ PyInstaller.__main__.run([ ceshi_zhuangyong.py, --onefile, --add-data, model1.pkl;., --add-data, func.py;., ])--add-data的参数格式是“源文件;目标目录”Windows 用分号Linux 和 macOS 用冒号。第二项写成.表示放在解压目录的根路径下。打包出来的程序运行时模型文件会在临时目录里被释放如果代码里用了相对路径加载模型PyInstaller 的单文件模式可能找不到模型需要用sys._MEIPASS指向临时目录。老实讲我当年第一次打包时没加--add-dataexe 在自己机器上跑得好端端的换台电脑就各种报错。后来养成的习惯是每一次改完识别流程一定要清一遍__pycache__再用一张固定的ceshi1.jpg跑一遍完整验证最后才去打包。这一步走完至少能保证别人拿到手不是坏的。希望帮到你。本文还有配套的精品资源点击获取
返回列表