前几天在翻一翻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实现的。

  

【工程应用十】 基于Hessian矩阵的Frangi滤波算法 == 血管图像增强 == Matlab中fibermetric函数的自我实现、加速和优化。的更多相关文章

  1. C#处理医学图像(一):基于Hessian矩阵的血管肺纹理骨骼增强对比

    在医院实际环境中,经常遇到有问题的患者,对于一些特殊的场景,比如骨折,肺结节,心脑血管问题 需要图像对比增强来更为清晰的显示病灶助于医生确诊,先看效果: 肺纹理增强: 肺结节增强: 血管对比增强: 骨 ...

  2. C#处理医学图像(二):基于Hessian矩阵的医学图像增强与窗宽窗位

    根据本系列教程文章上一篇说到,在完成C++和Opencv对Hessian矩阵滤波算法的实现和封装后, 再由C#调用C++ 的DLL,(参考:C#处理医学图像(一):基于Hessian矩阵的血管肺纹理骨 ...

  3. matlab中repmat函数的用法(堆叠矩阵)

    matlab中repmat函数的用法 B = repmat(A,m,n) B = repmat(A,[m n]) B = repmat(A,[m n p...]) 这是一个处理大矩阵且内容有重复时使用 ...

  4. MATLAB中mesh函数的使用:基于像素强度画3D密度图(create a 3D density plot based on the pixel intensity:mesh function)

    所用的函数非常简单,只需要用到mesh函数,示例代码如下: Ima=imread('F:\pathto\test.jpg'); surf_ima = surf(rgb2gray(Ima)); %黑色的 ...

  5. Hessian矩阵与多元函数极值

    Hessian矩阵与多元函数极值 海塞矩阵(Hessian Matrix),又译作海森矩阵,是一个多元函数的二阶偏导数构成的方阵.虽然它是一个具有悠久历史的数学成果.可是在机器学习和图像处理(比如SI ...

  6. C++ 特殊矩阵的压缩存储算法

    1. 前言 什么是特殊矩阵? C++,一般使用二维数组存储矩阵数据. 在实际存储时,会发现矩阵中有许多值相同的数据或有许多零数据,且分布呈现出一定的规律,称这类型的矩阵为特殊矩阵. 为了节省存储空间, ...

  7. 基本滤波算法比较 (转载http://blog.sina.com.cn/s/blog_69f2aa5a01014du5.html)

    最近在做关于数据采集方面的东西,这就不免涉及到了滤波的算法,在网上找到了关于几种算法的比较. 数字滤波方法有很多种,每种方法有其不同的特点和使用范围.从大的范围可分为3类. 1.克服大脉冲干扰的数字滤 ...

  8. matlab中矩阵的表示与简单操作

    原文地址为:matlab矩阵的表示和简单操作 一.矩阵的表示在MATLAB中创建矩阵有以下规则: a.矩阵元素必须在”[ ]”内: b.矩阵的同行元素之间用空格(或”,”)隔开: c.矩阵的行与行之间 ...

  9. 基于MATLAB的滤波算法

    目前比较经典的图像去噪算法主要有以下三种: 均值滤波:也称线性滤波,主要思想为邻域平均法,即用几个像素灰度  的平均值来代替每个像素的灰度.有效抑制加性噪声,但容易引起图像模糊,  可以对其进行改进, ...

  10. 基于Vivado HLS在zedboard中的Sobel滤波算法实现

     基于Vivado HLS在zedboard中的Sobel滤波算法实现 平台:zedboard  + Webcam 工具:g++4.6  + VIVADO HLS  + XILINX EDK + ...

随机推荐

  1. 【Azure Storage Account】利用App Service作为反向代理, 并使用.NET Storage Account SDK实现上传/下载操作

    问题描述 在使用Azure上的存储服务 Storage Account 的时候,有时需要代替 它原本提供的域名进行访问,比如默认的域名为:mystorageaccount.blob.core.chin ...

  2. 晶振测试仪GDS-80系列参数

    晶振测试仪GDS-80系列 一.产品简介 晶振测试仪GDS-80系列是高性价比的晶振测试系统,采用网络分析技术,实现智能化测量,符合IEC-444标准.测量频率范围10KHz-200KHz,1MHz- ...

  3. 近1000 star,Forest 1.5.0 正式版发布

    简介 Forest是一个高层的.极简的轻量级HTTP调用API框架. 相比于直接使用Httpclient您不再用写一大堆重复的代码了,而是像调用本地方法一样去发送HTTP请求. 不需要调用HTTP底层 ...

  4. VSCode 下载总量超 900 万次的主题插件 Material Theme Free 被告知存在恶意代码

    2025年2月26日,VSCode 打开之后,Duang 突然一个弹窗出现,提示 Material Theme 被强制删除,VSCode 也回到了最初的长相,不说难看吧,只是不太习惯. 起因 英文原文 ...

  5. 【Bug记录】Powershell 无法将“vue”项识别为 cmdlet、函数、脚本文件或可运行程序的名称 - PowerShell 执行策略

    Powershell 无法将"vue"项识别为 cmdlet.函数.脚本文件或可运行程序的名称 造成该问题主要是 PowerShell 执行策略,不支持执行全局脚本和程序的运行. ...

  6. mongodb关机重启

    正确关闭 mongodb 查看 mongodb 进程 ps -ef | grep mongodb # 或者 ps -aux | grep mongodb 杀掉 mongodb 进程(不推荐) kill ...

  7. DCL(Double-checked Locking双重校验锁)实现单例模式的原理、问题与解决方案

    ​ 好的,要深入理解DCL(Double-Checked Locking)双重校验锁的原理.问题以及解决方法. 首先,我需要回忆一下单例模式的基本概念,因为DCL通常用于实现单例模式. 单例模式确保一 ...

  8. leetcode - 743

    网络延迟时间 迪杰斯特拉的朴素算法流程: 邻接矩阵的方法 点击查看代码 class Solution { public int networkDelayTime(int[][] times, int ...

  9. 史上最全EffectiveJava总结(二)

    方法 49.检查参数的有效性 每次编写方法或构造函数时,都应该考虑参数存在哪些限制,并在文档中记录下来,然后在方法的开头显式地检查. 如果没有在方法开头就验证参数,可能会违反故障原子性.因为方法可能会 ...

  10. 【Web】Servlet三大作用域、JSP四大作用域

    request 生命周期: 创建:客户端向服务器发送一次请求,服务器就会创建request对象. 销毁:服务器对这次请求作出响应后就会销毁request对象. 有效:仅在当前请求中有效. 作用:常用于 ...