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

2022-01-17 15:16:25 浏览数 (1)

一、环境介绍

操作系统: 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文件代码:

代码语言:javascript复制
#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  数量:%3n").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文件代码:

代码语言:javascript复制
#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_NAMESPACE
namespace 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文件

代码语言:javascript复制
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}/bin
else: 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命令进入到正样本的目录下。

执行命令如下:

代码语言:javascript复制
命令1-进入到正样本目录下:cd /d D:linux-share-dirOpenCV_TrainingDataPositiveSample

命令2-将目录下所有图片名字和路径输出到pos.txt文件:dir /b/s/p/w *.jpg > pos.txt

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

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

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

修改效果如下:

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

3.3 创建负样本描述文件

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

代码语言:javascript复制
命令1:cd /d D:linux-share-dirOpenCV_TrainingDataNegativeSample

命令2:dir /b/s/p/w *.jpg > neg.txt

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

3.4 生成正样本的.vec文件

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

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

代码语言:javascript复制
命令1:cd /d D:linux-share-dirOpenCV_TrainingData

命令2:C:OpenCV_3.4.7opencv-vc-3.4.7buildx64vc15binopencv_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 开始训练样本

代码语言:javascript复制
命令1:cd /d D:linux-share-dirOpenCV_TrainingData
命令2:C:OpenCV_3.4.7opencv-vc-3.4.7buildx64vc15binopencv_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文件,这个文件就是最终训练成功的文件,可以替换到到上面代码里测试。

0 人点赞