基于边缘的主动轮廓模型——从零到一用python实现snake
作者:互联网
从零到一实现snake算法
1、Snake算法原理
Kass等人最早于1988年提出了主动轮廓模型,该方法通过构造能量函数,从而将图像分割转化为求解能量泛函极值的问题,其核心思想在于,作者认为和之前的分割方法相比,在不同的图像解读(image interpretation)任务中,表观或者浅层次(level set)的图像判读任务应该需要一些深层次(high leve)的图像信息,并论文[1]中进行了举例分析,有兴趣的同学可以自行翻阅论文查看,这也和主动轮廓模型的两大特点不谋而合:全局性和可拓展性。全局性也就是说在图像处理任务进行中,不仅要关注局部区域或者边缘信息,同样也要关注图像或曲线的整体信息如曲线整体的长度和曲率等。再者针对不同的图像特点,能够设计或者添加个性化的分割策略,接下来我将根据公式对此进一步分析。
对于图像I(x,y),C(s)=C(x(s),y(s))为演化曲线,定义snake模型的能量函数为:Esnake=Eint+Eext 这里我们区分内部能量和外部能量的依据就是看它是否和演化曲线本身的特性相关。比如说下式中,定义内部能量项的两部分内容分别代表的就是曲线的长度和曲率: Eint=∫0121∗(α(s)∣cs(s)∣2+β(s)∣css(s)∣2)ds 其中公式中的两项内容分别表示的就是曲线的长度项和曲线的光滑程度, α(s)和β(s)可以根据曲线上某一点的特征进行个性化处理,但是大多数情况下我们一般定义它们为常数,用于调节曲线长度项和光滑程度占比。
而外部能量项通常由两部分组成构成:Eext=Eimg+Econs Eimg通常表示和图像相关的外部能量项目,主要包含如下三种信息,
(1)
- 图像的像素信息(Line functional): Eline=∫01I(x(s),y(s))ds
- 图像的梯度信息(edge functional)(注意负号):Eedge=∫01−∣∇I(x(s),y(s))ds,
- 或者曲线周围小范围空间的梯度信息(scale space):Eline=∫01−gδ∗∇2I(x(s),y(s))ds在实际使用中图像的梯度信息Eedge使用的较为广泛,以及后面的实现过程中,我们也会用Eedge作为图像力,负号的作用就在于,在边缘梯度较大的情况下,整体的能量泛函越小,曲线也就更加趋向于演化到图像的边缘位置。
最后还有一个限制项Econ,这一项就是对曲线演化添加一定的限制作用,比如限制整体曲线到某一点或者某一区域R的距离:Econ=∫01∣∣C(x(s),y(s))−R∣∣ds具体的情况大家可以根据图片特点或者需求灵活定义。
2、基于曲线演化的实现方法
好了,终于到了激动人心的实现环节了,大家是不是跟我一样激动的直搓手,哈哈哈哈哈哈哈
开始之前我先说明两点内容:1)本次实现过程基于较为简单基础的能量泛函,大家可以先实现体验一边,然后再触类旁通,举一反三;2)以方面需要对一些数学知识进行回顾分析,另一方面希望大家仔细品一品从连续到离散的变化过程。
2.1演化方程推导
首先我们定义能量函数:Esnake=∫0121∗(α∣cs(s)∣2+β∣css(s)∣2−∣∇I(x(s),y(s))∣2)ds,当能量函数最小时,就可以获得C(x(s),y(s))的曲线方程,本质上这也是一个求能量泛函极值的问题,直接根据欧拉-拉格朗日方程(变分原理),当能量泛函取极值时,应满足:−αc′′(s)+βc′′′′(ss)+∇Eext=0 可是就算推到这种程度我们以然没办法直接求出曲线c(x(s),y(s)),尤其是我们希望公式能够以偏微分方程的形式出现,这样我们就可以利用计算机进行编程序迭代计算了,于是我们在曲线中引入了一个辅组变量t,则解可以表示为c(t,s),随时间变化的解c(t,s)应该使得能量函数逐渐变小(推导过程后续会补充),从而可以得出:∂t∂c(s)=αc′′(s)−βc′′′′(ss)−∇Eext
2.2离散化过程
接下来就是整个离散化过程了,首先,我们先明确一下概念,上式中的s表示什么意思,因为我们看到的图片都是离散的点,所以我们刚开定义初始化的snake曲线时,实际上定义的也就是一系列离散的点的集合,而曲线的演化过程实际上也就是这些固定数目的点的演化过程,离散化后,我们用i表示s,那假设初始化的snake曲线有N个点,那i∈[0,N−1],且x[N]=x[0],x[N+1]=x[2]....,那么对于曲线上任一点c(x(i),y(i)):x′′[i]=x[i−1]−2x[i]+x[i+1] x′′′′[i]=x[i−2]−4x[i−1]+6x[i]−4x[i+1]+x[i+2]对y也是同理,假设图像x方向和y方向的梯度为G(x,y)(均为正值),那外部力则分别为Gx[x,y],Gy[x,y],假定每次迭代后,曲线变化较小,我们用Gt−1(x,y)代替Gt(x,y),那么对任一点c(x(i),y(i))就可以推导出离散后的公式为:
∂t∂xt[i]=α(xt[i−1]−2xt[i]+xt[i+1])+β(−xt[i−2]+4xt[i−1]−6xt[i]+4xt[i+1]−xt[i+2])+Gx(xt[i],yt[i])用λ表示步长,则可以进一步表示为:λxt[i]−xt−1[i]=α(xt[i−1]−2xt[i]+xt[i+1])+β(−xt[i−2]+4xt[i−1]−6xt[i]+4xt[i+1]−xt[i+2])+Gx(xt−1[i],yt−1[i])同理, λyt[i]−yt−1[i]=α(yt[i−1]−2yt[i]+yt[i+1])+β(−yt[i−2]+4yt[i−1]−6yt[i]+4yt[i+1]−yt[i+2])+Gy(yt−1[i],yt−1[i]) ,如果一条曲线上有N个点,那就对x和y分别列出N个式子,如果用矩阵表示的话,那就是:λXt−Xt−1=AXt+Gx(xt−1,yt−1)矩阵A则为:⎝⎜⎜⎜⎜⎜⎜⎜⎜⎜⎜⎜⎛−2α+6βα+4β−β00⋮−βα+4βα+4β−2α+6βα+4β−β0⋮0−β−βα+4β−2α+6βα+4β−β⋮000−βα+4β−2α+6βα+4β⋱0000−βα+4β−2α+6β⋮00000−βα+4β00⋯⋯⋯⋯⋯⋯⋯−β0000−2α+6βα+4βα+4β−β000α+4β−2α+6β⎠⎟⎟⎟⎟⎟⎟⎟⎟⎟⎟⎟⎞,求解矩阵即可得出:Xt=(1−λA)−1Xt−1+λGx(Xt−1,Yt−1) Yt=(1−λA)−1Yt−1+λGy(Xt−1,Yt−1)至此,需要的核心演化公式到手。
2.3 代码实现
看完上面的离散化过程,是不是大家现在都胸有成竹,跃跃欲试,蠢蠢欲动啦,哈哈,接下来就让我们一起开始愉快的coding吧。
声明使用的依赖库主要有:
import numpy as np
import cv2
import matplotlib.pyplot as plt
from pylab import*
import skimage.filters as filt
代码实现总共分为如下几个步骤:
- 读入图片:
随手用windows画图工具画了个圆(简约派):
然后读入图片,注意
Image = cv2.imread('xxx.bmp',1)
image = cv2.cvtColor(Image,cv2.COLOR_BGR2GRAY)
img = np.array(image,dtype = np.float)
plt.imshow(img,cmap = 'gray') # 读入灰度图
print(shape(img))
- 定义初始化snake曲线,注意 注意 注意,此处我们需要首先明确x,y分别表示矩阵的列和行,同时也就意味着绘图的时候x,y分别表示x-y坐标系的横坐标轴和纵坐标轴呀,一定要品明白哈,不然会出大问题的哈(说多了都是泪)
t = np.linspace(0,2*np.pi,60,endpoint = True)
y = 100+30*np.cos(t)
x = 100+30*np.sin(t)
- 然后定义关键参数,部分参数值参考原论文:
alpha = 0.001
beta = 0.4
gamma = 100 # 迭代步长
sigma = 20 # 滤波用的高斯分布参数
iterations = 300 # 迭代次数
- 计算矩阵,有没有发现整个矩阵只由参数控制,而和图片无关:
N = np.size(x)
a = gamma*(2*alpha+6*beta)+1
b = gamma*(-alpha-4*beta)
c = gamma*beta
p = np.zeros((N,N),dtype=np.float)
p[0] = np.c_[a,b,c,np.zeros((1,N-5)),c,b]
print(p[0].shape)
for i in range(N):
p[i] = np.roll(p[0],i)
p = np.linalg.inv(p)
- 计算外部力,因为手绘图像噪声较小,很难直接通过梯度产生外部力,因此先对图像进行高斯滤波处理,注意此处的卷积核和高斯分布参数都选择的较大,大家可以品一品为什么要这样做:
smoothed = cv2.GaussianBlur((img-img.min()) / (img.max()-img.min()),(89,89),sigma)
giy,gix = np.gradient(smoothed)
gmi = (gix**2+giy**2)**0.5
gmi = (gmi - gmi.min()) / (gmi.max() - gmi.min())
Iy,Ix = np.gradient(gmi)
- 定义函数,确保演化曲线不超出图像边界,并返回整数形位置信息:
def fmax(x,y):
x[x < 0] = 0
y[y < 0] = 0
x[x > img.shape[1]-1] = img.shape[1]-1
y[y > img.shape[0]-1] = img.shape[0]-1
return y.round().astype(int),x.round().astype(int)
- 跌打计算,并显示图像:
plt.plot(y,x,'.')
for i in range(iterations):
fex = Ix[fmax(x,y)]
fey = Iy[fmax(x,y)]
x = np.dot(p,x+gamma*fex)
y = np.dot(p,y+gamma*fey)
if i%10 ==0:
plt.plot(x,y,'.')
plt.show()
计算结果,撒花,撒花,撒花!
2.4 讨论分析
未完待续
3、基于水平集的实现方法
未完待续
标签:python,曲线,零到,beta,snake,np,alpha,yt,xt 来源: https://blog.csdn.net/weixin_40673873/article/details/106664590