以前看過matlab的bwlookup函式,但是總感覺有點神秘,一直沒有去仔細分析,最近在分析計算二值影像的尤拉數時,發現自己寫的程式碼和matlab的總是對不少,於是又去翻了下matlab的原始碼,看到了matlab裡實現尤拉數的程式碼非常簡單,如下所示:
if n==4
lut = 4*[0 0.25 0.25 0 0.25 0 .5 -0.25 0.25 0.5 0 -0.25 0 ...
-0.25 -0.25 0] + 2;
else
lut = 4*[0 0.25 0.25 0 0.25 0 -.5 -0.25 0.25 -0.5 0 -0.25 0 ...
-0.25 -0.25 0] + 2;
end
% Need to zero-pad the input
b = padarray(a,[1 1],'both');
weights = bwlookup(b,lut);
if coder.isColumnMajor
e = (sum(weights(:),'double') - 2*numel(b)) / 4;
else
e = (sum(sum(weights,2,'double'),1,'double') - 2*numel(b)) / 4;
end
在內部就是呼叫了bwlookup函式,那沒辦法了,就仔細看了M的幫助文件,發現原來這個函式真的非常簡單,我們貼下M的幫助文件:
A = bwlookup(BW,lut)
The bwlookup
function performs these steps to determine the value of each pixel in the processed image A
:
-
Locate the pixel neighborhood in input image
BW
based on the coordinates of the target pixel inA
. The function zero-pads border pixels of imageBW
when the neighborhood extends past the edge ofBW
. -
Calculate an index,
idx
, based on the binary pixel pattern of the neighborhood. -
Set the target pixel in
A
as the value of the lookup table at the indexidx
, in other words, the value oflut(idx)
.
如果當前的點位於第二行第一列,則其2*2的領域範圍的畫素資訊為:
0 0 1 1
按照位置資訊即對應的值計算索引為: 1 + 0 * 1 + 1 * 2 + 0 * 4 + 1 * 8 = 11, 對用的查表結果為 lut[11] = 2。
使用類似的計算方法可以得到其他位置經過查表後的結果為:
6 13 12 11 2 8 14 3 12 11 6 6 14 3 6 6
注意,以上所有的下標起點都是1(matlab內部的約定),而且行列順序和我們常用的C語言的二維陣列也是掉了邊的。
除了剛剛說的尤拉數的計算(其實沒有搞懂尤拉數里的那個查詢表是如何設計的,且8領域的尤拉數也是用的2*2的表,而不是3*3的),我們以matlab的bwarea函式為例,說明這個函式的價值。
matlab中關於bwarea函式的定義為:
bwarea
透過對影像中每個畫素的面積求和來估計影像中所有 on
畫素的面積。單個畫素的面積是透過觀察其 2×2 鄰域來確定的。有六種不同的情形,每種情形表示一個不同面積:
-
具有零個
on
畫素的情形(面積 = 0) -
具有一個
on
畫素的情形(面積 = 1/4) -
具有兩個相鄰
on
畫素的情形(面積 = 1/2) -
具有兩個對角
on
畫素的情形(面積 = 3/4) -
具有三個
on
畫素的情形(面積 = 7/8) -
具有所有四個
on
畫素的情形(面積 = 1)
用圖形化的表達方式上述六種情況對應如下(為了顯示,使用綠色表示黑色值,紅色表示白色值):
索引值 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
面積值 0 1/4 1/4 1/2 1/4 1/2 3/4 7/8 1/4 3/4 1/2 7/8 1/2 7/8 7/8 1
把面積值放大八倍得到面積值依次為: 0 2 2 4 2 4 6 7 2 6 4 7 4 7 7 8,我們翻看matlab的bwarea函式,可以得到其程式碼如下:
lut = [0 2 2 4 2 4 6 7 2 6 4 7 ...
4 7 7 8];
% Need to zero-pad the input.
bb = false(size(b)+2);
bb(2:end-1,2:end-1) = b;
weights = bwlookup(bb,lut);
a = sum(weights(:),[],'double')/8;
查詢表和這裡的一摸一樣。
使用查詢表的優點是把領域所有的組合提前計算出結果來,而不用到程式裡進行一些列複雜的判斷,這樣在很多情況下是可以提高速度的,比如剛剛這個面積計算的東西,如果在迴圈內部做,則要做N種判斷,那如果是涉及到3*3的領域,那就更為複雜了。
樸素的來說,有了bwlookup的原理,要把這個演算法移植到C++中,就是一個比較簡單的過程了,假如是處理16元素的表,也就是涉及到了4個畫素,假定他們的值分別為P0\P1\P2\P3,則最基本的C程式碼如下所示:
int Index = 0;
if (P0 == 255) Index += 1;
if (P1 == 255) Index += 2;
if (P2 == 255) Index += 4;
if (P3 == 255) Index += 8;
LinePD[X] = Lut[Index];
處理512個元素的過程也是類似的,只不過索引值多加了幾個(注意,這裡用255表示白色,而不是1)。
如果考慮指令集最佳化,一個自然的翻譯過程如下所示:
__m128i Index = _mm_setzero_si128();
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(1)), _mm_cmpeq_epi8(P0, _mm_set1_epi8(255)));
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(2)), _mm_cmpeq_epi8(P1, _mm_set1_epi8(255)));
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(4)), _mm_cmpeq_epi8(P2, _mm_set1_epi8(255)));
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(8)), _mm_cmpeq_epi8(P3, _mm_set1_epi8(255)));
_mm_storeu_si128((__m128i*)(LinePD + X), _mm_shuffle_epi8(Lut128, Index));
一次性處理16個位元組,最為關鍵的一點是,這個查表可以方便的直接使用_mm_shuffle_epi8非常高效的實現,這個在我前期的文章裡多次提到(16個位元組表的查詢,我們假定lut的型別為位元組型別,這個在實際的應用中也足夠了),
__m128i Index = _mm_setzero_si128();
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(1)), P0, _mm_set1_epi8(255));
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(2)), P1, _mm_set1_epi8(255));
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(4)), P2, _mm_set1_epi8(255));
Index = _mm_blendv_epi8(Index, _mm_add_epi8(Index, _mm_set1_epi8(8)), P3, _mm_set1_epi8(255));
_mm_storeu_si128((__m128i*)(LinePD + X), _mm_shuffle_epi8(Lut128, Index));
另外,再觀察這個C語言的特殊性,我們可以把上述C程式碼修改為:
int Index = 0;
Index += (1 & P0);
Index += (2 & P1);
Index += (4 & P2);
Index += (8 & P3);
LinePD[X] = Lut[Index];
即直接使用位運算替換掉那個判斷,這樣對應的SSE指令也可以進行修改為如下程式碼:
__m128i Index = _mm_setzero_si128();
Index = _mm_add_epi8(Index, _mm_and_si128(_mm_set1_epi8(1), P0));
Index = _mm_add_epi8(Index, _mm_and_si128(_mm_set1_epi8(2), P1));
Index = _mm_add_epi8(Index, _mm_and_si128(_mm_set1_epi8(4), P2));
Index = _mm_add_epi8(Index, _mm_and_si128(_mm_set1_epi8(8), P3));
// 可以直接接用shuffle實現查詢表
_mm_storeu_si128((__m128i*)(LinePD + X), _mm_shuffle_epi8(Lut128, Index));
相比於原始的SSE程式碼,這個改動也約有30%的效能提升的。
對於512個元素的表,情況會有所不同,因為索引值大於了255,所以已經無法用位元組型別來儲存累加後得到的索引了,至少的用ushort型別,但是呢,前8個位置的索引相加還是不會超出位元組型別的,所以前8個位置還是可以一次性處理16個畫素,到最後一個位置時,單獨轉換為16位,然後再接用2次16資料的處理指令,就可以一次性得到16個位元組對應的查詢表索引。
注意,512個元素的查表如果是純C++程式碼, 最後一個的 & 操作,一定要注意資料範圍,不能寫成 Index += (256 & P9);, 而是 Index += (256 & (65280 + P9));
但是這個時候,由於表的大小時512,所以已經無法使用SSE去最佳化這個查表功能了,只能把索引值單個的提取出來,然後用普通的C語言去執行程式碼。如果CPU能支援AVX2,那麼AVX2倒是又有關指令能直接查表,不過也要做很多的改造工程。
我們測試,對於512個元素的表,最佳化後的SSE指令處理一副 3000*2000的二值圖,大概耗時在2ms不到,速度還是相當的快的。
搜尋matlab的程式碼,除了bweuler及bwarea內使用了bwlookup,另外就是bwmorph裡也大量的使用了bwlookup,而且都是使用的512個元素的表,也就是說使用3*3的領域,我們看bwmorph的幫助文件,有很多相關內容:
這些對應的查詢表在 MATLAB\R2023b\toolbox\images\images\+images\+internal 目錄下以lut開頭的檔案中可以找到,比如說,這個clean的表內容如下:
這些表的內容都是提前設計好的,然後可以多次重複的呼叫,從而得到某種效果。
一般來說,這種3*3的領域運算元,在迭代了一定的次數後效果就不會有變化了。
我們在morph中也能看到一些常用的運算元,比如這個remove就可以得到二值影像的邊界,這個majority可以平滑噪音(和我部落格裡專門講的那個majority效果還是有差異的)。
原圖 remove majority
fatten skeleton thin
個人感覺這個方法在處理一些比較複雜的領域資訊時還是很有幫助的,特別是不方便用判斷條件一個一個的寫的,如果能提前把這個表弄出來,那就效率能得到很大的提升的。
在個人的SSE Demo裡,也整合了這個演算法,其內容在Binary(二值處理)--》Processing(後處理)-->> Morph(形態學)中,有興趣的朋友可以看看。
SSE Demo下載地址: https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar