HOG/svm Step by Step

来源:互联网 发布:西游记动画2010知乎 编辑:程序博客网 时间:2024/06/16 12:11

完整代码: 我的github

opencv3.1.0 ubuntu14.04通过编译
在最新版本的opencv3.1.x下代码有少许变化, 变化不大, 代码已更新,ubuntu 16.04 通过编译
需要注意的是 3.1.x 后 加载分类器的xml文件, 直接load(path)就行
比如

cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::load(path);

github上有的改了有的没改
csdn的md编辑器 和 ubuntu的中文输入法有冲突(ibus和sogou都不行), 打字有点费劲,所以部分参数我没有解释太全, 比如hog各个参数之类的, 如果有不明白的地方欢迎留言探讨

1.先让程序跑起来

看了hog源码,看到detectMultiScale函数不是用opencl就是用的cuda,看不懂具体是怎么实现的,所以这一部份源码等以后讲吧,自己实现的肯定要慢一些的.hog那篇论文最后也说了还有很大的提升速度(speed up)空间,所以先不管它,先跑起来再说.
demo程序源码 我的github
任意键显示下一张图片, esc退出程序

2.模拟detectMultiScale检测过程

1)sliding window
滑窗是为了定位对象(object)在哪里.因为一般的图片是不会像我们训练时输入的裁剪好尺寸一致的样本,即便尺寸可能一致,也可能因为尺度(scale)的不同而造成误检.
解决尺度问题的办法一般是对图像进行一系列的缩放,或者分别用各个尺度不同的样本训练.
但是这并不能很好的解决尺度问题, 因为很可能被检测的对象(object)的实际尺度是落在我们设定的某两个尺度的中间.增加尺度的级数(level)计算量又过大.
我读过有论文将目标先分割出来然后直接resize的,只见过一两篇那样的论文,我读过的论文还是有限的, 不太清楚有没有更好的办法,但是大部分论文一般都是用我说的第一种方法,先用弱分类器找出目标然后再识别.

先搞清几个参数winSize(滑窗大小),stepSize(步长,常用4到8pixels)

用c++写出来就是

cv::Size winSize(60,60); //这个自己定int stepSize = 30; //step , stridefor(inty =0; y <= img.rows – winSize.height; y+=stepSize){    for(int x = 0; x <= img.cols - winSize.width; x+=stepSize){        cv::Rect rect(x,y, winSize.width, winSize.height);        cv::rectangle(img, rect, cv::Scalar(255,0,0),1,8,0);        cv::waitKey(100); //不然太快看不到滑窗就结束了    }}

这里写图片描述
(不好意思csdn对图片大小有限制, 图像有点小)

可以看到, 一个固定大小的窗口(下文都是64x128)在图像上滑动,
实际上在每个窗口(window)内也有一个固定大小的block在window内滑动, 同样也有滑动步长, 步长是cell大小的整数倍
在每一个block内也有cell在内部滑动, 对于每个cell内的元素, 统计梯度的方向, 得到9个bin的梯度方向直方图.
hog算法中还有个加权投影的过程, 这里不细说了, 看看论文原文吧

计算滑窗时,有很多的重叠区域,怎样减少重复计算呢

2)multi-scale

关于多尺度,SIFT那篇论文介绍的是创建高斯金字塔,而且提供了一个经实验测得的一个sigma参数
在opencv中buildPyramid()可以创建高斯金字塔.但是每次缩放一半,这个变化的太大,并不太合适

在hog源码中多尺度的检测是怎么做的呢?首先是你自己提供的scale,它先给你化成有效的scale(实际就是圆整),然后
直接resize.按我的印象好像应该blur一下,但它确实没有这么做,印象SIFT进行高斯blur的参数也是经实验测得的,可能这
个参数不太好选择.
所以对于给定scale, 得到的图片大小应该是:

double scale;cv::Size effect_size = cv::Size(cvRound(img.cols/scale) ,cvRound(img.rows/scale) );cv::resize(img,out, effect_size);

注意检测到的位置要对应好在原图像的位置

3.准备工作
下载inriaperson数据http://pascal.inrialpes.fr/data/human/

解压后我们先分析一下这个数据要怎么使用.用INRIA数据前一定要检查列表文件提供的路径的正确性,我已经吃过苦头了

首先是正样本,放在了../train/pos文件夹里,它们都是未裁剪好的样本
裁剪好的样本分别放在了
train_64x128_H96
96X160H96/train/
并统一了尺寸,可以看到这两个文件夹里pos样本是从原本的../train/pos分别以96X160截取的, 而train_64x128_H96中
的图片都是96X160H96/train/的引用

对于负样本,并没有直接提供裁剪好的由你来使用,而是提供了不含行人的hard example(难例,难样本),当从这里检测
到人说明是误报,故可将误报的区域保存为图片作为负样本

这里我想用one-class svm训练,然后从负样本去找会误报的区域,将该误报区域保存为neg,然后利用得到的负样本再与
正样本进行训练.
实际上我在这里用one-class纯属为了熟悉它,看看效果,没什么特别的理由,用one-class得到的模型, 1000多张hardexample图片产生了2260个误报区域,很多区域看起来并不像正样本那样正中央有形状像直立的人的物体,效果不应该算好,到底怎么选择参数来减少误报呢?这个问题等我有能力再回答.

可惜的是对于one-class svm , trainauto不能自己选择最优参数,此时和train的功能相同了

关于one-class svm我没有找到什么资料,公式在libsvm中提供了,具体可以去看Schoelkopfet al. (2001) (人名Scholkopf中第一个o上面有两个点,用oe代替了)

具体公式和各个参数意义应该去看看libsvm与libsvm指定的参考论文, 想深究原理就去看原始的论文,公认的教材,
官方的教程,少看别人的二手货,不说有的博客内容是抄的且不给出处,opencv自带的sample代码改改变量名也就算了,
连变量类型也改(譬如double改成float,这可能没什么,万一有的地方改瞎了怎么办?还不说出处).
如果使用libsvm出问题了可以去参考官网的faq.

svm的各个参数
C—惩罚因子
惩罚因子C与松弛变量http://blog.csdn.net/qll125596718/article/details/6910921

P —ϵ(epsilon)ofa SVM optimization problem. For SVM::EPS_SVR.和EPS_SVR的损失函数有关,本文不涉及,不深究

NU—the parameter ν of nu-SVC, one-class SVM, and nu-SVR (default 0.5).这个ν的倒数加到了松弛变量上,具体位置请看libsvm

classweights —惩罚因子c的权重,optional(可选)Thelarger weight, the larger penalty on misclassification of datafrom the corresponding class. Default value is emptyMat.
TermCriteria — Termination criteria of the iterative SVM training procedure which solves a partial case of constrained quadratic optimization problem. You can specify tolerance and/or the maximum number of iterations (opencv)

至于γ(gamma)是用RBF核函数时用到的,如下,同样可知degree,coef0 .
custom kernel是自定义的kernel
这里写图片描述

到现在,因为我们要先用one-classsvm建模,得到负样本后再用C_SVC,核函数选择最简单的线性核
故需要的参数为c,ν, classweights(这个可以不用),termcriteria

4.trainning data
http://blog.csdn.net/traumland/article/details/51660431

注意前面我已经说了我选择用one-class svm, 没有什么特别理由
仅为看看one-class svm效果

5.为hog制作SVMdetector
如果要用detectMultiScale这个函数,必须先提供训练好的svm数据,opencv内部提供了几个训练好的,可以直接使用,
具体请看opencv的文档

也可以提供上面自己训练的svm. 训练好后, 需要自己去创建SVMdetector
opencv的样例程序有四个是关于hog的setSVMdetector,可以先去看看opencv的sample文件

制作svm_detector, 代码很清楚,不赘述了

void get_svm_detector(const Ptr<SVM>& svm, vector< float >& hog_detector ){    // get the support vectors    Mat sv = svm->getSupportVectors();    const int sv_total = sv.rows;    // get the decision function    Mat alpha, svidx;    double rho = svm->getDecisionFunction(0, alpha, svidx);    CV_Assert( alpha.total() == 1 && svidx.total() == 1 &&sv_total == 1 );    CV_Assert( (alpha.type() == CV_64F && alpha.at<double>(0)== 1.) ||    (alpha.type() == CV_32F && alpha.at<float>(0)== 1.f) );    CV_Assert( sv.type() == CV_32F );    hog_detector.clear();    hog_detector.resize(sv.cols + 1);    memcpy(&hog_detector[0],sv.ptr(),sv.cols*sizeof(hog_detector[0]));    hog_detector[sv.cols] = (float)-rho;}  

6.训练样本
通过one-classsvm建的模型,找到hardexample,将得到的误报存储成负样本图片,
(再次声明, 用one-class没有特别的理由)
然后利用C_SVC再对正负样本进行训练.网上很多都是随机的在负样本中截取某些区域.

one-class产生的负样本,与INRIA提供的正样本数大致处于1:1的关系(正负样本不是一定要某个比例)
利用这些样本,再用C_SVC的trainAuto来自动优化参数,得到的xml文件比one class的要小(想想为什么?).
train了五个多小时,又找到1000多个hard example.

关于尺度,正样本中的人,相对负样本背景来说,很多地方都显得大了些,而后面多尺度检测不能放大待检测的图片(detectMultiScale源码中,输入的scale0小于1时,则按图像原始尺寸检测),
最大尺寸为图像原始尺寸,这样会导致模型训练的不够好.

如果想检测比样本小的目标,我觉得要么自己重写detectMultiScale,
要么再训练其他尺度的样本(要注意特征的维度要一致).

看到网上有说训练出来的xml只有几十k, 我也遇到这种情况了, 我的问题出在特征的维度上面. 特征的维度变成了1

关于特征维度的计算

现在仅支持8x8 大小的cell以及 16x16 的block, 也就是说一个block内有4个cell
源码计算公式很清楚

return (size_t)nbins*        (blockSize.width/cellSize.width)*        (blockSize.height/cellSize.height)*        ((winSize.width -  blockSize.width)/blockStride.width + 1)*        ((winSize.height -blockSize.height)/blockStride.height + 1);

数值代进去, 可以算出来 9*2*2*7*15 = 3780

我程序中的问题出在Mat类的resize函数, 平时用.resize(1)直接就是把 nxm 矩阵 转为 1x(m*n) 矩阵,
但是这里直接用Mat(vector<>)得到的Mat , resize后只保留了第一个元素, 不知道你们的程序是不是这么用的, 是不是和我一样的问题.

训练one-class并保存hard example : 我的github
训练c_svc 并保存hard example : 我的github
7.模型评估

首先是四个概念
true positives (tp), 正
true negatives(tn), 负
false positives (fp), 说明实际应该是负
false negatives (fn) 说明实际应该是正

true/false — 分类器是否判断正确(是否与实际一致)
pos/neg — 对于分类器判断是 正/负
( 这是我的理解)

precision = tp/( tp+ fp) — 判断为正的样本中, 分类器判断正确的比例
recall = tp/(tp + fn) — 在正的样本中, 分类器判断正确的比例
accuracy = (tp + tn)/(tp+ tn+fp+fn) — 所有样本中, 分类器判断正确的比例

参考
https://en.wikipedia.org/wiki/Precision_and_recall

opencv计算error的源码

float StatModel::calcError( const Ptr<TrainData>& data, bool testerr, OutputArray _resp ) const{    Mat samples = data->getSamples();    int layout = data->getLayout();    Mat sidx = testerr ? data->getTestSampleIdx() : data->getTrainSampleIdx();    const int* sidx_ptr = sidx.ptr<int>();    int i, n = (int)sidx.total();    bool isclassifier = isClassifier();    Mat responses = data->getResponses();    if( n == 0 )        n = data->getNSamples();    if( n == 0 )        return -FLT_MAX;    Mat resp;    if( _resp.needed() )        resp.create(n, 1, CV_32F);    double err = 0;    for( i = 0; i < n; i++ )    {        int si = sidx_ptr ? sidx_ptr[i] : i;        Mat sample = layout == ROW_SAMPLE ? samples.row(si) : samples.col(si);        float val = predict(sample);        float val0 = responses.at<float>(si);        if( isclassifier )            err += fabs(val - val0) > FLT_EPSILON;        else            err += (val - val0)*(val - val0);        if( !resp.empty() )            resp.at<float>(i) = val;        /*if( i < 100 )        {            printf("%d. ref %.1f vs pred %.1f\n", i, val0, val);        }*/    }    if( _resp.needed() )        resp.copyTo(_resp);    return (float)(err / n * (isclassifier ? 100 : 1));}

libsvm是计算accuracy

上面是常用的三个评价标准, 我们的工作就是尽可能让precision 和 recall 大

最朴素的方法是, 将训练集与测试集分开, 用训练集训练模型, 用测试集得到precision和recall

还有一种方法是, 我们先对训练样本进行切分, 分成多个小份, 选择其中几个作为训练样本, 剩下的一组作为测试,
得到一组precision和recall
再取另外一组样本作为训练样本(它们可以和之前的训练样本有部分重叠), 剩下的作为测试.
如此反复几次(次数由你来决定), 取precision和recall 的平均值, 作为它们的最终值,
这个叫做cross- validation, cs231n前几集有讲
这个过程opencv中的trainAuto函数可以帮你完成

比如 用我的评估代码 : 我的github
得到
evaluating
(ps : 另外几两个由于负样本的缺失无法计算.
实际上 通过这种multi-scale和滑窗找false positive, 你说总的负样本数, 统计的标准是什么?走过的总步数?尺度总级数? )

可以看到, 测试集达到recall 97% 精度已经很高了
而在MIT 的正样本集上直接测试 recall只有 60%
(感觉两个数据集拍摄场景不大一样. MIT那个整体偏亮一些, 背景单一一些, 另外MIT数据只有正样本)

加入MIT数据作为训练集后(另外C也由原来的0.1 变为0.0065)
这里写图片描述

8.提高识别准确率

要注意到一点, 训练集上的效果一般不会好于实际应用. 在测试集上效果不错, 拿到实际的场景下面就没那么好了, 需要进一步的优化, 这些天我就一直在优化参数, 因为训练好的分类器拿到INRIA Test下面效果很不好, FP少了TN也变少了, 没有gpu, hog的 detectMultiScale 奇慢无比

要想继续提高精度, 一方面增加已标注样本的数量, 找难例, 添加到负样本继续训练, 一方面就是靠参数的调节

Dalal-cvpr05(也就是提出hog的那篇论文) 设置的C = 0.01, 所有结果都是在C=0.01下的

关于参数优化部分我的经验很少, 我用的trainAuto自动去寻优, 不断的从难例找负样本, 在其他数据集上找标注好的样本进行测试, 训练.
自动寻优可能会造成过拟合的情况, 所以不是把工作推给自动调参就好了, 觉得还是要理解参数的意义以及相应变化规律

针对HOG/svm行人识别, 由于选的是线性分类器, 主要是调节C.
C是为了解决线性不可分的情况, 引入了松弛变量(比如噪声, 异常值, 参考<<统计学习方法>> ), 表示对误分样本的重视程度
libsvm
(图片来源 : libsvm)

如上面公式, 一般来说, c越大, 得到的margin(间隔)就越小, 分类越准确, 但是因为数据集很大容易造成线性不可分
当c越小, 忽略某些离散点(outliers, 比如间隔内的点), 间隔就会越大, c太小会造成误分类的情况

另外, 正负样本比例问题, 前面我有提到.关于这部分网上有说有所谓,有说没所谓的. 实际上正负样本比例过大会造成数据偏斜(unbalanced), 肯定是有些关系的, 为了解决这个问题, 有用某个参数来增加或减少某部分样本的权重, 有点像opencv ml模块中的class weights, 但是我不是很确定是不是这个参数(应该是这个, 是个维度与类数(class count)一致的一维向量), 也没有过多研究, 知道有这么个事情就够了.

关于这部分我的知识量还太少就不乱说了. 但是普遍的一个观点就是越多的数据效果越好, 在我增加训练集正负样本时准确率确实提高了, 相对来说参数、特征能改变的就有限, 当然选对特征和参数也很重要

完整代码: 我的github

1 0
原创粉丝点击