这是一篇和GrabCut同时期的算法,论文名称就叫《Lazy Snapping》,他的作用是用户交互指定前景背景来分割,如图所示。
我还是使用的Eigen库+maxflow库实现,代码库链接如下:MaxtirError/LazySnaping: LazySnaping algorithm with pure C++ and Qt GUI (github.com)
这篇文章仍然用到了两个简单的前置算法:分水岭算法和k-means聚类,下面简单介绍
这个算法被用于图像预分割,想法非常巧妙。首先,图像转灰度,其次,把灰度看成高度,那么整张图片形成了一张地形图,尝试把水注入地形图中,当两块水域合并的时候,修筑堤坝,分割这两块小区域。实际上就是以每个局部极小值为中心把图像分割成若干块。
我并没有去阅读这个算法的原论文,而是基于上面的想法写了如下算法。
图像转灰度图
从小到大扫描灰度,对于扫描到的新点 u u u,考虑和他四相邻的节点 v i v_i vi:
对于已经被扫描过且满足 G r a y ( u ) − G r a y ( v ) < = t h r e d ( 20 ) Gray(u)-Gray(v)<=thred(20) Gray(u)−Gray(v)<=thred(20)的 v i v_i vi,直接把这个新点 u u u划分到随便一个 v i v_i vi所在区域。
如果不存在这样的 v i v_i vi,把 u u u划分到一个新的区域
扫描完所有灰度之后,可以得到一个划分。
如下是某张图划分的结果
可以看到,很多相似的像素区域(比如天空)被很好地划分了。
这个算法最显著的效果就是:优化了参数空间。
原图大小是960*720,优化过后的region只有32298,只有原图的4%左右
类似GMM的一个聚类算法,但是比GMM简单很多。输入是一堆数据 { X i } \{X_i\} {Xi},希望找到一个大小为k的划分 { S 1 ⋯ S k } \{S_1\cdots S_k\} {S1⋯Sk},使得 E n e r g y = ∑ k ∑ X i ∈ S k ∣ ∣ X i − μ k ∣ ∣ Energy=\sum_k \sum_{X_i\in S_k} ||X_i-\mu_k|| Energy=∑k∑Xi∈Sk∣∣Xi−μk∣∣最小化。也就是每个 X i X_i Xi到它属于集合的某个中心距离和最小。
算法流程实际上就是一个简单的迭代:
初始化每个集合 S i S_i Si的k个质心 μ i \mu_i μi
计算所有点最近的质心,把这些点划分到对应的集合里面。
对于每个集合,用属于这个集合的所有点的均值作为新的质心。
重复2-3直到能量收敛
这个算法理解起来很简单,就是一个简单的Local/Global模型。但是有一个问题初始值的决定。如果随机k个点作为初始质心的化,很容易出现空簇(也就是某个集合突然没有点离它最近了),这个时候能量会收敛到一个局部极小值。
为了解决上述问题,有一个办法是,先选一个点,对于已经选好的初始点集 S S S,每次选择距离这个集合 S S S最远的一个点加入 S S S作为新的初始点,使得所有初始点的位置尽可能分散在样本点中。这就是kmeans++算法,效果会好一点。
给定一张色彩图数据 C i C_i Ci,和用户交互数据 T B , T F T_B,T_F TB,TF,也就是确定为背景和前景的点集。
求一个“较好”的划分 X i ∈ { 0 , 1 } X_i\in\{0,1\} Xi∈{0,1},分离前景(1)和背景(0)
同样地,我们还是需要定义什么样的划分是优秀的,这里其实也是利用Cut的思想,定义能量函数为
E ( x ) = ∑ i E 1 ( x i ) + λ ∑ ( i , j ) ∈ C E 2 ( x i , x j ) E(x)=\sum_i E_1(x_i)+\lambda \sum_{(i,j)\in C} E_2(x_i,x_j) E(x)=i∑E1(xi)+λ(i,j)∈C∑E2(xi,xj)
也即似然能量 E 1 ( x i ) E_1(x_i) E1(xi)和边界能量 E 2 ( x i , x j ) E_2(x_i,x_j) E2(xi,xj), C C C表示 ( i , j ) (i,j) (i,j)相邻
字面意思,就是当前像素和用户指定的前景/背景的相似程度。
因此,对于点击 T B , T F T_B,T_F TB,TF,分别用kmeans聚类算法得到 K n B , K m F K_n^B,K_m^F KnB,KmF,聚类的个数 K K K文章用的是64。某个像素点到这两个聚类的距离定义为
d X = min i ∣ ∣ C ( i ) − K i X ∣ ∣ d^X=\min_i||C(i)-K^X_i|| dX=imin∣∣C(i)−KiX∣∣
那么,似然能量被定义为
E 1 ( x i ) = [ x i = 0 ] d B d B + d F + [ x i = 1 ] d F d B + d F E_1(x_i)=[x_i=0]\frac{d^B}{d^B+d^F}+[x_i=1]\frac{d^F}{d^B+d^F} E1(xi)=[xi=0]dB+dFdB+[xi=1]dB+dFdF
也就是两块区域的边界分割产生的能量,具体地,定义 g ( x ) = 1 x + 1 g(x)=\frac{1}{x+1} g(x)=x+11,边界能量被定义为
E 2 ( x i , x j ) = ∣ x i − x j ∣ ] g ( ∣ ∣ C ( i ) − C ( j ) ∣ ∣ 2 ) E_2(x_i,x_j)=|x_i-x_j|]g(||C(i)-C(j)||^2) E2(xi,xj)=∣xi−xj∣]g(∣∣C(i)−C(j)∣∣2)
也就是越相似的像素相邻像素被分割到不同类所产生的能量越大
我们需要做的就是最小化能量。
调用分水岭算法,先预分割图像,并处理出预分割后每个小区域的平均像素,以及小区域之间的连接情况。
把用户交互的前景/背景像素转化成被选中的小区域(区域内只要有一个像素被选中,就视为被选中。
kmeans聚类算法算两类区域的聚类。
根据最小割模型分割图像最小化能量。
注解:
kmeans聚类算法有两种选择,一种是使用原始数据聚类,一种是用平均像素聚类。原文我没有找到用的是那种方法,实测下来,两种方法的效果是类似的,但平均像素聚类速度更快。
原文中边界能量和似然能量用的都是小区域平均像素计算。
最小割集合划分方法在GrabCut算法中有介绍,这里从略。
λ \lambda λ取100-400效果都差不多。
速度和效率都很高。
有几个小问题:
边界有一些毛边。
有一些块状小区域分割错误(背景和前景都有错误)
并未实现的解决办法:
边界的毛边,原文中是采取用户交互+算法处理的。具体方法大致是用一个距离阈值和用户拖拽来把边界点集拟合成一个比较平滑的多边形。
关于小区域分割错误的情况,一方面可以通过用户交互来修正,一方面可以设置一个大小阈值把分割错误的区域修正。
事实上,得到了大致的边界结果之后,剩余的处理办法很多,也比较简单。