double
_L2HysThreshold=0.2,
bool
_histogramNormType=L2Hys,
大家好,由于我的毕业设计是“基于图像的手势识别”,因而对 HOG 算法和 SVM
算法有一定的研究,下面将我的学习心得和论文算法部分和大家分享。计算机视
觉是很有发展潜力的,希望大家共同分享,共同进步。
首先关于 HOG 算法:
#include "_cvaux.h"
/********************************************************************
*********************
struct CV_EXPORTS HOGDescriptor
{
public:
enum { L2Hys=0 };
HOGDescriptor() : winSize(64,128), blockSize(16,16), blockStride(8,8),
cellSize(8,8), nbins(9), derivAperture(1), winSigma(-1),
histogramNormType(L2Hys), L2HysThreshold(0.2), gammaCorrection(true)
{}
HOGDescriptor(Size _winSize, Size _blockSize, Size _blockStride,
Size _cellSize, int _nbins, int _derivAperture=1, double _winSigma=-1,
int
_gammaCorrection=false)
:
winSize(_winSize),
cellSize(_cellSize),
nbins(_nbins), derivAperture(_derivAperture), winSigma(_winSigma),
histogramNormType(_histogramNormType), L2HysThreshold(_L2HysThreshold),
gammaCorrection(_gammaCorrection)
{}
HOGDescriptor(const String& filename)
{
load(filename);
}
virtual ~HOGDescriptor() {}
size_t getDescriptorSize() const;
bool checkDetectorSize() const;
double getWinSigma() const;
virtual void setSVMDetector(const vector& _svmdetector);
virtual bool load(const String& filename, const String& objname=String());
virtual void save(const String& filename, const String& objname=String()) const;
blockSize(_blockSize),
blockStride(_blockStride),
virtual void compute(const Mat& img,
vector
& descriptors,
Size winStride=Size(), Size padding=Size(),
const vector& locations=vector()) const;
virtual void detect(const Mat& img, vector& foundLocations,
double hitThreshold=0, Size winStride=Size(),
Size padding=Size(),
const vector& searchLocations=vector()) const;
virtual void detectMultiScale(const Mat& img, vector& foundLocations,
double hitThreshold=0, Size winStride=Size(),
Size padding=Size(), double scale=1.05,
int groupThreshold=2) const;
//Mat& angleOfs,与后文 Mat& qangle 不一致,怀疑是笔误,由于 qangle 与 angleOfs
有不同含义,尽量改过来
virtual void computeGradient(const Mat& img, Mat& grad, Mat& angleOfs,
Size paddingTL=Size(), Size paddingBR=Size()) const;
static vector getDefaultPeopleDetector();
Size winSize;//窗口大小
Size blockSize;//Block 大小
Size blockStride;//block 每次移动宽度包括水平和垂直两个方向
Size cellSize;//Cell 单元大小
int nbins;//直方图 bin 数目
int derivAperture;//不知道什么用
double winSigma;//高斯函数的方差
int histogramNormType;//直方图归一化类型,具体见论文
double L2HysThreshold;//L2Hys 化中限制最大值为 0.2
bool gammaCorrection;//是否 Gamma 校正
vector svmDetector;//检测算子
};
*********************************************************************
*************/
namespace cv
{
size_t HOGDescriptor::getDescriptorSize() const
{
//检测数据的合理性
CV_Assert(blockSize.width % cellSize.width == 0 &&
blockSize.height % cellSize.height == 0);
CV_Assert((winSize.width - blockSize.width) % blockStride.width == 0 &&
(winSize.height - blockSize.height) % blockStride.height == 0 );
//Descriptor 的大小
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*(16/8)*(16/8)*((64-16)/8+1)*((128-16)/8+1)=9*2*2*7*15=3780,实际上的检测
算子为 3781,多的 1 表示偏置
}
double HOGDescriptor::getWinSigma() const
{
//winSigma 默认为-1,然而有下式知,实际上为 4;否则自己选择参数
return winSigma >= 0 ? winSigma : (blockSize.width + blockSize.height)/8.;
}
bool HOGDescriptor::checkDetectorSize() const
{
//size_t:unsigned int
size_t detectorSize = svmDetector.size(), descriptorSize = getDescriptorSize();
//三种情况任意一种为 true 则表达式为 true,实际上是最后一种
return detectorSize == 0 ||
detectorSize == descriptorSize ||
detectorSize == descriptorSize + 1;
}
void HOGDescriptor::setSVMDetector(const vector& _svmDetector)
{
svmDetector = _svmDetector;
CV_Assert( checkDetectorSize() );
}
bool HOGDescriptor::load(const String& filename, const String& objname)
{
//XML/YML 文件存储
FileStorage fs(filename, FileStorage::READ);
//objname 为空,!1=0,选择 fs.getFirstTopLevelNode();否则为 fs[objname]
//注意到 FileStorage 中[]重载了:FileNode operator[](const string& nodename)
(returns the top-level node by name )
FileNode obj = !objname.empty() ? fs[objname] : fs.getFirstTopLevelNode();
if( !obj.isMap() )
return false;
FileNodeIterator it = obj["winSize"].begin();
it >> winSize.width >> winSize.height;
it = obj["blockSize"].begin();
it >> blockSize.width >> blockSize.height;
it = obj["blockStride"].begin();
it >> blockStride.width >> blockStride.height;
it = obj["cellSize"].begin();
it >> cellSize.width >> cellSize.height;
obj["nbins"] >> nbins;
obj["derivAperture"] >> derivAperture;
obj["winSigma"] >> winSigma;
obj["histogramNormType"] >> histogramNormType;
obj["L2HysThreshold"] >> L2HysThreshold;
obj["gammaCorrection"] >> gammaCorrection;
FileNode vecNode = obj["SVMDetector"];
if( vecNode.isSeq() )
{
vecNode >> svmDetector;
CV_Assert(checkDetectorSize());
}
return true;
}
void HOGDescriptor::save(const String& filename, const String& objName) const
{
FileStorage fs(filename, FileStorage::WRITE);
//空的对象名则取默认名,输出有一定格式,对象名后紧接{
fs << (!objName.empty() ? objName : FileStorage::getDefaultObjectName(filename))
<< "{";
//之后依次为:
fs << "winSize" << winSize
<< "blockSize" << blockSize
<< "blockStride" << blockStride
<< "cellSize" << cellSize
<< "nbins" << nbins
<< "derivAperture" << derivAperture
<< "winSigma" << getWinSigma()
<< "histogramNormType" << histogramNormType
<< "L2HysThreshold" << L2HysThreshold
<< "gammaCorrection" << gammaCorrection;
if( !svmDetector.empty() )
fs << "SVMDetector" << "[:" << svmDetector << "]";
//注意还要输出"}"
fs << "}";
}
//img:原始图像
//grad:记录每个像素所属 bin 对应的权重的矩阵,为幅值乘以权值
//这个权值是关键,也很复杂:包括高斯权重,三次插值的权重,在本函数中先
值考虑幅值和相邻 bin 间的插值权重
//qangle:记录每个像素角度所属的 bin 序号的矩阵,均为 2 通道,为了线性插值
//paddingTL:Top 和 Left 扩充像素数
//paddingBR:类似同上
//功能:计算 img 经扩张后的图像中每个像素的梯度和角度
void HOGDescriptor::computeGradient(const Mat& img, Mat& grad, Mat& qangle,
Size paddingTL, Size paddingBR) const
{
//先判断是否为单通道的灰度或者 3 通道的图像
CV_Assert( img.type() == CV_8U || img.type() == CV_8UC3 );
//计算 gradient 的图的大小,由 64*128==》112*160,则会产生 5*7=35 个窗口
(windowstride:8)
//每个窗口 105 个 block,105*36=3780 维特征向量
//paddingTL.width=16,paddingTL.height=24
Size gradsize(img.cols + paddingTL.width + paddingBR.width,
img.rows + paddingTL.height + paddingBR.height);
//注意 grad 和 qangle 是 2 通道的矩阵,为 3D-trilinear 插值中的 orientation 维度,
另两维为坐标 x 与 y
grad.create(gradsize, CV_32FC2); //
qangle.create(gradsize, CV_8UC2); // [0..nbins-1] - quantized gradient orientation
//wholeSize 为 parent matrix 大小,不是扩展后 gradsize 的大小
//roiofs 即为 img 在 parent matrix 中的偏置
//对于正样本 img=parent matrix;但对于负样本 img 是从 parent img 中抽取的 10
个随机位置
//至于 OpenCv 具体是怎么操作,使得 img 和 parent img 相联系,不是很了解
//wholeSize 与 roiofs 仅在 padding 时有用,可以不管,就认为传入的 img==parent
img,是否是从 parent img 中取出无所谓
Size wholeSize;
Point roiofs;
img.locateROI(wholeSize, roiofs);
int i, x, y;
int cn = img.channels();
//产生 1 行 256 列的向量,lut 为列向量头地址
Mat_ _lut(1, 256);
const float* lut = &_lut(0,0);
//gamma 校正,作者的编程思路很有意思
//初看不知道这怎么会与图像的 gamma 校正有关系,压根 img 都没出现,看到
后面大家会豁然开朗的
if( gammaCorrection )
for( i = 0; i < 256; i++ )
_lut(0,i) = std::sqrt((float)i);
else
for( i = 0; i < 256; i++ )
_lut(0,i) = (float)i;
//开辟空间存 xmap 和 ymap,其中各占 gradsize.width+2 和 gradsize.height+2 空间
//+2 是为了计算 dx,dy 时用[-1,0,1]算子,即使在扩充图像中,其边缘计算梯度时还
是要再额外加一个像素的
//作者很喜欢直接用内存地址及之间的关系,初看是有点头大的
//另外再说说 xmap 与 ymap 的作用:其引入是因为 img 图像需要扩充到 gradsize
大小
//如果我们计算 img 中位于(-5,-6)像素时,需要将基于 img 的(-5,-6)坐标,映
射为基于 grad 和 qangle 的坐标(xmap,ymap)
AutoBuffer mapbuf(gradsize.width + gradsize.height + 4);
int* xmap = (int*)mapbuf + 1;
int* ymap = xmap + gradsize.width + 2;
// BORDER_REFLECT_101:(左插值)gfedcb|abcdefgh(原始像素)|gfedcba(右插值),一
种插值模式 const int borderType = (int)BORDER_REFLECT_101;
//borderInterpolate 函数完成两项操作,一是利用插值扩充 img,二是返回
x-paddingTL.width+roiofs.x 映射后的坐标 xmap
//例如,ximg=x(取 0)-paddingTL.width(取 24)+roiofs.x(取 0)=-24 ==>xmap[0]=0
// 即 img 中 x=-24, 映 射 到 grad 中 xmap=0, 并 且 存 在 xmap[0] 中 , 至 于
borderInterpolate 的具体操作可以不必细究
for( x = -1; x < gradsize.width + 1; x++ )
xmap[x] = borderInterpolate(x - paddingTL.width + roiofs.x,
wholeSize.width, borderType);
for( y = -1; y < gradsize.height + 1; y++ )
ymap[y] = borderInterpolate(y - paddingTL.height + roiofs.y,
wholeSize.height, borderType);
// x- & y- derivatives for the whole row
// 由于后面的循环是以行为单位,每次循环内存重复使用,所以只要记录一行
的信息而不是整个矩阵
int width = gradsize.width;
AutoBuffer _dbuf(width*4);
float* dbuf = _dbuf;
//注意到内存的连续性方便之后的编程
Mat Dx(1, width, CV_32F, dbuf);
Mat Dy(1, width, CV_32F, dbuf + width);
Mat Mag(1, width, CV_32F, dbuf + width*2);
Mat Angle(1, width, CV_32F, dbuf + width*3);
int _nbins = nbins;
float angleScale = (float)(_nbins/CV_PI);//9/pi
for( y = 0; y < gradsize.height; y++ )
{
//指向每行的第一个元素,img.data 为矩阵的第一个元素地址
const uchar* imgPtr = img.data + img.step*ymap[y];
const uchar* prevPtr = img.data + img.step*ymap[y-1];
const uchar* nextPtr = img.data + img.step*ymap[y+1];
float* gradPtr = (float*)grad.ptr(y);
uchar* qanglePtr = (uchar*)qangle.ptr(y);
//1 通道
if( cn == 1 )
{
for( x = 0; x < width; x++ )
{
int x1 = xmap[x];
//imgPtr 指向 img 第 y 行首元素,imgPtr[x]即表示第(x,y)像素,其亮度值位于 0~255,
对应 lut[0]~lut[255]
//即若像素亮度为 120,则对应 lut[120],若有 gamma 校正,lut[120]=sqrt(120)
//由于补充了虚拟像素,即在 imgPtr[-1]无法表示 gradsize 中-1 位置元素,而需要
有个转换
//imgPtr[-1-paddingTL.width+roiofs.x],即 imgPtr[xmap[-1]],即 gradsize 中-1 位置元
素为 img 中 xmap[-1]位置的元素
dbuf[x] = (float)(lut[imgPtr[xmap[x+1]]] - lut[imgPtr[xmap[x-1]]]);
//由于内存的连续性,隔 width,即存 Dy
dbuf[width + x] = (float)(lut[nextPtr[x1]] - lut[prevPtr[x1]]);
}
}
else
//3 通道,3 通道中取最大值
{
for( x = 0; x < width; x++ )
{
int x1 = xmap[x]*3;
const uchar* p2 = imgPtr + xmap[x+1]*3;
const uchar* p0 = imgPtr + xmap[x-1]*3;
float dx0, dy0, dx, dy, mag0, mag;
dx0 = lut[p2[2]] - lut[p0[2]];
dy0 = lut[nextPtr[x1+2]] - lut[prevPtr[x1+2]];
mag0 = dx0*dx0 + dy0*dy0;
dx = lut[p2[1]] - lut[p0[1]];
dy = lut[nextPtr[x1+1]] - lut[prevPtr[x1+1]];
mag = dx*dx + dy*dy;
if( mag0 < mag )
{
dx0 = dx;
dy0 = dy;
mag0 = mag;
}
dx = lut[p2[0]] - lut[p0[0]];
dy = lut[nextPtr[x1]] - lut[prevPtr[x1]];
mag = dx*dx + dy*dy;
if( mag0 < mag )
{
dx0 = dx;
dy0 = dy;
mag0 = mag;
}
dbuf[x] = dx0;
dbuf[x+width] = dy0;
}
}