Open CV机器学习机器学习

SVM+HOG 之牛奶盒检测(三、C++实战操作)

2018-08-30  本文已影响791人  SIENTIST

由于运用的是SVM二分类,因此需要准备2批数据,一批正样本数据,一批负样本数据。这样才能让SVM进行学习,知道哪些是目标,哪些不是目标。

OpenCV中的SVM+HOG,检测的尺寸一共有2种,一种是64 * 128,一种是64 * 64。这里我选择64 * 128的尺寸。

我要识别的是蒙牛牛奶盒 图片.png

我相信我绝对是全世界第一个做牛奶盒识别的,哈哈哈哈哈

首先是正样本:各种背景里有一个要识别的物体,如下图所示。
尺寸必须为 64 * 128(也可以为64 * 64),其他尺寸不行


93.jpg

我采集了各种背景下的牛奶盒图片,一共521张,也就是说正样本数量为521


图片.png

接着是采集负样本: 负样本尺寸必须和正样本一样,也是64 * 128。
负样本很好搞,直接去随便拍一些图像中没有牛奶盒的图片,然后在图片上随机裁剪出64 * 128尺寸的图片就行了


图片.png

以下是我编写的在图片上随机裁剪的程序,每张图片都能输出一定数量的负样本

#include <iostream>  
#include <iostream>
#include <fstream>  
#include <stdlib.h> //srand()和rand()函数  
#include <time.h> //time()函数  
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/imgproc/imgproc.hpp>  
#include <opencv2/objdetect/objdetect.hpp>  
#include <opencv2/ml/ml.hpp>  

using namespace std;
using namespace cv;

int imageCount = 0; //裁剪出来的负样本图片个数  

int main()
{
    Mat src;
    string ImgName;
    string readAddress = "D:\\盒装牛奶检测\\负样本\\负样本原始数据\\";
    string saveAddress = "D:\\盒装牛奶检测\\负样本\\裁剪后负样本数据\\";
    string saveName;//裁剪出来的负样本图片文件名  
    ifstream fin(readAddress+"NegativeSample_Txt.txt");//打开原始负样本图片文件列表  

    //一行一行读取文件列表  
    while (getline(fin, ImgName))
    {
        ImgName = readAddress + ImgName;

        src = imread(ImgName);//读取图片  
        int originalWidth = src.cols;
        int originalHeight = src.rows;
        int width = originalWidth / 4;
        int height = originalHeight / 4;
        resize(src, src, Size(width, height)); //将图片尺寸压缩,以获取更多信息

        //图片大小应该能能至少包含一个64*128的窗口  
        if (src.cols >= 64 && src.rows >= 128) //图片尺寸大小满足要求
        {
            srand(time(NULL));//设置随机数种子  

            //从每张图片中随机裁剪200个64*128大小的负样本  
            for (int i = 0; i<200; i++)
            {
                int x = (rand() % (src.cols - 64)); //左上角x坐标   rand()%a 能够得到0到a内的随机数
                int y = (rand() % (src.rows - 128)); //左上角y坐标  
                Mat imgROI = src(Rect(x, y, 64, 128));

                saveName = saveAddress + to_string(++imageCount) + ".jpg";
                imwrite(saveName, imgROI);//保存文件  

                if (imageCount % 10 == 0) //每生成10张图片输出一次数据
                {
                    system("cls");
                    cout << endl <<"            原始图像尺寸: " << originalWidth << " * " << originalHeight << endl;
                    cout << "           resize后图像尺寸: " << width  << " * " << height << endl;
                    cout << endl << "           已裁剪图片数量: " << imageCount << endl;
                }
            }
        }
        
        //break; //--------------
    }

    //system("pause");

    return 0;
}

——————————————————————————————————

在程序中有一段这样的的代码
while (getline(fin, ImgName))
这个意思是逐行读取文件 fin 中的字符,将其赋给变量 ImgName

fin 中每一行字符都是一张图片的文件名,这样便能一张一张得读取图片了。
问题来了,怎么得到一个含有所有图片文件名的txt文件呢?
用Windows中的.bat批处理程序就行了!
怎么搞呢?首先新建一个txt文件,把下面这段程序写进去

dir /b *.jpg>addressTxt.txt

保存,然后把txt文件的后缀改为 .bat 就可以了


图片.png

然后把这个.bat文件放到你存着图片的文件夹中,双击打开 .bat 文件,然后就能得到一个写着该文件夹中所有文件名称的txt文件了。


图片.png
有正样本和负样本,就能开始训练了。我的正样本数量是518,负样本数量是113000。
训练之后会得到一个XML文件,这个文件中保存着可用于检测的SVM参数,下次要检测的时候,只用读取这个XML文件就行了,不需要重新训练。

训练完毕,在测试集图片中进行检测,会发现有的牛奶盒没有检测到,还有的没有牛奶盒的地方却检测到了。
这时候就要把那些本没有牛奶盒却检测到了的图片截取下来,这些称为“难样本”。
还有那些有牛奶盒却没有检测到的也截取下来,为AugPosImg图片。
这2波图片加入到数据集中,进行第二次训练,就能显著提升准确率。
——————————————————————————————
OpenCV3.1中使用 svm = SVM::load<SVM>("svm_image.xml");来读取XML文件,
而OpenCV3.2中使用svm->load<SVM>("SVM_HOG.xml");来读取XML文件,这是一个不大不小的坑。
——————————————————————————————
detectMultiScale()函数详解:
detectMultiScale是多尺度检测的意思,因为一副图片里待检测目标有大有小,但检测的滑动窗口是固定的大小,这个怎么办?就只能对图像进行缩放来检测,也就是要把图像缩小或放大到不同的尺寸来进行检测。
函数原型如下:

多尺度.png
1、img : 输入的图像。图像可以是彩色也可以是灰度的。

2、foundLocations : 存取检测到的目标位置,为矩阵向量vector

3、hitThreshold (可选) : opencv documents的解释是特征到SVM超平面的距离的阈值,所以说这个参数可能是控制HOG特征与SVM最优超平面间的最大距离,当距离小于阈值时则判定为目标。

4.winStride(可选) : HoG检测窗口移动时的步长(水平及竖直)。
winStride和scale都是比较重要的参数,需要合理的设置。一个合适参数能够大大提升检测精确度,同时也不会使检测时间太长。

5.padding(可选) : 在原图外围添加像素。我自己的经验是padding设为0能很大提高检测速度
常见的pad size 有(8, 8), (16, 16), (24, 24), (32, 32).

6.scale(可选),缩放的尺度。scale参数可以具体控制金字塔的层数,参数越小,层数越多,检测时间也长。通常scale在1.01-1.5这个区间。


scale.png

7.finalThreshold(可选): 这个参数不太清楚,据说是为了优化最后的bounding box。我设为默认

8.useMeanShiftGrouping(可选) :bool 类型,决定是否应用meanshift 来消除重叠。default为false,通常也设为false,另行应用non-maxima supperssion效果更好。

——————————————————————————————

SVM中有一些参数需要调整,比如惩罚参数C,网上没查到一劳永逸的调参数方法,大部分人都是用的交叉验证,我一般用的默认参数,不过OpenCV中提供了SVM的自动训练模块:

                      SVM的自动训练模块
Ptr<cv::ml::TrainData>tdata;    //将训练数据和标签整合成tdata
    tdata = TrainData::create(trainingDataMat, cv::ml::SampleTypes::ROW_SAMPLE, labelsMat);
    svm->trainAuto(tdata, 10,
        SVM::getDefaultGrid(SVM::C),
        SVM::getDefaultGrid(SVM::GAMMA),
        SVM::getDefaultGrid(SVM::P),
        SVM::getDefaultGrid(SVM::NU),
        SVM::getDefaultGrid(SVM::COEF),
        SVM::getDefaultGrid(SVM::DEGREE),
        true);

    k_fold: 交叉验证参数。训练集被分成k_fold的自子集,
            其中一个子集是用来测试模型,其他子集则成为训练集,
            所以,SVM算法复杂度是执行k_fold的次数。
    *Grid: (6个)对应的SVM迭代网格参数。
    balanced: 如果是true则这是一个2类分类问题。这将会创建更多的平衡交叉验证子集。

另外OpenCV中还有关于SVM+HOG检测的GPU加速模块,OpenCV官网中有介绍 https://docs.opencv.org/2.4/modules/gpu/doc/object_detection.html

————————————————————————————

这是我训练后的检测结果


2.jpg 3.jpg 4.jpg

可以看到,其中有一些误判,本来没有牛奶盒的地方也框出来了。
我并没有截出难样本进行第二次训练,而且我只要518张正样本,因此有些误判也正常。不过原本我只是做实验的,就没进行第二次训练了。

后来我又用这个做过交通锥桶的识别,可以看出,效果还是很好的,只是运算速度有点慢


交通锥桶识别.jpg

总代码:

#include <iostream>
#include <fstream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/ml/ml.hpp>
#include <stdlib.h>
#include<ctime>  //时间

using namespace std;
using namespace cv;
using namespace cv::ml;

#define PosSamNO 518  //正样本数量  519
#define NegSamNO 113000 // 负样本数量 113400
#define HardExampleNO 0 // 难例数量
#define AugPosSamNO 0 //未检测出的正样本数量

#define TRAIN 0  //若值为1,则开始训练

void train_SVM_HOG();
void SVM_HOG_detect();

int main(){

    if (TRAIN)
        train_SVM_HOG();

    SVM_HOG_detect();

    return 0;
}

void train_SVM_HOG()
{

    //                检测窗口(64,128),       块尺寸(16,16),     块步长(8,8),   cell尺寸(8,8), 直方图bin个数9   
    HOGDescriptor hog(Size(64, 128),        Size(16, 16),       Size(8, 8),     Size(8, 8),         9);
    int DescriptorDim; //HOG描述子的维数,由图片大小、检测窗口大小、块大小、细胞单元中直方图bin个数决定  
    
    Ptr<SVM> svm = SVM::create();
    svm->setType(SVM::C_SVC);
//  svm->setC(0.01); //设置惩罚参数C,默认值为1
    svm->setKernel(SVM::LINEAR); //线性核
    svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 3000, 1e-6)); //3000是迭代次数,1e-6是精确度。
    //setTermCriteria是用来设置算法的终止条件, SVM训练的过程就是一个通过 迭代 方式解决约束条件下的二次优化问题,这里我们指定一个最大迭代次数和容许误

差,以允许算法在适当的条件下停止计算


    string ImgName;//图片的名字
    string PosSampleAdress = "D:\\盒装牛奶检测\\64_128__牛奶图片\\";
    string NegSampleAdress = "D:\\盒装牛奶检测\\负样本\\裁剪后负样本数据\\";
    string HardSampleAdress = "";

    ifstream finPos(PosSampleAdress + "PosSamAddressTxt.txt"); //正样本地址txt文件
    ifstream finNeg(NegSampleAdress + "NegSampleAdressTxt.txt");         //负样本地址txt文件

    if (!finPos){
        cout << "正样本txt文件读取失败" << endl;
        return;
    }
    if (!finNeg){
        cout << "负样本txt文件读取失败" << endl;
        return;
    }

    Mat sampleFeatureMat; // 所有训练样本的特征向量组成的矩阵,行数等于所有样本的个数,列数为HOG描述子维数  
    Mat sampleLabelMat;   // 所有训练样本的类别向量,行数等于所有样本的个数, 列数为1: 1表示有目标,-1表示无目标  

    //---------------------------逐个读取正样本图片,生成HOG描述子-------------
    for (int num = 0; num < PosSamNO && getline(finPos, ImgName); num++) //getline(finPos, ImgName) 从文件finPos中读取图像的名称ImgName
    {
        system("cls");
        cout << endl << "正样本处理: " << ImgName << endl;
        ImgName = PosSampleAdress + ImgName;
        Mat src = imread(ImgName);

        vector<float> descriptors; //浮点型vector(类似数组),用于存放HOG描述子
        hog.compute(src, descriptors, Size(8, 8));//计算HOG描述子,检测窗口移动步长(8,8)

        if (0 == num) //处理第一个样本时初始化特征向量矩阵和类别矩阵,因为只有知道了特征向量的维数才能初始化特征向量矩阵 
        {
            DescriptorDim = descriptors.size(); //HOG描述子的维数   
            //初始化所有训练样本的特征向量组成的矩阵,行数等于所有样本的个数,列数等于HOG描述子维数sampleFeatureMat  
            sampleFeatureMat = Mat::zeros(PosSamNO + AugPosSamNO + NegSamNO + HardExampleNO, DescriptorDim, CV_32FC1); 
            //初始化训练样本的类别向量,行数等于所有样本的个数,列数等于1   
            sampleLabelMat = Mat::zeros(PosSamNO + AugPosSamNO + NegSamNO + HardExampleNO, 1, CV_32SC1);//sampleLabelMat的数据类型必须为有符号

整数型
        }

        
        for (int i = 0; i<DescriptorDim; i++)
            sampleFeatureMat.at<float>(num, i) = descriptors[i];

        sampleLabelMat.at<int>(num, 0) = 1;  //样本标签矩阵 值为1
    }

    if (AugPosSamNO > 0)
    {
        cout << endl << "处理在测试集中未被被检测到的样本: " << endl;
        ifstream finAug("DATA/AugPosImgList.txt");
        if (!finAug){
            cout << "Aug positive txt文件读取失败" << endl;
            return;
        }

        for (int num = 0; num < AugPosSamNO && getline(finAug, ImgName); ++num)
        {
            ImgName = "DATA/INRIAPerson/AugPos/" + ImgName;
            Mat src = imread(ImgName);
            vector<float> descriptors;
            hog.compute(src, descriptors, Size(8, 8));
            for (int i = 0; i < DescriptorDim; ++i)
                sampleFeatureMat.at<float>(num + PosSamNO, i) = descriptors[i];
            sampleLabelMat.at<int>(num + PosSamNO, 0) = 1;
        }
    }

    
    for (int num = 0; num < NegSamNO && getline(finNeg, ImgName); num++)
    {
        system("cls");
        cout << "负样本图片处理: " << ImgName << endl;
        ImgName = NegSampleAdress + ImgName;
        Mat src = imread(ImgName);

        vector<float> descriptors;
        hog.compute(src, descriptors, Size(8, 8));

        for (int i = 0; i<DescriptorDim; i++)
            sampleFeatureMat.at<float>(num + PosSamNO, i) = descriptors[i];

        sampleLabelMat.at<int>(num + PosSamNO + AugPosSamNO, 0) = -1;
    }


    if (HardExampleNO > 0)
    {
        ifstream finHardExample(HardSampleAdress+"HardSampleAdressTxt.txt");
        if (!finHardExample){
            cout << "难样本txt文件读取失败" << endl;
            return;
        }

        for (int num = 0; num < HardExampleNO && getline(finHardExample, ImgName); num++)
        {
            system("cls");
            cout << endl << "处理难样本图片: " << ImgName << endl;
            ImgName = HardSampleAdress + ImgName;
            Mat src = imread(ImgName);

            vector<float> descriptors;
            hog.compute(src, descriptors, Size(8, 8));

            for (int i = 0; i<DescriptorDim; i++)
                sampleFeatureMat.at<float>(num + PosSamNO + NegSamNO, i) = descriptors[i];
            sampleLabelMat.at<int>(num + PosSamNO + AugPosSamNO + NegSamNO, 0) = -1;
        }
    }

    cout << endl << "       开始训练..." << endl;
    svm->train(sampleFeatureMat, ROW_SAMPLE, sampleLabelMat);
//  svm->trainAuto(sampleFeatureMat, ROW_SAMPLE, sampleLabelMat,10);


    svm->save("SVM_HOG.xml");
    cout << "       训练完毕,XML文件已保存" << endl;

}


void SVM_HOG_detect()
{
    
    Ptr<SVM> svm = SVM::load<SVM>("SVM_HOG.xml"); //或者svm = Statmodel::load<SVM>("SVM_HOG.xml"); static function
            //Ptr<SVM> svm = SVM::load(path);
            //  cv::Ptr<cv::ml::SVM> svm_ = cv::ml::SVM::load<SVM>(path);
    // svm->load<SVM>("SVM_HOG.xml"); 这样使用不行

    if (svm->empty()){ //empty()函数 字符串是空的话返回是true
        cout << "读取XML文件失败。" << endl;
        return;
    }
    else{
        cout << "读取XML文件成功。" << endl;
    }

    
    Mat svecsmat = svm->getSupportVectors();//svecsmat元素的数据类型为float

    int svdim = svm->getVarCount();

    int numofsv = svecsmat.rows;

    Mat alphamat = Mat::zeros(numofsv, svdim, CV_32F);//alphamat和svindex必须初始化,否则getDecisionFunction()函数会报错
    Mat svindex = Mat::zeros(1, numofsv, CV_64F);

    Mat Result;
    double rho = svm->getDecisionFunction(0, alphamat, svindex);
    alphamat.convertTo(alphamat, CV_32F);//将alphamat元素的数据类型重新转成CV_32F

    cout << "1" << endl;
    Result = -1 * alphamat * svecsmat;//float
    cout << "2" << endl;

    vector<float> vec;
    for (int i = 0; i < svdim; ++i)
    {
        vec.push_back(Result.at<float>(0, i));
    }
    vec.push_back(rho);
    
    //保存HOG检测的文件
    ofstream fout("HOGDetectorForOpenCV.txt");
    for (int i = 0; i < vec.size(); ++i)
    {
        fout << vec[i] << endl;
    }
    cout << "保存完毕" << endl;

    //----------读取图片进行检测----------------------------
//  HOGDescriptor hog_test;
    HOGDescriptor hog_test(Size(64, 128), Size(16, 16), Size(8, 8), Size(8, 8), 9);
    hog_test.setSVMDetector(vec);

    Mat src = imread("3.jpg",0);
    if (!src.data){
        cout << "测试图片读取失败" << endl;
        return;
    }
    vector<Rect> found, found_filtered;

    int p = 1;
    resize(src, src, Size(src.cols / p, src.rows / p));

    clock_t startTime, finishTime;
    cout << "开始检测" << endl;

    startTime = clock();                                                //1.05
    hog_test.detectMultiScale(src, found, 0, Size(8, 8), Size(32, 32), 1.05, 2);   //多尺度检测
    finishTime = clock();
    cout << "检测所用时间为" <<  (finishTime - startTime)*1.0/CLOCKS_PER_SEC << " 秒 " << endl;

    cout << endl << "矩形框的尺寸为 : " << found.size() << endl;

    //找出所有没有嵌套的矩形,并放入found_filtered中,如果有嵌套的话,则取外面最大的那个矩形放入found_filtered中
    for (int i = 0; i < found.size(); i++)
    {
        Rect r = found[i];
        int j = 0;
        for (; j < found.size(); j++)
        if (j != i && (r & found[j]) == r)
            break;
        if (j == found.size())
            found_filtered.push_back(r);
    }
    cout << endl << "嵌套矩形框合并完毕" << endl;

    //画矩形框,因为hog检测出的矩形框比实际的框要稍微大些,所以这里需要做一些调整
    for (int i = 0; i<found_filtered.size(); i++)
    {
        Rect r = found_filtered[i];

        r.x += cvRound(r.width*0.1); //int cvRound(double value) 对一个double型的数进行四舍五入,并返回一个整型数!
        r.width = cvRound(r.width*0.8);
        r.y += cvRound(r.height*0.07);
        r.height = cvRound(r.height*0.8);

        rectangle(src, r.tl(), r.br(), Scalar(0, 255, 0), 3);
    }

    imwrite("ImgProcessed.jpg", src);
    namedWindow("src", 0);
    imshow("src", src);
    waitKey(0);
}



上一篇下一篇

猜你喜欢

热点阅读