写点什么

OpenCV 编程:OpenCV3.X 训练自己的分类器

作者:DS小龙哥
  • 2022 年 7 月 17 日
  • 本文字数:6966 字

    阅读完需:约 23 分钟

一、环境介绍

操作系统: windows10 64 位


QT 版本: 5.12.6 (我的程序里主要是 QT+OpenCV 实现图像处理显示的)


OpenCV 版本: OpenCV3.4.7

二、下载安装 OpenCV

windows 下不用下载源码,可以直接在官网下载编译好的文件解压即可使用。


OpenCV 官网下载地址: https://opencv.org/releases/ 下载之后解压到指定目录即可,我这里是直接解压到 C 盘的。



因为在官网下载的版本是 VC 版本,而我的 QT 使用的是 MinGW 编译器,上面下在官网下载的安装包里的库用不了,需要再下载一个 MinGW 版本。 下载地址:https://github.com/huihut/OpenCV-MinGW-Build



为什么需要下载两个版本? 其实主要是 MinGW 版本的 OpenCV 里带的两个训练分类器(opencv_traincascade.exe)的文件在我电脑上无法使用,可能库冲突,具体问题没有深究,就干脆再下载了一个 VC 版本是 OpenCV,VC 版本里 opencv_traincascade.exe 文件是可以正常使用。 其实下载的 VC 版本 OpenCV 主要是为了用这两个文件(opencv_traincascade.exe、opencv_createsamples.exe)



三、测试 OpenCV 自带的分类器

3.1 自带的分类器文件介绍

OpenCV 的官方已经提供了很多训练好的分类器文件,在 OpenCV 的安装目录下有。



上面文件中提供了常见的 人脸检测、眼睛检测、猫脸检测、行人检测等,看 XML 文件的命名即可得知。


下面编写 QT 程序,调用 OpenCV 的级联分类器进行测试。

3.2 QT 的示例代码

下面的 QT 界面很简单,主要是为了测试分类器文件。


xxx.cpp 文件代码:


#include "widget.h"#include "ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget){ ui->setupUi(this); dir="C:/";
ui->label_source->setAlignment(Qt::AlignVCenter); ui->label_2->setAlignment(Qt::AlignVCenter);}
Widget::~Widget(){ delete ui;}

QImage Widget::Mat2QImage(const Mat& mat){ // 8-bits unsigned, NO. OF CHANNELS = 1 if(mat.type() == CV_8UC1) { QImage image(mat.cols, mat.rows, QImage::Format_Indexed8); // Set the color table (used to translate colour indexes to qRgb values) image.setColorCount(256); for(int i = 0; i < 256; i++) { image.setColor(i, qRgb(i, i, i)); } // Copy input Mat uchar *pSrc = mat.data; for(int row = 0; row < mat.rows; row ++) { uchar *pDest = image.scanLine(row); memcpy(pDest, pSrc, mat.cols); pSrc += mat.step; } return image; } // 8-bits unsigned, NO. OF CHANNELS = 3 else if(mat.type() == CV_8UC3) { // Copy input Mat const uchar *pSrc = (const uchar*)mat.data; // Create QImage with same dimensions as input Mat QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); return image.rgbSwapped(); } else if(mat.type() == CV_8UC4) { // Copy input Mat const uchar *pSrc = (const uchar*)mat.data; // Create QImage with same dimensions as input Mat QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_ARGB32); return image.copy(); } else { return QImage(); }}
//打开本地图片void Widget::on_pushButton_open_clicked(){ filename=QFileDialog::getOpenFileName(this,"选择打开的文件",dir,tr("*.bmp *.jpg *.png")); if(filename.isEmpty())return; QFileInfo info(filename); dir=info.path(); //保存当前路径

}
//人脸检测代码void Widget::opencv_face(QImage qImage){ QTime time; time.start();
//定义级联分类器 CascadeClassifier face_cascade; //加载分类文件 // if( !face_cascade.load("C:/OpenCV_3.4.7/OpenCV-MinGW-Build-OpenCV-3.4.7/etc/haarcascades/haarcascade_frontalcatface.xml") ) { qDebug()<<"haarcascade_frontalface_alt.xml 分类器加载错误"; return; } Mat frame=QImage2cvMat(qImage); cvtColor( frame, frame, COLOR_BGR2GRAY );//转换成灰度图像
std::vector<Rect> faces; //正脸检测 face_cascade.detectMultiScale(frame,faces); qDebug()<<tr("耗时:%1 ms 识别:%2 数量:%3\n").arg(time.elapsed()).arg(faces.size()).arg(faces.size());
for ( size_t i = 0; i < faces.size(); i++ ) { #if 1 Point center(faces[i].x + faces[i].width/2, faces[i].y + faces[i].height/2); ellipse(frame, center, Size( faces[i].width/2, faces[i].height/2 ), 0, 0, 360, Scalar( 255, 0, 255 ), 4 ); rectangle(frame, cvPoint(cvRound(faces[i].x), cvRound(faces[i].y)), cvPoint(cvRound((faces[i].x + faces[i].width-1)), cvRound((faces[i].y + faces[i].height-1))), Scalar(255, 255, 255), 3, 8, 0); #endif
//提取识别结果 Mat frame1; for(size_t i=0;i<faces.size();i++) { Point center(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height / 2); frame1= frame(Rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height)); } /*在控件上显示识别结果*/ ui->label_2->setPixmap(QPixmap::fromImage(Mat2QImage(frame1))); }
/*在控件上显示*/ QImage display_image=Mat2QImage(frame); ui->label_source->setPixmap(QPixmap::fromImage(display_image));}
Mat Widget::QImage2cvMat(QImage image){ Mat mat; switch(image.format()) { case QImage::Format_ARGB32: case QImage::Format_RGB32: case QImage::Format_ARGB32_Premultiplied: mat = Mat(image.height(), image.width(), CV_8UC4, (void*)image.constBits(), image.bytesPerLine()); break; case QImage::Format_RGB888: mat = Mat(image.height(), image.width(), CV_8UC3, (void*)image.constBits(), image.bytesPerLine()); cvtColor(mat, mat, CV_BGR2RGB); break; case QImage::Format_Indexed8: mat = Mat(image.height(), image.width(), CV_8UC1, (void*)image.constBits(), image.bytesPerLine()); break; } return mat;}

void Widget::on_pushButton_start_clicked(){ opencv_face(QImage(filename).scaled(ui->label_source->size(),Qt::KeepAspectRatio));}
复制代码


xxx.h 文件代码:

#ifndef WIDGET_H#define WIDGET_H
#include <QWidget>#include "opencv2/core/core.hpp"#include "opencv2/core/core_c.h"#include "opencv2/objdetect.hpp"#include "opencv2/highgui.hpp"#include "opencv2/imgproc.hpp"
#include <QFileDialog>#include <QTime>#include <QDebug>
using namespace cv;
QT_BEGIN_NAMESPACEnamespace Ui { class Widget; }QT_END_NAMESPACE
class Widget : public QWidget{ Q_OBJECT
public: Widget(QWidget *parent = nullptr); ~Widget(); QImage Mat2QImage(const Mat& mat); void opencv_face(QImage qImage); Mat QImage2cvMat(QImage image); QString dir; QString filename;private slots: void on_pushButton_open_clicked();
void on_pushButton_start_clicked();
private: Ui::Widget *ui;};#endif // WIDGET_H
复制代码


xxx.pro 文件


QT       += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use# any Qt feature that has been marked deprecated (the exact warnings# depend on your compiler). Please consult the documentation of the# deprecated API in order to know how to port your code away from it.DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.# In order to do so, uncomment the following line.# You can also select to disable deprecated APIs only up to a certain version of Qt.#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \ main.cpp \ widget.cpp
HEADERS += \ widget.h
FORMS += \ widget.ui
# Default rules for deployment.qnx: target.path = /tmp/$${TARGET}/binelse: unix:!android: target.path = /opt/$${TARGET}/bin!isEmpty(target.path): INSTALLS += target
#linu平台的路径设置linux {message('运行linu版本')#添加opencv头文件的路径,需要根据自己的头文件路径进行修改INCLUDEPATH+=/home/wbyq/work_pc/opencv-3.4.9/_install/install/include\ /home/wbyq/work_pc/opencv-3.4.9/_install/install/include/opencv\ /home/wbyq/work_pc/opencv-3.4.9/_install/install/include/opencv2
LIBS+=/home/wbyq/work_pc/opencv-3.4.9/_install/install/lib/libopencv_*}
win32{ message('运行win32版本') #添加opencv头文件的路径,需要根据自己的头文件路径进行修改 INCLUDEPATH+=C:/OpenCV_3.4.7/OpenCV-MinGW-Build-OpenCV-3.4.7/include \ C:/OpenCV_3.4.7/OpenCV-MinGW-Build-OpenCV-3.4.7/include/opencv \ C:/OpenCV_3.4.7/OpenCV-MinGW-Build-OpenCV-3.4.7/include/opencv2 LIBS+=C:/OpenCV_3.4.7/OpenCV-MinGW-Build-OpenCV-3.4.7/x86/mingw/bin/libopencv_*.dll}
复制代码


xxx.ui 文件:



3.3 测试人脸分类器效果

把代码中的分类器文件换成:haarcascade_frontalface_alt2.xml




这份 QT 代码只是为了简单的测试,就没有开线程去识别,如果识别耗时比较久的话,识别过程中 UI 界面会卡住,等一会即可。

3.4 测试猫脸分类器效果

把代码中的分类器文件换成:haarcascade_frontalcatface.xml



3.5 测试行人检测分类器效果

把代码中的分类器文件换成:haarcascade_fullbody.xml



四、训练自己的分类器

4.1 前言

如果自己实际要检测的物体在 OpenCV 自带的分类器里没有,或者 OpenCV 自带的分类器识别精度不满足要求,就可以使用 OpenCV 自带的分类器程序自己训练。训练的方法网上的教程非常多,下面就重复造轮子简单的叙述一下训练过程。


说明: 因为下面的内容主要是简单的叙述一下训练过程,所以我准备的样本数量都较少,如果实际训练需要看下面说明增加样本数量。

4.1 准备训练的正负样本素材说明

想要让计算器识别指定的物体,那么首先得让计算器知道你要识别的物体长什么样,需要提前学习一番。学习过程中,需要准备一份正样本和一份负样本。 正样本就是要识别的物体;负样就是用来与正样本比较的,负样本里不包含正样本里图片或者相似的图片,但是也不能乱选,最好与正样本的取景在一个环境下,这样效果最好,减少误识别。


样本图片最好使用灰度图(也就是黑白图);样本数量越多越好,尽量高于 1000,样本间差异性越大越好,正负样本比例可以为 1:3,训练样本官方推荐最佳尺寸为 20x20,样本图片的命名不要出现特殊字符,使用正常点的名字即可。


正样本的所有的图片尺寸必须一致,如果不一致的或者尺寸较大的,可以先将所有样本统一缩放到 20*20。


推荐个简单的图片处理工具:https://blog.csdn.net/xiaolong1126626497/article/details/106085795


尺寸大小决定的是训练的时间长短,大尺寸也可以训练,如果图片太小也会损失很多细节,尺寸可以根据实际情况权衡,但是太大的图片样本训练可能会导致内存不够用的情况,具体情况可以根据训练效果和情况进行调整。


注意,为了提高训练准确率,负样本不能乱选。


比如: 检测的正样本是车轮,那么负样本就最好是车身,马路、护栏、等等环境。

4.2 正样本图片示例

下面是识别汽车的正样本图片,正样本图片可以创建一个 PositiveSample 文件夹存放。



4.3 负样本图片示例

负样本图片可以创建一个 NegativeSample 文件夹存放。负样本图片不要求样本的尺寸,但要大于等于正样本的大小;且负样本不能重复,要增大负样本的差异性。负样本也要灰度化,同正样本操作相同。



4.4 创建工作目录

在电脑任意目录,创建一个工作目录 OpenCV_TrainingData,将存放正负样本的目录拷贝到 OpenCV_TrainingData 目录下,再创建一个 XML 目录,用于存放生成的训练文件。



3.2 创建正样本描述文件

打开电脑命令行终端。



使用 cd 命令进入到正样本的目录下。



执行命令如下:


命令1-进入到正样本目录下:cd /d D:\linux-share-dir\OpenCV_TrainingData\PositiveSample
命令2-将目录下所有图片名字和路径输出到pos.txt文件:dir /b/s/p/w *.jpg > pos.txt
复制代码


打开生成的 pos.txt 文件内容如下。



将文件的内容稍作修改,加上检测目标个数、目标图片左上位置坐标、图片宽高参数


我这里准备的样本图片尺寸都是 40x40,所以填写的代码:1 0 0 40 40


修改效果如下:



如果图片数量很大,手动修改比较麻烦, 直接使用文本编辑器,搜索替换即可。

3.3 创建负样本描述文件

负样本描述文件创建方法与正样本描述文件一样,进入到负样本图片的目录下,生成 neg.txt 文件,代码如下:


命令1:cd /d D:\linux-share-dir\OpenCV_TrainingData\NegativeSample
命令2:dir /b/s/p/w *.jpg > neg.txt
复制代码


注意:负样本 neg.txt 文件不需要做任何修改,以下就是最终文件。



3.4 生成正样本的.vec 文件

为了方便填路径,将生成的正负样本描述文件 pos.txt 和 neg.txt 拷贝到上层目录下。



正样本的.vec 文件生成,执行命令如下:


命令1:cd /d D:\linux-share-dir\OpenCV_TrainingData
命令2:C:\OpenCV_3.4.7\opencv-vc-3.4.7\build\x64\vc15\bin\opencv_createsamples.exe -vec pos.vec -info pos.txt -num 54 -w 40 -h 40
复制代码


参数介绍:


opencv_createsamples.exe: 生成样本描述文件的可执行程序(opencv 自带),前面是我电脑上的路径。


-vec pos.vec 指定生成的 vec 文件


-info pos.txt 指定源样本的描述文件


-num 54 指定标定目标样本总数量,就是样本描述文件里所有第 2 列的数字之和。


-w 40 指定样本缩放后的宽,如果之前图片不是 40,那么这里就会缩放成 40,有了这个参数就可以省去之前的图片处理过程。


-h 40 指定样本缩放后的高,如果之前图片不是 40,那么这里就会缩放成 40,有了这个参数就可以省去之前的图片处理过程。


我电脑上 OpenCV 的安装路径:



生成结果如下:


执行成功之后在当前目录下生成 pos.vec 文件。


说明: 负样本不需要生成 vec 文件。

3.5 开始训练样本

命令1:cd /d D:\linux-share-dir\OpenCV_TrainingData命令2:C:\OpenCV_3.4.7\opencv-vc-3.4.7\build\x64\vc15\bin\opencv_traincascade.exe -data XML -vec pos.vec -bg neg.txt -numPos 50 -numNeg 133 -numStages 20 -w 40 -h 40 -mode ALL
复制代码


参数介绍:


-data 指定输出目录,训练生成的 xml 文件就放在这个目录下


-vec 指定正样本生成的 vec 文件


-bg 指定负样本数据文件,即前面生成的 neg.txt 文件


-numPos 指定正样本数目,这个数值一定要比准备正样本时的数目少,不然会报 can not get new positive sample。


参考理由:minHitRate:影响每个强分类器阈值,当设置为 0.95 时如果正训练样本个数为 10000 个,那么其中的 500 个就很可能背叛别为负样本,第二次选择的时候必须多选择后面的 500 个,按照这种规律我们为后面的每级多增加 numPos*minHitRate 个正样本,根据训练的级数可以得到如下公式


numPos+(numStages-1)numPos(1-minHitRate)《=准备的训练样本


以上式子也只是根据训练级数和准备的正样本总和设置一个参与训练的正样本个数,只能作为估算,小于计算出来的数可能没有问题,但是大于那个数肯定有问题


现在解释下”可能有问题“是如何理解的:因为我们总是默认每次添加固定个数的正训练样本,但是有时候后面的固定个数的正训练样本中也可能存在不满足条件的样本,这些样本跟我们排除的样本类似,所以比如我们打算添加 500 个样本就够了,但是实际需要添加 600 个,这时候就出现问题了。


从上面例子的结果中可以看出,每级我们允许丢掉 12000*0.001 个正样本=12,需要注意的是万一第 11 个或者第 10 个跟第 12 个的阈值是一样的,那么我们之丢掉了前面的 10 个或者 9 个而已,因此每次增加的个数可能要小于 12 个,大于 12 个的情况就是上面所说的”可能有问题“。


-numStages 指定训练级数


-numNeg 指定负样本数目


-w 40 -h 40 指定样本图尺寸


-mode 指定 haar 特征的种类,basio 仅仅使用垂直特征,al1 表示使用垂直以及 45 度旋转特征


开始训练:


训练成功之后在 XML 目录下会生成 cascade.xml 文件,这个文件就是最终训练成功的文件,可以替换到到上面代码里测试。



发布于: 刚刚阅读数: 3
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
OpenCV编程:OpenCV3.X训练自己的分类器_7月月更_DS小龙哥_InfoQ写作社区