轉帖來自:http://www.cnblogs.com/tornadomeet/archive/2012/08/15/2640754.html
感謝作者!
一、網上一些參考資料
在博客目標檢測學習_1(用opencv自帶hog實現行人檢測) 中已經使用了opencv自帶的函數detectMultiScale()實現了對行人的檢測,當然了,該算法採用的是hog算法,那麼hog算法是怎樣實現的呢?這一節就來簡單分析一下opencv中自帶 hog源碼。
網上也有不少網友對opencv中的hog源碼進行了分析,很不錯,看了很有收穫。比如:
http://blog.csdn.net/raocong2010/article/details/6239431
該博客對該hog算法中用到的block,cell等概念有一定的圖標解釋;
http://blog.csdn.net/pp5576155/article/details/7029699
該博客是轉載的,裏面有opencv源碼的一些註釋,很有幫助。
http://gz-ricky.blogbus.com/logs/85326280.html
本博客對hog描述算子長度的計算做了一定介紹。
http://hi.baidu.com/susongzhi/item/3a3c758d7ff5cbdc5e0ec172
該博客對hog中快速算法的三線插值將得很詳細。
http://blog.youtueye.com/work/opencv-hog-peopledetector-trainning.html
這篇博客對hog怎樣訓練和檢測做了一定的講解。
二、關於源碼的一些簡單說明
本文不是講解hog理論的,所以需要對hog算法有一定了解,這些可以去參考hog提出者的博士論文,寫得很詳細。
按照正常流程,hog行人檢測分爲訓練過程和檢測過程,訓練過程主要是訓練得到svm的係數。在opencv源碼中直接採用訓練好了的svm係數,所以訓練過程源碼中沒有涉及到多少。
首先還是對hog源碼中一些固定參數來個簡單說明:
檢測窗口大小爲128*64;
Block大小爲16*16;
Cell大小爲8*8;
Block在檢測窗口中上下移動尺寸爲8*8;
1個cell的梯度直方圖化成9個bin;
滑動窗口在檢測圖片中滑動的尺寸爲8*8;
代碼中的一個hog描述子是針對一個檢測窗口而言的,所以一個檢測窗口共有105=((128-16)/8+1)*((64-16)/8+1)個block;一個block中有4個cell,而一個cell的hog描述子向量的長度爲9;所以檢測窗口的hog向量長度=3780=105*4*9維。
三、hog訓練部分流程的簡單理解
雖然hog源碼中很少涉及到訓練部分的代碼,不過了解下訓練過程的流程會對整個檢測過程有個整體認識。
訓練過程中正樣本大小統一爲128*64,即檢測窗口的大小;該樣本圖片可以包含1個或多個行人。對該圖片提前的hog特徵長度剛好爲3780維,每一個特徵對應一個正樣本標籤進行訓練。在實際的訓練過程中,我們並不是去google上收集或者拍攝剛好128*64大小且有行人的圖片,而是收集包含行人的任意圖片(當然了,尺寸最好比128*64大),然後手工對這些正樣本進行標註,即對有行人的地方畫個矩形,其實也就是存了2個頂點的座標而已,並把這個矩形的信息存儲起來;最好自己寫一個程序,每讀入一張圖片,就把矩形區域的內容截取出來並縮放到統一尺寸128*64,這樣,對處理過後的該圖片進行hog特徵提取就可以當做正樣本了。
負樣本不需要統一尺寸,只需比128*64大,且圖片中不能包含任何行人。實際過程中,由於是負樣本,裏面沒有目標信息,所以不需要人工進行標註。程序中可以對該圖片隨機進行截取128*64大小的圖片,並提取出其hog特徵作爲負樣本。
四、ho行人檢測過程
檢測過程中採用的是滑動窗口法,對應本代碼中,滑動窗口法的流程如下:
由上圖可以看出,檢測時,會對輸入圖片進行尺度縮放(一般是縮小),在每一層的圖像上採用固定大小的滑動窗口(128*64)滑動,沒個滑動窗口都提取出hog特徵,送入到svm分類器中,看該窗口中是否有目標。有則存下目標區域來,無則繼續滑動。
檢測過程中用到的函數爲detectMultiScale(),其參數分配圖如下:
五、計算檢測窗口中圖像的梯度
計算梯度前如果需要gamma校正的話就先進行gamma校正,所謂的gamma校正就是把原來的每個通道像素值範圍從0~255變換到0~15.97(255開根號)。據作者說這樣校正過後的圖像計算的效果會更好,在計算梯度前不需要進行高斯濾波操作。
梯度的計算是分別計算水平梯度圖和垂直梯度圖,然後求幅值和相位。水平梯度卷積算子爲:
垂直梯度卷積算子爲:
在閱讀該源碼的時候,要特別注意梯度幅值和角度的存儲方式。因爲是對一個滑動窗口裏的圖像進行的,所以梯度幅值和角度按照道理來說應該都是128*64=8192維的向量。但實際過程中這2者都是用的128*64*2=16384維的向量。爲什麼呢?
因爲這裏的梯度和角度都是用到了二線插值的。每一個點的梯度角度可能是0~180度之間的任意值,而程序中將其離散化爲9個bin,即每個bin佔20度。所以滑動窗口中每個像素點的梯度角度如果要離散化到這9個bin中,則一般它都會有2個相鄰的bin(如果恰好位於某個bin的中心,則可認爲對該bin的權重爲1即可)。從源碼中可以看到梯度的幅值是用來計算梯度直方圖時權重投票的,所以每個像素點的梯度幅值就分解到了其角度相鄰的2個bin了,越近的那個bin得到的權重越大。因此幅度圖像用了2個通道,每個通道都是原像素點幅度的一個分量。同理,不難理解,像素點的梯度角度也用了2個通道,每個通道中存儲的是它相鄰2個bin的bin序號。序號小的放在第一通道。
二線插值的示意圖如下:
其中,假設那3條半徑爲離散化後bin的中心,紅色虛線爲像素點O(像素點在圓心處)的梯度方向,梯度幅值爲A,該梯度方向與最近的相鄰bin爲bin0,這兩者之間的夾角爲a.這該像素點O處存儲的梯度幅值第1通道爲A*(1-a),第2通道爲A*a;該像素點O處存儲的角度第1通道爲0(bin的序號爲0),第2通道爲1(bin的序號爲1)。
另外在計算圖像的梯度圖和相位圖時,如果該圖像時3通道的,則3通道分別取梯度值,並且取梯度最大的那個通道的值爲該點的梯度幅值。
六、HOG緩存結構體
HOG緩存思想是該程序作者加快hog算法速度採用的一種內存優化技術。由於我們對每幅輸入圖片要進行4層掃描,分別爲圖像金字塔層,每層中滑動窗口,每個滑動窗口中滑動的block,每個block中的cell,其實還有每個cell中的像素點;有這麼多層,每一層又是一個二維的,所以速度非常慢。作者的採用的思想是HOG緩存,即把計算得到的每個滑動窗口的數據(其實最終是每個block的hog描述子向量)都存在內存查找表中,由於滑動窗口在滑動時,很多個block都會重疊,因此重疊處計算過的block信息就可以直接從查找表中讀取,這樣就節省了很多時間。
在這個HOG存儲結構體中,會計算滑動窗口內的hog描述子,而這又涉及到滑動窗口,block,cell直接的關係,其之間的關係可以參考下面示意圖:
外面最大的爲待檢測的圖片,對待檢測的圖片需要用滑動窗口進行滑動來判斷窗口中是否有目標,每個滑動窗口中又有很多個重疊移動的block,每個block中還有不重疊的cell。其實該程序的作者又將每個block中的像素點對cell的貢獻不同,有將每個cell分成了4個區域,即圖中藍色虛線最小的框。
那麼block中不同的像素點對它的cell(默認參數爲1個block有4個cell)的影響是怎樣的呢?請看下面示意圖。
如果所示,黑色框代表1個block,紅實線隔開的爲4個cell,每個cell用綠色虛線隔開的我們稱之爲4個區域,所以該block中共有16個區域,分別爲A、B、C、…、O、P。
程序中將這16個區域分爲4組:
第1組:A、D、M、P;該組內的像素點計算梯度方向直方圖時只對其所在的cell有貢獻。
第2組:B、C、N、O;該組內的像素點計算梯度直方圖時對其所在的左右cell有貢獻。
第3組:E、I、H、L;該組內的像素點計算梯度直方圖時對其所在的上下cell有貢獻。
第4組:F、G、J、K;該組內的像素點對其上下左右的cell計算梯度直方圖時都有貢獻。
那到底是怎麼對cell貢獻的呢?舉個例子來說,E區域內的像素點對cell0和cell2有貢獻。本來1個block對滑動窗口貢獻的向量維數爲36維,即每個cell貢獻9維,其順序分別爲cell0,cell1,cell2,cell3.而E區域內的像素由於同時對cell0和cell2有貢獻,所以在計算E區域內的像素梯度投票時,不僅要投向它本來的cell0,還要投向下面的cell2,即投向cell0和cell2有一個權重,該權重與該像素點所在位置與cell0,cell2中心位置的距離有關。具體的關係可以去查看源碼。
該結構體變量內存分配圖如下,可以增強讀代碼的直觀性:
在讀該部分源碼時,需要特別注意以下幾個地方:
1) 結構體BlockData中有2個變量。1個BlockData結構體是對應的一個block數據。histOfs和imgOffset.其中histOfs表示爲該block對整個滑動窗口內hog描述算子的貢獻那部分向量的起始位置;imgOffset爲該block在滑動窗口圖片中的座標(當然是指左上角座標)。
2) 結構體PixData中有5個變量,1個PixData結構體是對應的block中1個像素點的數據。其中gradOfs表示該點的梯度幅度在滑動窗口圖片梯度幅度圖中的位置座標;qangleOfs表示該點的梯度角度在滑動窗口圖片梯度角度圖中的位置座標;histOfs[]表示該像素點對1個或2個或4個cell貢獻的hog描述子向量的起始位置座標(比較抽象,需要看源碼才懂)。histWeight[]表示該像素點對1個或2個或4個cell貢獻的權重。gradWeight表示該點本身由於處在block中位置的不同因而對梯度直方圖貢獻也不同,其權值按照二維高斯分佈(以block中心爲二維高斯的中心)來決定。
3) 程序中的count1,cout2,cout4分別表示該block中對1個cell、2個cell、4個cell有貢獻的像素點的個數。
七、其他一些函數
該程序中還有一些其它的函數。
getblock()表示的是給定block在滑動窗口的位置以及圖片的hog緩存指針,來獲得本次block中計算hog特徵所需要的信息。
normalizeBlockHistogram()指對block獲取到的hog部分描述子進行歸一化,其實該歸一化有2層,具體看代碼。
windowsInImage()實現的功能是給定測試圖片和滑動窗口移動的大小,來獲得該層中水平和垂直方向上需要滑動多少個滑動窗口。
getWindow()值獲得一個滑動窗口矩形。
compute()是實際上計算hog描述子的函數,在測試和訓練階段都能用到。
detect()是檢測目標是用到的函數,在detectMultiScale()函數內部被調用。
八、關於HOG的初始化
Hog初始化可以採用直接賦初值;也直接從文件節點中讀取(有相應的格式,好像採用的是xml文件格式);當然我們可以讀取初始值,也可以在程序中設置hog算子的初始值並寫入文件,這些工作可以採用源碼中的read,write,load,save等函數來完成。
九、hog源碼的註釋
在讀源碼時,由於裏面用到了intel的ipp庫,優化了算法的速度,所以在程序中遇到#ifdef HAVE_IPP後面的代碼時,可以直接跳過不讀,直接讀#else後面的代碼,這並不影響對原hog算法的理解。
首先來看看hog源碼中用到的頭文件目錄圖,如下:
下面是我對hog源碼的一些註釋,由於本人接觸c++比較少,可能有些c++的語法常識也給註釋起來了,還望大家能理解。另外程序中還有一些細節沒有讀懂,或者說是註釋錯了的,大家可以一起來討論下,很多細節要在源碼中才能看懂。
hog.cpp:
/*M///////////////////////////////////////////////////////////////////////////////////////
//
// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
// By downloading, copying, installing or using the software you agree to this license.
// If you do not agree to this license, do not download, install,
// copy or use the software.
//
//
// License Agreement
// For Open Source Computer Vision Library
//
// Copyright (C) 2000-2008, Intel Corporation, all rights reserved.
// Copyright (C) 2009, Willow Garage Inc., all rights reserved.
// Third party copyrights are property of their respective owners.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistribution's of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistribution's in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * The name of the copyright holders may not be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// This software is provided by the copyright holders and contributors "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the Intel Corporation or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
//
//M*/
#include "precomp.hpp"
#include <iterator>
#ifdef HAVE_IPP
#include "ipp.h"
#endif
/****************************************************************************************\
The code below is implementation of HOG (Histogram-of-Oriented Gradients)
descriptor and object detection, introduced by Navneet Dalal and Bill Triggs.
The computed feature vectors are compatible with the
INRIA Object Detection and Localization Toolkit
(http://pascal.inrialpes.fr/soft/olt/)
\****************************************************************************************/
namespace cv
{
size_t HOGDescriptor::getDescriptorSize() const
{
//下面2個語句是保證block中有整數個cell;保證block在窗口中能移動整數次
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 );
//返回的nbins是每個窗口中檢測到的hog向量的維數
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);
}
//winSigma到底是什麼作用呢?
double HOGDescriptor::getWinSigma() const
{
return winSigma >= 0 ? winSigma : (blockSize.width + blockSize.height)/8.;
}
//svmDetector是HOGDescriptor內的一個成員變量,數據類型爲向量vector。
//用來保存hog特徵用於svm分類時的係數的.
//該函數返回爲真的實際含義是什麼呢?保證與hog特徵長度相同,或者相差1,但爲什麼
//相差1也可以呢?
bool HOGDescriptor::checkDetectorSize() const
{
size_t detectorSize = svmDetector.size(), descriptorSize = getDescriptorSize();
return detectorSize == 0 ||
detectorSize == descriptorSize ||
detectorSize == descriptorSize + 1;
}
void HOGDescriptor::setSVMDetector(InputArray _svmDetector)
{
//這裏的convertTo函數只是將圖像Mat屬性更改,比如說通道數,矩陣深度等。
//這裏是將輸入的svm係數矩陣全部轉換成浮點型。
_svmDetector.getMat().convertTo(svmDetector, CV_32F);
CV_Assert( checkDetectorSize() );
}
#define CV_TYPE_NAME_HOG_DESCRIPTOR "opencv-object-detector-hog"
//FileNode是opencv的core中的一個文件存儲節點類,這個節點用來存儲讀取到的每一個文件元素。
//一般是讀取XML和YAML格式的文件
//又因爲該函數是把文件節點中的內容讀取到其類的成員變量中,所以函數後面不能有關鍵字const
bool HOGDescriptor::read(FileNode& obj)
{
//isMap()是用來判斷這個節點是不是一個映射類型,如果是映射類型,則每個節點都與
//一個名字對應起來。因此這裏的if語句的作用就是需讀取的文件node是一個映射類型
if( !obj.isMap() )
return false;
//中括號中的"winSize"是指返回名爲winSize的一個節點,因爲已經知道這些節點是mapping類型
//也就是說都有一個對應的名字。
FileNodeIterator it = obj["winSize"].begin();
//操作符>>爲從節點中讀入數據,這裏是將it指向的節點數據依次讀入winSize.width,winSize.height
//下面的幾條語句功能類似
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;
obj["nlevels"] >> nlevels;
//isSeq()是判斷該節點內容是不是一個序列
FileNode vecNode = obj["SVMDetector"];
if( vecNode.isSeq() )
{
vecNode >> svmDetector;
CV_Assert(checkDetectorSize());
}
//上面的都讀取完了後就返回讀取成功標誌
return true;
}
void HOGDescriptor::write(FileStorage& fs, const String& objName) const
{
//將objName名字輸入到文件fs中
if( !objName.empty() )
fs << objName;
fs << "{" CV_TYPE_NAME_HOG_DESCRIPTOR
//下面幾句依次將hog描述子內的變量輸入到文件fs中,且每次輸入前都輸入
//一個名字與其對應,因此這些節點是mapping類型。
<< "winSize" << winSize
<< "blockSize" << blockSize
<< "blockStride" << blockStride
<< "cellSize" << cellSize
<< "nbins" << nbins
<< "derivAperture" << derivAperture
<< "winSigma" << getWinSigma()
<< "histogramNormType" << histogramNormType
<< "L2HysThreshold" << L2HysThreshold
<< "gammaCorrection" << gammaCorrection
<< "nlevels" << nlevels;
if( !svmDetector.empty() )
//svmDetector則是直接輸入序列,也有對應的名字。
fs << "SVMDetector" << "[:" << svmDetector << "]";
fs << "}";
}
//從給定的文件中讀取參數
bool HOGDescriptor::load(const String& filename, const String& objname)
{
FileStorage fs(filename, FileStorage::READ);
//一個文件節點有很多葉子,所以一個文件節點包含了很多內容,這裏當然是包含的
//HOGDescriptor需要的各種參數了。
FileNode obj = !objname.empty() ? fs[objname] : fs.getFirstTopLevelNode();
return read(obj);
}
//將類中的參數以文件節點的形式寫入文件中。
void HOGDescriptor::save(const String& filename, const String& objName) const
{
FileStorage fs(filename, FileStorage::WRITE);
write(fs, !objName.empty() ? objName : FileStorage::getDefaultObjectName(filename));
}
//複製HOG描述子到c中
void HOGDescriptor::copyTo(HOGDescriptor& c) const
{
c.winSize = winSize;
c.blockSize = blockSize;
c.blockStride = blockStride;
c.cellSize = cellSize;
c.nbins = nbins;
c.derivAperture = derivAperture;
c.winSigma = winSigma;
c.histogramNormType = histogramNormType;
c.L2HysThreshold = L2HysThreshold;
c.gammaCorrection = gammaCorrection;
//vector類型也可以用等號賦值
c.svmDetector = svmDetector; c.nlevels = nlevels; }
//計算圖像img的梯度幅度圖像grad和梯度方向圖像qangle.
//paddingTL爲需要在原圖像img左上角擴增的尺寸,同理paddingBR
//爲需要在img圖像右下角擴增的尺寸。
void HOGDescriptor::computeGradient(const Mat& img, Mat& grad, Mat& qangle,
Size paddingTL, Size paddingBR) const
{
//該函數只能計算8位整型深度的單通道或者3通道圖像.
CV_Assert( img.type() == CV_8U || img.type() == CV_8UC3 );
//將圖像按照輸入參數進行擴充,這裏不是爲了計算邊緣梯度而做的擴充,因爲
//爲了邊緣梯度而擴充是在後面的代碼完成的,所以這裏爲什麼擴充暫時還不明白。
Size gradsize(img.cols + paddingTL.width + paddingBR.width,
img.rows + paddingTL.height + paddingBR.height);
grad.create(gradsize, CV_32FC2); // <magnitude*(1-alpha), magnitude*alpha>
qangle.create(gradsize, CV_8UC2); // [0..nbins-1] - quantized gradient orientation
Size wholeSize;
Point roiofs;
//locateROI在此處是如果img圖像是從其它父圖像中某一部分得來的,那麼其父圖像
//的大小尺寸就爲wholeSize了,img圖像左上角相對於父圖像的位置點就爲roiofs了。
//對於正樣本,其父圖像就是img了,所以這裏的wholeSize就和img.size()是一樣的,
//對應負樣本,這2者不同;因爲裏面的關係比較不好懂,這裏權且將wholesSize理解爲
//img的size,所以roiofs就應當理解爲Point(0, 0)了。
img.locateROI(wholeSize, roiofs);
int i, x, y;
int cn = img.channels();
//_lut爲行向量,用來作爲浮點像素值的存儲查找表
Mat_<float> _lut(1, 256);
const float* lut = &_lut(0,0);
//gamma校正指的是將0~256的像素值全部開根號,即範圍縮小了,且變換範圍都不成線性了,
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;
//創建長度爲gradsize.width+gradsize.height+4的整型buffer
AutoBuffer<int> mapbuf(gradsize.width + gradsize.height + 4);
int* xmap = (int*)mapbuf + 1;
int* ymap = xmap + gradsize.width + 2;
//言外之意思borderType就等於4了,因爲opencv的源碼中是如下定義的。
//#define IPL_BORDER_REFLECT_101 4
//enum{...,BORDER_REFLECT_101=IPL_BORDER_REFLECT_101,...}
//borderType爲邊界擴充後所填充像素點的方式。
/*
Various border types, image boundaries are denoted with '|'
* BORDER_REPLICATE: aaaaaa|abcdefgh|hhhhhhh
* BORDER_REFLECT: fedcba|abcdefgh|hgfedcb
* BORDER_REFLECT_101: gfedcb|abcdefgh|gfedcba
* BORDER_WRAP: cdefgh|abcdefgh|abcdefg
* BORDER_CONSTANT: iiiiii|abcdefgh|iiiiiii with some specified 'i'
*/
const int borderType = (int)BORDER_REFLECT_101;
for( x = -1; x < gradsize.width + 1; x++ )
/*int borderInterpolate(int p, int len, int borderType)
其中參數p表示的是擴充後圖像的一個座標,相對於對應的座標軸而言;
len參數表示對應源圖像的一個座標軸的長度;borderType爲擴充類型,
在上面已經有過介紹.
所以這個函數的作用是從擴充後的像素點座標推斷出源圖像中對應該點
的座標值。
*/
//這裏的xmap和ymap實際含義是什麼呢?其實xmap向量裏面存的就是
//擴充後圖像第一行像素點對應與原圖像img中的像素橫座標,可以看
//出,xmap向量中有些元素的值是相同的,因爲擴充圖像肯定會對應
//到原圖像img中的某一位置,而img本身尺寸內的像素也會對應該位置。
//同理,ymap向量裏面存的是擴充後圖像第一列像素點對應於原圖想img
//中的像素縱座標。
xmap[x] = borderInterpolate(x - paddingTL.width + roiofs.x,
wholeSize.width, borderType) - roiofs.x;
for( y = -1; y < gradsize.height + 1; y++ )
ymap[y] = borderInterpolate(y - paddingTL.height + roiofs.y,
wholeSize.height, borderType) - roiofs.y;
// x- & y- derivatives for the whole row
int width = gradsize.width;
AutoBuffer<float> _dbuf(width*4);
float* dbuf = _dbuf;
//DX爲水平梯度圖,DY爲垂直梯度圖,Mag爲梯度幅度圖,Angle爲梯度角度圖
//該構造方法的第4個參數表示矩陣Mat的數據在內存中存放的位置。由此可以
//看出,這4幅圖像在內存中是連續存儲的。
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;
//angleScale==9/pi;
float angleScale = (float)(_nbins/CV_PI);
#ifdef HAVE_IPP
Mat lutimg(img.rows,img.cols,CV_MAKETYPE(CV_32F,cn));
Mat hidxs(1, width, CV_32F);
Ipp32f* pHidxs = (Ipp32f*)hidxs.data;
Ipp32f* pAngles = (Ipp32f*)Angle.data;
IppiSize roiSize;
roiSize.width = img.cols;
roiSize.height = img.rows;
for( y = 0; y < roiSize.height; y++ )
{
const uchar* imgPtr = img.data + y*img.step;
float* imglutPtr = (float*)(lutimg.data + y*lutimg.step);
for( x = 0; x < roiSize.width*cn; x++ )
{
imglutPtr[x] = lut[imgPtr[x]];
}
}
#endif
for( y = 0; y < gradsize.height; y++ )
{
#ifdef HAVE_IPP
const float* imgPtr = (float*)(lutimg.data + lutimg.step*ymap[y]);
const float* prevPtr = (float*)(lutimg.data + lutimg.step*ymap[y-1]);
const float* nextPtr = (float*)(lutimg.data + lutimg.step*ymap[y+1]);
#else
//imgPtr在這裏指的是img圖像的第y行首地址;prePtr指的是img第y-1行首地址;
//nextPtr指的是img第y+1行首地址;
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];
#endif
float* gradPtr = (float*)grad.ptr(y);
uchar* qanglePtr = (uchar*)qangle.ptr(y);
//輸入圖像img爲單通道圖像時的計算
if( cn == 1 )
{
for( x = 0; x < width; x++ )
{
int x1 = xmap[x];
#ifdef HAVE_IPP
dbuf[x] = (float)(imgPtr[xmap[x+1]] - imgPtr[xmap[x-1]]);
dbuf[width + x] = (float)(nextPtr[x1] - prevPtr[x1]);
#else
//下面2句把Dx,Dy就計算出來了,因爲其對應的內存都在dbuf中
dbuf[x] = (float)(lut[imgPtr[xmap[x+1]]] - lut[imgPtr[xmap[x-1]]]);
dbuf[width + x] = (float)(lut[nextPtr[x1]] - lut[prevPtr[x1]]);
#endif
}
}
//當cn==3時,也就是輸入圖像爲3通道圖像時的處理。
else
{
for( x = 0; x < width; x++ )
{
//x1表示第y行第x1列的地址
int x1 = xmap[x]*3;
float dx0, dy0, dx, dy, mag0, mag;
#ifdef HAVE_IPP
const float* p2 = imgPtr + xmap[x+1]*3;
const float* p0 = imgPtr + xmap[x-1]*3;
dx0 = p2[2] - p0[2];
dy0 = nextPtr[x1+2] - prevPtr[x1+2];
mag0 = dx0*dx0 + dy0*dy0;
dx = p2[1] - p0[1];
dy = nextPtr[x1+1] - prevPtr[x1+1];
mag = dx*dx + dy*dy;
if( mag0 < mag )
{
dx0 = dx;
dy0 = dy;
mag0 = mag;
}
dx = p2[0] - p0[0];
dy = nextPtr[x1] - prevPtr[x1];
mag = dx*dx + dy*dy;
#else
//p2爲第y行第x+1列的地址
//p0爲第y行第x-1列的地址
const uchar* p2 = imgPtr + xmap[x+1]*3;
const uchar* p0 = imgPtr + xmap[x-1]*3;
//計算第2通道的幅值
dx0 = lut[p2[2]] - lut[p0[2]];
dy0 = lut[nextPtr[x1+2]] - lut[prevPtr[x1+2]];
mag0 = dx0*dx0 + dy0*dy0;
//計算第1通道的幅值
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;
}
//計算第0通道的幅值
dx = lut[p2[0]] - lut[p0[0]];
dy = lut[nextPtr[x1]] - lut[prevPtr[x1]];
mag = dx*dx + dy*dy;
#endif
//取幅值最大的那個通道
if( mag0 < mag )
{
dx0 = dx;
dy0 = dy;
mag0 = mag;
}
//最後求出水平和垂直方向上的梯度圖像
dbuf[x] = dx0;
dbuf[x+width] = dy0;
}
}
#ifdef HAVE_IPP
ippsCartToPolar_32f((const Ipp32f*)Dx.data, (const Ipp32f*)Dy.data, (Ipp32f*)Mag.data, pAngles, width);
for( x = 0; x < width; x++ )
{
if(pAngles[x] < 0.f)
pAngles[x] += (Ipp32f)(CV_PI*2.);
}
ippsNormalize_32f(pAngles, pAngles, width, 0.5f/angleScale, 1.f/angleScale);
ippsFloor_32f(pAngles,(Ipp32f*)hidxs.data,width);
ippsSub_32f_I((Ipp32f*)hidxs.data,pAngles,width);
ippsMul_32f_I((Ipp32f*)Mag.data,pAngles,width);
ippsSub_32f_I(pAngles,(Ipp32f*)Mag.data,width);
ippsRealToCplx_32f((Ipp32f*)Mag.data,pAngles,(Ipp32fc*)gradPtr,width);
#else
//cartToPolar()函數是計算2個矩陣對應元素的幅度和角度,最後一個參數爲是否
//角度使用度數表示,這裏爲false表示不用度數表示,即用弧度表示。
//如果只需計算2個矩陣對應元素的幅度圖像,可以採用magnitude()函數。
//-pi/2<Angle<pi/2;
cartToPolar( Dx, Dy, Mag, Angle, false );
#endif
for( x = 0; x < width; x++ )
{
#ifdef HAVE_IPP
int hidx = (int)pHidxs[x];
#else
//-5<angle<4
float mag = dbuf[x+width*2], angle = dbuf[x+width*3]*angleScale - 0.5f;
//cvFloor()返回不大於參數的最大整數
//hidx={-5,-4,-3,-2,-1,0,1,2,3,4};
int hidx = cvFloor(angle);
//0<=angle<1;angle表示的意思是與其相鄰的較小的那個bin的弧度距離(即弧度差)
angle -= hidx;
//gradPtr爲grad圖像的指針
//gradPtr[x*2]表示的是與x處梯度方向相鄰較小的那個bin的幅度權重;
//gradPtr[x*2+1]表示的是與x處梯度方向相鄰較大的那個bin的幅度權重
gradPtr[x*2] = mag*(1.f - angle);
gradPtr[x*2+1] = mag*angle;
#endif
if( hidx < 0 )
hidx += _nbins;
else if( hidx >= _nbins )
hidx -= _nbins;
assert( (unsigned)hidx < (unsigned)_nbins );
qanglePtr[x*2] = (uchar)hidx;
hidx++;
//-1在補碼中的表示爲11111111,與-1相與的話就是自己本身了;
//0在補碼中的表示爲00000000,與0相與的結果就是0了.
hidx &= hidx < _nbins ? -1 : 0;
qanglePtr[x*2+1] = (uchar)hidx;
}
}
}
struct HOGCache
{
struct BlockData
{
BlockData() : histOfs(0), imgOffset() {}
int histOfs;
Point imgOffset;
};
struct PixData
{
size_t gradOfs, qangleOfs;
int histOfs[4];
float histWeights[4];
float gradWeight;
};
HOGCache();
HOGCache(const HOGDescriptor* descriptor,
const Mat& img, Size paddingTL, Size paddingBR,
bool useCache, Size cacheStride);
virtual ~HOGCache() {};
virtual void init(const HOGDescriptor* descriptor,
const Mat& img, Size paddingTL, Size paddingBR,
bool useCache, Size cacheStride);
Size windowsInImage(Size imageSize, Size winStride) const;
Rect getWindow(Size imageSize, Size winStride, int idx) const;
const float* getBlock(Point pt, float* buf);
virtual void normalizeBlockHistogram(float* histogram) const;
vector<PixData> pixData;
vector<BlockData> blockData;
bool useCache;
vector<int> ymaxCached;
Size winSize, cacheStride;
Size nblocks, ncells;
int blockHistogramSize;
int count1, count2, count4;
Point imgoffset;
Mat_<float> blockCache;
Mat_<uchar> blockCacheFlags;
Mat grad, qangle;
const HOGDescriptor* descriptor;
};
//默認的構造函數,不使用cache,塊的直方圖向量大小爲0等
HOGCache::HOGCache()
{
useCache = false;
blockHistogramSize = count1 = count2 = count4 = 0;
descriptor = 0;
}
//帶參的初始化函數,採用內部的init函數進行初始化
HOGCache::HOGCache(const HOGDescriptor* _descriptor,
const Mat& _img, Size _paddingTL, Size _paddingBR,
bool _useCache, Size _cacheStride)
{
init(_descriptor, _img, _paddingTL, _paddingBR, _useCache, _cacheStride);
}
//HOGCache結構體的初始化函數
void HOGCache::init(const HOGDescriptor* _descriptor,
const Mat& _img, Size _paddingTL, Size _paddingBR,
bool _useCache, Size _cacheStride)
{
descriptor = _descriptor;
cacheStride = _cacheStride;
useCache = _useCache;
//首先調用computeGradient()函數計算輸入圖像的權值梯度幅度圖和角度量化圖
descriptor->computeGradient(_img, grad, qangle, _paddingTL, _paddingBR);
//imgoffset是Point類型,而_paddingTL是Size類型,雖然類型不同,但是2者都是
//一個二維座標,所以是在opencv中是允許直接賦值的。
imgoffset = _paddingTL;
winSize = descriptor->winSize;
Size blockSize = descriptor->blockSize;
Size blockStride = descriptor->blockStride;
Size cellSize = descriptor->cellSize;
int i, j, nbins = descriptor->nbins;
//rawBlockSize爲block中包含像素點的個數
int rawBlockSize = blockSize.width*blockSize.height;
//nblocks爲Size類型,其長和寬分別表示一個窗口中水平方向和垂直方向上block的
//個數(需要考慮block在窗口中的移動)
nblocks = Size((winSize.width - blockSize.width)/blockStride.width + 1,
(winSize.height - blockSize.height)/blockStride.height + 1);
//ncells也是Size類型,其長和寬分別表示一個block中水平方向和垂直方向容納下
//的cell個數
ncells = Size(blockSize.width/cellSize.width, blockSize.height/cellSize.height);
//blockHistogramSize表示一個block中貢獻給hog描述子向量的長度
blockHistogramSize = ncells.width*ncells.height*nbins;
if( useCache )
{
//cacheStride= _cacheStride,即其大小是由參數傳入的,表示的是窗口移動的大小
//cacheSize長和寬表示擴充後的圖像cache中,block在水平方向和垂直方向出現的個數
Size cacheSize((grad.cols - blockSize.width)/cacheStride.width+1,
(winSize.height/cacheStride.height)+1);
//blockCache爲一個float型的Mat,注意其列數的值
blockCache.create(cacheSize.height, cacheSize.width*blockHistogramSize);
//blockCacheFlags爲一個uchar型的Mat
blockCacheFlags.create(cacheSize);
size_t cacheRows = blockCache.rows;
//ymaxCached爲vector<int>類型
//Mat::resize()爲矩陣的一個方法,只是改變矩陣的行數,與單獨的resize()函數不相同。
ymaxCached.resize(cacheRows);
//ymaxCached向量內部全部初始化爲-1
for(size_t ii = 0; ii < cacheRows; ii++ )
ymaxCached[ii] = -1;
}
//weights爲一個尺寸爲blockSize的二維高斯表,下面的代碼就是計算二維高斯的係數
Mat_<float> weights(blockSize);
float sigma = (float)descriptor->getWinSigma();
float scale = 1.f/(sigma*sigma*2);
for(i = 0; i < blockSize.height; i++)
for(j = 0; j < blockSize.width; j++)
{
float di = i - blockSize.height*0.5f;
float dj = j - blockSize.width*0.5f;
weights(i,j) = std::exp(-(di*di + dj*dj)*scale);
}
//vector<BlockData> blockData;而BlockData爲HOGCache的一個結構體成員
//nblocks.width*nblocks.height表示一個檢測窗口中block的個數,
//而cacheSize.width*cacheSize.heigh表示一個已經擴充的圖片中的block的個數
blockData.resize(nblocks.width*nblocks.height);
//vector<PixData> pixData;同理,Pixdata也爲HOGCache中的一個結構體成員
//rawBlockSize表示每個block中像素點的個數
//resize表示將其轉換成列向量
pixData.resize(rawBlockSize*3);
// Initialize 2 lookup tables, pixData & blockData.
// Here is why:
//
// The detection algorithm runs in 4 nested loops (at each pyramid layer):
// loop over the windows within the input image
// loop over the blocks within each window
// loop over the cells within each block
// loop over the pixels in each cell
//
// As each of the loops runs over a 2-dimensional array,
// we could get 8(!) nested loops in total, which is very-very slow.
//
// To speed the things up, we do the following:
// 1. loop over windows is unrolled in the HOGDescriptor::{compute|detect} methods;
// inside we compute the current search window using getWindow() method.
// Yes, it involves some overhead (function call + couple of divisions),
// but it's tiny in fact.
// 2. loop over the blocks is also unrolled. Inside we use pre-computed blockData[j]
// to set up gradient and histogram pointers.
// 3. loops over cells and pixels in each cell are merged
// (since there is no overlap between cells, each pixel in the block is processed once)
// and also unrolled. Inside we use PixData[k] to access the gradient values and
// update the histogram
//count1,count2,count4分別表示block中同時對1個cell,2個cell,4個cell有貢獻的像素點的個數。
count1 = count2 = count4 = 0;
for( j = 0; j < blockSize.width; j++ )
for( i = 0; i < blockSize.height; i++ )
{
PixData* data = 0;
//cellX和cellY表示的是block內該像素點所在的cell橫座標和縱座標索引,以小數的形式存在。
float cellX = (j+0.5f)/cellSize.width - 0.5f;
float cellY = (i+0.5f)/cellSize.height - 0.5f;
//cvRound返回最接近參數的整數;cvFloor返回不大於參數的整數;cvCeil返回不小於參數的整數
//icellX0和icellY0表示所在cell座標索引,索引值爲該像素點相鄰cell的那個較小的cell索引
//當然此處就是由整數的形式存在了。
//按照默認的係數的話,icellX0和icellY0只可能取值-1,0,1,且當i和j<3.5時對應的值才取-1
//當i和j>11.5時取值爲1,其它時刻取值爲0(注意i,j最大是15,從0開始的)
int icellX0 = cvFloor(cellX);
int icellY0 = cvFloor(cellY);
int icellX1 = icellX0 + 1, icellY1 = icellY0 + 1;
//此處的cellx和celly表示的是真實索引值與最近鄰cell索引值之間的差,
//爲後面計算同一像素對不同cell中的hist權重的計算。
cellX -= icellX0;
cellY -= icellY0;
//滿足這個if條件說明icellX0只能爲0,也就是說block橫座標在(3.5,11.5)之間時
if( (unsigned)icellX0 < (unsigned)ncells.width &&
(unsigned)icellX1 < (unsigned)ncells.width )
{
//滿足這個if條件說明icellY0只能爲0,也就是說block縱座標在(3.5,11.5)之間時
if( (unsigned)icellY0 < (unsigned)ncells.height &&
(unsigned)icellY1 < (unsigned)ncells.height )
{
//同時滿足上面2個if語句的像素對4個cell都有權值貢獻
//rawBlockSize表示的是1個block中存儲像素點的個數
//而pixData的尺寸大小爲block中像素點的3倍,其定義如下:
//pixData.resize(rawBlockSize*3);
//pixData的前面block像素大小的內存爲存儲只對block中一個cell
//有貢獻的pixel;中間block像素大小的內存存儲對block中同時2個
//cell有貢獻的pixel;最後面的爲對block中同時4個cell都有貢獻
//的pixel
data = &pixData[rawBlockSize*2 + (count4++)];
//下面計算出的結果爲0
data->histOfs[0] = (icellX0*ncells.height + icellY0)*nbins;
//爲該像素點對cell0的權重
data->histWeights[0] = (1.f - cellX)*(1.f - cellY);
//下面計算出的結果爲18
data->histOfs[1] = (icellX1*ncells.height + icellY0)*nbins;
data->histWeights[1] = cellX*(1.f - cellY);
//下面計算出的結果爲9
data->histOfs[2] = (icellX0*ncells.height + icellY1)*nbins;
data->histWeights[2] = (1.f - cellX)*cellY;
//下面計算出的結果爲27
data->histOfs[3] = (icellX1*ncells.height + icellY1)*nbins;
data->histWeights[3] = cellX*cellY;
}
else
//滿足這個else條件說明icellY0取-1或者1,也就是說block縱座標在(0, 3.5)
//和(11.5, 15)之間.
//此時的像素點對相鄰的2個cell有權重貢獻
{
data = &pixData[rawBlockSize + (count2++)];
if( (unsigned)icellY0 < (unsigned)ncells.height )
{
//(unsigned)-1等於127>2,所以此處滿足if條件時icellY0==1;
//icellY1==1;
icellY1 = icellY0;
cellY = 1.f - cellY;
}
//不滿足if條件時,icellY0==-1;icellY1==0;
//當然了,這2種情況下icellX0==0;icellX1==1;
data->histOfs[0] = (icellX0*ncells.height + icellY1)*nbins;
data->histWeights[0] = (1.f - cellX)*cellY;
data->histOfs[1] = (icellX1*ncells.height + icellY1)*nbins;
data->histWeights[1] = cellX*cellY;
data->histOfs[2] = data->histOfs[3] = 0;
data->histWeights[2] = data->histWeights[3] = 0;
}
}
//當block中橫座標滿足在(0, 3.5)和(11.5, 15)範圍內時,即
//icellX0==-1或==1
else
{
if( (unsigned)icellX0 < (unsigned)ncells.width )
{
//icellX1=icllX0=1;
icellX1 = icellX0;
cellX = 1.f - cellX;
}
//當icllY0=0時,此時對2個cell有貢獻
if( (unsigned)icellY0 < (unsigned)ncells.height &&
(unsigned)icellY1 < (unsigned)ncells.height )
{
data = &pixData[rawBlockSize + (count2++)];
data->histOfs[0] = (icellX1*ncells.height + icellY0)*nbins;
data->histWeights[0] = cellX*(1.f - cellY);
data->histOfs[1] = (icellX1*ncells.height + icellY1)*nbins;
data->histWeights[1] = cellX*cellY;
data->histOfs[2] = data->histOfs[3] = 0;
data->histWeights[2] = data->histWeights[3] = 0;
}
else
//此時只對自身的cell有貢獻
{
data = &pixData[count1++];
if( (unsigned)icellY0 < (unsigned)ncells.height )
{
icellY1 = icellY0;
cellY = 1.f - cellY;
}
data->histOfs[0] = (icellX1*ncells.height + icellY1)*nbins;
data->histWeights[0] = cellX*cellY;
data->histOfs[1] = data->histOfs[2] = data->histOfs[3] = 0;
data->histWeights[1] = data->histWeights[2] = data->histWeights[3] = 0;
}
}
//爲什麼每個block中i,j位置的gradOfs和qangleOfs都相同且是如下的計算公式呢?
//那是因爲輸入的_img參數不是代表整幅圖片而是檢測窗口大小的圖片,所以每個
//檢測窗口中關於block的信息可以看做是相同的
data->gradOfs = (grad.cols*i + j)*2;
data->qangleOfs = (qangle.cols*i + j)*2;
//每個block中i,j位置的權重都是固定的
data->gradWeight = weights(i,j);
}
//保證所有的點都被掃描了一遍
assert( count1 + count2 + count4 == rawBlockSize );
// defragment pixData
//將pixData中按照內存排滿,這樣節省了2/3的內存
for( j = 0; j < count2; j++ )
pixData[j + count1] = pixData[j + rawBlockSize];
for( j = 0; j < count4; j++ )
pixData[j + count1 + count2] = pixData[j + rawBlockSize*2];
//此時count2表示至多對2個cell有貢獻的所有像素點的個數
count2 += count1;
//此時count4表示至多對4個cell有貢獻的所有像素點的個數
count4 += count2;
//上面是初始化pixData,下面開始初始化blockData
// initialize blockData
for( j = 0; j < nblocks.width; j++ )
for( i = 0; i < nblocks.height; i++ )
{
BlockData& data = blockData[j*nblocks.height + i];
//histOfs表示該block對檢測窗口貢獻的hog描述變量起點在整個
//變量中的座標
data.histOfs = (j*nblocks.height + i)*blockHistogramSize;
//imgOffset表示該block的左上角在檢測窗口中的座標
data.imgOffset = Point(j*blockStride.width,i*blockStride.height);
}
//一個檢測窗口對應一個blockData內存,一個block對應一個pixData內存。
}
//pt爲該block左上角在滑動窗口中的座標,buf爲指向檢測窗口中blocData的指針
//函數返回一個block描述子的指針
const float* HOGCache::getBlock(Point pt, float* buf)
{
float* blockHist = buf;
assert(descriptor != 0);
Size blockSize = descriptor->blockSize;
pt += imgoffset;
CV_Assert( (unsigned)pt.x <= (unsigned)(grad.cols - blockSize.width) &&
(unsigned)pt.y <= (unsigned)(grad.rows - blockSize.height) );
if( useCache )
{
//cacheStride可以認爲和blockStride是一樣的
//保證所獲取到HOGCache是我們所需要的,即在block移動過程中會出現
CV_Assert( pt.x % cacheStride.width == 0 &&
pt.y % cacheStride.height == 0 );
//cacheIdx表示的是block個數的座標
Point cacheIdx(pt.x/cacheStride.width,
(pt.y/cacheStride.height) % blockCache.rows);
//ymaxCached的長度爲一個檢測窗口垂直方向上容納的block個數
if( pt.y != ymaxCached[cacheIdx.y] )
{
//取出blockCacheFlags的第cacheIdx.y行並且賦值爲0
Mat_<uchar> cacheRow = blockCacheFlags.row(cacheIdx.y);
cacheRow = (uchar)0;
ymaxCached[cacheIdx.y] = pt.y;
}
//blockHist指向該點對應block所貢獻的hog描述子向量,初始值爲空
blockHist = &blockCache[cacheIdx.y][cacheIdx.x*blockHistogramSize];
uchar& computedFlag = blockCacheFlags(cacheIdx.y, cacheIdx.x);
if( computedFlag != 0 )
return blockHist;
computedFlag = (uchar)1; // set it at once, before actual computing
}
int k, C1 = count1, C2 = count2, C4 = count4;
//
const float* gradPtr = (const float*)(grad.data + grad.step*pt.y) + pt.x*2;
const uchar* qanglePtr = qangle.data + qangle.step*pt.y + pt.x*2;
CV_Assert( blockHist != 0 );
#ifdef HAVE_IPP
ippsZero_32f(blockHist,blockHistogramSize);
#else
for( k = 0; k < blockHistogramSize; k++ )
blockHist[k] = 0.f;
#endif
const PixData* _pixData = &pixData[0];
//C1表示只對自己所在cell有貢獻的點的個數
for( k = 0; k < C1; k++ )
{
const PixData& pk = _pixData[k];
//a表示的是幅度指針
const float* a = gradPtr + pk.gradOfs;
float w = pk.gradWeight*pk.histWeights[0];
//h表示的是相位指針
const uchar* h = qanglePtr + pk.qangleOfs;
//幅度有2個通道是因爲每個像素點的幅值被分解到了其相鄰的兩個bin上了
//相位有2個通道是因爲每個像素點的相位的相鄰處都有的2個bin的序號
int h0 = h[0], h1 = h[1];
float* hist = blockHist + pk.histOfs[0];
float t0 = hist[h0] + a[0]*w;
float t1 = hist[h1] + a[1]*w;
//hist中放的爲加權的梯度值
hist[h0] = t0; hist[h1] = t1;
}
for( ; k < C2; k++ )
{
const PixData& pk = _pixData[k];
const float* a = gradPtr + pk.gradOfs;
float w, t0, t1, a0 = a[0], a1 = a[1];
const uchar* h = qanglePtr + pk.qangleOfs;
int h0 = h[0], h1 = h[1];
//因爲此時的像素對2個cell有貢獻,這是其中一個cell的貢獻
float* hist = blockHist + pk.histOfs[0];
w = pk.gradWeight*pk.histWeights[0];
t0 = hist[h0] + a0*w;
t1 = hist[h1] + a1*w;
hist[h0] = t0; hist[h1] = t1;
//另一個cell的貢獻
hist = blockHist + pk.histOfs[1];
w = pk.gradWeight*pk.histWeights[1];
t0 = hist[h0] + a0*w;
t1 = hist[h1] + a1*w;
hist[h0] = t0; hist[h1] = t1;
}
//和上面類似
for( ; k < C4; k++ )
{
const PixData& pk = _pixData[k];
const float* a = gradPtr + pk.gradOfs;
float w, t0, t1, a0 = a[0], a1 = a[1];
const uchar* h = qanglePtr + pk.qangleOfs;
int h0 = h[0], h1 = h[1];
float* hist = blockHist + pk.histOfs[0];
w = pk.gradWeight*pk.histWeights[0];
t0 = hist[h0] + a0*w;
t1 = hist[h1] + a1*w;
hist[h0] = t0; hist[h1] = t1;
hist = blockHist + pk.histOfs[1];
w = pk.gradWeight*pk.histWeights[1];
t0 = hist[h0] + a0*w;
t1 = hist[h1] + a1*w;
hist[h0] = t0; hist[h1] = t1;
hist = blockHist + pk.histOfs[2];
w = pk.gradWeight*pk.histWeights[2];
t0 = hist[h0] + a0*w;
t1 = hist[h1] + a1*w;
hist[h0] = t0; hist[h1] = t1;
hist = blockHist + pk.histOfs[3];
w = pk.gradWeight*pk.histWeights[3];
t0 = hist[h0] + a0*w;
t1 = hist[h1] + a1*w;
hist[h0] = t0; hist[h1] = t1;
}
normalizeBlockHistogram(blockHist);
return blockHist;
}
void HOGCache::normalizeBlockHistogram(float* _hist) const
{
float* hist = &_hist[0];
#ifdef HAVE_IPP
size_t sz = blockHistogramSize;
#else
size_t i, sz = blockHistogramSize;
#endif
float sum = 0;
#ifdef HAVE_IPP
ippsDotProd_32f(hist,hist,sz,&sum);
#else
//第一次歸一化求的是平方和
for( i = 0; i < sz; i++ )
sum += hist[i]*hist[i];
#endif
//分母爲平方和開根號+0.1
float scale = 1.f/(std::sqrt(sum)+sz*0.1f), thresh = (float)descriptor->L2HysThreshold;
#ifdef HAVE_IPP
ippsMulC_32f_I(scale,hist,sz);
ippsThreshold_32f_I( hist, sz, thresh, ippCmpGreater );
ippsDotProd_32f(hist,hist,sz,&sum);
#else
for( i = 0, sum = 0; i < sz; i++ )
{
//第2次歸一化是在第1次的基礎上繼續求平和和
hist[i] = std::min(hist[i]*scale, thresh);
sum += hist[i]*hist[i];
}
#endif
scale = 1.f/(std::sqrt(sum)+1e-3f);
#ifdef HAVE_IPP
ippsMulC_32f_I(scale,hist,sz);
#else
//最終歸一化結果
for( i = 0; i < sz; i++ )
hist[i] *= scale;
#endif
}
//返回測試圖片中水平方向和垂直方向共有多少個檢測窗口
Size HOGCache::windowsInImage(Size imageSize, Size winStride) const
{
return Size((imageSize.width - winSize.width)/winStride.width + 1,
(imageSize.height - winSize.height)/winStride.height + 1);
}
//給定圖片的大小,已經檢測窗口滑動的大小和測試圖片中的檢測窗口的索引,得到該索引處
//檢測窗口的尺寸,包括座標信息
Rect HOGCache::getWindow(Size imageSize, Size winStride, int idx) const
{
int nwindowsX = (imageSize.width - winSize.width)/winStride.width + 1;
int y = idx / nwindowsX;//商
int x = idx - nwindowsX*y;//餘數
return Rect( x*winStride.width, y*winStride.height, winSize.width, winSize.height );
}
void HOGDescriptor::compute(const Mat& img, vector<float>& descriptors,
Size winStride, Size padding,
const vector<Point>& locations) const
{
//Size()表示長和寬都是0
if( winStride == Size() )
winStride = cellSize;
//gcd爲求最大公約數,如果採用默認值的話,則2者相同
Size cacheStride(gcd(winStride.width, blockStride.width),
gcd(winStride.height, blockStride.height));
size_t nwindows = locations.size();
//alignSize(m, n)返回n的倍數大於等於m的最小值
padding.width = (int)alignSize(std::max(padding.width, 0), cacheStride.width);
padding.height = (int)alignSize(std::max(padding.height, 0), cacheStride.height);
Size paddedImgSize(img.cols + padding.width*2, img.rows + padding.height*2);
HOGCache cache(this, img, padding, padding, nwindows == 0, cacheStride);
if( !nwindows )
//Mat::area()表示爲Mat的面積
nwindows = cache.windowsInImage(paddedImgSize, winStride).area();
const HOGCache::BlockData* blockData = &cache.blockData[0];
int nblocks = cache.nblocks.area();
int blockHistogramSize = cache.blockHistogramSize;
size_t dsize = getDescriptorSize();//一個hog的描述長度
//resize()爲改變矩陣的行數,如果減少矩陣的行數則只保留減少後的
//那些行,如果是增加行數,則保留所有的行。
//這裏將描述子長度擴展到整幅圖片
descriptors.resize(dsize*nwindows);
for( size_t i = 0; i < nwindows; i++ )
{
//descriptor爲第i個檢測窗口的描述子首位置。
float* descriptor = &descriptors[i*dsize];
Point pt0;
//非空
if( !locations.empty() )
{
pt0 = locations[i];
//非法的點
if( pt0.x < -padding.width || pt0.x > img.cols + padding.width - winSize.width ||
pt0.y < -padding.height || pt0.y > img.rows + padding.height - winSize.height )
continue;
}
//locations爲空
else
{
//pt0爲沒有擴充前圖像對應的第i個檢測窗口
pt0 = cache.getWindow(paddedImgSize, winStride, (int)i).tl() - Point(padding);
CV_Assert(pt0.x % cacheStride.width == 0 && pt0.y % cacheStride.height == 0);
}
for( int j = 0; j < nblocks; j++ )
{
const HOGCache::BlockData& bj = blockData[j];
//pt爲block的左上角相對檢測圖片的座標
Point pt = pt0 + bj.imgOffset;
//dst爲該block在整個測試圖片的描述子的位置
float* dst = descriptor + bj.histOfs;
const float* src = cache.getBlock(pt, dst);
if( src != dst )
#ifdef HAVE_IPP
ippsCopy_32f(src,dst,blockHistogramSize);
#else
for( int k = 0; k < blockHistogramSize; k++ )
dst[k] = src[k];
#endif
}
}
}
void HOGDescriptor::detect(const Mat& img,
vector<Point>& hits, vector<double>& weights, double hitThreshold,
Size winStride, Size padding, const vector<Point>& locations) const
{
//hits裏面存的是符合檢測到目標的窗口的左上角頂點座標
hits.clear();
if( svmDetector.empty() )
return;
if( winStride == Size() )
winStride = cellSize;
Size cacheStride(gcd(winStride.width, blockStride.width),
gcd(winStride.height, blockStride.height));
size_t nwindows = locations.size();
padding.width = (int)alignSize(std::max(padding.width, 0), cacheStride.width);
padding.height = (int)alignSize(std::max(padding.height, 0), cacheStride.height);
Size paddedImgSize(img.cols + padding.width*2, img.rows + padding.height*2);
HOGCache cache(this, img, padding, padding, nwindows == 0, cacheStride);
if( !nwindows )
nwindows = cache.windowsInImage(paddedImgSize, winStride).area();
const HOGCache::BlockData* blockData = &cache.blockData[0];
int nblocks = cache.nblocks.area();
int blockHistogramSize = cache.blockHistogramSize;
size_t dsize = getDescriptorSize();
double rho = svmDetector.size() > dsize ? svmDetector[dsize] : 0;
vector<float> blockHist(blockHistogramSize);
for( size_t i = 0; i < nwindows; i++ )
{
Point pt0;
if( !locations.empty() )
{
pt0 = locations[i];
if( pt0.x < -padding.width || pt0.x > img.cols + padding.width - winSize.width ||
pt0.y < -padding.height || pt0.y > img.rows + padding.height - winSize.height )
continue;
}
else
{
pt0 = cache.getWindow(paddedImgSize, winStride, (int)i).tl() - Point(padding);
CV_Assert(pt0.x % cacheStride.width == 0 && pt0.y % cacheStride.height == 0);
}
double s = rho;
//svmVec指向svmDetector最前面那個元素
const float* svmVec = &svmDetector[0];
#ifdef HAVE_IPP
int j;
#else
int j, k;
#endif
for( j = 0; j < nblocks; j++, svmVec += blockHistogramSize )
{
const HOGCache::BlockData& bj = blockData[j];
Point pt = pt0 + bj.imgOffset;
//vec爲測試圖片pt處的block貢獻的描述子指針
const float* vec = cache.getBlock(pt, &blockHist[0]);
#ifdef HAVE_IPP
Ipp32f partSum;
ippsDotProd_32f(vec,svmVec,blockHistogramSize,&partSum);
s += (double)partSum;
#else
for( k = 0; k <= blockHistogramSize - 4; k += 4 )
//const float* svmVec = &svmDetector[0];
s += vec[k]*svmVec[k] + vec[k+1]*svmVec[k+1] +
vec[k+2]*svmVec[k+2] + vec[k+3]*svmVec[k+3];
for( ; k < blockHistogramSize; k++ )
s += vec[k]*svmVec[k];
#endif
}
if( s >= hitThreshold )
{
hits.push_back(pt0);
weights.push_back(s);
}
}
}
//不用保留檢測到目標的可信度,即權重
void HOGDescriptor::detect(const Mat& img, vector<Point>& hits, double hitThreshold,
Size winStride, Size padding, const vector<Point>& locations) const
{
vector<double> weightsV;
detect(img, hits, weightsV, hitThreshold, winStride, padding, locations);
}
struct HOGInvoker
{
HOGInvoker( const HOGDescriptor* _hog, const Mat& _img,
double _hitThreshold, Size _winStride, Size _padding,
const double* _levelScale, ConcurrentRectVector* _vec,
ConcurrentDoubleVector* _weights=0, ConcurrentDoubleVector* _scales=0 )
{
hog = _hog;
img = _img;
hitThreshold = _hitThreshold;
winStride = _winStride;
padding = _padding;
levelScale = _levelScale;
vec = _vec;
weights = _weights;
scales = _scales;
}
void operator()( const BlockedRange& range ) const
{
int i, i1 = range.begin(), i2 = range.end();
double minScale = i1 > 0 ? levelScale[i1] : i2 > 1 ? levelScale[i1+1] : std::max(img.cols, img.rows);
//將原圖片進行縮放
Size maxSz(cvCeil(img.cols/minScale), cvCeil(img.rows/minScale));
Mat smallerImgBuf(maxSz, img.type());
vector<Point> locations;
vector<double> hitsWeights;
for( i = i1; i < i2; i++ )
{
double scale = levelScale[i];
Size sz(cvRound(img.cols/scale), cvRound(img.rows/scale));
//smallerImg只是構造一個指針,並沒有複製數據
Mat smallerImg(sz, img.type(), smallerImgBuf.data);
//沒有尺寸縮放
if( sz == img.size() )
smallerImg = Mat(sz, img.type(), img.data, img.step);
//有尺寸縮放
else
resize(img, smallerImg, sz);
//該函數實際上是將返回的值存在locations和histWeights中
//其中locations存的是目標區域的左上角座標
hog->detect(smallerImg, locations, hitsWeights, hitThreshold, winStride, padding);
Size scaledWinSize = Size(cvRound(hog->winSize.width*scale), cvRound(hog->winSize.height*scale));
for( size_t j = 0; j < locations.size(); j++ )
{
//保存目標區域
vec->push_back(Rect(cvRound(locations[j].x*scale),
cvRound(locations[j].y*scale),
scaledWinSize.width, scaledWinSize.height));
//保存縮放尺寸
if (scales) {
scales->push_back(scale);
}
}
//保存svm計算後的結果值
if (weights && (!hitsWeights.empty()))
{
for (size_t j = 0; j < locations.size(); j++)
{
weights->push_back(hitsWeights[j]);
}
}
}
}
const HOGDescriptor* hog;
Mat img;
double hitThreshold;
Size winStride;
Size padding;
const double* levelScale;
//typedef tbb::concurrent_vector<Rect> ConcurrentRectVector;
ConcurrentRectVector* vec;
//typedef tbb::concurrent_vector<double> ConcurrentDoubleVector;
ConcurrentDoubleVector* weights;
ConcurrentDoubleVector* scales;
};
void HOGDescriptor::detectMultiScale(
const Mat& img, vector<Rect>& foundLocations, vector<double>& foundWeights,
double hitThreshold, Size winStride, Size padding,
double scale0, double finalThreshold, bool useMeanshiftGrouping) const
{
double scale = 1.;
int levels = 0;
vector<double> levelScale;
//nlevels默認的是64層
for( levels = 0; levels < nlevels; levels++ )
{
levelScale.push_back(scale);
if( cvRound(img.cols/scale) < winSize.width ||
cvRound(img.rows/scale) < winSize.height ||
scale0 <= 1 )
break;
//只考慮測試圖片尺寸比檢測窗口尺寸大的情況
scale *= scale0;
}
levels = std::max(levels, 1);
levelScale.resize(levels);
ConcurrentRectVector allCandidates;
ConcurrentDoubleVector tempScales;
ConcurrentDoubleVector tempWeights;
vector<double> foundScales;
//TBB並行計算
parallel_for(BlockedRange(0, (int)levelScale.size()),
HOGInvoker(this, img, hitThreshold, winStride, padding, &levelScale[0], &allCandidates, &tempWeights, &tempScales));
//將tempScales中的內容複製到foundScales中;back_inserter是指在指定參數迭代器的末尾插入數據
std::copy(tempScales.begin(), tempScales.end(), back_inserter(foundScales));
//容器的clear()方法是指移除容器中所有的數據
foundLocations.clear();
//將候選目標窗口保存在foundLocations中
std::copy(allCandidates.begin(), allCandidates.end(), back_inserter(foundLocations));
foundWeights.clear();
//將候選目標可信度保存在foundWeights中
std::copy(tempWeights.begin(), tempWeights.end(), back_inserter(foundWeights));
if ( useMeanshiftGrouping )
{
groupRectangles_meanshift(foundLocations, foundWeights, foundScales, finalThreshold, winSize);
}
else
{
//對矩形框進行聚類
groupRectangles(foundLocations, (int)finalThreshold, 0.2);
}
}
//不考慮目標的置信度
void HOGDescriptor::detectMultiScale(const Mat& img, vector<Rect>& foundLocations,
double hitThreshold, Size winStride, Size padding,
double scale0, double finalThreshold, bool useMeanshiftGrouping) const
{
vector<double> foundWeights;
detectMultiScale(img, foundLocations, foundWeights, hitThreshold, winStride,
padding, scale0, finalThreshold, useMeanshiftGrouping);
}
typedef RTTIImpl<HOGDescriptor> HOGRTTI;
CvType hog_type( CV_TYPE_NAME_HOG_DESCRIPTOR, HOGRTTI::isInstance,
HOGRTTI::release, HOGRTTI::read, HOGRTTI::write, HOGRTTI::clone);
vector<float> HOGDescriptor::getDefaultPeopleDetector()
{
static const float detector[] = {
0.05359386f, -0.14721455f, -0.05532170f, 0.05077307f,
0.11547081f, -0.04268804f, 0.04635834f, ........
};
//返回detector數組的從頭到尾構成的向量
return vector<float>(detector, detector + sizeof(detector)/sizeof(detector[0]));
}
//This function renurn 1981 SVM coeffs obtained from daimler's base.
//To use these coeffs the detection window size should be (48,96)
vector<float> HOGDescriptor::getDaimlerPeopleDetector()
{
static const float detector[] = {
0.294350f, -0.098796f, -0.129522f, 0.078753f,
0.387527f, 0.261529f, 0.145939f, 0.061520f,
........
};
//返回detector的首尾構成的向量
return vector<float>(detector, detector + sizeof(detector)/sizeof(detector[0]));
}
}
objdetect.hpp中關於hog的部分:
//////////////// HOG (Histogram-of-Oriented-Gradients) Descriptor and Object Detector //////////////
struct CV_EXPORTS_W HOGDescriptor
{
public:
enum { L2Hys=0 };
enum { DEFAULT_NLEVELS=64 };
CV_WRAP HOGDescriptor() : winSize(64,128), blockSize(16,16), blockStride(8,8),
cellSize(8,8), nbins(9), derivAperture(1), winSigma(-1),
histogramNormType(HOGDescriptor::L2Hys), L2HysThreshold(0.2), gammaCorrection(true),
nlevels(HOGDescriptor::DEFAULT_NLEVELS)
{}
//可以用構造函數的參數來作爲冒號外的參數初始化傳入,這樣定義該類的時候,一旦變量分配了
//內存,則馬上會被初始化,而不用等所有變量分配完內存後再初始化。
CV_WRAP HOGDescriptor(Size _winSize, Size _blockSize, Size _blockStride,
Size _cellSize, int _nbins, int _derivAperture=1, double _winSigma=-1,
int _histogramNormType=HOGDescriptor::L2Hys,
double _L2HysThreshold=0.2, bool _gammaCorrection=false,
int _nlevels=HOGDescriptor::DEFAULT_NLEVELS)
: winSize(_winSize), blockSize(_blockSize), blockStride(_blockStride), cellSize(_cellSize),
nbins(_nbins), derivAperture(_derivAperture), winSigma(_winSigma),
histogramNormType(_histogramNormType), L2HysThreshold(_L2HysThreshold),
gammaCorrection(_gammaCorrection), nlevels(_nlevels)
{}
//可以導入文本文件進行初始化
CV_WRAP HOGDescriptor(const String& filename)
{
load(filename);
}
HOGDescriptor(const HOGDescriptor& d)
{
d.copyTo(*this);
}
virtual ~HOGDescriptor() {}
//size_t是一個long unsigned int型
CV_WRAP size_t getDescriptorSize() const;
CV_WRAP bool checkDetectorSize() const;
CV_WRAP double getWinSigma() const;
//virtual爲虛函數,在指針或引用時起函數多態作用
CV_WRAP virtual void setSVMDetector(InputArray _svmdetector);
virtual bool read(FileNode& fn);
virtual void write(FileStorage& fs, const String& objname) const;
CV_WRAP virtual bool load(const String& filename, const String& objname=String());
CV_WRAP virtual void save(const String& filename, const String& objname=String()) const;
virtual void copyTo(HOGDescriptor& c) const;
CV_WRAP virtual void compute(const Mat& img,
CV_OUT vector<float>& descriptors,
Size winStride=Size(), Size padding=Size(),
const vector<Point>& locations=vector<Point>()) const;
//with found weights output
CV_WRAP virtual void detect(const Mat& img, CV_OUT vector<Point>& foundLocations,
CV_OUT vector<double>& weights,
double hitThreshold=0, Size winStride=Size(),
Size padding=Size(),
const vector<Point>& searchLocations=vector<Point>()) const;
//without found weights output
virtual void detect(const Mat& img, CV_OUT vector<Point>& foundLocations,
double hitThreshold=0, Size winStride=Size(),
Size padding=Size(),
const vector<Point>& searchLocations=vector<Point>()) const;
//with result weights output
CV_WRAP virtual void detectMultiScale(const Mat& img, CV_OUT vector<Rect>& foundLocations,
CV_OUT vector<double>& foundWeights, double hitThreshold=0,
Size winStride=Size(), Size padding=Size(), double scale=1.05,
double finalThreshold=2.0,bool useMeanshiftGrouping = false) const;
//without found weights output
virtual void detectMultiScale(const Mat& img, CV_OUT vector<Rect>& foundLocations,
double hitThreshold=0, Size winStride=Size(),
Size padding=Size(), double scale=1.05,
double finalThreshold=2.0, bool useMeanshiftGrouping = false) const;
CV_WRAP virtual void computeGradient(const Mat& img, CV_OUT Mat& grad, CV_OUT Mat& angleOfs,
Size paddingTL=Size(), Size paddingBR=Size()) const;
CV_WRAP static vector<float> getDefaultPeopleDetector();
CV_WRAP static vector<float> getDaimlerPeopleDetector();
CV_PROP Size winSize;
CV_PROP Size blockSize;
CV_PROP Size blockStride;
CV_PROP Size cellSize;
CV_PROP int nbins;
CV_PROP int derivAperture;
CV_PROP double winSigma;
CV_PROP int histogramNormType;
CV_PROP double L2HysThreshold;
CV_PROP bool gammaCorrection;
CV_PROP vector<float> svmDetector;
CV_PROP int nlevels;
};
十、總結
該源碼的作者採用了一些加快算法速度的優化手段,比如前面講到的緩存查找表技術,同時程序中也使用了intel的多線程技術,即TBB並行技術等。
讀源碼花了一些時間,不過收穫也不少,很佩服寫出這些代碼的作者。