OpenCV实现连通区域填充
前言
本博客主要解决的问题来源于数据结构老师的一次作业,作业内容如下图所示。
要处理的图像如下:
环境配置
第一部分:使用轮廓查找和漫水填充的方法实现区域染色
流程图:
源程序代码:
void deal_test_1()
{
Mat test_1_gray, test_1_threshold, test_1_gauss;
Mat test_1_sobelx, test_1_sobely, test_1_sobelxy;
Mat test_1_origin = imread("C:\\Users\\17513\\Desktop\\数据结构报告\\栈和队列\\test.jpg");
Mat test_1_copy = test_1_origin.clone();
/*转换为灰度图*/
cvtColor(test_1_origin, test_1_gray, COLOR_BGR2GRAY);
/*高斯滤波*/
GaussianBlur(test_1_gray, test_1_gauss, Size(5, 5), 0, 0);
/*二值化*/
threshold(test_1_gauss, test_1_threshold, 127, 255, THRESH_BINARY);
/*Sobel算子*/
Sobel(test_1_threshold, test_1_sobelx, CV_64F, 1, 0, 3);
convertScaleAbs(test_1_sobelx, test_1_sobelx);
Sobel(test_1_threshold, test_1_sobely, CV_64F, 0, 1, 3);
convertScaleAbs(test_1_sobely, test_1_sobely);
addWeighted(test_1_sobelx, 1, test_1_sobely, 1, 0, test_1_sobelxy);
/*再次二值化*/
threshold(test_1_sobelxy, test_1_threshold, 127, 255, THRESH_BINARY);
/*寻找轮廓*/
vector> contours;
findContours(test_1_threshold, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
/*最小外接矩形*/
Point2f rect[4];
vector boundRect(contours.size()); //定义外接矩形集合
vector box(contours.size()); //定义最小外接矩形集合
srand((int)time(0));
for (int i = 0; i < contours.size(); i++)
{
box[i] = minAreaRect(Mat(contours[i])); //计算每个轮廓最小外接矩形
box[i].points(rect); //把最小外接矩形四个端点复制给rect数组
floodFill(test_1_copy, Point(box[i].center.x, box[i].center.y), Scalar(rand() % 255, rand() & 255, rand() % 255), &boundRect[i], Scalar(20, 20, 20), Scalar(20, 20, 20));
}
cv_show("666", test_1_copy);
}
代码分析:
- 这部分代码比较基础。首先对图片进行基本的处理,再使用轮廓查找的方式找的图形的轮廓。再通过轮廓算出其最小外接矩形,这样就可以大致确定每个图形所在的区域,也就是ROI区域。
- 在获取ROI区域后的难点是如何对图像进行染色,而且还要保证每个图形染的颜色是不同的。
对于染色方法我这里使用的是漫水填充的算法。这个算法参考
【OpenCV入门教程之十五】水漫金山:OpenCV漫水填充算法(Floodfill)
具体函数的使用方法也比较简单。函数需要提供seedPoint即漫水填充算法的起点,在程序中这个点我给的是每个图像最小外接矩形的中心点。此外还需要提供填充的颜色,为了保证颜色的不同,采用随机数的方式选择不用的BGR颜色,随机数的范围是0到255.
效果图:
程序最终效果还可以接受,但有些图形的外边明显没有进行染色,这可能是因为图像经过基本处理后发生改变与原图像不同导致。
第二部分:使用队列实现种子填充法
流程图:
源程序代码:
基本的图像处理:
Mat labelImg;
Mat colorLabelImg;
Mat test_1_gray, test_1_threshold, test_1_gauss;
Mat test_1_origin = imread("C:\\Users\\17513\\Desktop\\数据结构报告\\栈和队列\\test.jpg");
Mat test_1_copy;
cvtColor(test_1_origin, test_1_gray, COLOR_BGR2GRAY);
GaussianBlur(test_1_gray, test_1_gauss, Size(5, 5), 0, 0);
threshold(test_1_gauss, test_1_threshold, 127, 255, THRESH_BINARY);
代码分析:
一些基本的图像处理。灰度图、高斯滤波和二值化。
种子填充法相关代码:
void SeedFillOld(const Mat& binImg, Mat& lableImg)
{
if (binImg.empty() || binImg.type() != CV_8UC1)
{
return;
}
lableImg.release();
binImg.convertTo(lableImg, CV_32SC1);
int label = 1;
int rows = binImg.rows;
int cols = binImg.cols;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (lableImg.at(i, j) == 255)
{
queue> neighborPixels;
neighborPixels.push(pair(i, j)); // 像素位置:
++label;
while (!neighborPixels.empty())
{
pair curPixel = neighborPixels.front();
int curX = curPixel.first;
int curY = curPixel.second;
if (lableImg.at(curX, curY) != label)
{
lableImg.at(curX, curY) = label;
neighborPixels.pop();
if (lableImg.at(curX, curY - 1) == 255)
{
neighborPixels.push(std::pair(curX, curY - 1));
}
if (lableImg.at(curX, curY + 1) == 255)
{
neighborPixels.push(std::pair(curX, curY + 1));
}
if (lableImg.at(curX - 1, curY) == 255)
{
neighborPixels.push(std::pair(curX - 1, curY));
}
if (lableImg.at(curX + 1, curY) == 255)
{
neighborPixels.push(std::pair(curX + 1, curY));
}
}
else
{
neighborPixels.pop();
}
}
}
}
}
}
代码分析:
- 种子填充法
参考博客:
OpenCV_连通区域分析(Connected Component Analysis-Labeling)
OpenCV-二值图像连通域分析
在上面这两个博客中的种子填充法都是使用堆栈来实现的,因此在本程序中需要考虑换成队列。
- 算法的简单分析:
(1) 首先需要获取原图像的列数和行数方便后面对每个像素点的访问。
(2) 通过遍历访问像素点,如果像素点(i, j)的值等于255(白色点)则将其坐标点存入neighborPixels队列中,并且标签label加1。
(3) 如果neighborPixels队列非空,则取出neighborPixels队列的队头。判断队头代表的像素点是否与当前label相等,如果相等则直接删除并重复步骤(3),否则进行步骤(4)。如果neighborPixels队列为空则执行步骤(2)。
(4) 将队头点赋值为label并从队列中删除。对队头点进行4领域判断。上下左右四个点,哪个点的像素值为255哪个值就入队。重复步骤(3)。
(5) 当所有像素点被遍历完之后种子填充法结束。
- 种子填充法比较容易理解,我认为难点在于将原本代码中的堆栈转换为队列。经过仔细分析,发现如果仅仅是把堆栈换成队列会导致代码重复,及会出现一个像素点被多次访问的情况。为了解决这个问题,我在程序中多加了一个判断(第135行)。因为一个像素点被访问后会被“贴上”值为label的“标签”,所以对像素点的“标签”进行判断就可以知道这个点有没有被访问过。如果访问过则直接删除,否则正常执行程序即可。
- 本程序中使用的是4领域,还可以换成8领域,不过我没试过,不知道效果怎么样。
第三部分:对图像染色
源程序代码:
Scalar GetRandomColor()
{
uchar r = 255 * (rand() / (1.0 + RAND_MAX));
uchar g = 255 * (rand() / (1.0 + RAND_MAX));
uchar b = 255 * (rand() / (1.0 + RAND_MAX));
return Scalar(b, g, r);
}
void LabelColor(const Mat& labelImg, Mat& colorLabelImg)
{
int num = 0;
if (labelImg.empty() ||
labelImg.type() != CV_32SC1)
{
return;
}
map colors;
int rows = labelImg.rows;
int cols = labelImg.cols;
colorLabelImg.release();
colorLabelImg.create(rows, cols, CV_8UC3);
colorLabelImg = Scalar::all(0);
for (int i = 0; i < rows; i++)
{
const int* data_src = (int*)labelImg.ptr(i);
uchar* data_dst = colorLabelImg.ptr(i);
for (int j = 0; j < cols; j++)
{
int pixelValue = data_src[j];
if (pixelValue > 1)
{
if (colors.count(pixelValue) <= 0)
{
colors[pixelValue] = GetRandomColor();
num++;
}
Scalar color = colors[pixelValue];
*data_dst++ = color[0];
*data_dst++ = color[1];
*data_dst++ = color[2];
}
else
{
data_dst++;
data_dst++;
data_dst++;
}
}
}
printf("color num : %d \n", num);
}
代码分析:
- 颜色BGR也是取的随机数,可以保证每个区域颜色不同。
- 通过遍历进行染色,判断方式就是对每个像素点的值也就是标签进行染色。同一个标签的点染同一个颜色。
效果图:
从图中可以看出,效果比轮廓法要好,颜色填充比较饱满。
完整代码:
#include
#include
#include
#include
#include
#include