OpenCV实战 | Hessian矩阵以及在血管增强中的应用

点击上方“小白学视觉”,选择加"星标"或“置顶

重磅干货,第一时间送达

术语解释

- 由于本文代码基于OpenCV基础库,所以题目中添加了“OpenCV实现”字样。

- 由于图像的二维特性,所以下文中所有“Hessian矩阵”都特指“二维Hessian矩阵”。

Hessian矩阵等相关理论基础

这里的基础理论有点多,你可以先过一遍,然后在读代码的时候再回过头来加深理解,这样效果比较好。

1. Hessian矩阵的由来及定义

由高等数学知识可知,若一元函数f(x)

b20bfeef8c778c1013bafa37632a2ff8.png

点的某个邻域内具有任意阶导数,则

 aaefd113ffb71173482a2d4343c1a055.png在 352829ff7ac0c793f1a161ae72de33ec.png点处的泰勒展开式为: 

030d61c2e7f33fbb4bbfc044b3343eb6.png

其中c29394ffd87dd7f7f2d5dc8946f7413d.png39d9393e4d82270c502f1eafe465b42b.png

二元函数138da0526258784cd269d25e9b0e50f6.png374d8c2f8e3445ad775c117f31aa7529.png点处的泰勒展开式为:

81b4cd51a36edb3e4d790fc26eef42e3.png

其中

6b4c6d2e85346d3691ca487913725b86.png

将上述展开式写成矩阵形式,则有:

085cd2758b9ac353a7c36627f6c2e867.png

即为

cba17896212b0ecbe65f373f3986790e.png

其中:

272777dfabfce9557d6b95a97ad03687.png

 4d8a80257b5f15fa8401d8f34eec88a9.png 是 ffcfc8ec585aa41084c1011b4ca4ca86.png 在 f34ca373f83d7e00dfffd25b1d651eab.png 点处的Hessian矩阵。它是由函数39aeb1aefc7098e060b7ce220cdd4834.png在 92946b401ed364cffa507447d36eba9a.png点处的二阶偏导数所组成的方阵。我们一般将其表示为:

b934f58e4928837054053e2c233d5aca.jpeg

我们也可以将其简写为:

3b558aba15bd76faf556e058fbe77fc0.png

2.数字图像处理之尺度空间理论

尺度空间理论的基本思想是:在图像信息处理模型中引入一个被视为尺度的参数,通过连续变化尺度参数获得多尺度下的尺度空间表示序列,对这些序列进行尺度空间主轮廓的提取,并以该主轮廓作为一种特征向量,实现边缘、角点检测和不同分辨率上的特征提取等。

尺度空间理论的特点是:将传统的单尺度图像信息处理技术纳入尺度不断变化的动态分析框架中,更容易获取图像的本质特征。尺度空间中各尺度图像的模糊程度逐渐变大,能够模拟人在距离目标由近到远时目标在视网膜上的形成过程。

尺度空间理论的一个直观理解:我们人在远近不同距离上对同一物体,都可以实现辨识。

高斯卷积核是实现尺度变换的唯一线性核(高斯函数可以称为最伟大和最重要的函数)。一幅图像的尺度空间可以定义为:

0f60bca0c551c161bf67ff1c01e287e5.png

其中符号"*"表示卷积操作。 

σ是尺度空间因子,值越小表示图像被平滑的越少,相应的尺度也就越小。大尺度对应于图像的概貌特征,小尺度对应于图像的细节特征。

3.基于尺度理论的Hessian简化算法

对于二维图像IHessian矩阵描述每个像素在主方向上的二维导数为:

2b74a14568bfaa7e15917882e15d8a10.png

根据尺度空间理论,二阶导数可以通过图像与高斯函数的卷积获得,例如,在点(x,y)处有

98a9ff0a2411cee6e1b919e534071ca4.png

其中89c6a46abd5a706781a14c602d961022.png,为尺度f56e43d4811d4999099aa977069844ff.png为的高斯函数。

如果我们接受这个理论,那么就可以得到推论:

对全图直接做偏导操作  = 对原图以特定的高斯核做卷积

基于这个推论,对于图的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矩阵的定义:

3487f7046cb54f4ab7226192e090214e.png

根据二维矩阵特征值的定义:“设A是n阶方阵,如果存在数m和非零n维列向量 x,使得 Ax=mx 成立,则称 m 是矩阵A的一个特征值(characteristic value)或本征值(eigenvalue)”,可以得到等式

c2ffce38953b92ffb997e19cf25f4d83.png

并且根据图像的特性,可以得到

a6a6ea12728cdcef57bbe040c7c01205.jpeg=10d53f6fb0bf07caf63983e42c059db3.jpeg

带入以上方程得到Hessian的特征值的解:

f126252f2a7cacc2c7241cac6efae682.jpeg

请记住这个结论,我们在代码部分将再一次提及。

5.Hessian矩阵特征值的图像性质

一个Hessian矩阵可以分解为两个特征值以及定义的特征向量。79a308b1e77ae7d1af5f44fada3bfde4.png1d539f6ff7c01dd067f46704cbe348f0.png

其中最大的绝对特征值8dbff617d1ba2b50d2c44aea0b67de1d.png表示最大的局部灰度变化,其特征向量则代表它方向,可以认为是切线方向;而较小的那个代表垂直方向,也就是法线方向。

这张图可以很好地表明切线和法线的概念。

e8f1f384cb0bcf7bf6f7ec51dfd0648e.jpeg

这些都将在下面的算法中得到利用。

6.高斯方程及二阶导数

前面提到了高斯函数,这里补充一些知识,下面有用。

高斯分布即正态分布,其曲线图如下:

0cdaedf90df458c690878038c7f89632.jpeg

二维高斯分布,其曲线图如下:

48a451b71270ad1a8c197898fd9f8def.jpeg

根据定义,我们可以求得一下内容。

二维高斯函数的一阶偏导数:

7e446069ed6745375dec9b2a169915db.jpeg

二维高斯函数的二阶偏导数

596e88ae3094b3ec0949497ad684f20e.jpeg

这里从原函数到二阶偏导的推断都是本科的知识,建议大家自己推一下,很简单,对于这个问题的深入认识很有帮助,后面我们在代码部分将再一次提及。

血管增强”算法(Frangi算法)原理

ujkHessian矩阵及其特征值能够很好地描述常见的几何形状的信息,我们将利用它进行血管增强;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.认识血管及其增强

在采集到的图片中,血管应该呈现“管状网络”结构,这为我们进行图像处理提供基本依据。上面提高的"Frangi"算法本身就是用来识别管线的。

2.Frangi论文基本原理

基于前面我们说明的”加速算法“,首先将血管在多尺度下进行Gaussian滤波处理,然后计算每个像素点的二阶导数构造Hessian矩阵,并且计算出两个特征值(这个地方在代码实现的时候有技巧)。

虽然我们已经得到了Hessian矩阵及其特征值,从图像上已经能够看出增强的效果,但是这还不够。接下来

将求得的特征值带入事先建立好的血管相似性函数中获取在不同尺度下的滤波响应。

64171c541c1bb3c2a2189fc48b45a167.png

当尺度和局部结构匹配时计算得到最大滤波响应,从而判断当前像素点是否属于血管结构。

为了尽可能地得到增强的效果,在论文中采用的是“多尺度”叠加的方法,具体来说就是采用不同的卷积核同时进行处理,得到多张处理效果,而后对结果中“着色”效果比较好的部分进行叠加。

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.项目结构

首先从结构开始说明,这样如果你有一定基础就可以自己先进行研究,然后对比我的讲解,对于学习有帮助。

8bf8896e65bb6f9f5a1215148e7ff937.png

这个项目很简单,除了main.cpp外,frangi算法封装到了frangi.h+frangi.cpp中,以函数形式直接提供。

1int main(int argc, char *argv[]) {
 2    //使用默认参数设定Frangi
 3    frangi2d_opts_t opts;
 4    frangi2d_createopts(&opts);
 5    //读取图片,进行处理
 6    Mat input_img = imread("E:/template/51.bmp", 0);
 7    Mat input_img_fl;
 8    //转换为单通道,浮点运算
 9    input_img.convertTo(input_img_fl, CV_32FC1);
10    //进行处理
11    Mat vesselness, scale, angles;
12    frangi2d(input_img_fl, vesselness, scale, angles, opts);
13    //显示结果
14    vesselness.convertTo(vesselness, CV_8UC1, 255);
15    scale.convertTo(scale, CV_8UC1, 255);
16    angles.convertTo(angles, CV_8UC1, 255);
17    imshow("result", vesselness);    
18}

而main.cpp也尽可能简单,除了必要的图片格式转换外,主要过程封装在

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

打开frangi.h,我们可以看见以下内容:

1/
 2//Frangi filter//
 3/
 4//frangi滤波主要过程
 5void frangi2d(const cv::Mat &src, cv::Mat &J, cv::Mat &scale, cv::Mat &directions, frangi2d_opts_t opts);
 6
 7//Helper functions//
 8
 9//计算Hessian矩阵 with parameter sigma on src, save to Dxx, Dxy and Dyy. 
10void frangi2d_hessian(const cv::Mat &src, cv::Mat &Dxx, cv::Mat &Dxy, cv::Mat &Dyy, float sigma);
11//参数设置 (sigma_start = 3, sigma_end = 7, sigma_step = 1, BetaOne = 1.6, BetaTwo 0.08)
12void frangi2d_createopts(frangi2d_opts_t *opts);
13//计算特征值 from Dxx, Dxy, Dyy. Save results to lambda1, lambda2, Ix, Iy. 
14void 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);

保持的是一个主要函数+三个Helper函数的过程。

2.计算Hessian矩阵

我们来看frangi2d_hessian这个函数,正如注释说明,它就是Hessian运算的具体实现:

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

其中比较难以理解的是这段,似乎做了较多的数值计算

1for (int x = -round(3*sigma); x <= round(3*sigma); x++){
 2        j=0;
 3        for (int y = -round(3*sigma); y <= round(3*sigma); y++){
 4            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));
 5            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));
 6            j++;
 7        }
 8        i++;
 9    }
10    for (int j=0; j < n_kern_y; j++){
11        for (int i=0; i < n_kern_x; i++){
12            kern_yy_f[j*n_kern_x + i] = kern_xx_f[i*n_kern_x + j];
13        }
14    }

看上去比较奇怪。这里由于代码比较长难以理解,实际上它是在计算Dxx等的卷积核。首先我们回忆一下在理论阶段得到的一个重要结论:

对全图直接做偏导操作  = 对原图以特定的高斯核做卷积

这里我们就是首先把“特定的高斯核”计算出来。然后我们回忆当时介绍的二维高斯函数的二阶偏导数

 111e02e9698ff2506d2aff5e11720069.jpeg

那么它翻译成代码是什么样子的了?matlab版本

1//生成卷积核
2    //DGaussxx = 1/(2*pi*Sigma^4) * (X.^2/Sigma^2 - 1) .* exp(-(X.^2 + Y.^2)/(2*Sigma^2));  
3    //DGaussxy = 1/(2*pi*Sigma^6) * (X .* Y)           .* exp(-(X.^2 + Y.^2)/(2*Sigma^2));
4    //DGaussyy = DGaussxx';

c++(opencv)版本

1cv::Mat tmp1 = 1/(2*PI*Sigma*Sigma*Sigma*Sigma)*(EWM(X,X)/(Sigma*Sigma)-1);
2cv::Mat tmp2 = -1*((EWM(X,X)+EWM(Y,Y))/(2*Sigma*Sigma));
3exp(tmp2,tmp2);

这样你就能够理解这段代码的含义了。接下来我们就是需要用这3个特定的卷积核进行卷积

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

这个地方,关于OpenCV的filter2D  函数是如何实现卷积的,和Matlab有何区别,可以参考《实际比较filter2D和imfilter之间的关系》

3.Hessian特征值的计算

我们回忆一下最前面得到的结论:

42fcf7939322544317237630c62e510c.jpeg

然后就可以理解这里的代码:

1//calculate eigenvectors of J, v1 and v2
 2Mat tmp, tmp2;
 3tmp2 = Dxx - Dyy;
 4sqrt(tmp2.mul(tmp2) + 4*Dxy.mul(Dxy), tmp);
 5Mat v2x = 2*Dxy;
 6Mat v2y = Dyy - Dxx + tmp;
 7//normalize
 8Mat mag;
 9sqrt((v2x.mul(v2x) + v2y.mul(v2y)), mag);
10Mat v2xtmp = v2x.mul(1.0f/mag);
11v2xtmp.copyTo(v2x, mag != 0);
12Mat v2ytmp = v2y.mul(1.0f/mag);
13v2ytmp.copyTo(v2y, mag != 0);
14//eigenvectors are orthogonal
15Mat v1x, v1y;
16v2y.copyTo(v1x);
17v1x = -1*v1x;
18v2x.copyTo(v1y);
19//compute eigenvalues
20Mat mu1 = 0.5*(Dxx + Dyy + tmp);
21Mat mu2 = 0.5*(Dxx + Dyy - tmp);

基本上就是将数学公式翻译成为了代码,注意一下.mul是OpenCV中的“点乘”方法。

注意:

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

temp = 8def64dbc4cb531a420575cd4c992944.png

4.frangi2d主要过程

下面我们着重讲解

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

函数,它是整个Frangi算法的主要过程

1for (float sigma = opts.sigma_start; sigma <= opts.sigma_end; sigma += opts.sigma_step){……}
2
3//多尺度叠加
4for (int i=1; i < ALLfiltered.size(); i++){
5    maxVals = max(maxVals, ALLfiltered[i]);
6    whatScale.setTo(sigma, ALLfiltered[i] == maxVals);
7    ALLangles[i].copyTo(outAngles, ALLfiltered[i] == maxVals);
8    sigma += opts.sigma_step;
9}

程序最外面的框架告诉我们,整个程序是多次运算(尺度)的叠加。

1//根据论文定义,对结果进行修正
2Dxx = Dxx*sigma*sigma;
3Dyy = Dyy*sigma*sigma;
4Dxy = Dxy*sigma*sigma;
5
6//calculate (abs sorted) eigenvalues and vectors
7Mat lambda1, lambda2, Ix, Iy;
8frangi2_eig2image(Dxx, Dxy, Dyy, lambda1, lambda2, Ix, Iy);

在每次计算中,首先计算出9c4a4b23408274783c8e3c36d7990f95.png的值,代码中对于的变量叫做lambda1,lambda2。

1//根据定义,计算“血管增强响应函数”
 2lambda2.setTo(nextafterf(0, 1), lambda2 == 0);
 3Mat Rb = lambda1.mul(1.0/lambda2);
 4Rb = Rb.mul(Rb);
 5Mat S2 = lambda1.mul(lambda1) + lambda2.mul(lambda2);
 6Mat tmp1, tmp2;
 7exp(-Rb/beta, tmp1);
 8exp(-S2/c, tmp2);
 9//保存单尺度结果
10Mat Ifiltered = tmp1.mul(Mat::ones(src.rows, src.cols, src.type()) - tmp2);

这里就是Frangi算中最有价值的地方:“血管响应函数”,它的数学表示方法为:

03aeac8df86905c56d054ab611b4ea10.png

其中97f45801b89af3a3a59c0457f208c995.png和C都是超参数,它们在代码前面都已经根据输入参数进行了变形。

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

论文中给出了参考值,代码中的变量名字都是对应的。你也可以根据修改查看。

在后来的文献中,也出现了其它“血管增强”函数,比如LiQiang

bc31a2b0d23a5675639d0e31173a5e53.png

以及GVF

113ea8ddf169f249efe2deb3cfeec5a5.png

0681348ac0d4626ff2df3f847d538896.png

基于前面计算出来的特征值,这些都很容易实现。

 四、参考文献:

11.Hessian矩阵以及在图像中的应用
 2https://blog.csdn.net/lwzkiller/article/details/55050275
 32.血管分割技术文献综述
 4https://blog.csdn.net/u013102349/article/details/53819314
 53.《基于Hessian矩阵和熵的CT序列图像裂缝分割方法》 王慧倩等 仪器仪表学报 2016年8月
 64.百度百科
 7https://baike.baidu.com/item/%E9%BB%91%E5%A1%9E%E7%9F%A9%E9%98%B5/2248782?fr=aladdin
 85. 数字图像处理之尺度空间理论 https://blog.csdn.net/TJC3014583925/article/details/88836485>
 96. CLAHE的实现和研究 
10https://www.cnblogs.com/jsxyhelu/p/6435601.html
117.《实际比较filter2D和imfilter之间的关系》
12https://www.cnblogs.com/jsxyhelu/p/6597544.html
138.《视觉图像:高斯方程及各阶导数》
14https://jingyan.baidu.com/album/5bbb5a1bedf94413eba179ba.html?picindex=2

全部代码:

1链接:https://pan.baidu.com/s/1Q0bbqkJJSHqhN7Htg3dhzQ
2提取码:gzmg

最后感谢大家学习至此,如果你在理解我所表述的这些内容中获得的感悟,有我写下它的时候得到的感悟的十分之一那么多,那么你一定是一名幸运的读者。

好消息!

小白学视觉知识星球

开始面向外开放啦👇👇👇

 
 

791d190146d1b95c5076101198d36da7.jpeg

下载1:OpenCV-Contrib扩展模块中文版教程

在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。


下载2:Python视觉实战项目52讲
在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。


下载3:OpenCV实战项目20讲
在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。


交流群

欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值