由于運(yùn)用的是SVM二分類召衔,因此需要準(zhǔn)備2批數(shù)據(jù),一批正樣本數(shù)據(jù)祭陷,一批負(fù)樣本數(shù)據(jù)苍凛。這樣才能讓SVM進(jìn)行學(xué)習(xí),知道哪些是目標(biāo)兵志,哪些不是目標(biāo)醇蝴。
OpenCV中的SVM+HOG,檢測(cè)的尺寸一共有2種毒姨,一種是64 * 128哑蔫,一種是64 * 64。這里我選擇64 * 128的尺寸弧呐。
我相信我絕對(duì)是全世界第一個(gè)做牛奶盒識(shí)別的闸迷,哈哈哈哈哈
首先是正樣本:各種背景里有一個(gè)要識(shí)別的物體,如下圖所示俘枫。
尺寸必須為 64 * 128(也可以為64 * 64)腥沽,其他尺寸不行
我采集了各種背景下的牛奶盒圖片,一共521張鸠蚪,也就是說正樣本數(shù)量為521
接著是采集負(fù)樣本: 負(fù)樣本尺寸必須和正樣本一樣今阳,也是64 * 128师溅。
負(fù)樣本很好搞,直接去隨便拍一些圖像中沒有牛奶盒的圖片盾舌,然后在圖片上隨機(jī)裁剪出64 * 128尺寸的圖片就行了
以下是我編寫的在圖片上隨機(jī)裁剪的程序墓臭,每張圖片都能輸出一定數(shù)量的負(fù)樣本
#include <iostream>
#include <iostream>
#include <fstream>
#include <stdlib.h> //srand()和rand()函數(shù)
#include <time.h> //time()函數(shù)
#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; //裁剪出來的負(fù)樣本圖片個(gè)數(shù)
int main()
{
Mat src;
string ImgName;
string readAddress = "D:\\盒裝牛奶檢測(cè)\\負(fù)樣本\\負(fù)樣本原始數(shù)據(jù)\\";
string saveAddress = "D:\\盒裝牛奶檢測(cè)\\負(fù)樣本\\裁剪后負(fù)樣本數(shù)據(jù)\\";
string saveName;//裁剪出來的負(fù)樣本圖片文件名
ifstream fin(readAddress+"NegativeSample_Txt.txt");//打開原始負(fù)樣本圖片文件列表
//一行一行讀取文件列表
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)); //將圖片尺寸壓縮,以獲取更多信息
//圖片大小應(yīng)該能能至少包含一個(gè)64*128的窗口
if (src.cols >= 64 && src.rows >= 128) //圖片尺寸大小滿足要求
{
srand(time(NULL));//設(shè)置隨機(jī)數(shù)種子
//從每張圖片中隨機(jī)裁剪200個(gè)64*128大小的負(fù)樣本
for (int i = 0; i<200; i++)
{
int x = (rand() % (src.cols - 64)); //左上角x坐標(biāo) rand()%a 能夠得到0到a內(nèi)的隨機(jī)數(shù)
int y = (rand() % (src.rows - 128)); //左上角y坐標(biāo)
Mat imgROI = src(Rect(x, y, 64, 128));
saveName = saveAddress + to_string(++imageCount) + ".jpg";
imwrite(saveName, imgROI);//保存文件
if (imageCount % 10 == 0) //每生成10張圖片輸出一次數(shù)據(jù)
{
system("cls");
cout << endl <<" 原始圖像尺寸: " << originalWidth << " * " << originalHeight << endl;
cout << " resize后圖像尺寸: " << width << " * " << height << endl;
cout << endl << " 已裁剪圖片數(shù)量: " << imageCount << endl;
}
}
}
//break; //--------------
}
//system("pause");
return 0;
}
——————————————————————————————————
在程序中有一段這樣的的代碼
while (getline(fin, ImgName))
這個(gè)意思是逐行讀取文件 fin 中的字符妖谴,將其賦給變量 ImgName
fin 中每一行字符都是一張圖片的文件名窿锉,這樣便能一張一張得讀取圖片了。
問題來了膝舅,怎么得到一個(gè)含有所有圖片文件名的txt文件呢嗡载?
用Windows中的.bat批處理程序就行了!
怎么搞呢仍稀?首先新建一個(gè)txt文件洼滚,把下面這段程序?qū)戇M(jìn)去
dir /b *.jpg>addressTxt.txt
保存,然后把txt文件的后綴改為 .bat 就可以了
然后把這個(gè).bat文件放到你存著圖片的文件夾中技潘,雙擊打開 .bat 文件遥巴,然后就能得到一個(gè)寫著該文件夾中所有文件名稱的txt文件了。
有正樣本和負(fù)樣本享幽,就能開始訓(xùn)練了。我的正樣本數(shù)量是518琉闪,負(fù)樣本數(shù)量是113000。
訓(xùn)練之后會(huì)得到一個(gè)XML文件砸彬,這個(gè)文件中保存著可用于檢測(cè)的SVM參數(shù)颠毙,下次要檢測(cè)的時(shí)候,只用讀取這個(gè)XML文件就行了砂碉,不需要重新訓(xùn)練蛀蜜。
訓(xùn)練完畢,在測(cè)試集圖片中進(jìn)行檢測(cè)增蹭,會(huì)發(fā)現(xiàn)有的牛奶盒沒有檢測(cè)到滴某,還有的沒有牛奶盒的地方卻檢測(cè)到了。
這時(shí)候就要把那些本沒有牛奶盒卻檢測(cè)到了的圖片截取下來滋迈,這些稱為“難樣本”霎奢。
還有那些有牛奶盒卻沒有檢測(cè)到的也截取下來,為AugPosImg圖片饼灿。
這2波圖片加入到數(shù)據(jù)集中幕侠,進(jìn)行第二次訓(xùn)練,就能顯著提升準(zhǔn)確率碍彭。
注意晤硕!
注意悼潭!
注意!
——————————————————————————————
OpenCV3.1中使用 svm = SVM::load<SVM>("svm_image.xml");
來讀取XML文件舞箍,
而OpenCV3.2中使用svm->load<SVM>("SVM_HOG.xml");
來讀取XML文件舰褪,這是一個(gè)不大不小的坑。
——————————————————————————————
注意疏橄!
注意占拍!
注意!
detectMultiScale()函數(shù)詳解:
detectMultiScale是多尺度檢測(cè)的意思软族,因?yàn)橐桓眻D片里待檢測(cè)目標(biāo)有大有小刷喜,但檢測(cè)的滑動(dòng)窗口是固定的大小,這個(gè)怎么辦立砸?就只能對(duì)圖像進(jìn)行縮放來檢測(cè)掖疮,也就是要把圖像縮小或放大到不同的尺寸來進(jìn)行檢測(cè)。
函數(shù)原型如下:
1颗祝、img : 輸入的圖像浊闪。圖像可以是彩色也可以是灰度的。
2螺戳、foundLocations : 存取檢測(cè)到的目標(biāo)位置搁宾,為矩陣向量vector
3、hitThreshold (可選) : opencv documents的解釋是特征到SVM超平面的距離的閾值倔幼,所以說這個(gè)參數(shù)可能是控制HOG特征與SVM最優(yōu)超平面間的最大距離盖腿,當(dāng)距離小于閾值時(shí)則判定為目標(biāo)。
4.winStride(可選) : HoG檢測(cè)窗口移動(dòng)時(shí)的步長(水平及豎直)损同。
winStride和scale都是比較重要的參數(shù)翩腐,需要合理的設(shè)置。一個(gè)合適參數(shù)能夠大大提升檢測(cè)精確度膏燃,同時(shí)也不會(huì)使檢測(cè)時(shí)間太長茂卦。
5.padding(可選) : 在原圖外圍添加像素。我自己的經(jīng)驗(yàn)是padding設(shè)為0能很大提高檢測(cè)速度
常見的pad size 有(8, 8), (16, 16), (24, 24), (32, 32).
6.scale(可選)组哩,縮放的尺度等龙。scale參數(shù)可以具體控制金字塔的層數(shù),參數(shù)越小伶贰,層數(shù)越多蛛砰,檢測(cè)時(shí)間也長。通常scale在1.01-1.5這個(gè)區(qū)間黍衙。
7.finalThreshold(可選): 這個(gè)參數(shù)不太清楚暴备,據(jù)說是為了優(yōu)化最后的bounding box。我設(shè)為默認(rèn)
8.useMeanShiftGrouping(可選) :bool 類型们豌,決定是否應(yīng)用meanshift 來消除重疊涯捻。default為false浅妆,通常也設(shè)為false,另行應(yīng)用non-maxima supperssion效果更好障癌。
——————————————————————————————
SVM中有一些參數(shù)需要調(diào)整凌外,比如懲罰參數(shù)C,網(wǎng)上沒查到一勞永逸的調(diào)參數(shù)方法涛浙,大部分人都是用的交叉驗(yàn)證康辑,我一般用的默認(rèn)參數(shù),不過OpenCV中提供了SVM的自動(dòng)訓(xùn)練模塊:
SVM的自動(dòng)訓(xùn)練模塊
Ptr<cv::ml::TrainData>tdata; //將訓(xùn)練數(shù)據(jù)和標(biāo)簽整合成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: 交叉驗(yàn)證參數(shù)轿亮。訓(xùn)練集被分成k_fold的自子集疮薇,
其中一個(gè)子集是用來測(cè)試模型,其他子集則成為訓(xùn)練集我注,
所以按咒,SVM算法復(fù)雜度是執(zhí)行k_fold的次數(shù)。
*Grid: (6個(gè))對(duì)應(yīng)的SVM迭代網(wǎng)格參數(shù)但骨。
balanced: 如果是true則這是一個(gè)2類分類問題励七。這將會(huì)創(chuàng)建更多的平衡交叉驗(yàn)證子集。
另外OpenCV中還有關(guān)于SVM+HOG檢測(cè)的GPU加速模塊奔缠,OpenCV官網(wǎng)中有介紹 https://docs.opencv.org/2.4/modules/gpu/doc/object_detection.html
————————————————————————————
這是我訓(xùn)練后的檢測(cè)結(jié)果
可以看到掠抬,其中有一些誤判,本來沒有牛奶盒的地方也框出來了校哎。
我并沒有截出難樣本進(jìn)行第二次訓(xùn)練两波,而且我只要518張正樣本,因此有些誤判也正常闷哆。不過原本我只是做實(shí)驗(yàn)的雨女,就沒進(jìn)行第二次訓(xùn)練了。
后來我又用這個(gè)做過交通錐桶的識(shí)別阳准,可以看出,效果還是很好的馏臭,只是運(yùn)算速度有點(diǎn)慢
總代碼:
#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> //時(shí)間
using namespace std;
using namespace cv;
using namespace cv::ml;
#define PosSamNO 518 //正樣本數(shù)量 519
#define NegSamNO 113000 // 負(fù)樣本數(shù)量 113400
#define HardExampleNO 0 // 難例數(shù)量
#define AugPosSamNO 0 //未檢測(cè)出的正樣本數(shù)量
#define TRAIN 0 //若值為1野蝇,則開始訓(xùn)練
void train_SVM_HOG();
void SVM_HOG_detect();
int main(){
if (TRAIN)
train_SVM_HOG();
SVM_HOG_detect();
return 0;
}
void train_SVM_HOG()
{
// 檢測(cè)窗口(64,128), 塊尺寸(16,16), 塊步長(8,8), cell尺寸(8,8), 直方圖bin個(gè)數(shù)9
HOGDescriptor hog(Size(64, 128), Size(16, 16), Size(8, 8), Size(8, 8), 9);
int DescriptorDim; //HOG描述子的維數(shù),由圖片大小括儒、檢測(cè)窗口大小绕沈、塊大小、細(xì)胞單元中直方圖bin個(gè)數(shù)決定
Ptr<SVM> svm = SVM::create();
svm->setType(SVM::C_SVC);
// svm->setC(0.01); //設(shè)置懲罰參數(shù)C帮寻,默認(rèn)值為1
svm->setKernel(SVM::LINEAR); //線性核
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 3000, 1e-6)); //3000是迭代次數(shù)乍狐,1e-6是精確度。
//setTermCriteria是用來設(shè)置算法的終止條件固逗, SVM訓(xùn)練的過程就是一個(gè)通過 迭代 方式解決約束條件下的二次優(yōu)化問題浅蚪,這里我們指定一個(gè)最大迭代次數(shù)和容許誤
差藕帜,以允許算法在適當(dāng)?shù)臈l件下停止計(jì)算
string ImgName;//圖片的名字
string PosSampleAdress = "D:\\盒裝牛奶檢測(cè)\\64_128__牛奶圖片\\";
string NegSampleAdress = "D:\\盒裝牛奶檢測(cè)\\負(fù)樣本\\裁剪后負(fù)樣本數(shù)據(jù)\\";
string HardSampleAdress = "";
ifstream finPos(PosSampleAdress + "PosSamAddressTxt.txt"); //正樣本地址txt文件
ifstream finNeg(NegSampleAdress + "NegSampleAdressTxt.txt"); //負(fù)樣本地址txt文件
if (!finPos){
cout << "正樣本txt文件讀取失敗" << endl;
return;
}
if (!finNeg){
cout << "負(fù)樣本txt文件讀取失敗" << endl;
return;
}
Mat sampleFeatureMat; // 所有訓(xùn)練樣本的特征向量組成的矩陣,行數(shù)等于所有樣本的個(gè)數(shù)惜傲,列數(shù)為HOG描述子維數(shù)
Mat sampleLabelMat; // 所有訓(xùn)練樣本的類別向量洽故,行數(shù)等于所有樣本的個(gè)數(shù), 列數(shù)為1: 1表示有目標(biāo)盗誊,-1表示無目標(biāo)
//---------------------------逐個(gè)讀取正樣本圖片时甚,生成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; //浮點(diǎn)型vector(類似數(shù)組),用于存放HOG描述子
hog.compute(src, descriptors, Size(8, 8));//計(jì)算HOG描述子哈踱,檢測(cè)窗口移動(dòng)步長(8,8)
if (0 == num) //處理第一個(gè)樣本時(shí)初始化特征向量矩陣和類別矩陣荒适,因?yàn)橹挥兄懒颂卣飨蛄康木S數(shù)才能初始化特征向量矩陣
{
DescriptorDim = descriptors.size(); //HOG描述子的維數(shù)
//初始化所有訓(xùn)練樣本的特征向量組成的矩陣,行數(shù)等于所有樣本的個(gè)數(shù)开镣,列數(shù)等于HOG描述子維數(shù)sampleFeatureMat
sampleFeatureMat = Mat::zeros(PosSamNO + AugPosSamNO + NegSamNO + HardExampleNO, DescriptorDim, CV_32FC1);
//初始化訓(xùn)練樣本的類別向量刀诬,行數(shù)等于所有樣本的個(gè)數(shù),列數(shù)等于1
sampleLabelMat = Mat::zeros(PosSamNO + AugPosSamNO + NegSamNO + HardExampleNO, 1, CV_32SC1);//sampleLabelMat的數(shù)據(jù)類型必須為有符號(hào)
整數(shù)型
}
for (int i = 0; i<DescriptorDim; i++)
sampleFeatureMat.at<float>(num, i) = descriptors[i];
sampleLabelMat.at<int>(num, 0) = 1; //樣本標(biāo)簽矩陣 值為1
}
if (AugPosSamNO > 0)
{
cout << endl << "處理在測(cè)試集中未被被檢測(cè)到的樣本: " << 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 << "負(fù)樣本圖片處理: " << 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 << " 開始訓(xùn)練..." << endl;
svm->train(sampleFeatureMat, ROW_SAMPLE, sampleLabelMat);
// svm->trainAuto(sampleFeatureMat, ROW_SAMPLE, sampleLabelMat,10);
svm->save("SVM_HOG.xml");
cout << " 訓(xùn)練完畢哑子,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()函數(shù) 字符串是空的話返回是true
cout << "讀取XML文件失敗舅列。" << endl;
return;
}
else{
cout << "讀取XML文件成功。" << endl;
}
Mat svecsmat = svm->getSupportVectors();//svecsmat元素的數(shù)據(jù)類型為float
int svdim = svm->getVarCount();
int numofsv = svecsmat.rows;
Mat alphamat = Mat::zeros(numofsv, svdim, CV_32F);//alphamat和svindex必須初始化卧蜓,否則getDecisionFunction()函數(shù)會(huì)報(bào)錯(cuò)
Mat svindex = Mat::zeros(1, numofsv, CV_64F);
Mat Result;
double rho = svm->getDecisionFunction(0, alphamat, svindex);
alphamat.convertTo(alphamat, CV_32F);//將alphamat元素的數(shù)據(jù)類型重新轉(zhuǎn)成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檢測(cè)的文件
ofstream fout("HOGDetectorForOpenCV.txt");
for (int i = 0; i < vec.size(); ++i)
{
fout << vec[i] << endl;
}
cout << "保存完畢" << endl;
//----------讀取圖片進(jìn)行檢測(cè)----------------------------
// 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 << "測(cè)試圖片讀取失敗" << 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 << "開始檢測(cè)" << endl;
startTime = clock(); //1.05
hog_test.detectMultiScale(src, found, 0, Size(8, 8), Size(32, 32), 1.05, 2); //多尺度檢測(cè)
finishTime = clock();
cout << "檢測(cè)所用時(shí)間為" << (finishTime - startTime)*1.0/CLOCKS_PER_SEC << " 秒 " << endl;
cout << endl << "矩形框的尺寸為 : " << found.size() << endl;
//找出所有沒有嵌套的矩形,并放入found_filtered中,如果有嵌套的話,則取外面最大的那個(gè)矩形放入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;
//畫矩形框帐要,因?yàn)閔og檢測(cè)出的矩形框比實(shí)際的框要稍微大些,所以這里需要做一些調(diào)整
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) 對(duì)一個(gè)double型的數(shù)進(jìn)行四舍五入,并返回一個(gè)整型數(shù)弥奸!
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);
}