前几天在翻一翻matlab中的帮助文档,无意中发现一个叫fibermetric的图像处理函数,感觉有点意思,可以增强或者说突出一些类似于管状的对象,后面看了下算法的帮助文档,在百度上找了找,原来这也是一种比较经典的增强算法。
核心的论文是《Multiscale vessel enhancement filtering》,可以从这里下载得到:https://www.researchgate.net/publication/2388170_Multiscale_Vessel_Enhancement_Filtering;
至于论文的思想,除了直接看原始论文,我也还参考了一下几篇文章:
https://ww2.mathworks.cn/matlabcentral/fileexchange/24409-hessian-based-frangi-vesselness-filter Hessian based Frangi Vesselness filter 原始作者的matlab实现
https://github.com/BoguslawObara/vesselness2d 2d multiscale vessel enhancement filtering
https://www.cnblogs.com/jsxyhelu/p/18157603 Hessian矩阵以及在血管增强中的应用------OpenCV实现【2024年更新】
https://blog.csdn.net/piaoxuezhong/article/details/78428785 眼底图像血管增强与分割--(5)基于Hessian矩阵的Frangi滤波算法
https://blog.csdn.net/lwzkiller/article/details/55050275 Hessian矩阵以及在图像中的应用
https://zhuanlan.zhihu.com/p/127951058 Multiscale Vessel Enhancement Filtering(多尺度血管增强滤波,基本是对原英文的翻译)
但是说实在的我没有看明白原理,懵懵懂懂的,反正大概就是知道通过判断Hessian矩阵的两个特征值之间的某些关系可以确定某个位置是否属于血管或者说管状结构,我觉得呢大概看懂论文里这个表可能就比较好了:

我们重点关注2D的情况。
表中Lambda1和Lambda2分别为某个尺度下的Hessian矩阵的特征值,并且是|Lambda1| < |Lambda2|,对于管状对象,一般是|Lambda1| << |Lambda2|,其中 <<表示远远小于,另外呢,一个先验就是在血管图像中背景部分的像素其Hessian矩阵的特征值一般都比较小,因此,基于这两个特征呢,构造了一下两个中间变量来衡量一个位置的像素是属于血管还是背景:


对于2D图像,RB可以简化为 RB = |Lambda1| / |Lambda2|,而S则即为 S = sqrt(Lambda1^2 + Lambda2^2);
利用这两个中间变量,然后构架了下式作为某个像素的输出响应:

其中Beta和C是一些可调节参数,Beta默认可设置为0.5。C后面再说。
注意观察,上面公式的下半部分是两个指数函数的乘积,而指数函数内部的参数必然是负值(2个平方数相除在求负),这个指数函数范围的有效值为【0,1】,后面部分的1-exp(//)的范围也必然是【0,1】。 因此2项的乘积必然也是在【0,1】之间。

第一个指数项中,如果RB值越小,则指数值越大,第二个项目中,如果S值越大,则指数值越大, 这恰好正确的反映了前面所说血管区域的特征。
对于某一个尺度下的响应使用上述公式,而为了获得更为理想的效果,可以使用连续的多个尺度进行计算,然后取每个尺度下的最大值作为最终的响应值。
具体到实现代码,我们首先参考了论文作者自己的代码,这个可在https://ww2.mathworks.cn/matlabcentral/fileexchange/24409-hessian-based-frangi-vesselness-filter Hessian based Frangi Vesselness filter下载。
我们贴一下这个代码的主要函数:
sigmas=options.FrangiScaleRange(1):options.FrangiScaleRatio:options.FrangiScaleRange(2);
sigmas = sort(sigmas, 'ascend');
beta = 2*options.FrangiBetaOne^2;
c = 2*options.FrangiBetaTwo^2;
% Make matrices to store all filterd images
ALLfiltered=zeros([size(I) length(sigmas)]);
ALLangles=zeros([size(I) length(sigmas)]);
% Frangi filter for all sigmas
for i = 1:length(sigmas),% Make 2D hessian
[Dxx,Dxy,Dyy] = Hessian2D(I,sigmas(i));
% Correct for scale
Dxx = (sigmas(i)^2)*Dxx;
Dxy = (sigmas(i)^2)*Dxy;
Dyy = (sigmas(i)^2)*Dyy;
% Calculate (abs sorted) eigenvalues and vectors
[Lambda2,Lambda1,Ix,Iy]=eig2image(Dxx,Dxy,Dyy);
% Compute the direction of the minor eigenvector
angles = atan2(Ix,Iy);
% Compute some similarity measures
Lambda1(Lambda1==0) = eps;
Rb = (Lambda2./Lambda1).^2;
S2 = Lambda1.^2 + Lambda2.^2;
% Compute the output image
Ifiltered = exp(-Rb/beta) .*(ones(size(I))-exp(-S2/c));
% see pp. 45
if(options.BlackWhite)
Ifiltered(Lambda1<0)=0;
else
Ifiltered(Lambda1>0)=0;
end
% store the results in 3D matrices
ALLfiltered(:,:,i) = Ifiltered;
ALLangles(:,:,i) = angles;
end
其中Hessian2D的主要代码如下:
function [Dxx,Dxy,Dyy] = Hessian2D(I,Sigma)
if nargin < 2, Sigma = 1; end
% Make kernel coordinates
[X,Y] = ndgrid(-round(3*Sigma):round(3*Sigma));
% Build the gaussian 2nd derivatives filters
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';
Dxx = imfilter(I,DGaussxx,'conv');
Dxy = imfilter(I,DGaussxy,'conv');
Dyy = imfilter(I,DGaussyy,'conv');
eig2image的主要代码为:
function [Lambda1,Lambda2,Ix,Iy]=eig2image(Dxx,Dxy,Dyy)
% | Dxx Dxy |
% | |
% | Dxy Dyy |
% Compute the eigenvectors of J, v1 and v2
tmp = sqrt((Dxx - Dyy).^2 + 4*Dxy.^2);
v2x = 2*Dxy; v2y = Dyy - Dxx + tmp;
% Normalize
mag = sqrt(v2x.^2 + v2y.^2); i = (mag ~= 0);
v2x(i) = v2x(i)./mag(i);
v2y(i) = v2y(i)./mag(i);
% The eigenvectors are orthogonal
v1x = -v2y;
v1y = v2x;
% Compute the eigenvalues
mu1 = 0.5*(Dxx + Dyy + tmp);
mu2 = 0.5*(Dxx + Dyy - tmp);
% Sort eigen values by absolute value abs(Lambda1)<abs(Lambda2)
check=abs(mu1)>abs(mu2);
Lambda1=mu1; Lambda1(check)=mu2(check);
Lambda2=mu2; Lambda2(check)=mu1(check);
Ix=v1x; Ix(check)=v2x(check);
Iy=v1y; Iy(check)=v2y(check);
其实不是很复杂,抛开多个尺度取最大值部分,对于单个尺度的结果的计算,主要是以下几个部分:
1、某个尺度下的图像的二级导数(fxx,fyy以及fxy).
2、由这些导数构成了图像的Hessian矩阵,求解这个矩阵的特征值。
3、根据特征值按照前面所描述的公式计算响应值。
这里所谓某个尺度下图像,就是对原始图像进行该尺度的高斯模糊后得到的图像,在所有的可搜索的博文或者代码中,我们都看到了大家都是按照论文作者提供的matlab代码的方式,直接根据高斯模糊卷积核的公式,先求出其3种二级偏导数的理论计算公式,然后在根据公式取求取卷积的结果,直接得到二级偏导数,即如下过程:
对于2维的高斯模糊,其卷积核为:

其一阶偏导数为:

二阶偏导数为:

在上述Hessian2D的matlab代码中,我们可以找到和这个二级导数对应的离散计算表达式,注意一般高斯相关的计算,因为在3*Sigma范围外,其权重基本已经衰减到了99%以外了,因此,一般只需要计算3*Sigma半径内的卷积,如下面的曲面所示:

Hessian2D中的imfilter函数即为实现该卷积的过程。
至于Hessian2D的特征值的计算,因为有固定的计算公式,直接算就可以了,这个没有什么好说的。
后续响应值的计算,也就是按步就班的来。也没有什么可谈的。
前几天我也一直按照这个思路来编码实现效果,那么做完后呢,确实有结果,不过和matlab的fibermetric相比呢,结果总是有一些区别,而且速度也有较大的差异。我就一直在想,为什么计算某个尺度下二级导数,一定要直接从那个高斯函数求导后做卷积呢,不能先计算出高斯模糊后的结果后,然后在利用这个结果直接求离散化的二阶导数吗。
幸好matlab是个相对不封闭的工具,我们在matlab的窗口中输入 edit fibermetric,后可以看到他的具体实现,相关代码如下(做了精简):
% Output can be double or single
B = zeros(size(V),'like',V);
numel(thickness)
for id = 1:numel(thickness)
sigma = thickness(id)/6;
if (ismatrix(V))
Ig = imgaussfilt(V, sigma, 'FilterSize', 2*ceil(3*sigma)+1);
elseif (ndims(V)==3)
Ig = imgaussfilt3(V,sigma, 'FilterSize', 2*ceil(3*sigma)+1);
end
out = images.internal.builtins.fibermetric(Ig, c, isBright, sigma);
B = max(B,out);
end
代码不多,但是给出的信息量还是蛮大的,第一输入参数的thickness和卷积核的参数关系有了对应,即 :
sigma = thickness(id)/6;
为什么是除以6,我想和刚才那个3*Sigma里的3应该精密相关吧。
第二,可以明显的看到他是先对原始图像进行了高斯模糊的,然后在调用一个内部的函数builtins.fibermetric对这个模糊后的图像进行处理的。由于matlab里buildins函数是不可以看到源代码的,所以其具体的进一步实现我们无从得知。但是,这无疑和原始论文作者提供的思路是不一样,而和我刚才的怀疑则高度一致。于是我按照我自己的想法写了个过程,其中模糊使用如下方式:
unsigned char* Blur = (unsigned char*)malloc(Height * Stride * sizeof(unsigned char));
IM_GaussBlur_SSE(Src, Blur, Width, Height, Stride, Sigma);
得到的结果趋势和matlab的结果基本一致,但是细节上有很多噪点和干扰。
开始的时候我一直认为是这样做不行,但是看到matlab的实现,我又可以肯定是没有问题的。因此一直耽搁咋这里。
后面某一刻,我就在想,因为高斯模糊或者或其他的模糊,都会把图像的细节减少,把尖锐的地方磨平,因此,模糊的Sigma越大,在离散化计算梯度的时候,如果使用字节版本的模糊呢,由于现相关领域内的像素差异实际上很小了(由浮点数据裁剪到字节数据时丢失了很多信息),因此,就会出现较大的误差,导致趋势对,而结果不够完美。
因此,后续我把这个模糊改为浮点版本的模糊,结果就非常的完美了。
这里还有个细节,关于图像的二阶离散的梯度的计算,我们在百度上能搜到的结果是这样的:

对于fxx,fyy我觉得还是比较合理的,而对于fxy这个结果明显不太对称和合理,总感觉有点问题。
这个问题的解惑呢,也还是要看机遇,在继续翻matlab的函数时,我又发现他还有一个maxhessiannorm函数,并且他和fibermetric是精密相关,通过edit maxhessiannorm发现这个函数是完全分享代码的,其核心代码如下:
sigma = thickness/6;
[Gxx, Gyy, Gxy] = images.internal.hessian2D(I, sigma);
[eigVal1, eigVal2] = images.internal.find2DEigenValues(Gxx, Gyy, Gxy);
absEigVal1 = abs(eigVal1);
absEigVal2 = abs(eigVal2);
maxHessianNorm = max([max(absEigVal1(:)), max(absEigVal2)]);
C = maxHessianNorm;
他的作用是获取最大的Hessian矩阵的特征值,而且和fibermetric的过程是对应的。
我们看这里的images.internal.hessian2D,还好这个internal是可以看到代码的。
Ig = imgaussfilt(I, sigma, 'FilterSize', 2*ceil(3*sigma)+1);
[Gx, Gy] = imgradientxy(Ig, 'central');
[Gxx, Gxy] = imgradientxy(Gx, 'central');
[ ~ , Gyy] = imgradientxy(Gy, 'central');
Gxx = (sigma^2)*Gxx;
Gyy = (sigma^2)*Gyy;
Gxy = (sigma^2)*Gxy;
第一行和fibermetric函数是一模一样的,而后续的三行就是计算二阶梯度,在去看imgradientxy的相关注释,有下面的语句
'central' : Central difference gradient dI/dx = (I(x+1)- I(x-1))/ 2
这样,我们一步一步的做个推导,具体如下:

简单来说,对于如下的一个中线点像素 P12,

其二级偏导数分别为:
// 求该尺度下图像的二阶梯度
float Dxx = (P14 + P10 - P12 - P12) * 0.25f;
float Dyy = (P22 + P2 - P12 - P12) * 0.25f;
float Dxy = (P18 + P6 - P16 - P8) * 0.25f;
这个时候在去看Dxy则是完全对称的了。
同时,这个地方还给我解惑了另外一个问题,即在fibermetric的代码中,还有这样一句话
out = images.internal.builtins.fibermetric(Ig, c, isBright, sigma);
按理说,前面高斯模糊需要这个Sigma是无可厚非的,那后面这里这个函数怎么还需要传递sigma呢,一直不理解,后面看看这个images.internal.hessian2D就明白了,原来是要在这里用sigma方法放大。
Gxx = (sigma^2)*Gxx;
Gyy = (sigma^2)*Gyy;
Gxy = (sigma^2)*Gxy;
最后,在matlab的函数里,还有一个StructureSensitivity参数,这个我测试了半天,我认为他就是公式中的c变量,这里有一个很好的理由支撑他, 因为在maxhessiannorm的解释文档中,有如下内容:
% C = maxhessiannorm(I) calculates the maximum of Frobenius norm of the
% hessian of intensity image I. As this function is used in the context
% of fibermetric, default thickness of 4 is used to find the hessian and
% returns the obtained value as C.
第一,这里直接用的字母C作为返回值,这个是一种遥相呼应,第二,这个函数计算的是特征值的最大值,和前面的公式变量S精密相关,而S又和C在计算时绑定在一起,因此,他应该就是C的一个外在表现。
至此,大部分的工作都已经完成了,而通过这样的改动,我所实现的结果基本和maltab的完全一致,并且速度方面有高斯模糊有快速的算法,因此算法的执行速度和参数基本无关了。而传统的那种卷积实现,当尺度较大时,必然会影响速度。
使用相同的图做测试,500*500的,单尺度时matlab大概需要15ms, 我实现的版本大概在3ms,还是有相当大的提高的。
matlab测试图 Thickness等于7时的结果图
那一副医学图像做测试,效果确实不错:
原图 尺度数量为5,最大尺度为16的结果

同样参数原始作者版本的结果,明显没有maltab的清晰
这个算法我测试确实对血管图像的提取效果比较显著,在贴几个图片。
另外,在Halcon中有个lines_gauss函数,以前我就研究过他,并且小有成果,我现在可以100%肯定这个函数和我现在研究的这个东西有很大的关联,也许在不久就可以把这个函数研究成功,因为,我们用lines_gauss这个常用的几个图片测试,会得到类似这样的结果:
原图 分析结果
本文Demo下载地址: https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,本算法位于Detection(检测相关)--》 FiberMetric(纤维分析) 菜单下,里面的所有算法都是基于SSE实现的。