目录
- 图割
- 从图像创建图
- 用户交互式分割
- 利用聚类进行分割
- 变分法
图割
图论中的图是由若干节点(有时也称顶点)和连接节点的边构成的集合,下面是一个示例。
图割是将一个有向图分割成两个互不相交的集合,可以用来解决很多计算机视觉方面的问题,诸如立体深度重建、图像拼接和图像分割等计算机视觉方面的不同问题。
图割的基本思想是,相似且彼此相近的像素应该划分到同一区域。
图割图像分割的思想是用图来表示图像,并对图进行划分以使割代价 E c u t E_{cut} Ecut 最小。在用图表示图像时,增加两个额外的节点,即源点和汇点;并仅考虑那些将源点和汇点分开的割。
寻找最小割等同于在源点和汇点间寻找最大流,此外,很多有效的算法都可以解决这些最大流/最小割问题。
下面给出一个用python-graph 工具包计算一幅较小的图的最大流/ 最小割的简单例子
from pygraph.classes.digraph import digraph
from pygraph.algorithms.minmax import maximum_flow
gr = digraph()
gr.add_nodes([0,1,2,3])
gr.add_edge((0,1), wt=4)
gr.add_edge((1,2), wt=3)
gr.add_edge((2,3), wt=5)
gr.add_edge((0,2), wt=3)
gr.add_edge((1,3), wt=4)
flows,cuts = maximum_flow(gr,0,3)
print 'flow is:', flows
print 'cut is:', cut
运行结果如下:
从图像创建图
给定一个邻域结构,我们可以利用图像像素作为节点定义一个图。这里我们将集中讨论最简单的像素四邻域和两个图像区域(前景和背景)情况。一个四邻域指一个像素与其正上方、正下方、左边、右边的像素直接相连。
下面给出创建这样一个图的步骤:
1.每个像素节点都有一个从源点的传入边;
2.每个像素节点都有一个到汇点的传出边;
3.每个像素节点都有一条传入边和传出边连接到它的近邻
假定我们已经在前景和背景像素(从同一图像或从其他的图像)上训练出了一个贝叶斯分类器,我们就可以为前景和背景计算概率 P F ( I i ) P_{F}\left ( I_{i} \right ) PF(Ii)和 P B ( I i ) P_{B}\left ( I_{i} \right ) PB(Ii),现在,可以为边的权重建立如下模型
w s i = p F ( I i ) p F ( I i ) + p B ( I i ) w i t = p B ( I i ) p F ( I i ) + p B ( I i ) w i j = κ e − ∣ I i − I j / / σ \begin{aligned}w_{s i} & =\frac{p_{F}\left(I_{i}\right)}{p_{F}\left(I_{i}\right)+p_{B}\left(I_{i}\right)} \\w_{i t} & =\frac{p_{B}\left(I_{i}\right)}{p_{F}\left(I_{i}\right)+p_{B}\left(I_{i}\right)} \\w_{i j} & =\kappa \mathrm{e}^{-\mid I_{i}-I_{j} / /_{\sigma}}\end{aligned} wsiwitwij=pF(Ii)+pB(Ii)pF(Ii)=pF(Ii)+pB(Ii)pB(Ii)=κe−∣Ii−Ij//σ,利用该模型,可以将每个像素和前景及背景连接起来,,权重等于上面归一化后的概率。
代码如下:
from pygraph.classes.digraph import digraph
from pygraph.algorithms.minmax import maximum_flow
import bayes
def build_bayes_graph(im,labels,sigma=1e2,kappa=2):""" 从像素四邻域建立一个图,前景和背景(前景用1 标记,背景用-1 标记,其他的用0 标记)由labels 决定,并用朴素贝叶斯分类器建模""" m,n = im.shape[:2]# 每行是一个像素的RGB 向量vim = im.reshape((-1,3))# 前景和背景(RGB)foreground = im[labels==1].reshape((-1,3))background = im[labels==-1].reshape((-1,3))train_data = [foreground,background]# 训练朴素贝叶斯分类器bc = bayes.BayesClassifier()bc.train(train_data)# 获取所有像素的概率bc_lables,prob = bc.classify(vim)prob_fg = prob[0]prob_bg = prob[1]# 用m*n+2 个节点创建图gr = digraph()gr.add_nodes(range(m*n+2))source = m*n # 倒数第二个是源点sink = m*n+1 # 最后一个节点是汇点# 归一化for i in range(vim.shape[0]):vim[i] = vim[i] / linalg.norm(vim[i])# 遍历所有的节点,并添加边for i in range(m*n):# 从源点添加边gr.add_edge((source,i), wt=(prob_fg[i]/(prob_fg[i]+prob_bg[i])))# 向汇点添加边gr.add_edge((i,sink), wt=(prob_bg[i]/(prob_fg[i]+prob_bg[i])))# 向相邻节点添加边if i%n != 0: # 左边存在edge_wt = kappa*exp(-1.0*sum((vim[i]-vim[i-1])**2)/sigma)gr.add_edge((i,i-1), wt=edge_wt)if (i+1)%n != 0: # 如果右边存在edge_wt = kappa*exp(-1.0*sum((vim[i]-vim[i+1])**2)/sigma)gr.add_edge((i,i+1), wt=edge_wt)if i//n != 0: 如果上方存在edge_wt = kappa*exp(-1.0*sum((vim[i]-vim[i-n])**2)/sigma)gr.add_edge((i,i-n), wt=edge_wt)if i//n != m-1: # 如果下方存在edge_wt = kappa*exp(-1.0*sum((vim[i]-vim[i+n])**2)/sigma)gr.add_edge((i,i+n), wt=edge_wt)return grdef show_labeling(im,labels):""" 显示图像的前景和背景区域。前景labels=1, 背景labels=-1,其他labels =0 """ imshow(im)contour(labels,[-0.5,0.5])contourf(labels,[-1,-0.5],colors='b',alpha=0.25)contourf(labels,[0.5,1],colors='r',alpha=0.25)axis('off')def cut_graph(gr,imsize):""" 用最大流对图gr 进行分割,并返回分割结果的二值标记""" m,n = imsizesource = m*n # 倒数第二个节点是源点sink = m*n+1 # 倒数第一个是汇点# 对图进行分割flows,cuts = maximum_flow(gr,source,sink)# 将图转为带有标记的图像res = zeros(m*n)for pos,label in cuts.items()[:-2]: # 不要添加源点/ 汇点res[pos] = labelreturn res.reshape((m,n)
在该例中将图像统一缩放到原图像尺寸的7%。图分割后将结果和训练区域一起画出来.
用户交互式分割
利用一些方法可以将图割分割与用户交互结合起来。例如,用户可以在一幅图像上为前景和背景提供一些标记。另一种方法是利用边界框或“lasso”工具选择一个包含前景的区域。
下面举一个例子,将用户输入编码成具有下面意义的位图图像
这里给出一个完整的示例代码,它会载入一幅图像及对应的标注信息,然后将其传递到我们的图割分割路径中:
from scipy.misc import imresize
import graphcut
def create_msr_labels(m,lasso=False):
""" 从用户的注释中创建用于训练的标记矩阵""" labels = zeros(im.shape[:2])# 背景labels[m==0] = -1labels[m==64] = -1# 前景if lasso:labels[m==255] = 1else:labels[m==128] = 1return labels# 载入图像和注释图im = array(Image.open('376043.jpg'))m = array(Image.open('376043.bmp'))# 调整大小scale = 0.1im = imresize(im,scale,interp='bilinear')m = imresize(m,scale,interp='nearest')# 创建训练标记labels = create_msr_labels(m,False)# 用注释创建图g = graphcut.build_bayes_graph(im,labels,kappa=2)# 图割res = graphcut.cut_graph(g,im.shape[:2])# 去除背景部分res[m==0] = 1res[m==64] = 1# 绘制分割结果figure()imshow(res)gray()xticks([])yticks([])savefig('labelplot.pdf')
首先,我们定义一个辅助函数用以读取这些标注图像,格式化这些标注图像便于将其传递给背景和前景训练模型函数,矩形框中只包含背景标记。在本例中,我们设置前景训练区域为整个“未知的”区域(矩形内部)。下一步我们创建图并分割。由于有用户输入,所以我们移除那些在标记背景区域里有任何前景的结果。最后,我们绘制出分割结果,并通过设置这些勾选标记到一个空列表来移去这些勾选标记。这样我们就可以得到一个很好的边框(否则,图像中的边界在黑白图中很难看到)
下图显示了利用RGB 向量作为原始图像的特征进行分割的一些结果,一个下采样掩膜和下采样分割结果。右边的图像是通过上面的脚本生成的图线。
利用聚类进行分割
在本节,我们将看到另外一种分割图像图的方法,即基于谱图理论的归一化分割算法,它将像素相似和空间近似结合起来对图像进行分割。
该方法来自定义一个分割损失函数,该损失函数不仅考虑了组的大小而且还用划分的大小对该损失函数进行“归一化”。
下面是其实现代码:
def ncut_graph_matrix(im,sigma_d=1e2,sigma_g=1e-2):""" 创建用于归一化割的矩阵,其中sigma_d 和sigma_g 是像素距离和像素相似性的权重参数 """m,n = im.shape[:2]N = m*n# 归一化,并创建RGB 或灰度特征向量if len(im.shape)==3:for i in range(3):im[:,:,i] = im[:,:,i] / im[:,:,i].max()vim = im.reshape((-1,3))else:im = im / im.max()vim = im.flatten()# x,y 坐标用于距离计算xx,yy = meshgrid(range(n),range(m))x,y = xx.flatten(),yy.flatten()# 创建边线权重矩阵W = zeros((N,N),'f')for i in range(N):for j in range(i,N):d = (x[i]-x[j])**2 + (y[i]-y[j])**2W[i,j] = W[j,i] = exp(-1.0*sum((vim[i]-vim[j])**2)/sigma_g) * exp(-d/sigma_d)return W
该函数获取图像数组,并利用输入的彩色图像RGB 值或灰度图像的灰度值创建一个特征向量。由于边的权重包含了距离部件,对于每个像素的特征向量,我们利用meshgrid() 函数来获取x 和y 值,然后该函数会在N 个像素上循环,并在N×N 归一化割矩阵W 中填充值。
我们可以顺序分割每个特征向量或获取一些特征向量对它们进行聚类来计算分割结果。这里选择第二种方法,它不需要修改任意分割数也能正常工作。将拉普拉斯矩阵进行特征分解后的前ndim 个特征向量合并在一起构成矩阵W,并对这些像素进行聚类。下面函数实现了该聚类过程
from scipy.cluster.vq import *
def cluster(S,k,ndim):""" 从相似性矩阵进行谱聚类""" # 检查对称性if sum(abs(S-S.T)) > 1e-10:print 'not symmetric'# 创建拉普拉斯矩阵rowsum = sum(abs(S),axis=0)D = diag(1 / sqrt(rowsum + 1e-6))L = dot(D,dot(S,D))# 计算L 的特征向量U,sigma,V = linalg.svd(L)# 从前ndim 个特征向量创建特征向量# 堆叠特征向量作为矩阵的列features = array(V[:ndim]).T# K-means 聚类 features = whiten(features)centroids,distortion = kmeans(features,k)code,distance = vq(features,centroids)return code,Vimport ncut
from scipy.misc import imresize
im = array(Image.open('C-uniform03.ppm'))
m,n = im.shape[:2]
# 调整图像的尺寸大小为(wid,wid)
wid = 50
rim = imresize(im,(wid,wid),interp='bilinear')
rim = array(rim,'f')
# 创建归一化割矩阵
A = ncut.ncut_graph_matrix(rim,sigma_d=1,sigma_g=1e-2)
# 聚类
code,V = ncut.cluster(A,k=3,ndim=3)
# 变换到原来的图像大小
codeim = imresize(code.reshape(wid,wid),(m,n),interp='nearest')
# 绘制分割结果
figure()
imshow(codeim)
gray()
show()
在该例中,我们用到了静态手势数据库的某幅手势图像,并且聚类数k 设置为3。分割结果如下图所示,取前4 个特征向量。
变分法
在本书中有很多利用最小化代价函数或能量函数来求解计算机视觉问题的例子,如前面章节中在图中最小化割;我们同样可以看到诸如ROF 降噪、K-means 和SVM的例子,这些都是优化问题。
当优化的对象是函数时,该问题称为变分问题,解决这类问题的算法称为变分法。我们看一个简单而有效的变分模型。
Chan-Vese 分割模型对于待分割图像区域假定一个分片常数图像模型。这里我们集中关注两个区域的情形,比如前景和背景,不过这个模型也可以拓展到多区域,我们接下来对该模型进行描述
分片常数Chan-Vese 分割模型如上图所示
最小化Chan-Vese 模型现在转变成为设定阈值的ROF 降噪问题
import rof
im = array(Image.open('ceramic-houses_t0.png').convert("L"))
U,T = rof.denoise(im,im,tolerance=0.001)
t = 0.4 # 阈值
import scipy.misc
scipy.misc.imsave('result.pdf',U < t*U.max()
为确保得到足够的迭代次数,我们调低ROF 迭代终止时的容忍阈值。下图显示了两幅难以分割图像的分割结果。
利用ROF 降噪最小化Chan-Vese 模型的一些图像分割示例:(a)为原始图像;(b)为经过ROF 降噪后的图像;(c)为最终分割结果