OpenCV计算机视觉开发实践:基于Qt C++ - 商品搜索 - 京东
9.8.1 基本概念
Flood Fill(漫水填充)算法是一种在许多图形绘制软件中常用的填充算法。通常情况下,该算法会自动选中与种子像素相连的区域,并利用指定颜色对该区域进行填充。这个算法常用于标记或分离图像的特定部分,以便进行进一步的分析和处理。Windows画图工具中的油漆桶功能和Photoshop的魔术棒选择工具,都是Flood Fill算法的改进和延伸。
漫水填充算法的原理很简单,就是从一个点开始遍历附近的像素点,并填充成新的颜色,直到封闭区域内所有像素点都被填充成新颜色为止。floodFill的实现方法常见的有4邻域像素填充法、8邻域像素填充法、基于扫描线的像素填充方法等。
在OpenCV中,漫水填充是填充算法中最通用的方法。使用C++重写过的floodFill函数有两个版本,一个是不带mask的版本,另一个是带mask的版本。这个mask就是用于进一步控制哪些区域将被填充颜色(比如对同一图像进行多次填充时)。这两个版本的floodFill,都必须在图像中选择一个种子点,然后把临近区域所有相似点填充上同样的颜色;不同之处在于,不一定将所有的邻近像素点都染上同一颜色。漫水填充操作的结果总是某个连续的区域。当邻近像素点位于给定的范围(从loDiff到upDiff)内或在原始seedPoint像素值范围内时,floodFill函数就会为这个点涂上颜色。
9.8.2 floodFill函数
在OpenCV中,漫水填充算法由floodFill函数实现,其作用是用指定的颜色从种子点开始填充一个连接域,连通性由像素值的接近程度来衡量。OpenCV中使用C++重写的两个floodFill函数的声明如下:
int cv::floodFill(InputOutputArray image, Point seedPoint, Scalar newVal, Rect * rect = 0, Scalar loDiff = Scalar(), Scalar upDiff = Scalar(), int flags = 4);
int cv::floodFill (InputOutputArray image, InputOutputArray mask, Point seedPoint, Scalar newVal, Rect * rect = 0, Scalar loDiff = Scalar(),Scalar upDiff = Scalar(),int flags = 4);
这两个函数除了第二个参数外,其他的参数都是共用的。其中参数image是一个输入/输出参数,表示一通道或三通道、8位或浮点图像,具体取值由之后的参数指明。参数mask也是输入/输出参数,是第二个版本的floodFill独享的,表示操作掩膜,它应该为单通道、8位、长和宽上都比输入图像image大两个像素点的图像。第二个版本的floodFill需要使用以及更新掩膜,所以这个mask参数我们一定要准备好并填在此处。需要注意的是,漫水填充不会填充mask的非零像素区域。例如,一个边缘检测算子的输出可以用来作为掩膜,以防止填充到边缘。同样地,也可以在多次的函数调用中使用同一个掩膜,以保证填充的区域不会重叠。
另外需要注意的是,mask会比需填充的图像大,所以mask中与输入图像(x,y)像素点相对应的点的坐标为(x+1,y+1)。参数seedPoint表示漫水填充算法的起始点。参数Scalar类型的newVal表示像素点被染色的值,即像素在重绘区域的新值。参数rect的有默认值为0,一个可选的参数,用于设置floodFill函数将要重绘区域的最小边界矩形区域;参数loDiff的默认值为Scalar(),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之负差(lower brightness/color difference)的最大值。参数upDiff的默认值为Scalar(),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之正差(lower brightness/color difference)的最大值。参数flags表示操作标志符,此参数包含3个部分:
(1)低八位部分(第0~7位)用于控制算法的连通性,可取4(4为默认值)或者8。如果设为4,表示填充算法只考虑当前像素水平方向和垂直方向的相邻点;如果设为 8,除上述相邻点外,还会包含对角线方向的相邻点。
(2)高八位部分(16~23位)可以为0或者如下两种选项标识符的组合:
- FLOODFILL_FIXED_RANGE:如果设置为这个标识符,就会考虑当前像素与种子像素之间的差,否则就考虑当前像素与其相邻像素的差。也就是说,这个范围是浮动的。
- FLOODFILL_MASK_ONLY:如果设置为这个标识符,函数不会去填充改变原始图像(也就是忽略第三个参数newVal),而是去填充掩膜图像(mask)。这个标识符只对第二个版本的floodFill有用,因为第一个版本里面没有mask参数。
(3)中间八位部分(8~15位)用于指定填充掩码图像的值。如果flags中间八位的值为0,则掩码会用1来填充。而所有flags可以用or操作符(即"|")连接起来。例如,如果想用8邻域填充,并填充固定像素值范围,即填充掩码而不是填充原图像,以及设填充值为38,那么输入的参数是这样的:
flags=8 | FLOODFILL_MASK_ONLY | FLOODFILL_FIXED_RANGE | (38<<8)
9.8.3 floodFill函数示例
下面来看一个关于floodfill的简单的调用范例。
【例9.7】利用floodfill进行图像分割
新建一个控制台工程,工程名是test。
在vc中打开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("mysky.jpg");
imshow("src", src);
Rect ccomp;
floodFill(src, Point(50, 300), Scalar(155, 255, 55), &ccomp, Scalar(20, 20, 20), Scalar(20, 20, 20));
imshow("res", src);
waitKey(0);
return 0;
}
代码很简单,主要就是floodFill的调用,读者可以根据实际参数对照floodFill的原型调用方法。
保存工程并运行,结果如图9-17所示。

图9-17
下面再来看一个综合例子,它可以根据用户鼠标的单击和滑杆调节来实现不同区域的图像分割。
【例9.8】功能更强大的floodFill分割
新建一个控制台工程,工程名是test。
在vc中打开main.cpp,并输入如下代码:
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <opencv2/imgproc/types_c.h>
#include <QDebug>
#include <iostream>
using namespace std;
using namespace cv;
// 全局变量声明部分
Mat g_srcImage, g_dstImage, g_grayImage, g_maskImage;// 定义原始图、目标图、灰度图、掩膜图
int g_nFillMode = 1; // 漫水填充的模式
int g_nLowDifference = 20, g_nUpDifference = 20;// 负差最大值,正差最大值
int g_nConnectivity = 4; // 表示floodFill函数标识符低八位的连通值
bool g_bIsColor = true; // 是否为彩色图的标识符布尔值
bool g_bUseMask = false; // 是否显示掩膜窗口的布尔值
int g_nNewMaskVal = 255; // 新的重新绘制的像素值
// ===============【onMouse()函数】=======================
static void onMouse(int event, int x, int y, int, void *) {
// 若鼠标左键没有按下,便返回
if (event != EVENT_LBUTTONDOWN)
return;
// -----------------【<1>调用floodFill函数之前的参数准备部分】-------------
Point seed = Point(x, y);
int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference;
int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference;
// 标识符的0~7位为g_nConnectivity,8~15位为g_nNewMaskVal左移8位的值,16~23位为CV_FLOODFILL_FIXED_RANGE或者0
int flags = g_nConnectivity + (g_nNewMaskVal << 8) + (g_nFillMode == 1 ? FLOODFILL_FIXED_RANGE : 0);
// 随机生成BGR值
int b = (unsigned)theRNG() & 255;// 随机返回一个0~255的值
int g = (unsigned)theRNG() & 255;
int r = (unsigned)theRNG() & 255;
Rect ccomp;// 定义重绘区域的最小边界矩阵区域
Scalar newVal = g_bIsColor ? Scalar(b, g, r) : Scalar(r*0.299 + g * 0.587 + b * 0.114);
Mat dst = g_bIsColor ? g_dstImage : g_grayImage;// 目标图的赋值
int area;
// ---------------------【<2>正式调用floodFill函数】------------------
if (g_bUseMask) {
threshold(g_maskImage, g_maskImage, 1, 128, THRESH_BINARY);
area = floodFill(dst, g_maskImage, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags);
imshow("mask", g_maskImage);
}
else {
area = floodFill(dst, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference), Scalar(UpDifference, UpDifference, UpDifference), flags);
}
imshow("res", dst);
qDebug()<< area << " 个像素被重新绘制\n";
}
// main()函数
int main(int argc, char** argv) {
// 载入原图
g_srcImage = imread("study.jpg", 1);
if (!g_srcImage.data) {
printf("Fail to open file.!\n");
return false;
}
g_srcImage.copyTo(g_dstImage);// 复制原图到目标图
cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);// 转为灰度图到g_grayImage
g_maskImage.create(g_srcImage.rows + 2, g_srcImage.cols + 2, CV_8UC1);// 用原图尺寸初始化mask
namedWindow("res", WINDOW_AUTOSIZE);
// 创建Trackbar
createTrackbar("负差最大值", "res", &g_nLowDifference, 255, 0);
createTrackbar("正差最大值", "res", &g_nUpDifference, 255, 0);
// 鼠标回调函数
setMouseCallback("res", onMouse, 0);
// 循环轮询按键
while (1) {
// 先显示效果图
imshow("res", g_bIsColor ? g_dstImage : g_grayImage);
// 获取按键键盘
int c = waitKey(0);
// 判断ESC是否被按下,若按下则退出
if (c == 27) {
cout << "game over.\n";
break;
}
// 根据不同按键进行不同操作
switch ((char)c) {
// 如果键盘1被按下,效果图在灰度图和彩色图之间转换
case '1':
if (g_bIsColor) {// 若原来为彩色图,则转换为灰度图,并将mask所有元素设置为0
qDebug() << "键盘'1'被按下,切换彩色/灰度模式,当前操作将【彩色模式】切换为【灰度模式】";
cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
g_maskImage = Scalar::all(0);// 将mask的所有元素设置为0
g_bIsColor = false;
}
else {
qDebug()<< "键盘'1'被按下,切换彩色/灰度模式,当前操作将【灰度模式】切换为【彩色模式】";
g_srcImage.copyTo(g_dstImage);
g_maskImage = Scalar::all(0);
g_bIsColor = true;
}
case '2':
if (g_bUseMask) {
destroyWindow("mask");
g_bUseMask = false;
}
else {
namedWindow("mask", 0);
g_maskImage = Scalar::all(0);
imshow("mask", g_maskImage);
g_bUseMask = true;
}
break;
case '3':// 如果键盘3被按下,恢复原始图像
qDebug()<< "按下键盘'3',恢复原始图像\n";
g_srcImage.copyTo(g_dstImage);
cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
g_maskImage = Scalar::all(0);
break;
case '4':
qDebug()<< "键盘'4'被按下,使用空范围的漫水填充\n";
g_nFillMode = 0;
break;
case '5':
qDebug() << "键盘'5'被按下,使用渐变、固定范围的漫水填充\n";
g_nFillMode = 1;
break;
case '6':
qDebug() << "键盘'6'被按下,使用渐变、浮动范围的漫水填充\n";
g_nFillMode = 2;
break;
case '7':
qDebug() << "键盘'7'被按下,操作标识符的低八位使用4位的连接模式\n";
g_nConnectivity = 4;
break;
case '8':
qDebug() << "键盘'8'被按下,操作标识符的低八位使用8位的连接模式\n";
g_nConnectivity = 8;
break;
}
}
return 0;
}
这个例子本质上也是floodFill的调用,但输入根据用户而定。
保存工程并运行,通过鼠标滚轮,可以放大或缩小图像,按住鼠标左键可以移动图像,运行结果如图9-18所示。

图9-18