說明:本文全部算法的涉及到的優化均指在PC上進行的,對於其餘構架是否合適未知,請自行試驗。c++
Box Filter,最經典的一種領域操做,在無數的場合中都有着普遍的應用,做爲一個很基礎的函數,其性能的好壞也直接影響着其餘相關函數的性能,最典型莫如如今很好的EPF濾波器:GuideFilter。所以其優化的檔次和程度是很是重要的,網絡上有不少相關的代碼和博客對該算法進行講解和優化,提出了很多O(1)算法,但所謂的0(1)算法也有優劣之分,0(1)只是表示執行時間和某個參數無關,但自己的耗時仍是有區別。比較流行的就是累積直方圖法,其實這個是很是不可取的,由於稍大的圖就會致使溢出,這裏咱們解析下 opencv的相關代碼,看看他是如何進行優化的。算法
首先找到opencv的代碼的位置,其在\opencv\sources\modules\imgproc\src\smooth.cpp中。網絡
Box Filter 是一種行列可分離的濾波,所以,opencv也是這樣處理的,先進行行方向的濾波,獲得中間結果,而後再對中間結果進行列方向的處理,獲得最終的結果。多線程
opencv 行方向處理的相關代碼以下:ide
template<typename T, typename ST>
struct RowSum :
public BaseRowFilter
{
RowSum( int _ksize, int _anchor ) :
BaseRowFilter()
{
ksize = _ksize;
anchor = _anchor;
}
virtual void operator()(const uchar* src, uchar* dst, int width, int cn)
{
const T* S = (const T*)src;
ST* D = (ST*)dst;
int i = 0, k, ksz_cn = ksize*cn;
width = (width - 1)*cn;
for( k = 0; k < cn; k++, S++, D++ )
{
ST s = 0;
for( i = 0; i < ksz_cn; i += cn )
s += S[i];
D[0] = s;
for( i = 0; i < width; i += cn )
{
s += S[i + ksz_cn] - S[i];
D[i+cn] = s;
}
}
}
};
這個代碼考慮了多個通道以及多種數據類型的狀況,爲了分析方便咱們重寫下單通道時的核心代碼。函數
for(Z = 0, Value = 0; Z < Size; Z++)
Value += RowData[Z];
LinePD[0] = Value;
for(X = 1; X < Width; X ++)
{
Value += RowData[X + Size - 1] - RowData[X - 1];
LinePD[X] = Value;
}
上述代碼中RowData是指對某一行像素進行擴展後的像素值,其寬度爲Width + Radius + Radius, Radius是值濾波的半徑, 而代碼中Size = 2 * Radius + 1,LinePD即所謂的中間結果,考慮數據類型能表達的範圍,必須使用 int類型。性能
對於每行第一個點很簡單,直接用for計算出行方向的指定半徑內的累加值。而以後全部點,用前一個累計值加上新加入的點,而後去除掉移出的哪個點,獲得新的累加值。這個方法在不少文獻中都有說起,並無什麼新鮮之處,這裏很少說。測試
按照正常的思惟,在列方向的處理應該和行方向徹底相同,只是處理的方向的不一樣、處理的數據源的不一樣以及最後須要對計算結果多一個歸一化的過程而已。事實也是如此,有不少算法都是這樣作的,可是因爲CPU構架的一些緣由(主要是cache miss的增長),一樣的算法沿列方向處理老是會比沿行方向慢一個檔次,解決方式有不少,例如先對中間結果進行轉置,而後再按照行方向的規則進行處理,處理完後在將數據轉置回去。轉置過程是有很是高效的處理方式的,藉助於SSE以及Cache優化,能實現比原始兩重for循環快4倍以上的效果。還有一種方式就正如opencv中列處理過程所示,這正是下面將要重點描述的。優化
在opencv的代碼中,並無直接沿列方向處理,而是繼續沿着行的方向一行一行處理,我先貼出我本身翻譯的有關純C的代碼進行解說:ui
for (Y = 0; Y < Size - 1; Y++) // 注意沒有最後一項哦
{
int *LinePS = (int *)(Sum->Data + ColPos[Y] * Sum->WidthStep);
for(X = 0; X < Width; X++) ColSum[X] += LinePS[X];
}
for (Y = 0; Y < Height; Y++)
{
unsigned char* LinePD = Dest->Data + Y * Dest->WidthStep;
int *AddPos = (int*)(Sum->Data + ColPos[Y + Size - 1] * Sum->WidthStep);
int *SubPos = (int*)(Sum->Data + ColPos[Y] * Sum->WidthStep);
for(X = 0; X < Width; X++)
{
Value = ColSum[X] + AddPos[X];
LinePD[X] = Value * Scale;
ColSum[X] = Value - SubPos[X];
}
}
上述代碼中定義了一個ColSum用於保存每行某個位置處在列方向上指定半徑內各中間元素的累加值,對於第一行,作特殊處理,其餘行的每一個元素的處理方式和BaseRowFilter裏的處理方式很像,只是在書寫上有所區別,特別注意的是對第一行的累加時,最後一個元素並無計算在內,這個處理技巧在下面的X循環裏有體現,請你們仔細體味下。
上述代碼這樣作的好處是,能有效的減小CPU的cache miss,可是總的計算量是沒有改變的,所以能有效的提升速度。
針對PC,在opencv內部,其在列方向上採用了SSE進行進一步的優化,咱們貼出其處理uchar類型的代碼:
代碼比較多,我稍微精簡下(注意,精簡後的是不可運行的,只是更清晰的表達了思路)。
virtual void operator()(const uchar** src, uchar* dst, int dststep, int count, int width)
{
int i;
int* SUM;
bool haveScale = scale != 1;
double _scale = scale;
if( width != (int)sum.size() )
{
sum.resize(width);
sumCount = 0;
}
SUM = &sum[0];
if( sumCount == 0 )
{
memset((void*)SUM, 0, width*sizeof(int));
for( ; sumCount < ksize - 1; sumCount++, src++ )
{
const int* Sp = (const int*)src[0];
i = 0;
for( ; i <= width-4; i+=4 )
{
__m128i _sum = _mm_loadu_si128((const __m128i*)(SUM+i));
__m128i _sp = _mm_loadu_si128((const __m128i*)(Sp+i));
_mm_storeu_si128((__m128i*)(SUM+i),_mm_add_epi32(_sum, _sp));
}
for( ; i < width; i++ )
SUM[i] += Sp[i];
}
}
else
{
src += ksize-1;
}
for( ; count--; src++ )
{
const int* Sp = (const int*)src[0];
const int* Sm = (const int*)src[1-ksize];
uchar* D = (uchar*)dst;
i = 0;
const __m128 scale4 = _mm_set1_ps((float)_scale);
for( ; i <= width-8; i+=8 )
{
__m128i _sm = _mm_loadu_si128((const __m128i*)(Sm+i));
__m128i _sm1 = _mm_loadu_si128((const __m128i*)(Sm+i+4));
__m128i _s0 = _mm_add_epi32(_mm_loadu_si128((const __m128i*)(SUM+i)),
_mm_loadu_si128((const __m128i*)(Sp+i)));
__m128i _s01 = _mm_add_epi32(_mm_loadu_si128((const __m128i*)(SUM+i+4)),
_mm_loadu_si128((const __m128i*)(Sp+i+4)));
__m128i _s0T = _mm_cvtps_epi32(_mm_mul_ps(scale4, _mm_cvtepi32_ps(_s0)));
__m128i _s0T1 = _mm_cvtps_epi32(_mm_mul_ps(scale4, _mm_cvtepi32_ps(_s01)));
_s0T = _mm_packs_epi32(_s0T, _s0T1);
_mm_storel_epi64((__m128i*)(D+i), _mm_packus_epi16(_s0T, _s0T));
_mm_storeu_si128((__m128i*)(SUM+i), _mm_sub_epi32(_s0,_sm));
_mm_storeu_si128((__m128i*)(SUM+i+4),_mm_sub_epi32(_s01,_sm1));
}
for( ; i < width; i++ )
{
int s0 = SUM[i] + Sp[i];
D[i] = saturate_cast<uchar>(s0*_scale);
SUM[i] = s0 - Sm[i];
}
dst += dststep;
}
}
在行方向上,ColSum每一個元素的更新相互之間是沒有任何關係的,所以,藉助於SSE能夠實現一次性的四個元素的更新,而且上述代碼還將第一行的特殊計算也用SSE實現了,雖然這部分計算量是很是小的。
在具體的SSE細節上,對於uchar類型,雖然中間結果是用int類型表達的,可是因爲SSE沒有整形的除法指令(是否有?),所以上面借用了浮點數的乘法SSE指令實現,固然就多了_mm_cvtepi32_ps 以及 _mm_cvtps_epi32這樣的類型轉換的函數。若是有整形除法,那就能更好了。
SSE的實現,無非也就是用_mm_loadu_si128加載數據,用_mm_add_epi32, _mm_mul_ps之類的函數進行基本的運算,用_mm_storeu_si128來保存數據,並無什麼特別複雜的部分,注意到考慮到數據的廣泛性,不必定都是16字節對齊的,所以在加載和保存是須要使用u方面的函數,其實如今的_mm_loadu_si128和_mm_load_si128在處理速度上已經沒有特別明顯的區別了。
注意到在每一個SSE代碼後面,總還有部分C代碼,這是由於咱們實際數據寬度並不必定是4的整數倍,未被SSE處理的部分必須使用C實現,這其實對讀者來講是很是好的事情,由於咱們能從這部分C代碼中搞明白上述的SSE代碼是幹什麼的。
以上就是opencv的Box Filter實現的關鍵代碼,若是讀者去看具體細節,opencv還有針對手機上的Neon優化,其實這個和SSE的意思基本是同樣的。
那是否還有改進的空間呢,從個人認知中,在對垂直方向的處理上,應該沒有了,可是水平方向呢, SSE有沒有用武之地,通過個人思考我認爲仍是有的。
在行方向的計算中,這個for循環是主要的計算。
for(X = 1; X < Width; X ++)
{
Value += RowData[X + Size - 1] - RowData[X - 1];
LinePD[X] = Value;
}
本人認爲雖然這裏的操做是先後依賴的,全局沒法並行化,可是觀察這一行代碼:
Value += RowData[X + Size - 1] - RowData[X - 1];
其中RowData[X + Size - 1] - RowData[X - 1]; 並非先後依賴的,是能夠並行的,所以若是提早快速的計算出全部的差值,那麼提速的空間還比較大,即須要提早計算出下面的函數:
for(X = 0; X < (Width - 1); X++)
Diff[X] = AddPos[X] - SubPos[X];
這裏用SSE實現則很是簡單了:
unsigned char *AddPos = RowData + Size * Channel;
unsigned char *SubPos = RowData;
X = 0; // 注意這個賦值在下面的循環外部,這能夠避免當Width<8時第二個for循環循環變量未初始化
__m128i Zero = _mm_setzero_si128();
for (; X <= (Width - 1) * Channel - 8; X += 8)
{
__m128i Add = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i const *)(AddPos + X)), Zero);
__m128i Sub = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i const *)(SubPos + X)), Zero);
_mm_store_si128((__m128i *)(Diff + X + 0), _mm_sub_epi32(_mm_unpacklo_epi16(Add, Zero), _mm_unpacklo_epi16(Sub, Zero))); // 因爲採用了_aligned_malloc函數分配內存,但是使用_mm_store_si128
_mm_store_si128((__m128i *)(Diff + X + 4), _mm_sub_epi32(_mm_unpackhi_epi16(Add, Zero), _mm_unpackhi_epi16(Sub, Zero)));
}
for(; X < (Width - 1) * Channel; X++)
Diff[X] = AddPos[X] - SubPos[X];
和列方向的SSE處理代碼不一樣的是,這裏加載的是uchar數據,所以加載的函數就有所不一樣,處理的方式也有區別,上面幾個SSE函數各位查查MSDN就能理解其意思,仍是頗有味道的。
通過這樣的處理,通過測試發現,速度可以比opencv的版本在提升30%,也是額外的驚喜。
再有的優化可能提速有限,好比把剩下的一些for循環內部分紅四路等等。
在個人I5臺式機中,使用上述算法對1024*1024的三通道彩色圖像進行半徑爲5的測試,進行100次的計算純算法部分的耗時爲800ms,若是是純C版本大概爲1800ms;當半徑爲200時,SSE版本約爲950ms, C版本約1900ms;當半徑爲400時,SSE版本約爲1000ms, C版本約2100ms;可見,半徑增大,耗時稍有增長,這主要是因爲算法中有部分初始化的計算和半徑有關,可是這些都是不重要的。
在不使用多線程(雖然本算法很是適合多線程計算),不使用GPU,只使用單線程\CPU進行計算的狀況下,我的以爲目前我這個代碼是很難有質的超越的,從某個方面講,代碼中的用於計算的時間佔用的時間比從內存等待數據的時間可能還要短,而彷佛也沒有更好的算法能進一步減小內存數據的訪問量。
本人在這裏邀請各位高手對目前我優化的這個代碼進行進一步的優化,但願高人不要謙虛。
運行界面:
本文的代碼是針對經常使用的圖像數據進行的優化處理,在不少場合下,須要對int或者float類型進行處理,好比GuideFilter,若是讀者理解了本文解讀的代碼的原理,更改代碼以便適應他們則是一件很是簡單的事情,若是您不會,那麼也請你不要給我留言,或者我能夠有償提供,由於從本質上講我喜歡提供漁,而不是魚,漁具已經送給你了,你卻找我要魚,那隻能..............。
本文源代碼下載地址(VS2010編寫):Box Filter
****************************做者: laviewpbt 時間: 2015.12.17 聯繫QQ: 33184777 轉載請保留本行信息**********************