OpenCV实现连通区域填充


OpenCV实现连通区域填充

前言

本博客主要解决的问题来源于数据结构老师的一次作业,作业内容如下图所示。

问题照片

要处理的图像如下:

原图像

环境配置

  • VS2019
  • C++
  • OpenCV-4.1.0

第一部分:使用轮廓查找和漫水填充的方法实现区域染色

流程图:

漫水填充

源程序代码:

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<vector<Point>> contours;
    findContours(test_1_threshold, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
    /*最小外接矩形*/
Point2f rect[4];
vector<Rect> boundRect(contours.size());  //定义外接矩形集合
    vector<RotatedRect> 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);
}

代码分析:

  1. 这部分代码比较基础。首先对图片进行基本的处理,再使用轮廓查找的方式找的图形的轮廓。再通过轮廓算出其最小外接矩形,这样就可以大致确定每个图形所在的区域,也就是ROI区域。
  2. 在获取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<int>(i, j) == 255)
            {
                queue<pair<int, int>> neighborPixels;
                neighborPixels.push(pair<int, int>(i, j));     // 像素位置: <i,j>
                ++label;
                while (!neighborPixels.empty())
                {
                    pair<int, int> curPixel = neighborPixels.front();
                    int curX = curPixel.first;
                    int curY = curPixel.second;
                    if (lableImg.at<int>(curX, curY) != label)
                    {
                        lableImg.at<int>(curX, curY) = label;

                        neighborPixels.pop();

                        if (lableImg.at<int>(curX, curY - 1) == 255)
                        {
                            neighborPixels.push(std::pair<int, int>(curX, curY - 1));
                        }
                        if (lableImg.at<int>(curX, curY + 1) == 255)
                        {
                            neighborPixels.push(std::pair<int, int>(curX, curY + 1));
                        }
                        if (lableImg.at<int>(curX - 1, curY) == 255)
                        {
                            neighborPixels.push(std::pair<int, int>(curX - 1, curY));
                        }
                        if (lableImg.at<int>(curX + 1, curY) == 255)
                        {
                            neighborPixels.push(std::pair<int, int>(curX + 1, curY));
                        }
                    }
                    else
                    {
                        neighborPixels.pop();
                    }
                }
            }
        }
    }
}

代码分析:

  1. 种子填充法
    参考博客:
    OpenCV_连通区域分析(Connected Component Analysis-Labeling)
    OpenCV-二值图像连通域分析
    在上面这两个博客中的种子填充法都是使用堆栈来实现的,因此在本程序中需要考虑换成队列。
  2. 算法的简单分析:
    (1) 首先需要获取原图像的列数和行数方便后面对每个像素点的访问。
    (2) 通过遍历访问像素点,如果像素点(i, j)的值等于255(白色点)则将其坐标点存入neighborPixels队列中,并且标签label加1。
    (3) 如果neighborPixels队列非空,则取出neighborPixels队列的队头。判断队头代表的像素点是否与当前label相等,如果相等则直接删除并重复步骤(3),否则进行步骤(4)。如果neighborPixels队列为空则执行步骤(2)。
    (4) 将队头点赋值为label并从队列中删除。对队头点进行4领域判断。上下左右四个点,哪个点的像素值为255哪个值就入队。重复步骤(3)。
    (5) 当所有像素点被遍历完之后种子填充法结束。
  3. 种子填充法比较容易理解,我认为难点在于将原本代码中的堆栈转换为队列。经过仔细分析,发现如果仅仅是把堆栈换成队列会导致代码重复,及会出现一个像素点被多次访问的情况。为了解决这个问题,我在程序中多加了一个判断(第135行)。因为一个像素点被访问后会被“贴上”值为label的“标签”,所以对像素点的“标签”进行判断就可以知道这个点有没有被访问过。如果访问过则直接删除,否则正常执行程序即可。
  4. 本程序中使用的是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<int, Scalar> 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<int>(i);
        uchar* data_dst = colorLabelImg.ptr<uchar>(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);
}

代码分析:

  1. 颜色BGR也是取的随机数,可以保证每个区域颜色不同。
  2. 通过遍历进行染色,判断方式就是对每个像素点的值也就是标签进行染色。同一个标签的点染同一个颜色。

效果图:

种子填充法

从图中可以看出,效果比轮廓法要好,颜色填充比较饱满。

完整代码:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <queue>
#include <stack>
#include <map>
#include <list>
using namespace cv;
using namespace std;

void cv_show(string name, Mat img)
{
	/*用于显示图像*/
	imshow(name, img);
	waitKey(0);
}

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<vector<Point>> contours;
	findContours(test_1_threshold, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
	/*绘制轮廓*/
	//drawContours(test_1_copy, contours, -1, Scalar(0, 255, 255), 2);
	/*最小外接矩形*/
	Point2f rect[4];
	vector<Rect> boundRect(contours.size());  //定义外接矩形集合
	vector<RotatedRect> box(contours.size()); //定义最小外接矩形集合
	srand((int)time(0));
	for (int i = 0; i < contours.size(); i++)
	{
		box[i] = minAreaRect(Mat(contours[i]));  //计算每个轮廓最小外接矩形
		//boundRect[i] = boundingRect(Mat(contours[i]));
		//circle(test_1_copy, Point(box[i].center.x, box[i].center.y), 2, Scalar(0, 255, 0), -1, 8);
		//绘制最小外接矩形的中心点
		/*rectangle(test_1_copy, Point(boundRect[i].x, boundRect[i].y), Point(boundRect[i].x +
			boundRect[i].width, boundRect[i].y + boundRect[i].height), Scalar(0, 255, 0), 1, 8);*/
		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);
}

void deal_test_2()
{
	Mat img_origin = imread("C:\\Users\\17513\\Desktop\\数据结构报告\\栈和队列\\test2.jpg");
	Mat img_copy = img_origin.clone();
	Mat img_gray, img_gauss, img_threshold;
	Mat img_sobelx, img_sobely, img_sobelxy;
	Mat dstImage;
	Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
	cvtColor(img_copy, img_gray, COLOR_BGR2GRAY);
	GaussianBlur(img_gray, img_gauss, Size(5, 5), 0, 0);
	threshold(img_gray, img_threshold, 127, 255, THRESH_BINARY);
	morphologyEx(img_threshold, dstImage, 0, element);
	//Sobel(dstImage, img_sobelx, CV_64F, 1, 0, 3);
	//convertScaleAbs(img_sobelx, img_sobelx);
	//Sobel(img_threshold, img_sobely, CV_64F, 0, 1, 3);
	//convertScaleAbs(img_sobely, img_sobely);
	//addWeighted(img_sobelx, 1, img_sobely, 1, 0, img_sobelxy);
	threshold(dstImage, img_threshold, 127, 255, THRESH_BINARY);
	vector<vector<Point>> contours;
	vector<vector<Point>> cnts;
	vector<vector<Point>> cnts_max;
	findContours(img_threshold, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
	for (int i = 0; i < contours.size(); i++)
	{
		double size = contourArea(contours[i]);
		if (size >= 10 && size < 1000)
			cnts.push_back(contours[i]);
		if (size >= 1000)
			cnts_max.push_back(contours[i]);
		// cout << i << "=" << size << endl;
	}
	Mat img_copy2 = img_copy.clone();
	int sum;
	sum = cnts.size() + cnts_max.size() * 2;
	drawContours(img_copy2, cnts, -1, Scalar(0, 0, 255), 1);
	//drawContours(img_copy, cnts, -1, Scalar(0, 0, 255), 2);
	//cout << "contours = " << contours.size() << endl;
	//cout << "cnts = " << cnts.size() << endl;
	//cout << "cnts_max = " << cnts_max.size() << endl;
	cout << "sum = " << sum << endl;
	//cout << "一共有" << cnts.size() << "个米粒" << endl;
	imshow("米粒轮廓", img_copy2);
	waitKey(0);
}

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<int>(i, j) == 255)
			{
				queue<pair<int, int>> neighborPixels;
				neighborPixels.push(pair<int, int>(i, j));     // 像素位置: <i,j>
				++label;
				while (!neighborPixels.empty())
				{
					pair<int, int> curPixel = neighborPixels.front();
					int curX = curPixel.first;
					int curY = curPixel.second;
					if (lableImg.at<int>(curX, curY) != label)
					{
						lableImg.at<int>(curX, curY) = label;

						neighborPixels.pop();

						if (lableImg.at<int>(curX, curY - 1) == 255)
						{
							neighborPixels.push(std::pair<int, int>(curX, curY - 1));
						}
						if (lableImg.at<int>(curX, curY + 1) == 255)
						{
							neighborPixels.push(std::pair<int, int>(curX, curY + 1));
						}
						if (lableImg.at<int>(curX - 1, curY) == 255)
						{
							neighborPixels.push(std::pair<int, int>(curX - 1, curY));
						}
						if (lableImg.at<int>(curX + 1, curY) == 255)
						{
							neighborPixels.push(std::pair<int, int>(curX + 1, curY));
						}
					}
					else
					{
						neighborPixels.pop();
					}
				}
			}
		}
	}
}

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<int, Scalar> 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<int>(i);
		uchar* data_dst = colorLabelImg.ptr<uchar>(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);
}

void deal_test_3()
{
	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);
	SeedFillOld(test_1_threshold, labelImg);
	LabelColor(labelImg, colorLabelImg);
	cv_show("666", colorLabelImg);
}

int main()
{
	int mode;
	cout << "请输入要执行的程序编号:";
	cin >> mode;
	if (mode == 1)
		deal_test_1();
	else if (mode == 2)
		deal_test_2();
	else if (mode == 3)
		deal_test_3();
	else
		cout << "请输入正确的编号!" << endl;
	return 0;
}

文章作者: LightningMaster
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LightningMaster !
评论
  目录