OpenCV編程:OpenCV3.X訓練自己的分類器

一、環境介紹

操作系統: 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_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文件

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命令進入到正樣本的目錄下。

執行命令如下:

命令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文件,這個文件就是最終訓練成功的文件,可以替換到到上面代碼裏測試。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章