目录
一、图割(Graph Cut)
图论中的图是由若干节点(也称顶点)和连接节点的边构成的集合。边可以是有向的(或无向的,并且可能有与它们相关联的权重。
图割是将一个有向图分割成两个互不相交的集合,可以用来解决很多计算机视觉方面的问题。从图像像素和像素的近邻创建一个图并引入一个能量或“代价”函数,可以利用图割方法将图像分割成两个或多个区域。图割的基本思想是,相似且彼此相近的像素应该划分到同一区域。
图割 C的“代价”函数定义为所有割的边的权重求合相加,即:
图割图像分割的思想是用图来表示图像,并对图进行划分以使割代价 最小。在用图表示图像时,增加源点和汇点这两个额外的节点;并仅考虑那些将源点和汇点分开的割。寻找最小割等同于在源点和汇点间寻找最大流。
计算一幅图像的最大流 / 最小割的示例为
from pygraph.core.pygraph.classes.digraph import digraph
from pygraph.core.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:', cuts)
可以得到结果为
上述结果中0表示包含图源点的部分,1表示与汇点相连的节点。
1.从图像创建图
给定一个邻域结构,可以利用图像像素作为节点定义一个图。除了像素节点外,还需要两个特定的节点——“源”点和“汇”点,来分别代表图像的前景和背景。创建一个图的大致步骤为:
- 每个像素节点都有一个从源点的传入边
- 每个像素节点都有一个到汇点的传出边
- 每个像素节点都有一条传入边和传出边连接到它的近邻
实现的主函数为:
from PIL import Image
from matplotlib import pyplot as plt
import numpy as np
# from scipy.misc import imresize
import graphcut
def resize_image(image_array, scale):
image = Image.fromarray(image_array)
new_size = (int(image.width * scale), int(image.height * scale))
resized_image = image.resize(new_size, Image.BILINEAR)
return np.array(resized_image)
im = np.array(Image.open(r"D:\test\empire.jpg"))
im = resize_image(im, 0.07)
size = im.shape[:2]
# 添加两个矩形训练区域
labels = np.zeros(size)
labels[3:18,3:18] = -1
labels[-18:-3,-18:-3] = 1
# 创建图
g = graphcut.build_bayes_graph(im,labels,kappa=1)
# 对图进行分割
# import pdb; pdb.set_trace()
res = graphcut.cut_graph(g,size)
plt.figure()
plt.subplot(131)
plt.title("Original image")
plt.imshow(im)
# plt.gray()
plt.axis('off')
plt.subplot(132)
graphcut.show_labeling(im,labels)
plt.subplot(133)
plt.title("Graph cut result")
plt.imshow(res)
plt.gray()
plt.axis('off')
plt.show()
具体功能函数为:
from matplotlib import pyplot as plt
import numpy as np
from pygraph.core.pygraph.classes.digraph import digraph
from pygraph.core.pygraph.algorithms.minmax import maximum_flow
import bayes
def build_bayes_graph(im,labels,sigma=1e2,kappa=2):
m,n = im.shape[:2]
# 每行是一个像素的 RGB 向量
# import pdb; pdb.set_trace()
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] / np.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*np.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*np.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*np.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*np.exp(-1.0*sum((vim[i]-vim[i+n])**2)/sigma)
gr.add_edge((i,i+n), wt=edge_wt)
return gr
def show_labeling(im,labels):
plt.imshow(im)
plt.contour(labels,[-0.5,0.5])
plt.contourf(labels,[-1,-0.5],colors='b',alpha=0.25)
plt.contourf(labels,[0.5,1],colors='r',alpha=0.25)
plt.axis('off')
def cut_graph(gr,imsize):
m,n = imsize
source = m*n # 倒数第二个节点是源点
sink = m*n+1 # 倒数第一个是汇点
# 对图进行分割
flows,cuts = maximum_flow(gr,source,sink)
# 将图转为带有标记的图像
res = np.zeros(m*n)
items = list(cuts.items())
# import pdb; pdb.set_trace()
for pos,label in items[:-2]: # 不要添加源点 / 汇点
res[pos] = label
return res.reshape((m,n))
其中出现了如下错误:
经过网上查询之后问题得到了修改,最后可以得到结果为
原图为:
当修改K值为2时得到的结果为:
可以看到 K 值增大时,分割边界将变得更平滑,但细节部分会逐步丢失。
2.用户交互式分割
利用一些方法可以将图割分割与用户交互结合起来。例如,用户可以在一幅图像上 为前景和背景提供一些标记。另一种方法是利用边界框或“lasso”工具选择一个包含前景的区域。
def create_msr_labels(m,lasso=False):
labels = np.zeros(im.shape[:2])
# import pdb;pdb.set_trace()
labels[m==0] = -1
labels[m==64] = -1
# 前景
if lasso:
labels[m==255] = 1
else:
labels[m==128] = 1
return labels
im = Image.open(r"D:\idmDownload\GeoSeg-main\data\uavid1\train\images\00335.png")
m = np.array(Image.open(r"D:\idmDownload\GeoSeg-main\data\uavid1\train\labels\00335.png"))
# 调整大小
scale = 0.1
im = im.resize((102,102),Image.BILINEAR)
img = np.array(im)
im = np.array(im)
m = resize_image1(m,scale)
# 创建训练标记
labels = create_msr_labels(m,False)
# 用注释创建图
g = graphcut.build_bayes_graph(img,labels,kappa=2)
# 图割
# import pdb;pdb.set_trace()
res = graphcut.cut_graph(g,im.shape[:2])
# 去除背景部分
res[m==0] = 1
res[m==64] = 1
# 绘制分割结果
plt.figure()
plt.subplot(131)
plt.imshow(im)
plt.gray()
plt.xticks([])
plt.yticks([])
plt.subplot(132)
plt.imshow(m)
plt.gray()
plt.xticks([])
plt.yticks([])
plt.subplot(133)
plt.imshow(res)
plt.gray()
plt.xticks([])
plt.yticks([])
plt.savefig('labelplot.pdf')
plt.show()
最终得到的结果为:
并没有像课本中的例子一样出现分割后的结果,目前猜测是build_bayes_graph()函数存在问题。
二、利用聚类进行分割
其主要是基于谱图理论的归一化分割算法,将像素相似和空间近似结合起来对图像进行分割。首先需要自定义一个分割损失函数,归一化后的分割公式会将分割损失函数修改为:
A 和 B 表示两个割集。对于那些像素与其他像素具有相同连接数的图像,它是对划分大小的一种粗糙度量方式。归一化分割可以通过最小化下面的优化问题而求得:
可以通过松弛约束条件来让其变为一个容易求解的特征分解问题,缺点是你需要对输出设定阈值或进行聚类,使它重新成为一个离散分割,经过松弛后就变为了求解拉普拉斯矩阵特征向量问题:
即将前面的难以求解的问题变成了定义像素间边的权重的问题。连接像素i和像素j的边的权重:
第一部分度量像素和之间的像素相似性,() 定义为 RGB 向量或灰度值;第二部分度量图像中和的接近程度,() 定义为每个像素的坐标矢量。其实现代码为:
def ncut_graph_matrix(im,sigma_d=1e2,sigma_g=1e-2):
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 = np.meshgrid(range(n),range(m))
x,y = xx.flatten(),yy.flatten()
# 创建边线权重矩阵
W = np.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])**2
W[i,j] = W[j,i] = np.exp(-1.0*sum((vim[i]-vim[j])**2)/sigma_g) * np.exp(-d/sigma_d)
return W
这个函数可以获取图像数组,并利用输入的彩色图像 RGB 值或灰度图像的灰度值创建一个特征向量。其次可以顺序分割每个特征向量或获取一些特征向量对它们进行聚类来计算分割结果。这里使用的是将拉普拉斯矩阵进行特征分解后的前 ndim 个特征向量合并在一起构成矩阵 W,并对这些像素进行聚类。聚类的实现过程的函数为:
def cluster(S,k,ndim):
if np.sum(abs(S-S.T)) > 1e-10:
print ('not symmetric')
# 创建拉普拉斯矩阵
rowsum = np.sum(abs(S),axis=0)
D = np.diag(1 / np.sqrt(rowsum + 1e-6))
L = np.dot(D,np.dot(S,D))
# 计算 L 的特征向量
U,sigma,V = np.linalg.svd(L)
# 从前 ndim 个特征向量创建特征向量
# 堆叠特征向量作为矩阵的列
features = np.array(V[:ndim]).T
# K-means 聚类
features = whiten(features)
centroids,distortion = kmeans(features,k)
code,distance = vq(features,centroids)
return code,V
在以上两个函数的基础之上加上如下的实现代码,通过调用这两个函数来实现聚类分割的效果:
im = np.array(Image.open(r"D:\test\uniform\V-uniform56.ppm"))
# 调整图像的尺寸大小为 (wid,wid)
m,n = im.shape[:2]
wid = 50
rim = scipy_misc_imresize(im, (wid,wid), interp='bilinear')
rim = np.array(rim,'f')
A = ncut.ncut_graph_matrix(rim,sigma_d=1,sigma_g=1e-2)
# 聚类
code,V = ncut.cluster(A,k=4,ndim=3)
# 变换到原来的图像大小
codeim = resize_image(code,(50,50))
codeim = scipy_misc_imresize(code.reshape(wid,wid),(m,n),interp='nearest')
# 绘制分割结果
plt.figure()
plt.subplot(121)
plt.imshow(im)
plt.gray()
plt.subplot(122)
plt.imshow(codeim)
plt.gray()
plt.show()
plt.figure()
for i in range(4):
plt.subplot(141+i)
plt.imshow(scipy_misc_imresize(V[i].reshape(wid,wid),(m,n),interp='bilinear'))
plt.gray()
plt.show()
最终得到的结果为:
上图为对一张手势图进行聚类分割的图片,下面一张图是在这个聚类分割过程中图相似矩阵的4个特征向量显示为图片的结果。
三、变分法
当优化的对象是函数时,该问题称为变分问题,解决这类问题的算法称为变分法。Chan-Vese 分割模型对于待分割图像区域假定一个分片常数图像模型。分割是通过最小化 Chan-Vese 模型能量函数给出的:
其是用来度量与内部平均灰度常数 和外部平均灰度常数 的偏差的。可以通过分片常数图像将前面的能量函数修改为;
最小化 Chan-Vese 模型可以转变成为设定阈值的 ROF 降噪问题:
from PIL import Image
from PCV.tools import rof
from matplotlib import pyplot as plt
import numpy as np
im = np.array(Image.open(r"D:\test\pil.png").convert("L"))
U,T = rof.denoise(im,im,tolerance=0.001)
t = 0.4 # 阈值
image = Image.fromarray((U < t * U.max()).astype(np.uint8) * 255)
plt.figure()
plt.subplot(121)
plt.imshow(im,cmap='gray')
plt.title('Original Image')
plt.subplot(122)
plt.imshow(image,cmap='gray')
plt.title('segmentation Image')
# image.show()
plt.show()
可以得到结果为:
通过修改不同的阈值可以得到不同的结果,最上面一幅图为阈值为0.4得到的结果,中间的图为阈值为0.6得到的结果,最下面一幅图为阈值为0.8得到的结果。阈值对于图像而言应该处与一个合适的范围才能得到理想的结果。太低会导致想要分割的区域不够完全,而太高又会导致分割到一些不想要分割的位置。