OpenCV深入學習(8)--calcHist源碼分析

      距離上篇深入學習又過了N久了,當時分析過calcHist的源碼,不過有些地方不是很明白,後來一直忙着畢業,也沒繼續,但是心裏總是覺得有個東西沒有完成,這兩天有點時間就把看的calcHist的源碼整理了一下,貼出來,也算完了一個任務。

     calcHist的源碼中根據計算的Mat的深度分了幾種情況,分別調用不同的static函數實現的,其中8U的是用的普通函數,16U和32F的是用的模板,這裏只對8U的分析了一下,就是在源碼里加了一下注釋。讀讀源碼從中還真是學到不少東西。其中有些自己也不是很明白,錯誤之處還請指出,共同探討。

      首先是一個總的函數calcHist

/*計算給定圖像數組的直方圖*/
void cv::calcHist( const Mat* images, int nimages, const int* channels,
                   InputArray _mask, OutputArray _hist, int dims, const int* histSize,
                   const float** ranges, bool uniform, bool accumulate )
{
	/*從輸入掩膜代理中獲得掩膜矩陣*/
    Mat mask = _mask.getMat();
    /*確保直方圖維數至少是1維,並且指定直方圖bin大小的數組不是空的*/
    CV_Assert(dims > 0 && histSize);
    
	/*從輸出直方圖代理中獲得指向存儲輸出直方圖數據的數組的指針*/
	uchar* histdata = _hist.getMat().data;
	/*
	 創建輸出直方圖的存儲空間,該代理根據其代理對象不同,調用被代理對象的創建函數,ex:是Mat的話,
	 調用Mat::create(int ndims,const int *sizes, int type)函數來分配存儲空間
	 ---分配的是32F類型的存儲
	*/
    _hist.create(dims, histSize, CV_32F);
    Mat hist = _hist.getMat(), ihist = hist;//從代理中取出或者將代理對象轉換爲Mat;
	
	/*設置輸出的flag標誌位--注意最後的CV_32S,後面計算直方圖使用的都是ihist臨時矩陣,所以可以看出計算直方圖時用的是32S深度的矩陣*/
    ihist.flags = (ihist.flags & ~CV_MAT_TYPE_MASK)|CV_32S;
	/*
	非累加直方圖或者開始獲得的指向輸出存儲的指針與新分配的存儲的指針不是同一個指針--??,將輸出都設爲0;
	否則,即,採用累加直方圖,將hist轉換到ihist--32S【用ihist作爲臨時存儲??】
	*/   
    if( !accumulate || histdata != hist.data )
        hist = Scalar(0.);
    else
        hist.convertTo(ihist, CV_32S);
		
	/*爲計算直方圖時快速尋址分配的臨時數組,保存一些參數*/    
    vector<uchar*> ptrs;
    vector<int> deltas;
    vector<double> uniranges;
    Size imsize;
    
    CV_Assert( !mask.data || mask.type() == CV_8UC1 );
	/*調用本地靜態函數計算後面所用的統計直方圖時的參數---前8個參數爲輸入,後4個爲輸出,
	  第6個參數傳入的是輸出直方圖的size數組,即矩陣各維度的大小
	*/
    histPrepareImages( images, nimages, channels, mask, dims, hist.size, ranges, uniform,
						ptrs, deltas, imsize, uniranges );
	/*是否均勻直方圖,是的話用uniranges[0]指針初始化常量*/
    const double* _uniranges = uniform ? &uniranges[0] : 0;
    
    int depth = images[0].depth();//獲取深度--所有輸入圖像的深度是一樣的。
    
	/*
	 根據深度不同,調用相應的函數計算直方圖,深度爲CV_8U時調用的是本地靜態函數,
	 而其他兩種情況下調用的是函數模板;
	*/
    if( depth == CV_8U )/*8U的圖像的輸出直方圖值ihist是32S的*/
        calcHist_8u(ptrs, deltas, imsize, ihist, dims, ranges, _uniranges, uniform );
    else if( depth == CV_16U )
        calcHist_<ushort>(ptrs, deltas, imsize, ihist, dims, ranges, _uniranges, uniform );
    else if( depth == CV_32F )
        calcHist_<float>(ptrs, deltas, imsize, ihist, dims, ranges, _uniranges, uniform );
    else
        CV_Error(CV_StsUnsupportedFormat, "");
    /*將ihist在轉換到hist中,並且將深度變換爲32F--結合前面可見用於計算統計時使用的是32S深度的即整型格式,最後輸出時用的是32F,應該是爲了加快速度*/
    ihist.convertTo(hist, CV_32F);
}


其中首先調用的是一個histPrepareImages函數:

/*
根據給定參數計算統計直方圖所用的其他參數,即進行參數的轉換;
前8個參數是輸入的,後4個參數是輸出;
---函數返回之後,ptrs中是計算輸出直方圖的dims從0~dims-1的每一維所用到的圖像數據的首地址;
deltas中則是與之對應的圖像的通道數以及字節偏移;imsize中是圖像大小【如果是連續存儲的則進行了順序訪問的加速處理】;
uniranges中是均勻直方圖中的每個bin對應的灰度範圍相關的東西---【沒理解到底是怎麼對應的??】
*/
static void histPrepareImages( const Mat* images, int nimages, const int* channels,
                               const Mat& mask, int dims, const int* histSize,
                               const float** ranges, bool uniform,
                               vector<uchar*>& ptrs, vector<int>& deltas,
                               Size& imsize, vector<double>& uniranges )
{
    int i, j, c;
	/*
	直方圖的維數由channels或者nimages決定,如果channels爲0,則圖像數組的元素個數與dims必須一樣,即數組中每一幅圖像取一個通道;
	如果channels不爲0,則由channels和nimages共同決定dims:將images中的所有圖像按照通道依次攤開排列,通道數從0開始,
	channels數組指出有哪些通道用於計算直方圖,而且channels指出的是按照直方圖的第0維開始的,即直方圖的第一維計算的是
	channels[0]指定的通道圖像。
	*/
    CV_Assert( channels != 0 || nimages == dims );
    /*提取圖像大小*/
    imsize = images[0].size();
	/*獲取圖像深度以及圖像單通道的元素的大小*/
    int depth = images[0].depth(), esz1 = (int)images[0].elemSize1();
    bool isContinuous = true;
    /*重置uchar數組的大小和int數組的大小*/
    ptrs.resize(dims + 1);
    deltas.resize((dims + 1)*2);
	
    /*
	對每一維進行處理---將圖像數組按照通道攤開排列起來,將每一通道圖像的數據的首地址放到ptrs[i]中;
	deltas中保存的是每一維【或者說每一個通道圖像】的映射到該通道所處的圖像的通道數以及圖像字節對齊產生的偏移量
	*/
    for( i = 0; i < dims; i++ )
    {
		/*通道參數爲0---用維數指定通道--每一幅圖像只取通道0*/
        if(!channels)
        {
            j = i;
            c = 0;//通道爲0
            CV_Assert( images[j].channels() == 1 );
        }
        else
        {
		/*通道數不是0--提取通道*/
            c = channels[i];
            CV_Assert( c >= 0 );
			/*
			按照channels參數的解釋,將c設置爲當前維【第i維】對應的通道圖像
			--這個通道圖像或者是圖像數組中的一幅單通道的圖像,或者是數組中的一幅多通道圖像的某個通道;
			下面的for循環的目的是找出channels[i]對應的通道圖像在輸入數組的第幾個中;循環結束之後
			j是當前維即第i維對應的數組中的圖像【單通道或者多通道】,c指向該圖像中的一個通道;
			*/
			for( j = 0; j < nimages; c -= images[j].channels(), j++ )
                if( c < images[j].channels() )
                    break;
            CV_Assert( j < nimages );
        }
        /*確保每一幅圖像大小深度都是一樣的*/    
        CV_Assert( images[j].size() == imsize && images[j].depth() == depth );
		/*圖像是否是連續存儲的--字節對齊*/
        if( !images[j].isContinuous() )
            isContinuous = false;
			
        /*ptrs中存儲每一個用於計算直方圖的通道圖像的數據首地址*/
		ptrs[i] = images[j].data + c*esz1;
        deltas[i*2] = images[j].channels();//每一幅用於計算直方圖的圖像的通道數;
		/*
		images[j].step/esz1---從每行佔用的字節數步長計算圖像每行佔有的通道個數,
		imsize.width*deltas[i*2]---直接通過寬度*每個元素的通道數計算得到的通道個數,
		---所以這句應該是計算字節對齊導致的偏移--以深度類型爲單位的【即8U,32F,32S這些】,對於沒有字節對齊的是0---
		*/
        deltas[i*2+1] = (int)(images[j].step/esz1 - imsize.width*deltas[i*2]);
    }
    /*使用掩膜*/
    if( mask.data )
    {
        CV_Assert( mask.size() == imsize && mask.channels() == 1 );
        isContinuous = isContinuous && mask.isContinuous();
        ptrs[dims] = mask.data; //最後一個指針放的是指向掩膜的數據;
        deltas[dims*2] = 1;//通道數--掩膜爲8UC1,所以是1;
		//每一行的元素的個數
        deltas[dims*2 + 1] = (int)(mask.step/mask.elemSize1());
    }
	
    /*連續存儲,調整寬高,加快訪問速度*/
    if( isContinuous )
    {
        imsize.width *= imsize.height;
        imsize.height = 1;
    }
	
    /*bin範圍沒有指定,默認對0-255範圍的統計均勻直方圖--必須是8U深度的圖像*/
    if( !ranges )
    {
        CV_Assert( depth == CV_8U );
        /*調整bin均勻直方圖的範圍數組大小,並賦值*/
        uniranges.resize( dims*2 );
        for( i = 0; i < dims; i++ )
        {
            uniranges[i*2] = histSize[i]/256.;
            uniranges[i*2+1] = 0;
        }
    }
    else if( uniform )	/*採用均勻直方圖,並且指定了每一維的bin的個數*/
    {
        uniranges.resize( dims*2 );
        for( i = 0; i < dims; i++ )
        {
            CV_Assert( ranges[i] && ranges[i][0] < ranges[i][1] );	
			/*指定每一維的統計範圍*/
            double low = ranges[i][0], high = ranges[i][1];
            double t = histSize[i]/(high - low);
            uniranges[i*2] = t;
            uniranges[i*2+1] = -t*low;
        }
    }
    else
    {	
	/*非均勻直方圖,保證指定的範圍是小->大*/
        for( i = 0; i < dims; i++ )
        {
            size_t j, n = histSize[i];
            for( j = 0; j < n; j++ )
                CV_Assert( ranges[i][j] < ranges[i][j+1] );
        }
    }
}


然後calcHist中就調用8U深度圖像的統計函數calcHist_8u進行具體的統計計算過程

/*
8U深度的圖像的直方圖統計函數
首先生成了映射查找表加快處理速度---理解映射的對應關係是關鍵--對應關係需要對幾個輸入參數ptr,deltas以及最重要的查找表tab的存儲結構搞清楚;
後面具體的統計計算過程,對dims>=2的過程基本一樣,對dims=1的稍有不同,需要先搞清楚映射對應關係之後,這個統計計算過程纔可以比較清晰。
---參數ptrs; deltas; imsize;都是上面通過histPrepareImages函數計算獲得的,_uniranges指向的是histPrepareImages計算獲得的uniranges數組的第一個元素;
*/    
static void
calcHist_8u( vector<uchar*>& _ptrs, const vector<int>& _deltas,
             Size imsize, Mat& hist, int dims, const float** _ranges,
             const double* _uniranges, bool uniform )
{
	/*指向用於計算直方圖的圖像數據的指針的指針*/
    uchar** ptrs = &_ptrs[0];
	/*對應上面的計算圖像通道數以及偏移的數組的指針*/
    const int* deltas = &_deltas[0];
    uchar* H = hist.data;//輸出直方圖的數據指針--32S
    int i, x;
    const uchar* mask = _ptrs[dims];//掩膜的數據指針
    int mstep = _deltas[dims*2 + 1];//掩膜的數據指針移動步長
    vector<size_t> _tab;
	
    /*生成映射查找表-----要弄清楚映射規則需要弄清楚幾個數組或者矩陣的存儲結構:Mat即hist;_tab;ptr;delta;_uniranges等*/
    calcHistLookupTables_8u( hist, SparseMat(), dims, _ranges, _uniranges, uniform, false, _tab );
    const size_t* tab = &_tab[0];
    /*分不同的維不同對待*/
    if( dims == 1 )
    {
		/*d0--通道數,step0字節對齊偏移*/
        int d0 = deltas[0], step0 = deltas[1];
        int matH[256] = {0};//對各個灰度值的統計暫存
        const uchar* p0 = (const uchar*)ptrs[0];//圖像數據指針
        /**/
        for( ; imsize.height--; p0 += step0, mask += mstep )
        {
			/*不採用掩膜*/
            if( !mask )
            {
				/*單通道圖像*/
                if( d0 == 1 )
                {
					/*單通道,4字節對齊,對每個字節都進行統計*/
                    for( x = 0; x <= imsize.width - 4; x += 4 )
                    {
                        int t0 = p0[x], t1 = p0[x+1];
                        matH[t0]++; matH[t1]++;
                        t0 = p0[x+2]; t1 = p0[x+3];
                        matH[t0]++; matH[t1]++;
                    }
                    p0 += x;
                }
                else   /*多通道圖像*/				
                {   
					/*多通道,只對第0個通道進行統計,通道數d0做偏移*/
					for( x = 0; x <= imsize.width - 4; x += 4 )
                    {
                        int t0 = p0[0], t1 = p0[d0];
                        matH[t0]++; matH[t1]++;
                        p0 += d0*2;
                        t0 = p0[0]; t1 = p0[d0];
                        matH[t0]++; matH[t1]++;
                        p0 += d0*2;
                    }
                }
				/*非4字節的對齊,自己單獨處理*/
                for( ; x < imsize.width; x++, p0 += d0 )
                    matH[*p0]++;
            }
            else /*採用掩膜時直接統計*/
                for( x = 0; x < imsize.width; x++, p0 += d0 )
                    if( mask[x] )
                        matH[*p0]++;
        }
        /*將統計得到的數據存入直方圖矩陣中,採用了灰度值到bin的映射查找到對應的bin位置*/
        for( i = 0; i < 256; i++ )
        {
            size_t hidx = tab[i];//hidx--即直方圖中的位置
			//只統計不超限的數值
            if( hidx < OUT_OF_RANGE )
                *(int *)(H + hidx) += matH[i];
        }
    }
    else if( dims == 2 )/*2維直方圖*/
    {
		/*取出用於計算兩維直方圖的圖像的通道數,字節對齊偏移,數據指針等*/
        int d0 = deltas[0], step0 = deltas[1],
            d1 = deltas[2], step1 = deltas[3];
        const uchar* p0 = (const uchar*)ptrs[0];
        const uchar* p1 = (const uchar*)ptrs[1];
        /**/
        for( ; imsize.height--; p0 += step0, p1 += step1, mask += mstep )
        {
            if( !mask )/*無掩膜*/
            {    
				/*首先根據映射表計算出在直方圖中的位置,然後累加*/
				for( x = 0; x < imsize.width; x++, p0 += d0, p1 += d1 )
                {
					/* idx 爲第0維的偏移【tab[*p0]】+第1維的偏移【tab[*p1 + 256]】---類似於圖像的行偏移+列偏移*/
                    size_t idx = tab[*p0] + tab[*p1 + 256];
                    if( idx < OUT_OF_RANGE )
                        ++*(int*)(H + idx);
                }
			}
            else/*使用掩膜時*/
            {
				/*跟上面的一樣只是多了掩膜判斷*/
				for( x = 0; x < imsize.width; x++, p0 += d0, p1 += d1 )
                {
                    size_t idx;
                    if( mask[x] && (idx = tab[*p0] + tab[*p1 + 256]) < OUT_OF_RANGE )
                        ++*(int*)(H + idx);
                }
			}
        }
    }
    else if( dims == 3 )/*3維直方圖--與2維的類似,只是多了一維,尋找直方圖位置的idx的計算複雜了*/
    {
        int d0 = deltas[0], step0 = deltas[1],
            d1 = deltas[2], step1 = deltas[3],
            d2 = deltas[4], step2 = deltas[5];
        
        const uchar* p0 = (const uchar*)ptrs[0];
        const uchar* p1 = (const uchar*)ptrs[1];
        const uchar* p2 = (const uchar*)ptrs[2];
        /**/
        for( ; imsize.height--; p0 += step0, p1 += step1, p2 += step2, mask += mstep )
        {
            if( !mask )/**/
                for( x = 0; x < imsize.width; x++, p0 += d0, p1 += d1, p2 += d2 )
                {
					/*第0+1+2維的偏移獲得總的偏移量*/
                    size_t idx = tab[*p0] + tab[*p1 + 256] + tab[*p2 + 512];
                    if( idx < OUT_OF_RANGE )
                        ++*(int*)(H + idx);
                }
            else/**/
                for( x = 0; x < imsize.width; x++, p0 += d0, p1 += d1, p2 += d2 )
                {
                    size_t idx;
                    if( mask[x] && (idx = tab[*p0] + tab[*p1 + 256] + tab[*p2 + 512]) < OUT_OF_RANGE )
                        ++*(int*)(H + idx);
                }
        }
    }
    else/*高維直方圖(>3維)*/
    {
        for( ; imsize.height--; mask += mstep )
        {
            if( !mask )/*無掩膜圖像計算*/
                for( x = 0; x < imsize.width; x++ )
                {
                    uchar* Hptr = H;
                    for( i = 0; i < dims; i++ )
                    {
                        size_t idx = tab[*ptrs[i] + i*256];
                        if( idx >= OUT_OF_RANGE )
                            break;
                        Hptr += idx;
                        ptrs[i] += deltas[i*2];
                    }
                    /*判斷是否有超限的--即前面設置的在ranges範圍外的設置爲了OUT_OF_RANGE,統計數據時需要跨過這些值*/
                    if( i == dims )
                        ++*((int*)Hptr);
                    else
                        for( ; i < dims; i++ )
                            ptrs[i] += deltas[i*2];
                }
            else/*使用掩模圖像*/
                for( x = 0; x < imsize.width; x++ )
                {
                    uchar* Hptr = H;
                    int i = 0;
                    if( mask[x] )
                        for( ; i < dims; i++ )
                        {
                            size_t idx = tab[*ptrs[i] + i*256];
                            if( idx >= OUT_OF_RANGE )
                                break;
                            Hptr += idx;
                            ptrs[i] += deltas[i*2];
                        }
                    
                    if( i == dims )
                        ++*((int*)Hptr);
                    else
                        for( ; i < dims; i++ )
                            ptrs[i] += deltas[i*2];
                }
			/*字節對齊偏移*/
            for( i = 0; i < dims; i++ )
                ptrs[i] += deltas[i*2 + 1];
        }
    }
}


 

在calcHist_8u中使用了一個查找表用於加快處理速度,生成查找表的函數如下

static const size_t OUT_OF_RANGE = (size_t)1 << (sizeof(size_t)*8 - 2);

/*
建立查找表--即,將8u每個灰度值映射到相應的bin值上加快統計直方圖的計算速度,
_tab中是從直方圖的每一維0-255的值到輸出直方圖中的位置的映射,即tab中的index是第0維的0~255,第1維的0~255...第dims-1維的0~255;
而value則是每一個0~255的值映射到最終直方圖中的第i維的偏移【即相對於該維數據首地址的偏移量】。不在要求範圍之內的設爲OUT_OF_RANGE
*/
static void
calcHistLookupTables_8u( const Mat& hist, const SparseMat& shist,
                         int dims, const float** ranges, const double* uniranges,
                         bool uniform, bool issparse, vector<size_t>& _tab )
{
/*上下限?*/
    const int low = 0, high = 256;
    int i, j;
/*調整大小*/
    _tab.resize((high-low)*dims);
    size_t* tab = &_tab[0];
/*均勻直方圖*/    
    if( uniform )
    {
	/**/
        for( i = 0; i < dims; i++ )
        {
		/*
			uniranges的值在histPrepareImages函數中
		    double _low = ranges[i][0], _high = ranges[i][1];
            double t = histSize[i]/(_high - _low);
            uniranges[i*2] = t;
            uniranges[i*2+1] = -t*_low;
		*/
            double a = uniranges[i*2];
            double b = uniranges[i*2+1];
			/*是否是稀疏直方圖*/
            int sz = !issparse ? hist.size[i] : shist.size(i);
            size_t step = !issparse ? hist.step[i] : 1; //step中的是輸出直方圖的第i維的步長
            /*給tab賦值*/
            for( j = low; j < high; j++ )
            {
				/*
				--計算low-high範圍內的每個值被劃分到bin中哪一個
				idx=cvFloor(j*a+b)=cvFloor(j*t-t*_low)=cvFloor(t*(j-_low))=cvFloor(histSize[i]*(j-_low)/(_high-_low));
				--將a,b的值進行了替換,a,b的值見上面;而histSize[i]中的是給定的第i維的bin的個數,low是8u的下限0,_high和_low則是用戶給出的range範圍。
				*/
                int idx = cvFloor(j*a + b);
                size_t written_idx;
				/*
				超出大小範圍
				--沒有超範圍,要寫入的索引是idx與步長的乘積??
				--超出範圍,設置爲次高位爲1,其他位爲0的size_t的數。
				*/
                if( (unsigned)idx < (unsigned)sz )
                    written_idx = idx*step;
                else
                    written_idx = OUT_OF_RANGE;
                /*給tab賦值--tab中的idx爲每一維的灰度值,value爲該灰度值映射到輸出直方圖中的第i維的偏移--*/
                tab[i*(high - low) + j - low] = written_idx;
            }
        }
    }
    else
    {
		/*非均勻直方圖*/
        for( i = 0; i < dims; i++ )
        {
			/*取得ranges下限?*/
            int limit = std::min(cvCeil(ranges[i][0]), high);
            int idx = -1, sz = !issparse ? hist.size[i] : shist.size(i);
            size_t written_idx = OUT_OF_RANGE;
            size_t step = !issparse ? hist.step[i] : 1;
            
			/*
			根據指定的ranges各段的上下限設置非均勻直方圖的映射表;
			對於[low,ranges_low)和(ranges_high,high)範圍的值映射爲超限,
			中間的值每段映射到相應的值。採用對整個範圍按索引小->大循環,循環中
			再採用對分段範圍循環賦值,然後在大循環中修改小循環的循環跳出值的方法
			------循環一遍即可遍歷整個範圍,方法很好!!
			*/
            for(j = low;;)
            {
				/*對於從low到limit範圍的值映射爲指定的written_idx*/
                for( ; j < limit; j++ )
                    tab[i*(high - low) + j - low] = written_idx;
                /*
				取得ranges的下一個端點,並將其設爲下一個的bin的上限,
				並且計算該段數值對應的映射到輸出直方圖中的偏移位置。
				*/
                if( (unsigned)(++idx) < (unsigned)sz )
                {
                    limit = std::min(cvCeil(ranges[i][idx+1]), high);
                    written_idx = idx*step;
                }
                else
                {	
					/*從指定範圍上限到high的值設爲超限--退出循環*/
                    for( ; j < high; j++ )
                        tab[i*(high - low) + j - low] = OUT_OF_RANGE;
                    break;
                }
            }
        }
    }
}


整個對8U深度的圖像的直方圖統計就涉及到這幾個函數,以上註釋的錯誤之處還望不吝賜教!

calcHist的學習就到此爲止。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章