1. 程式人生 > >用MATLAB求影象直方圖的演算法

用MATLAB求影象直方圖的演算法

Matlab的影象工具箱中有計算直方圖的函式imhist。然而,課程設計總是有很多限制,比如這次的影象處理課程設計,對於影象處理工具箱的使用是有限制的。

      所以得自己寫計算直方圖的演算法。我看了一下imhist的程式碼,發現它呼叫了MEX,所以速度很快。可是我對於如何編寫MEX檔案沒有研究,手頭資料又比較有限,而且時間也很倉促,這兩週對付了六門考試和兩個課程設計……

      我以前只知道m語言的迴圈慢,但有多慢,不大瞭解。

      一直在考慮如何避免使用迴圈,想到的一種辦法是將影象的2D矩陣轉換成向量,並和向量0:255利用meshgrid構成兩個矩陣,然後對這兩個矩陣進行‘==’運算,對每列求和即可得到直方圖,但是我算了一下,發現這種方法記憶體消耗是非常驚人的,以至於在Matlab裡試驗時Matlab直接提示記憶體不足,而無法執行。

      後來決定使用一種折中的方法,使用一個規模不是很大的迴圈來進行計算,在這種思想的指導下,產生了如下的程式碼:

function bars=histogram(I)
%用==來提取某個灰度的畫素
%並用sum來計算個數
tic
bars=zeros(1,256);
for value=0:255
    bars(value+1)=sum(value==I(:));
end
bars=bars./numel(I);
toc

      tic和toc是用來設定計時器,以測試函式的效能。

      以如下方式使用這個函式:

      首先讀取一幅影象,例如:

>>RGB=imread('1.jpg');

      轉換為灰度圖:

>>I=rgb2gray(RGB);

      獲取直方圖:

>>bars=histogram(I);

      顯示直方圖:

>>bar(0:255,bars);

      有一點需要注意的是得到的直方圖向量是進行了歸一化的,即bars向量的每個分量只是對應的灰度在原圖中佔有的比例,而不是實際數值,這樣做是為了之後的直方圖均衡化處理更方便。

      這段程式碼的效能如何?對一幅540*720的灰度圖象,耗時3.304s。

      所以這段程式碼實在是太慢了,看來迴圈實在是m語言的一大忌諱。

      那麼能不能找到一種不用迴圈的方法呢?我又想了一種方法:

      首先將bars構造成一個1x256的0向量,然後使用如下語句計算直方圖:

bars(I(:))=bars(I(:))+1

      我考慮也許這樣就可以了,但是事與願違,這樣只會使影象出現過的畫素值在bars的對應位置中置1,而不會計算總數。

      今天中午吃飯的時候,我覺得可以利用diff(差分)的方法來避免迴圈,以下用例子來說明想法:

      有一個影象矩陣I:

>>I=[1 2 0;3 2 3;4 3 1]

I =

1 2 0
3 2 3
4 3 1

      將I變成一個向量並排序:

>> I=sort(I(:)).'

I =

0 1 1 2 2 3 3 3 4

      對I求diff:

>> A=diff(I)

A =

1 0 1 0 1 0 0 1

      在A的末尾補一個1:

>> A=[A 1]

A =

1 0 1 0 1 0 0 1 1

      將A與向量1:numel(I)對應項相乘:

>> A=A.*(1:numel(I))

A =

1 0 3 0 5 0 0 8 9

      將A中的非0元素取出:

>> A=nonzeros(A).'

A =

1 3 5 8 9

      在A的首部補一個0:

>> A=[0 A]

A =

0 1 3 5 8 9

      再求一次diff:

>> A=diff(A)

A =

1 2 2 3 1

      通過A的結果我們可以看到,原影象中有一個0,兩個1,兩個2,三個3,一個4。即最後的A就是影象的直方圖。

      利用這種思路,我重寫了histogram函式(注意現在的histogram還有一個嚴重的bug,所以不要使用下面的程式碼)function bars=histogram(I)

tic
bars=double(diff([0;nonzeros([diff(uint32(sort(I(:))));1].*uint32(1:numel(I)).')]).')./numel(I);
toc

      和先前那個函式一樣,對結果進行了歸一化。

      這段程式碼的效率怎樣呢?對付那幅540x720的影象,耗時0.12s,比開始的那個histogram快了27.5倍。

      但是這段程式碼有一個嚴重的bug,吃晚飯的時候我想,如果影象中沒有某個灰度的畫素,那麼這段程式碼肯定會得到極其錯誤的結果,例如:

>> I=[0 1 1;3 1 4;1 0 1]

I =

0 1 1
3 1 4
1 0 1

      這個矩陣裡沒有2。重複上面的過程,看我們得到什麼樣的結果:

>> I=sort(I(:)).'

I =

0 0 1 1 1 1 1 3 4

>> A=diff(I)

A =

0 1 0 0 0 0 2 1

>> A=[A 1]

A =

0 1 0 0 0 0 2 1 1

>> A=A.*(1:numel(I))

A =

0 2 0 0 0 0 14 8 9

>> A=nonzeros(A).'

A =

2 14 8 9

>> A=[0 A]

A =

0 2 14 8 9

>> A=diff(A)

A =

2 12 -6 1

      可以看出,完全是牛頭不對馬嘴。為什麼會這樣,原因就在於原矩陣中缺乏灰度為2的畫素導致排序後的向量不是連續地變化。怎樣解決這個看似棘手的問題呢?吃完飯的時候我就想出來了:將原來的影象補充每種灰度的畫素一個,然後對求得的直方圖向量每個元素減一,即可,如:

>> I=[0 1 1;3 1 4;1 0 1]

I =

0 1 1
3 1 4
1 0 1

>> I=sort([I(:).' 0:4])

I =

0 0 0 1 1 1 1 1 1 2 3 3 4 4

>> A=diff(I)

A =

0 0 1 0 0 0 0 0 1 1 0 1 0

>> A=[A 1]

A =

0 0 1 0 0 0 0 0 1 1 0 1 0 1

>> A=A.*(1:numel(I))

A =

0 0 3 0 0 0 0 0 9 10 0 12 0 14

>> A=nonzeros(A).'

A =

3 9 10 12 14

>> A=[0 A]

A =

0 3 9 10 12 14

>> A=diff(A)

A =

3 6 1 2 2

>> A=A-1

A =

2 5 0 1 1

      從A可看出,0有1個,1有5個,2有0個,3有一個,4有一個。也就是通過補充每種灰度的畫素各一個的方式使排序後的向量連續,最後減1即可去除加入的畫素的影響,所以最終實用的histogram函式如下:

function bars=histogram(I)

tic
I=[I(:);(0:255).'];
bars=(double(diff([0;nonzeros([diff(uint32(sort(I)));1].*uint32(1:numel(I)).')]).')-1)./(numel(I)-256);
toc

      所以m語言的迴圈實在是……你看上面的程式碼,又是sort又是diff,還比使用了迴圈的程式碼快那麼多。

      晚上的時候我想,如果用傳統的思路寫這個函式,會有多慢呢?試了一下:

function bars=histogram(I)

tic
bars=zeros(1,256);
I=uint16(I);
for v=1:numel(I)
    bars(I(v)+1)=bars(I(v)+1)+1;
end
bars=bars./numel(I);
toc

      我想它一定很慢,因為對付那幅540x720的影象,它要迴圈388800次。

      可是結果卻出乎我的意料:耗時0.04s,是我寫的幾個函式中速度最快的了!居然會有這種事情!我費盡心機不用迴圈,然而最簡單的使用迴圈的程式碼卻成了最快的程式碼!糊塗了。到底怎樣才能寫出最快的程式碼呢?