OpenCV3之——漫水填充:floodFill函数

定义 :

         漫水填充算法是一种用特定颜色填充连通区域,通过设置像素上下限及连通方式来达到不同的连通效果。漫水填充经常用来标记或分离图像的一部分,以便于对其进行进一步的处理和分析。也可以从输入图像获取掩码区域,掩码会加速处理过程,或只处理掩码指定的像素点,操作的结果总是某个连续的区域。

基本思想:

         所谓漫水填充,简单来说,就是自动选中了和种子点相连的区域,接着将该区域替换成指定的颜色,这是个非常有用的功能,经常用来标记或者分离图像的一部分进行处理或分析.漫水填充也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或者只处理掩码指定的像素点.

         以此填充算法为基础,类似photoshop的魔术棒选择工具就很容易实现了。漫水填充(FloodFill)是查找和种子点联通的颜色相同的点,魔术棒选择工具则是查找和种子点联通的颜色相近的点,将和初始种子像素颜色相近的点压进栈作为新种子

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

实现漫水填充算法:floodFill函数

          在OpenCV中,漫水填充算法由floodFill函数实现,其作用是用我们指定的颜色从种子点开始填充一个连接域,连通性由像素值的接近程度来衡量。下面是OpenCV3.X的两个floodFill函数原型:(...\opencv\sources\modules\imgproc\src\floodfill,cpp)

//第一个函数原型
int cv::floodFill( InputOutputArray _image, Point seedPoint,//不带mask
                  Scalar newVal, Rect* rect,
                  Scalar loDiff, Scalar upDiff, int flags )
{
    CV_INSTRUMENT_REGION()

    return floodFill(_image, Mat(), seedPoint, newVal, rect, loDiff, upDiff, flags);
//可以看到,第一个函数原型内部只是调用了第二个函数
}

//第二个函数原型
int cv::floodFill( InputOutputArray _image, InputOutputArray _mask,//带mask
                  Point seedPoint, Scalar newVal, Rect* rect,
                  Scalar loDiff, Scalar upDiff, int flags )
{
    CV_INSTRUMENT_REGION()

    ConnectedComp comp;
    std::vector buffer;

    if( rect )
        *rect = Rect();

    int i, connectivity = flags & 255;
    union {
        uchar b[4];
        int i[4];
        float f[4];
        double _[4];
    } nv_buf;
    nv_buf._[0] = nv_buf._[1] = nv_buf._[2] = nv_buf._[3] = 0;

    struct { Vec3b b; Vec3i i; Vec3f f; } ld_buf, ud_buf;
    Mat img = _image.getMat(), mask;
    if( !_mask.empty() )
        mask = _mask.getMat();
    Size size = img.size();

    int type = img.type();
    int depth = img.depth();
    int cn = img.channels();

    if ( (cn != 1) && (cn != 3) )
    {
        CV_Error( CV_StsBadArg, "Number of channels in input image must be 1 or 3" );
    }

    if( connectivity == 0 )
        connectivity = 4;
    else if( connectivity != 4 && connectivity != 8 )
        CV_Error( CV_StsBadFlag, "Connectivity must be 4, 0(=4) or 8" );

    bool is_simple = mask.empty() && (flags & FLOODFILL_MASK_ONLY) == 0;

    for( i = 0; i < cn; i++ )
    {
        if( loDiff[i] < 0 || upDiff[i] < 0 )
            CV_Error( CV_StsBadArg, "lo_diff and up_diff must be non-negative" );
        is_simple = is_simple && fabs(loDiff[i]) < DBL_EPSILON && fabs(upDiff[i]) < DBL_EPSILON;
    }

    if( (unsigned)seedPoint.x >= (unsigned)size.width ||
       (unsigned)seedPoint.y >= (unsigned)size.height )
        CV_Error( CV_StsOutOfRange, "Seed point is outside of image" );

    scalarToRawData( newVal, &nv_buf, type, 0);
    size_t buffer_size = MAX( size.width, size.height ) * 2;
    buffer.resize( buffer_size );

    if( is_simple )
    {
        size_t elem_size = img.elemSize();
        const uchar* seed_ptr = img.ptr(seedPoint.y) + elem_size*seedPoint.x;

        size_t k = 0;
        for(; k < elem_size; k++)
            if (seed_ptr[k] != nv_buf.b[k])
                break;

        if( k != elem_size )
        {
            if( type == CV_8UC1 )
                floodFill_CnIR(img, seedPoint, nv_buf.b[0], &comp, flags, &buffer);
            else if( type == CV_8UC3 )
                floodFill_CnIR(img, seedPoint, Vec3b(nv_buf.b), &comp, flags, &buffer);
            else if( type == CV_32SC1 )
                floodFill_CnIR(img, seedPoint, nv_buf.i[0], &comp, flags, &buffer);
            else if( type == CV_32FC1 )
                floodFill_CnIR(img, seedPoint, nv_buf.f[0], &comp, flags, &buffer);
            else if( type == CV_32SC3 )
                floodFill_CnIR(img, seedPoint, Vec3i(nv_buf.i), &comp, flags, &buffer);
            else if( type == CV_32FC3 )
                floodFill_CnIR(img, seedPoint, Vec3f(nv_buf.f), &comp, flags, &buffer);
            else
                CV_Error( CV_StsUnsupportedFormat, "" );
            if( rect )
                *rect = comp.rect;
            return comp.area;
        }
    }

    if( mask.empty() )
    {
        Mat tempMask( size.height + 2, size.width + 2, CV_8UC1 );
        tempMask.setTo(Scalar::all(0));
        mask = tempMask;
    }
    else
    {
        CV_Assert( mask.rows == size.height+2 && mask.cols == size.width+2 );
        CV_Assert( mask.type() == CV_8U );
    }

    memset( mask.ptr(), 1, mask.cols );
    memset( mask.ptr(mask.rows-1), 1, mask.cols );

    for( i = 1; i <= size.height; i++ )
    {
        mask.at(i, 0) = mask.at(i, mask.cols-1) = (uchar)1;
    }

    if( depth == CV_8U )
        for( i = 0; i < cn; i++ )
        {
            ld_buf.b[i] = saturate_cast(cvFloor(loDiff[i]));
            ud_buf.b[i] = saturate_cast(cvFloor(upDiff[i]));
        }
    else if( depth == CV_32S )
        for( i = 0; i < cn; i++ )
        {
            ld_buf.i[i] = cvFloor(loDiff[i]);
            ud_buf.i[i] = cvFloor(upDiff[i]);
        }
    else if( depth == CV_32F )
        for( i = 0; i < cn; i++ )
        {
            ld_buf.f[i] = (float)loDiff[i];
            ud_buf.f[i] = (float)upDiff[i];
        }
    else
        CV_Error( CV_StsUnsupportedFormat, "" );

    uchar newMaskVal = (uchar)((flags & 0xff00) == 0 ? 1 : ((flags >> 8) & 255));

    if( type == CV_8UC1 )
        floodFillGrad_CnIR(
                img, mask, seedPoint, nv_buf.b[0], newMaskVal,
                Diff8uC1(ld_buf.b[0], ud_buf.b[0]),
                &comp, flags, &buffer);
    else if( type == CV_8UC3 )
        floodFillGrad_CnIR(
                img, mask, seedPoint, Vec3b(nv_buf.b), newMaskVal,
                Diff8uC3(ld_buf.b, ud_buf.b),
                &comp, flags, &buffer);
    else if( type == CV_32SC1 )
        floodFillGrad_CnIR(
                img, mask, seedPoint, nv_buf.i[0], newMaskVal,
                Diff32sC1(ld_buf.i[0], ud_buf.i[0]),
                &comp, flags, &buffer);
    else if( type == CV_32SC3 )
        floodFillGrad_CnIR(
                img, mask, seedPoint, Vec3i(nv_buf.i), newMaskVal,
                Diff32sC3(ld_buf.i, ud_buf.i),
                &comp, flags, &buffer);
    else if( type == CV_32FC1 )
        floodFillGrad_CnIR(
                img, mask, seedPoint, nv_buf.f[0], newMaskVal,
                Diff32fC1(ld_buf.f[0], ud_buf.f[0]),
                &comp, flags, &buffer);
    else if( type == CV_32FC3 )
        floodFillGrad_CnIR(
                img, mask, seedPoint, Vec3f(nv_buf.f), newMaskVal,
                Diff32fC3(ld_buf.f, ud_buf.f),
                &comp, flags, &buffer);
    else
        CV_Error(CV_StsUnsupportedFormat, "");

    if( rect )
        *rect = comp.rect;
    return comp.area;
}
//简化版原型:
int cv::floodFill( InputOutputArray _image, 
                   InputOutputArray _mask,
                   Point seedPoint,
                   Scalar newVal, 
                   Rect* rect = 0,
                   Scalar loDiff = Scalar(), 
                   Scalar upDiff = Scalar(), 
                   int flags = 4 )

参数详解:两个版本唯一区别就是是否有第二个参数,其他参数都一样

  • 第一个参数,InputOutputArray类型的image, 输入/输出1通道或3通道,8位或浮点图像,具体参数由之后的参数具体指明。
  • 第二个参数, InputOutputArray类型的mask,这是第二个版本的floodFill独享的参数,表示操作掩模,。它应该为单通道、8位、长和宽上都比输入图像 image 大两个像素点的图像。第二个版本的floodFill需要使用以及更新掩膜,所以这个mask参数我们一定要将其准备好并填在此处。需要注意的是,漫水填充不会填充掩膜mask的非零像素区域。例如,一个边缘检测算子的输出可以用来作为掩膜,以防止填充到边缘。同样的,也可以在多次的函数调用中使用同一个掩膜,以保证填充的区域不会重叠。另外需要注意的是,掩膜mask会比需填充的图像大,所以 mask 中与输入图像(x,y)像素点相对应的点的坐标为(x+1,y+1)。
  • 第三个参数,Point类型的seedPoint,漫水填充算法的起始点。
  • 第四个参数,Scalar类型的newVal,像素点被染色的值,即在重绘区域像素的新值。
  • 第五个参数,Rect*类型的rect,有默认值0,一个可选的参数,用于设置floodFill函数将要重绘区域的最小边界矩形区域。
  • 第六个参数,Scalar类型的loDiff,有默认值Scalar( ),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之负差(lower brightness/color difference)的最大值。
  • 第七个参数,Scalar类型的upDiff,有默认值Scalar( ),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之正差(lower brightness/color difference)的最大值。
  • 第八个参数,int类型的flags,操作标志符,此参数包含三个部分,比较复杂,我们一起详细看看。
  1. 低八位(第0~7位)用于控制算法的连通性,可取4 (4为缺省值) 或者 8。如果设为4,表示填充算法只考虑当前像素水平方向和垂直方向的相邻点;如果设为 8,除上述相邻点外,还会包含对角线方向的相邻点。
  2. 高八位部分(16~23位)可以为0 或者如下两种选项标识符的组合:<1>FLOODFILL_FIXED_RANGE - 如果设置为这个标识符的话,就会考虑当前像素与种子像素之间的差,否则就考虑当前像素与其相邻像素的差。也就是说,这个范围是浮动的。<2>FLOODFILL_MASK_ONLY - 如果设置为这个标识符的话,函数不会去填充改变原始图像 (也就是忽略第三个参数newVal), 而是去填充掩模图像(mask)。这个标识符只对第二个版本的floodFill有用,因第一个版本里面压根就没有mask参数。
  3. 中间八位部分,上面关于高八位FLOODFILL_MASK_ONLY标识符中已经说的很明显,需要输入符合要求的掩码。Floodfill的flags参数的中间八位的值就是用于指定填充掩码图像的值的。但如果flags中间八位的值为0,则掩码会用1来填充。
flags=8 | FLOODFILL_MASK_ONLY | FLOODFILL_FIXED_RANGE | (38<<8)

 一、floodFill简单示例

#include 
using namespace cv;

int main() {
	Mat srcImage = imread("1.jpg");
	imshow("原始图", srcImage);

	Rect ccomp;
	floodFill(srcImage, Point(300, 300), Scalar(0, 100, 55), &ccomp, Scalar(10, 10, 10), Scalar(10, 10, 10));

	imshow("效果图", srcImage);
	waitKey(0);
	return(0);
}

运行结果:

OpenCV3之——漫水填充:floodFill函数_第1张图片

 

二、综合实例:漫水填充

#include 
using namespace cv;
using namespace std;

//全局变量声明部分
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("效果图", dst);
	cout << area << " 个像素被重新绘制\n";
}

//main()函数
int main(int argc, char** argv) {
	//载入原图
	g_srcImage = imread("1.jpg", 1);
	if (!g_srcImage.data) {
		printf("读取g_srcImage错误!\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("效果图", WINDOW_AUTOSIZE);

	//创建Trackbar
	createTrackbar("负差最大值", "效果图", &g_nLowDifference, 255, 0);
	createTrackbar("正差最大值", "效果图", &g_nUpDifference, 255, 0);

	//鼠标回调函数
	setMouseCallback("效果图", onMouse, 0);

	//循环轮询按键
	while (1) {
		//先显示效果图
		imshow("效果图", g_bIsColor ? g_dstImage : g_grayImage);

		//获取按键键盘
		int c = waitKey(0);
		//判断ESC是否按下,按下退出
		if (c == 27) {
			cout << "程序退出........、\n";
			break;
		}

		//根据按键不同进行不同的操作
		switch ((char)c) {
			//如果键盘1被按下,效果图在灰度图和彩色图之间转换
		case '1':
			if (g_bIsColor) {//若原来为彩色图,转换为灰度图,并将掩膜mask所有元素设置为0
				cout << "键盘‘1’按下,切换彩色/灰度模式,当前操作将【彩色模式】切换为【灰度模式】" << endl;
				cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
				g_maskImage = Scalar::all(0);//将mask所有元素设置为0
				g_bIsColor = false;
			}
			else {
				cout << "键盘‘1’按下,切换彩色/灰度模式,当前操作将【灰度模式】切换为【彩色模式】" << endl;
				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被按下,恢复原始图像
			cout << "按下键盘‘3’,恢复原始图像\n";
			g_srcImage.copyTo(g_dstImage);
			cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
			g_maskImage = Scalar::all(0);
			break;
		case '4':
			cout << "键盘‘4’被按下,使用空范围的漫水填充\n";
			g_nFillMode = 0;
			break;
		case '5':
			cout << "键盘‘5’被按下,使用渐变、固定范围的漫水填充\n";
			g_nFillMode = 1;
			break;
		case '6':
			cout << "键盘‘6’被按下,使用渐变、浮动范围的漫水填充\n";
			g_nFillMode = 2;
			break;
		case '7':
			cout << "键盘‘7’被按下,操作标识符的低八位使用4位的连接模式\n";
			g_nConnectivity = 4;
			break;
		case '8':
			cout << "键盘‘8’被按下,操作标识符的低八位使用8为的连接模式\n";
			g_nConnectivity = 8;
			break;
		}

	}

	return 0;

}

运行结果:(部分截图)

<1>原始图:

OpenCV3之——漫水填充:floodFill函数_第2张图片

<2>运行截图1

 OpenCV3之——漫水填充:floodFill函数_第3张图片

<2>运行截图2

 OpenCV3之——漫水填充:floodFill函数_第4张图片

<3>运行截图3

OpenCV3之——漫水填充:floodFill函数_第5张图片 

你可能感兴趣的:(OpenCV学习)