OpenCV计算机视觉开发入门与实践<三十>:漫水填充(floodFill)算法分割图像
2026/9/5 3:52:41 网站建设 项目流程

摘要:本文详细介绍了 OpenCV 中 Flood Fill(漫水填充)算法的原理与实战用法。文章首先解释了漫水填充的基本概念,即从种子点出发,将颜色相近的连通区域填充为指定颜色,并对比了 4 邻域、8 邻域等常见实现方式。随后重点剖析了 OpenCV 中floodFill函数的两个重载版本,逐一讲解了imagemaskseedPointnewValloDiffupDiffflags等核心参数的含义与注意事项,特别是flags中连通性、掩码值及FLOODFILL_FIXED_RANGEFLOODFILL_MASK_ONLY等标识符的组合用法。最后通过两个完整的 C++ 示例(例 9.7 与例 9.8),演示了如何利用floodFill实现图像分割,以及如何结合鼠标交互与滑动条构建功能更强大的分割工具。

1. 基本概念

Flood Fill(漫水填充)算法是一种在许多图形绘制软件中常用的填充算法。通常情况下,该算法会自动选中与种子像素相连的区域,并利用指定颜色对该区域进行填充。这个算法常用于标记或分离图像的特定部分,以便进行进一步的分析和处理。Windows画图工具中的油漆桶功能和Photoshop的魔术棒选择工具,都是Flood Fill算法的改进和延伸。

漫水填充算法的原理很简单,就是从一个点开始遍历附近的像素点,并填充成新的颜色,直到封闭区域内所有像素点都被填充成新颜色为止。floodFill的实现方法常见的有4邻域像素填充法、8邻域像素填充法、基于扫描线的像素填充方法等。

在OpenCV中,漫水填充是填充算法中最通用的方法。使用C++重写过的floodFill函数有两个版本,一个是不带mask的版本,另一个是带mask的版本。这个mask就是用于进一步控制哪些区域将被填充颜色(比如对同一图像进行多次填充时)。这两个版本的floodFill,都必须在图像中选择一个种子点,然后把临近区域所有相似点填充上同样的颜色;不同之处在于,不一定将所有的邻近像素点都染上同一颜色。漫水填充操作的结果总是某个连续的区域。当邻近像素点位于给定的范围(从loDiff到upDiff)内或在原始seedPoint像素值范围内时,floodFill函数就会为这个点涂上颜色。

2.floodFill函数

在OpenCV中,漫水填充算法由floodFill函数实现,其作用是用指定的颜色从种子点开始填充一个连接域,连通性由像素值的接近程度来衡量。OpenCV中使用C++重写的两个floodFill函数的声明如下:

intcv::floodFill(InputOutputArray image,Point seedPoint,Scalar newVal,Rect*rect=0,Scalar loDiff=Scalar(),Scalar upDiff=Scalar(),intflags=4);intcv::floodFill(InputOutputArray image,InputOutputArray mask,Point seedPoint,Scalar newVal,Rect*rect=0,Scalar loDiff=Scalar(),Scalar upDiff=Scalar(),intflags=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)。

下面来看一个关于floodfill的简单的调用范例。

【例9.7】利用floodfill进行图像分割

#include"opencv2/imgproc.hpp"#include"opencv2/highgui.hpp"#include<opencv2/imgproc/types_c.h>#include<iostream>usingnamespacestd;usingnamespacecv;intmain(){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);return0;}

下面再来看一个综合例子,它可以根据用户鼠标的单击和滑竿调节来实现不同区域的图像分割。

【例9.8】功能更强大的floodFill分割

#include"opencv2/imgproc.hpp"#include"opencv2/highgui.hpp"#include<opencv2/imgproc/types_c.h>#include<iostream>usingnamespacestd;usingnamespacecv;//全局变量声明部分Mat g_srcImage,g_dstImage,g_grayImage,g_maskImage;//定义原始图、目标图、灰度图、掩膜图intg_nFillMode=1;//漫水填充的模式intg_nLowDifference=20,g_nUpDifference=20;//负差最大值,正差最大值intg_nConnectivity=4;//表示floodFill函数标识符低八位的连通值boolg_bIsColor=true;//是否为彩色图的标识符布尔值boolg_bUseMask=false;//是否显示掩膜窗口的布尔值intg_nNewMaskVal=255;//新的重新绘制的像素值//===============【onMouse()函数】=======================staticvoidonMouse(intevent,intx,inty,int,void*){//若鼠标左键没有按下,便返回if(event!=EVENT_LBUTTONDOWN)return;//-----------------【<1>调用floodFill函数之前的参数准备部分】-------------Point seed=Point(x,y);intLowDifference=g_nFillMode==0?0:g_nLowDifference;intUpDifference=g_nFillMode==0?0:g_nUpDifference;//标识符的0~7位为g_nConnectivity,8~15位为g_nNewMaskVal左移8位的值,16~23位为CV_FLOODFILL_FIXED_RANGE或者0intflags=g_nConnectivity+(g_nNewMaskVal<<8)+(g_nFillMode==1?FLOODFILL_FIXED_RANGE:0);//随机生成BGR值intb=(unsigned)theRNG()&255;//随机返回一个0~255之间的值intg=(unsigned)theRNG()&255;intr=(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;//目标图的赋值intarea;//---------------------【<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()函数intmain(intargc,char**argv){cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);//载入原图g_srcImage=imread("study.jpg",1);if(!g_srcImage.data){printf("Fail to open file.!\n");returnfalse;}g_srcImage.copyTo(g_dstImage);//复制原图到目标图cvtColor(g_srcImage,g_grayImage,COLOR_BGR2GRAY);//转为灰度图到g_grayImageg_maskImage.create(g_srcImage.rows+2,g_srcImage.cols+2,CV_8UC1);//用原图尺寸初始化掩膜masknamedWindow("res",WINDOW_AUTOSIZE);//创建TrackbarcreateTrackbar("负差最大值","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);//获取按键键盘intc=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所有元素设置为0g_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;}}return0;}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询