9.7floodFill图像分割
基本概念
Flood Fill(洪水填充)是一种常用的图像处理技术,用于填充一个连通区域的颜色或者灰度值。在OpenCV中,可以使用floodFill函数来实现这一功能。Flood Fill算法常用于图像分割,尤其是在用户交互式标记或自动分割任务中。这个函数常用于图像分割任务,例如填充选定区域、对象分割等。floodFill 函数的工作原理类似于绘画软件中的“油漆桶工具”,它会从给定的起始点开始,查找具有相似颜色的所有相邻像素,并将它们的颜色替换为新颜色。
Flood Fill 原理
Flood Fill算法从一个指定的种子点开始,遍历所有与种子点颜色相近的连通像素,并将这些像素的值修改为目标颜色或灰度值。其主要步骤如下:
1. 选择一个种子点:确定图像中的一个初始点作为填充的起点。
2. 确定目标颜色:定义要填充的新颜色或灰度值。
3. 遍历连通区域:从种子点开始,递归地访问所有与其颜色相近且尚未填充的邻近像素,并将它们的颜色改为新颜色。
OpenCV中的Flood Fill
在OpenCV中,可以使用floodFill函数来实现Flood Fill算法。该函数有多个重载版本,最常用的是以下形式:
floodFill 函数的基本原型
int floodFill(
InputOutputArray img,
Point seedPoint,
Scalar newVal,
Rect* rect = 0,
Scalar loDiff = Scalar(),
Scalar upDiff = Scalar(),
int connectivity = 4,
int flags = 0,
UMat* mask = 0
);
参数说明
•img:输入/输出图像,图像会被修改。要进行填充的图像。
•seedPoint:指定的种子点坐标。填充的起始点坐标。
•newVal:新的颜色值或灰度值。新的颜色值,通常是一个标量或向量,取决于图像的通道数。
•rect:可选参数,指定要填充的矩形区域,如果指定了这个参数,则仅在这个区域内进行填充。
•loDiff:指定颜色差异的下限阈值,用来判断哪些像素应该被填充。
•upDiff:指定颜色差异的上限阈值,用来判断哪些像素应该被填充。
loDiff 和 upDiff:允许的颜色差异范围,用于确定哪些像素应该被填充。这两个参数可以分别指定每个通道的最大差异。
•connectivity:像素间连通性的定义,通常为4(仅考虑上下左右四个方向)或8(考虑上下左右及对角线八个方向)。
•flags:可以设置一些标志位来改变填充行为。
•floodfillFlags = 4 | (255 << 8):使用4连通性,并指定新值为255。
•floodfillFixedRange = 1 << 16:当设置了这个标志时,loDiff 和 upDiff 只会在当前像素值与种子点的颜色比较时使用,而不是在相邻像素之间。
•mask:掩模图像,可以用来限制填充的范围。
示例代码1
下面是一个使用OpenCV C++实现Flood Fill的示例代码:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// 回调函数用于鼠标事件
static void on_mouse( int event, int x, int y, int, void* )
{
Mat &img = *static_cast<Mat*>(void*)(&image);
if( event == EVENT_LBUTTONDOWN )
{
// 指定种子点
Point seed_point(x, y);
// 新的颜色值
Scalar new_val = Scalar(255, 0, 0); // 蓝色
// 颜色差异的上下限
Scalar lo_diff = Scalar(10, 10, 10);
Scalar up_diff = lo_diff;
// 进行Flood Fill
floodFill(img, seed_point, new_val, NULL, lo_diff, up_diff, 4, 1 << 16);
imshow("Flood Fill", img);
}
}
int main(int argc, char** argv)
{
// 加载图像
Mat image = imread("path_to_your_image.jpg");
if (image.empty())
{
cout << "Error loading image" << endl;
return -1;
}
// 创建窗口并注册回调函数
namedWindow("Flood Fill", WINDOW_AUTOSIZE);
setMouseCallback("Flood Fill", on_mouse, &image);
// 显示图像
imshow("Flood Fill", image);
while (true)
{
if (waitKey(0) >= 0) break;
}
return 0;
}
代码解释
1. 加载图像:使用 imread 函数加载图像。
2. 创建窗口:使用 namedWindow 创建一个名为"Flood Fill"的窗口。
3. 注册回调函数:使用 setMouseCallback 注册一个鼠标事件回调函数。
4. 鼠标事件处理:在回调函数中处理鼠标点击事件,当左键点击时,获取点击点坐标,并执行Flood Fill算法。
5. 更新显示:每次填充后更新图像显示。
注意事项
•颜色空间:确保图像的颜色空间适合Flood Fill操作。对于彩色图像,通常使用BGR格式。
•颜色差异:lo_diff 和 up_diff 参数用于控制填充的敏感度,较大的差异值会导致更大范围的填充。
•连通性:connectivity 参数决定了像素间的连通性,4表示四连通(仅水平和垂直邻居),8表示八连通(包括对角线邻居)。
应用场景
•图像分割:用户可以通过点击图像上的不同区域来进行交互式分割。
•背景替换:在某些情况下,Flood Fill可以用来快速替换图像中的背景。
•缺陷检测:在工业视觉应用中,Flood Fill可用于检测图像中的缺陷或异常区域。
运行结果1
示例代码2
下面是一个使用 C++ 实现 floodFill 的简单示例:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
// 加载图像
cv::Mat image = cv::imread("256.png");
if (image.empty())
{
std::cout << "Error: Image cannot be loaded!" << std::endl;
return -1;
}
// 创建一个复制图像用于显示填充结果
cv::Mat fillImage = image.clone();
// 定义种子点
cv::Point seedPoint(50, 50); // 你可以根据实际图像选择合适的坐标
// 新的颜色值
cv::Scalar newVal(255, 0, 0); // BGR格式,这里设置为蓝色
// 颜色差异范围
cv::Scalar loDiff(30, 30, 30); // 允许的最大颜色差异
cv::Scalar upDiff(20, 20, 20);
// 执行 floodFill 操作
//int numPixelsFilled = cv::floodFill(fillImage, seedPoint, newVal, nullptr, &loDiff, &upDiff);
int numPixelsFilled = cv::floodFill(fillImage, seedPoint, newVal, 0, loDiff, upDiff);
// 显示原始图像和填充后的图像
cv::namedWindow("Original Image", cv::WINDOW_NORMAL);
cv::imshow("Original Image", image);
cv::namedWindow("Filled Image", cv::WINDOW_NORMAL);
cv::imshow("Filled Image", fillImage);
// 输出填充了多少像素
std::cout << "Number of pixels filled: " << numPixelsFilled << std::endl;
// 等待按键并退出
cv::waitKey(0);
system("pause");
return 0;
}
参数解释
seedPoint:这是填充的起点。
newVal:这是要应用到区域的新颜色。
loDiff 和 upDiff:定义了颜色的差异范围。如果一个像素的颜色值与种子点的颜色值之差在指定的范围内,则该像素将被填充。
floodFill 的模式
floodFill 支持两种模式:固定范围模式(fixed range mode)和基于连通性模式(connectivity-based mode)。默认情况下,floodFill 使用固定范围模式,这意味着它会查找与种子点颜色相差不超过 loDiff 和 upDiff 的像素。如果你希望基于连通性来填充,可以通过传递一个掩码(mask)参数来启用连通性模式。掩码可以帮助跟踪已经访问过的像素,避免重复填充,并且可以使操作更加高效。
注意事项
floodFill 对于颜色差异的判断是基于像素值的绝对差值,而不是感知色彩差异,因此有时可能需要手动调整 loDiff 和 upDiff 的值以获得满意的结果。
如果图像很大或者连通区域很广,floodFill 可能会变得非常耗时。在这种情况下,考虑优化算法或减少图像尺寸。
在使用 floodFill 时,确保图像的边界条件处理得当,防止溢出等问题。
运行结果2
实验代码3
#include "pch.h"
//#pragma comment(lib, "opencv_world450d.lib")
#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("03.jpeg");
namedWindow("【原始图】", WINDOW_NORMAL);
imshow("【原始图】", src);
Rect ccomp;
floodFill(src, Point(50, 300), Scalar(155, 255, 55), &ccomp, Scalar(20, 20, 20), Scalar(20, 20, 20));
namedWindow("【效果图】", WINDOW_NORMAL);
imshow("【效果图】", src);
waitKey(0);
return 0;
}
运行结果3
9.8代码强化
强化代码4
#include "pch.h"
//#pragma comment(lib, "opencv_world450d.lib")
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <opencv2/imgproc/types_c.h>
#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("效果图", dst);
cout << area << " 个像素被重新绘制\n";
}
//main()函数
int main(int argc, char** argv) {
//载入原图
g_srcImage = imread("test.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;
}