由于运用的是SVM二分类,因此需要准备2批数据,一批正样本数据,一批负样本数据。这样才能让SVM进行学习,知道哪些是目标,哪些不是目标。
OpenCV中的SVM+HOG,检测的尺寸一共有2种,一种是64 * 128,一种是64 * 64。这里我选择64 * 128的尺寸。
我相信我绝对是全世界第一个做牛奶盒识别的,哈哈哈哈哈
首先是正样本:各种背景里有一个要识别的物体,如下图所示。
尺寸必须为 64 * 128(也可以为64 * 64),其他尺寸不行
我采集了各种背景下的牛奶盒图片,一共521张,也就是说正样本数量为521
接着是采集负样本: 负样本尺寸必须和正样本一样,也是64 * 128。
负样本很好搞,直接去随便拍一些图像中没有牛奶盒的图片,然后在图片上随机裁剪出64 * 128尺寸的图片就行了
以下是我编写的在图片上随机裁剪的程序,每张图片都能输出一定数量的负样本
#include
#include
#include
#include //srand()和rand()函数
#include //time()函数
#include
#include
#include
#include
#include
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 就可以了
然后把这个.bat文件放到你存着图片的文件夹中,双击打开 .bat 文件,然后就能得到一个写着该文件夹中所有文件名称的txt文件了。
有正样本和负样本,就能开始训练了。我的正样本数量是518,负样本数量是113000。
训练之后会得到一个XML文件,这个文件中保存着可用于检测的SVM参数,下次要检测的时候,只用读取这个XML文件就行了,不需要重新训练。
训练完毕,在测试集图片中进行检测,会发现有的牛奶盒没有检测到,还有的没有牛奶盒的地方却检测到了。
这时候就要把那些本没有牛奶盒却检测到了的图片截取下来,这些称为“难样本”。
还有那些有牛奶盒却没有检测到的也截取下来,为AugPosImg图片。
这2波图片加入到数据集中,进行第二次训练,就能显著提升准确率。
注意!
注意!
注意!
——————————————————————————————
OpenCV3.1中使用 svm = SVM::load
来读取XML文件,
而OpenCV3.2中使用svm->load
来读取XML文件,这是一个不大不小的坑。
——————————————————————————————
注意!
注意!
注意!
detectMultiScale()函数详解:
detectMultiScale是多尺度检测的意思,因为一副图片里待检测目标有大有小,但检测的滑动窗口是固定的大小,这个怎么办?就只能对图像进行缩放来检测,也就是要把图像缩小或放大到不同的尺寸来进行检测。
函数原型如下:
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这个区间。
7.finalThreshold(可选): 这个参数不太清楚,据说是为了优化最后的bounding box。我设为默认
8.useMeanShiftGrouping(可选) :bool 类型,决定是否应用meanshift 来消除重叠。default为false,通常也设为false,另行应用non-maxima supperssion效果更好。
——————————————————————————————
SVM中有一些参数需要调整,比如惩罚参数C,网上没查到一劳永逸的调参数方法,大部分人都是用的交叉验证,我一般用的默认参数,不过OpenCV中提供了SVM的自动训练模块:
SVM的自动训练模块
Ptrtdata; //将训练数据和标签整合成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
————————————————————————————
这是我训练后的检测结果
可以看到,其中有一些误判,本来没有牛奶盒的地方也框出来了。
我并没有截出难样本进行第二次训练,而且我只要518张正样本,因此有些误判也正常。不过原本我只是做实验的,就没进行第二次训练了。
后来我又用这个做过交通锥桶的识别,可以看出,效果还是很好的,只是运算速度有点慢
总代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include //时间
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::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 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(num, i) = descriptors[i];
sampleLabelMat.at(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 descriptors;
hog.compute(src, descriptors, Size(8, 8));
for (int i = 0; i < DescriptorDim; ++i)
sampleFeatureMat.at(num + PosSamNO, i) = descriptors[i];
sampleLabelMat.at(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 descriptors;
hog.compute(src, descriptors, Size(8, 8));
for (int i = 0; i(num + PosSamNO, i) = descriptors[i];
sampleLabelMat.at(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 descriptors;
hog.compute(src, descriptors, Size(8, 8));
for (int i = 0; i(num + PosSamNO + NegSamNO, i) = descriptors[i];
sampleLabelMat.at(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::load("SVM_HOG.xml"); //或者svm = Statmodel::load("SVM_HOG.xml"); static function
//Ptr svm = SVM::load(path);
// cv::Ptr svm_ = cv::ml::SVM::load(path);
// svm->load("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 vec;
for (int i = 0; i < svdim; ++i)
{
vec.push_back(Result.at(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 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