Hessian矩陣以及在血管增強中的應用——OpenCV實現【2024年更新】

有別於廣爲人知的Sobel、Canny等一階算法,基於Hessian矩陣能夠得到圖像二階結果,這將幫助我們深入分析圖像本質。

Hessian矩陣在圖像處理中有着廣泛的應用:其中在圖像分割領域,包括邊緣檢測、紋理分析等;在圖像增強領域,包括邊緣增強、邊緣消除等。

本文從Hessian矩陣定義出發,通過清晰簡潔的數學推導和講解實現公式到C++代碼的轉化。爲了幫助深入理解Hessian矩陣在圖像處理中的能力,我們將詳細講解和實現經典的“血管增強”算法(Frangi算法)。需要注意的是:

1、由於本文代碼基於OpenCV基礎庫,所以題目中添加了“OpenCV實現”字樣。

2、由於圖像的二維特性,所以下文中所有“Hessian矩陣”都特指“二維Hessian矩陣”。

一、理論基礎

這裏的基礎理論有點多,你可以先過一遍,然後在讀代碼的時候再回過頭來加深理解,這樣效果比較好。

1. Hessian矩陣的由來及定義

2024-04-25 14-00-18

2024-04-25 14-01-00

我們一般將其表示爲:
5f4634ef-c34f-4d7d-8c3e-ebf95527ada1

我們也可以將其簡寫爲:

eb250ea9-03dd-4a8e-a985-fe12f23c2c52

2.數字圖像處理之尺度空間理論

尺度空間理論的基本思想是:在圖像信息處理模型中引入一個被視爲尺度的參數,通過連續變化尺度參數獲得多尺度下的尺度空間表示序列,對這些序列進行尺度空間主輪廓的提取,並以該主輪廓作爲一種特徵向量,實現邊緣、角點檢測和不同分辨率上的特徵提取等。

尺度空間理論的特點是:將傳統的單尺度圖像信息處理技術納入尺度不斷變化的動態分析框架中,更容易獲取圖像的本質特徵。尺度空間中各尺度圖像的模糊程度逐漸變大,能夠模擬人在距離目標由近到遠時目標在視網膜上的形成過程。

尺度空間理論的一個直觀理解:我們人在遠近不同距離觀察同一物體,可以獲得不同認知;同時角度和位移的變化也是這樣。(比如下圖)

高斯卷積核是實現尺度變換的唯一線性核(個人認爲高斯函數是最偉大和最重要的函數,它某種程度可以讓自然的規律被量化)。

一幅圖像的尺度空間可以定義爲:

95321178-720e-4ba7-ba01-145be7f97f05

其中符號"*"表示卷積操作。

σ是尺度空間因子,值越小表示圖像被平滑的越少,相應的尺度也就越小。大尺度對應於圖像的概貌特徵,小尺度對應於圖像的細節特徵。

3.基於尺度理論的Hessian簡化算法

對於二維圖像I,它的Hessian矩陣描述每個像素在主方向上的二維導數爲:

e6ab9071-0cf9-4ff2-af38-e63c11f856fc

根據尺度空間理論,二階導數可以通過圖像與高斯函數的卷積獲得,例如,在點(x,y)處有

83ae7d1e-78c4-47a3-bab2-33509eb02cc3

其中,e008af61-fc62-43ff-a8bb-075ca1680373爲尺度爲c9d1dbd1-c280-4494-97b3-00b05eebd3ae的高斯函數。

如果我們接受這個理論,那麼就可以得到推論:

對全圖直接做偏導操作  = 對原圖以特定的高斯核做卷積

基於這個推論,對於圖的Hessian運算將極大地降低計算量,同時提高運算速度。(同時將計算的複雜度轉化爲對高斯運算的優化)

4.Hessian矩陣特徵值的求解方法

首先回憶高等數學的知識,根據定義求二階矩陣的特徵值:

根據定義,對於矩陣A,它的特徵值滿足

|λE-A|=0

其中

E是二階對角陣

(1 0)

(0 1)

我們表示A爲
|a   b|
|c   d|

則λE-A|

= (λ-a)(λ-d)-bc

= λ^2-(a+d)λ+ad-bc = 0
這個是一元二次方程,可以計算得到有兩個解,分別爲
λ1=(a+d+√((a-d)^2+4bc))/2
λ2=(a+d-√((a-d)^2+4bc))/2

由前面的資料,我們已經得到了Hessian矩陣的定義:

6cd8821b-ec70-4ae8-86be-00e7036721bd

根據二維矩陣特徵值的定義:“設A是n階方陣,如果存在數m和非零n維列向量 x,使得 Ax=mx 成立,則稱 m 是矩陣A的一個特徵值(characteristic value)或本徵值(eigenvalue)”,可以得到等式

317a2915-2742-41eb-af72-9ccb8211cd5c

並且根據圖像的特性,可以得到

3a14848d-1d19-47b1-919a-a42c03a44e97=e9e014e6-1c31-4165-afa2-1eb37a6d4417

帶入以上方程得到Hessian的特徵值的解:(這個非常類似我們求一元二次方程的口訣,不過是對於二元的情況)

86774ddb-d38d-4d67-b102-a9ba908c885f

請記住這個結論,我們在代碼部分將再一次提及。

5.Hessian矩陣特徵值的圖像性質

一個Hessian矩陣可以分解爲兩個特徵值以及定義的特徵向量。77c14693-0117-4ddf-9a78-142b7122f67cb16b53a0-65c4-4720-8a83-1045f3fdc557

其中最大的絕對特徵值730ff1e4-b436-46a8-b4c5-75b171a17c14表示最大的局部灰度變化,其特徵向量則代表它方向,可以認爲是切線方向;而較小的那個代表垂直方向,也就是法線方向。

這張圖可以很好地表明切線和法線的概念。

7bfc87dd-f580-4b9d-b0c5-93da1f47405e

這些都將在下面的算法中得到利用。

6.高斯方程及二階導數

前面提到了高斯函數,這裏補充一些知識,下面有用。

高斯分佈即正態分佈,其曲線圖如下:

668d8447-23e4-41c6-b9cc-e47933a49ac7

二維高斯分佈,其曲線圖如下:

3e61ce3d-7982-4a72-8624-95a53233576d

根據定義,我們可以求得一下內容。

二維高斯函數的一階偏導數:

12324fa2-fc84-4ae3-b0d0-d9c22ea42334

二維高斯函數的二階偏導數

6960b03b-471d-4ea0-8db7-0efad849ac02

這樣我們的數學方面的基礎知識準備完畢。

二、“血管增強”算法的原理

Hessian矩陣及其特徵值能夠很好地描述常見的幾何形狀的信息,我們將利用它進行血管增強;Hessian矩陣的簡化算法將爲我們代碼化提供可能方法。我們主要基於最著名的“Frangi濾波”論文。

  Frangi A F, Niessen W J, Vincken K L, et al. 《Multiscale vessel enhancement filtering》[C]//International Conference on Medical Image Computing and Computer-Assisted Intervention. Springer Berlin Heidelberg, 1998: 130-137.

1.認識血管及其增強

在採集到的圖片中,血管應該呈現“管狀網絡”結構。

82c94fb9-4984-4c19-ae1b-e7ae81c3b98a

比如這張比較標準的圖片,在近紅外光和濾光片的處理下,人眼就可以比較有效地識別出血管情況。這就爲我們進行圖像處理提供基本依據,上面提高的“Frangi濾波”算法本身就是用來識別血管一類管線目標的。

2.Frangi論文基本原理

基於前面我們說明的“加速算法”,首先將血管在多尺度下進行Gaussian濾波處理,然後計算每個像素點的二階導數構造Hessian矩陣,並且計算出兩個特徵值(這個地方在代碼實現的時候有技巧)。

雖然我們已經得到了Hessian矩陣及其特徵值,從圖像上已經能夠看出增強的效果,但是這還不夠。接下來

將求得的特徵值帶入事先建立好的血管相似性函數中獲取在不同尺度下的濾波響應。

5dea7036-0dd9-42e6-9905-74fe2fab48ea

當尺度和局部結構匹配時計算得到最大濾波響應,從而判斷當前像素點是否屬於血管結構。

爲了儘可能地得到增強的效果,在論文中採用的是“多尺度”疊加的方法,具體來說就是採用不同的卷積核同時進行處理,得到多張處理效果,而後對結果中“着色”效果比較好的部分進行疊加。

3.Frangi論文優缺點

該方法得到了一種被實際證明有效的血管增強方法,但是其中最爲關鍵的“血管相似性函數”其定義更像是一個實驗結果,同時本身較大較多的浮點運算難以在嵌入式系統上實時運行。最後該算法不能夠直接分割得到血管,往往用於血管分割的預處理階段。

三、編寫代碼,具體實現

下面開始講解具體代碼,整個可以運行的項目我會付到文章最後。在實現過程中,我們參考libfrangi https://ntnu-bioopt.github.io/software/libfrangi.html 提供的優質代碼進行講解,過程中我做了必要的精簡和註釋。必須要多說一句的是,這個代碼從內容到風格上,很大程度上參考了frangi的matlab版本代碼https://www.mathworks.com/matlabcentral/fileexchange/24409-hessian-based-frangi-vesselness-filter,如果你也擅長matlab,建議對比學習。

1.項目結構

首先從結構開始說明,這樣如果你有一定基礎就可以自己先進行研究,然後對比我的講解,對於學習有幫助。

edee0890-3948-467d-b893-d724276c849a

這個項目很簡單,除了main.cpp外,frangi算法封裝到了frangi.h+frangi.cpp中,以函數形式直接提供。

int main(int argc, char *argv[]) {
//使用默認參數設定Frangi
    frangi2d_opts_t opts;
    frangi2d_createopts(&opts);
//讀取圖片,進行處理
    Mat input_img = imread("E:/hand.bmp", 0);
    Mat input_img_fl;
//轉換爲單通道,浮點運算
    input_img.convertTo(input_img_fl, CV_32FC1);
//進行處理
    Mat vesselness, scale, angles;
    frangi2d(input_img_fl, vesselness, scale, angles, opts);
//顯示結果
    vesselness.convertTo(vesselness, CV_8UC1, 255);
    scale.convertTo(scale, CV_8UC1, 255);
    angles.convertTo(angles, CV_8UC1, 255);
    imshow("result", vesselness);   
}

main.cpp也儘可能簡單,除了必要的圖片格式轉換外,主要過程封裝在

frangi2d(input_img_fl, vesselness, scale, angles, opts);

打開frangi.h,我們可以看見以下內容:

//frangi濾波主要過程
void frangi2d(const cv::Mat &src, cv::Mat &J, cv::Mat &scale, cv::Mat &directions, frangi2d_opts_t opts);
////Helper functions
//計算Hessian矩陣 with parameter sigma on src, save to Dxx, Dxy and Dyy.
void frangi2d_hessian(const cv::Mat &src, cv::Mat &Dxx, cv::Mat &Dxy, cv::Mat &Dyy, float sigma);
//參數設置 (sigma_start = 3, sigma_end = 7, sigma_step = 1, BetaOne = 1.6, BetaTwo 0.08)
void frangi2d_createopts(frangi2d_opts_t *opts);
//計算特徵值 from Dxx, Dxy, Dyy. Save results to lambda1, lambda2, Ix, Iy.
void frangi2_eig2image(const cv::Mat &Dxx, const cv::Mat &Dxy, const cv::Mat &Dyy, cv::Mat &lambda1, cv::Mat &lambda2, cv::Mat &Ix, cv::Mat &Iy);

2.計算Hessian矩陣

我們來看frangi2d_hessian這個函數,正如註釋說明,它就是Hessian運算的具體實現:

//計算Hessian矩陣 with parameter sigma on src, save to Dxx, Dxy and Dyy. 
void frangi2d_hessian(const cv::Mat &src, cv::Mat &Dxx, cv::Mat &Dxy, cv::Mat &Dyy, float sigma);

其中大量的運算聚集在這裏:

for (int x = -round(3*sigma); x <= round(3*sigma); x++){
        j=0;
for (int y = -round(3*sigma); y <= round(3*sigma); y++){
            kern_xx_f[i*n_kern_y + j] = 1.0f/(2.0f*M_PI*sigma*sigma*sigma*sigma) * (x*x/(sigma*sigma) - 1) * exp(-(x*x + y*y)/(2.0f*sigma*sigma));
            kern_xy_f[i*n_kern_y + j] = 1.0f/(2.0f*M_PI*sigma*sigma*sigma*sigma*sigma*sigma)*(x*y)*exp(-(x*x + y*y)/(2.0f*sigma*sigma));
            j++;
        }
        i++;
    }
for (int j=0; j < n_kern_y; j++){
for (int i=0; i < n_kern_x; i++){
            kern_yy_f[j*n_kern_x + i] = kern_xx_f[i*n_kern_x + j];
        }
    }

這裏的具體內容是它是在計算Dxx等的卷積核,在此之前,我們得到結論:

對全圖直接做偏導操作  = 對原圖以特定的高斯核做卷積

所以這裏我們就是首先把“特定的高斯核”計算出來。然後我們回憶當時介紹的二維高斯函數的二階偏導數

6960b03b-471d-4ea0-8db7-0efad849ac02[1]

它的代碼表現形式是什麼樣子?

// 生成卷積核
//DGaussxx = 1/(2*pi*Sigma^4) * (X.^2/Sigma^2 - 1) .* exp(-(X.^2 + Y.^2)/(2*Sigma^2)); 
//DGaussxy = 1/(2*pi*Sigma^6) * (X .* Y)           .* exp(-(X.^2 + Y.^2)/(2*Sigma^2));
//DGaussyy = DGaussxx';
// c++(opencv)版本
    cv::Mat tmp1 = 1/(2*PI*Sigma*Sigma*Sigma*Sigma)*(EWM(X,X)/(Sigma*Sigma)-1);
    cv::Mat tmp2 = -1*((EWM(X,X)+EWM(Y,Y))/(2*Sigma*Sigma));
    exp(tmp2,tmp2);

接下來我們需要用這3個特定的卷積核進行卷積,這裏調用OpenCV的filter2D函數。

flip(Mat(n_kern_y, n_kern_x, CV_32FC1, kern_xx_f), kern_xx, -1);

3.Hessian特徵值的計算

我們回憶一下最前面得到的結論:

a23bd4b2-1476-4cac-83e8-78676d5b841d

然後就可以理解這裏的代碼:

//calculate eigenvectors of J, v1 and v2
    Mat tmp, tmp2;
    tmp2 = Dxx - Dyy;
    sqrt(tmp2.mul(tmp2) + 4*Dxy.mul(Dxy), tmp);
    Mat v2x = 2*Dxy;
    Mat v2y = Dyy - Dxx + tmp;
//normalize
    Mat mag;
    sqrt((v2x.mul(v2x) + v2y.mul(v2y)), mag);
    Mat v2xtmp = v2x.mul(1.0f/mag);
    v2xtmp.copyTo(v2x, mag != 0);
    Mat v2ytmp = v2y.mul(1.0f/mag);
    v2ytmp.copyTo(v2y, mag != 0);
//eigenvectors are orthogonal
    Mat v1x, v1y;
    v2y.copyTo(v1x);
    v1x = -1*v1x;
    v2x.copyTo(v1y);
//compute eigenvalues
    Mat mu1 = 0.5*(Dxx + Dyy + tmp);
    Mat mu2 = 0.5*(Dxx + Dyy - tmp);

基本上就是將數學公式翻譯成爲了代碼,注意一下.mul是OpenCV中的“點乘”方法,注意這裏:

tmp2 = Dxx - Dyy;
sqrt(tmp2.mul(tmp2) + 4*Dxy.mul(Dxy), tmp);

也就是

tmp = 5bfc10e7-0e9d-4237-adc0-0407c5c111d7

4.frangi2d主要過程

void frangi2d(const Mat &src, Mat &maxVals, Mat &whatScale, Mat &outAngles, frangi2d_opts_t opts)

函數,它是整個Frangi算法的主要過程。

for (float sigma = opts.sigma_start; sigma <= opts.sigma_end; sigma += opts.sigma_step){……}
//多尺度疊加
for (int i=1; i < ALLfiltered.size(); i++){
        maxVals = max(maxVals, ALLfiltered[i]);
        whatScale.setTo(sigma, ALLfiltered[i] == maxVals);
        ALLangles[i].copyTo(outAngles, ALLfiltered[i] == maxVals);
        sigma += opts.sigma_step;
    }

程序最外面的框架告訴我們,整個程序是多次運算(尺度)的疊加。

//根據論文定義,對結果進行修正
        Dxx = Dxx*sigma*sigma;
        Dyy = Dyy*sigma*sigma;
        Dxy = Dxy*sigma*sigma;
//calculate (abs sorted) eigenvalues and vectors
        Mat lambda1, lambda2, Ix, Iy;
        frangi2_eig2image(Dxx, Dxy, Dyy, lambda1, lambda2, Ix, Iy);

每次計算中,首先計算出15c59b60-498f-4935-9f8c-bb4e0a4f5261的值,代碼中對於的變量叫做lambda1,lambda2。

//根據定義,計算“血管增強響應函數”
        lambda2.setTo(nextafterf(0, 1), lambda2 == 0);
        Mat Rb = lambda1.mul(1.0/lambda2);
        Rb = Rb.mul(Rb);
        Mat S2 = lambda1.mul(lambda1) + lambda2.mul(lambda2);
        Mat tmp1, tmp2;
        exp(-Rb/beta, tmp1);
        exp(-S2/c, tmp2);
//保存單尺度結果
        Mat Ifiltered = tmp1.mul(Mat::ones(src.rows, src.cols, src.type()) - tmp2);

這裏就是Frangi算中最有價值的地方:“血管響應函數”,它的數學表示方法爲:

926f6ceb-d2fd-47a5-b4b3-341a96c2668e

其中2f5982cc-316f-4612-b62a-4188c1ecef89和C都是超參數,它們在代碼前面都已經根據輸入參數進行了變形。

float beta = 2*opts.BetaOne*opts.BetaOne;
float c = 2*opts.BetaTwo*opts.BetaTwo;

論文中給出了參考值,代碼中的變量名字都是對應的。你也可以根據修改查看。

在後來的文獻中,也出現了其它“血管增強”函數,比如LiQiang

429c6e9c-6dab-498c-ba80-0420fa8759f7

以及GVF

f147c106-a9bd-405e-a5e7-b08754f8e339

01f1feec-84a4-4a3a-ae04-b3a4637e9626

基於前面計算出來的特徵值,這些都很容易實現。

四、參考文獻:

1.Hessian矩陣以及在圖像中的應用 https://blog.csdn.net/lwzkiller/article/details/55050275

2.血管分割技術文獻綜述 https://blog.csdn.net/u013102349/article/details/53819314

3.《基於Hessian矩陣和熵的CT序列圖像裂縫分割方法》 王慧倩等 儀器儀表學報 2016年8月

4.百度百科 https://baike.baidu.com/item/%E9%BB%91%E5%A1%9E%E7%9F%A9%E9%98%B5/2248782?fr=aladdin

5. 數字圖像處理之尺度空間理論 https://blog.csdn.net/TJC3014583925/article/details/88836485>

6. CLAHE的實現和研究 https://www.cnblogs.com/jsxyhelu/p/6435601.html

7.《實際比較filter2D和imfilter之間的關係》https://www.cnblogs.com/jsxyhelu/p/6597544.html

8.《視覺圖像:高斯方程及各階導數》https://jingyan.baidu.com/album/5bbb5a1bedf94413eba179ba.html?picindex=2

代碼地址:jsxyhelu/libfrangi: C++/OpenCV implementation of the 2D Frangi multiscale vesselness filter. (github.com)

最後感謝大家學習至此,如果你在理解我所表述的這些內容中獲得的感悟,有我寫下它的時候得到的感悟的十分之一那麼多,那麼你一定是一名幸運的讀者。

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