OpenCV计算机视觉开发实践:基于Qt C++ - 商品搜索 - 京东
基本概念
使用grabCut算法可以用最小程度的用户交互来分解前景。从用户角度来看,grabCut算法是怎么工作的呢?首先画一个矩形方块把前景图圈起来,前景区域应该完全在矩形内;然后算法反复进行分割以达到最好的效果。但是,有些情况下分割得不是很好,比如把前景标成背景了,这种情况下用户需要再润色,就是在图像上有缺陷的地方画几笔。这几笔的意思是说"嘿,这个区域应该是前景,你把它标成背景了,下次迭代改过来"或者是反过来。那么下次迭代的结果会更好。比如图9-14所示的图像。

图9-14
首先将球员和足球包含在蓝色矩形框里,然后用白色笔(指出前景)和黑色笔(指出背景)来做一些润色。后台会发生什么呢?
(1)用户输入矩形,矩形外的所有东西都被确认是背景,矩形内的所有东西都是未知的。同样地,任何用户输入指定的前景和背景也都被认为是硬标记,在处理过程中不会变。
(2)计算机会根据我们给的数据做初始标记,它会标记出前景和背景像素。
(3)现在会使用高斯混合模型(GMM)来为前景和背景建模。
(4)根据我们给的数据,GMM学习和创建新的像素分布,未知像素被标为可能的前景或可能的背景(根据其他硬标记像素的颜色统计和它们之间的关系)。根据这个像素分布创建一幅图,图中的节点是像素。另外还有两个节点,即源节点和汇节点,每个前景像素和源节点相连,每个背景像素和汇节点相连。
(5)源节点和汇节点连接的像素的边的权重由像素是前景或者背景的概率决定。像素之间的权重是由边的信息或者像素的相似度决定。如果像素颜色有很大差异,它们之间的边的权重就比较低。
(6)最小分割算法是用来分割图的,它用最小成本函数把图切成两个分开的源节点和汇节点,成本函数是被切的边的权重之和。切完以后,所有连到源节点的像素称为前景,所有连到汇节点的像素称为背景。
(7)过程持续进行,直到分类覆盖。
整个过程如图9-15所示。

grabCut函数
grabCut算法是Graphcut算法的改进,Graphcut是一种直接基于图割算法的图像分割技术,只需确认前景和背景的输入,该算法就可以完成前景和背景的最优分割。grabCut算法利用图像中的纹理(颜色)信息和边界(反差)信息,只需少量的用户交互操作,即可获得较好的分割结果。与分水岭算法相比,grabCut的计算速度较慢,但能够提供更精确的分割结果。如果要从静态图像中提取前景物体(如从一幅图像剪切物体到另一幅图像),采用grabCut算法是最好的选择。其用法很简单,只需输入一幅图像,并对一些像素进行标记,指明其属于背景或前景,算法就会根据这些局部标记计算出整个图像中前景和背景的分割线。现在我们用OpenCV来实现grabCut算法。OpenCV中有一个函数grabCut,其声明如下:
void grabCut( InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode = GC_EVAL );
参数说明如下:
- img:表示输入图像。
- mask:表示输出掩码,如果使用掩码进行初始化,那么mask保存初始化掩码信息。在执行分割的时候,也可以将用户交互所设定的前景与背景保存到mask中,然后再传入grabCut函数,在处理结束之后,mask中会保存结果。mask只能取以下4种值:GCD_BGD(=0)表示背景,GCD_FGD(=1)表示前景,GCD_PR_BGD(=2)表示可能的背景,GCD_PR_FGD(=3)表示可能的前景。如果没有手动标记GCD_BGD或者GCD_FGD,那么结果只会有GCD_PR_BGD或GCD_PR_FGD。
- rect:表示用户选择的前景矩形区域,包含分割对象的矩形ROL,矩形外部的像素为背景,矩形内部的像素为前景,当参数mode= GC_INIT_WITH_RECT时使用。
- bgModel:表示输出背景图像。
- fgModel:表示输出前景图像。
- iterCount:表示迭代次数。
- mode:表示用于指示grabCut函数进行什么操作,可选的值有GC_INIT_WITH_RECT(=0),表示用矩形窗初始化grabCut;GC_INIT_WITH_MASK(=1)表示用掩码图像初始化GrabCut;GC_EVAL(=2)表示执行分割。
可以按以下方式来使用grabCut函数:
(1)用矩形窗或掩码图像初始化grabCut。
(2)执行分割。
(3)如果对结果不满意,在掩码图像中设定前景和(或)背景,再次执行分割。
(4)使用掩码图像中的前景或背景信息。
利用grabCut函数做图像分割时,通常还需要和compare函数联合作战。compare函数主要用于在两个图像之间进行逐像素的比较,并输出比较的结果,其声明如下:
void compare(InputArray src1, InputArray src2, OutputArray dst, int cmpop);
其中参数src1表示原始图像1(必须是单通道)或者一个数值,比如是一个Mat或者一个单纯的数字n;src2表示原始图像2(必须是单通道)或者一个数值,比如是一个Mat或者一个单纯的数字n;dst表示结果图像,类型是CV_8UC1,即单通道8位图,大小和src1、src2中最大的那个一样,比较结果为真的地方值为255,否则为0;cmpop表示操作类型,有以下几种类型:
enum { CMP_EQ=0, // 相等
CMP_GT=1, // 大于
CMP_GE=2, // 大于或等于
CMP_LT=3, // 小于
CMP_LE=4, // 小于或等于
CMP_NE=5 }; // 不相等
从参数的要求可以看出,compare函数只对以下3种情况进行比较:
1)array和array
此时输入的src1和src2必须是相同大小的单通道图,否则没办法进行比较。计算过程如下:
dst(i) = src1(i) cmpop src2(i)
也就是对src1和src2逐像素进行比较。
2)array和scalar
此时array仍然要求是单通道图,大小无所谓,因为scalar只是一个单纯的数字而已。比较过程是把array中的每个元素逐一和scalar进行比较,所以此时的dst大小和array是一样的。计算过程如下:
dst(i) = src1(i) cmpop scalar
3)scalar和array
这个就是第2种情况的反过程了,只是比较运算符cmpop左右的参数顺序不一样而已。计算过程如下:
dst(i) = scalar cmpop src2(i)
这个函数有一个很有用的地方就是:当你需要从一幅图像中找出某些特定像素值的像素时,可以用这个函数。它与threshold()函数类似,但是threshold()函数是对某个区间内的像素值进行操作,而compare()函数可以只是对某一个单独的像素值进行操作。比如我们要从图像中找出像素值为50的像素点,可以像下面这样做:
Mat result;
compare(image,50, result, cv::CMP_EQ);
【例9.6】利用grabCut做分割
新建一个控制台工程,工程名是test。
打开main.cpp,输入如下代码:
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <opencv2/imgproc/types_c.h>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat src = imread("bird.jpg");
Rect rect(84, 84, 406, 318);// 左上坐标(X,Y)和长宽
Mat result, bg, fg;
grabCut(src, result, rect, bg, fg, 1, GC_INIT_WITH_RECT);
imshow("grab", result);
/*threshold(result, result, 2, 255, CV_THRESH_BINARY);
imshow("threshold", result);*/
// result和GC_PR_FGD对应像素相等时,目标图像中该像素值置为255
compare(result, GC_PR_FGD, result, CMP_EQ);
imshow("result", result);
Mat foreground(src.size(), CV_8UC3, Scalar(255, 255, 255));
src.copyTo(foreground, result);// copyTo有两种形式,此形式表示result为mask
imshow("foreground", foreground);
waitKey(0);
return 0;
}
在上述代码中,首先加载了图片test.jpg,然后使用函数grabCut从原图中复制可能的前景到结果图像中。函数compare在两个图像之间进行逐像素的比较,并输出比较的结果。这里result和GC_PR_FGD对应像素相等时,目标图像的像素值置为255。最后把前景复制到矩阵foreground中,并进行显示。
保存工程并运行,结果如图9-16所示。

图9-16