OpenCV学习笔记(3)_OpenCV中的灰度阈值筛选和连通域分析实例

OpenCV学习笔记(3)_OpenCV中的灰度阈值筛选和连通域分析实例

文章目录

  • OpenCV学习笔记(3)_OpenCV中的灰度阈值筛选和连通域分析实例
    • 1. 实例来源
    • 2. 实例核心代码
    • 3. 实例知识点
      • 3.1 读取灰度图像
      • 3.2 cv::threshold
      • 3.3 cv::connectedComponentsWithStats
      • 3.4 连通域长宽筛选
      • 3.5 筛选结果提取
      • 3.6 筛选结果涂色显示

1. 实例来源

今天这个实例来自Halcon的示例程序:

threshold.hdev

read_image (Audi2, 'audi2')
fill_interlace (Audi2, ImageFilled, 'odd')
threshold (ImageFilled, Region, 0, 90)
connection (Region, ConnectedRegions)
select_shape (ConnectedRegions, SelectedRegions, 'width', 'and', 30, 70)
select_shape (SelectedRegions, Letters, 'height', 'and', 60, 110)
dev_clear_window ()
dev_set_colored (12)
dev_display (ImageFilled)
dev_display (Letters)

图片也来自halcon的示例图片audi2.png, 该示例通过简单的灰度阈值分割,和连通域分析,将一幅图像中的车牌字符逐个提取出来.

本文通过OpenCV来实现类似的功能.有所不同的是,例程中有一个对视频图像中的错位进行校正的函数fill_interlace(它的作用是去除图像中的锯齿状错位,如图1所示),为了简化,在OpenCV实例中没有进行相应的处理,而是将Halcon中已处理好的中间过程图像提取了出来用作本实例的源图像文件.

2. 实例核心代码

闲言少叙,直接上全部代码:

#include 
#include "opencv2/opencv.hpp"
#include 
#include 
#include 

int test1()
{
	srand(time(NULL));
	std::string filename = "../images/audi2.png";
	cv::Mat src = cv::imread(filename, CV_8UC1);
	cv::namedWindow("test1", CV_WINDOW_AUTOSIZE);

	cv::Mat dst, showpic;
	src.copyTo(dst);
    cv::threshold(src, dst, 90, 255, cv::THRESH_BINARY_INV);
	src.copyTo(showpic);
	

	cv::Mat labels, stats, centroids;
	int nccomps = cv::connectedComponentsWithStats(dst, labels, stats, centroids);

	std::vector<cv::Vec3b> colors(nccomps);
	std::vector<int> selectFlag(nccomps);
	selectFlag.assign(nccomps, 1);
	colors[0] = cv::Vec3b(0, 0, 0);
	for (int i = 1; i < nccomps; ++i)
	{
		colors[i] = cv::Vec3b(rand() % 256, rand() % 256, rand() % 256);
		if (stats.at<int>(i, cv::CC_STAT_HEIGHT) < 60 || stats.at<int>(i, cv::CC_STAT_HEIGHT) > 110
			|| stats.at<int>(i, cv::CC_STAT_WIDTH) < 30 || stats.at<int>(i, cv::CC_STAT_WIDTH) > 70)
		{
			colors[i] = cv::Vec3b(0, 0, 0);
			selectFlag[i] = 0;
		}			
	}

	cv::Mat imgBin(src.rows, src.cols, CV_8UC1, cv::Scalar(0));
	for (int y = 0; y < imgBin.rows; ++y)
	{
		for (int x = 0; x < imgBin.cols; ++x)
		{
			int label = labels.at<int>(y, x);

			// 如果是背景连通域,或者是未通过长宽筛选的连通域
			if (0 == label || 0 == selectFlag[label])
			{
				imgBin.at<uchar>(y, x) = 0;
				continue;
			}
			CV_Assert(0 <= label && label <= nccomps);
			imgBin.at<uchar>(y, x) = 255;
		}
	}

	cv::cvtColor(showpic, showpic, CV_GRAY2RGB);
	for (int y = 0; y < showpic.rows; ++y)
	{
		for (int x = 0; x < showpic.cols; ++x)
		{
			int label = labels.at<int>(y, x);

			// 如果是背景连通域,或者是未通过长宽筛选的连通域
			if (0 == label || 0 == selectFlag[label])
				continue;
			CV_Assert(0 <= label && label <= nccomps);
			showpic.at<cv::Vec3b>(y, x) = colors[label];
		}
	}

	cv::imshow("test1", showpic);
	cv::waitKey(0);
	return 0;
}

int main(int argc, char** argv)
{
	test1();
	return 0;
}

结果展示一下:

OpenCV学习笔记(3)_OpenCV中的灰度阈值筛选和连通域分析实例_第1张图片

3. 实例知识点

3.1 读取灰度图像

读取灰度图像的核心函数为cv::imread

该函数的原型是

Mat cv::imread	(	const String & 	filename,
int 	flags = IMREAD_COLOR 
)	

其中,filename是string类型的图像地址,flags是读取的颜色类型,例如本例代码中,CV_8UC1表示的是8位1通道的灰度图像.

int test1()
{
	……
	std::string filename = "../images/audi2.png";
	cv::Mat src = cv::imread(filename, CV_8UC1);
	……
}

3.2 cv::threshold

读取完图像后,对这个灰度图像进行阈值分割,核心函数为cv::threshold

该函数的原型是

double cv::threshold	(	InputArray 	src,
OutputArray 	dst,
double 	thresh,
double 	maxval,
int 	type 
)	

实例中的调用为

int test1
{
    ……
	cv::Mat dst;
	src.copyTo(dst);
    cv::threshold(src, dst, 90, 255, cv::THRESH_BINARY_INV);
    ……
}

注意,这里的type决定了threshold的方法,cv::THRESH_BINARY_INV表示的是

d s t ( x , y ) = { 0 i f s r c ( x , y ) > t h r e s h m a x v a l o t h e r w i s e dst(x,y)= \left \{ \begin{array} {lr} 0 \qquad \qquad if \quad src(x,y) > thresh \\ maxval \qquad otherwise \\ \end{array} \right. dst(x,y)={0ifsrc(x,y)>threshmaxvalotherwise

可参考OpenCV在线文档中关于ThresholdTypes的说明

此处设定的thresh为90,type设置的是cv::THRESH_BINARY_INV,则目标为提取图像中灰度在[0,90)范围内的像素点(黑底白点,白点表示被选中).

3.3 cv::connectedComponentsWithStats

这是本例的核心函数之二,它的功能比较复杂,本文不做详细解释. 参考OpenCV在线文档中关于connectedComponentsWithStats的说明

在Halcon中,有更加强大的select_shape算子,根据连通域特征对连通域进行筛选.

在OpenCV中,通过此函数可完成

  1. 连通域划分
  2. 面积计算
  3. 宽 高 计算
  4. 中心点坐标计算

函数原型为

int cv::connectedComponentsWithStats	(	InputArray 	image,
OutputArray 	labels,
OutputArray 	stats,
OutputArray 	centroids,
int 	connectivity = 8,
int 	ltype = CV_32S 
)	

labels以不同的灰度来标记不同的连通域,每一个像素的灰度就代表所属连通域的序号;

stats记录连通域的信息,包括面积、宽、高等;

centroids记录连通域的中心点坐标;

实例中的函数调用为

int test1
{
    ……
	cv::Mat labels, stats, centroids;
	int nccomps = cv::connectedComponentsWithStats(dst, labels, stats, centroids);
    ……
}

nccomps表示函数返回的连通域的数量.

3.4 连通域长宽筛选

对于本实例,其实3.1~3.3节的核心函数已经完成主要的功能,但在可视化的方面,需要本节及后续两小节的内容来完成.

连通域的长宽筛选,主要是通过connectedComponentsWithStats函数的返回结果来进行.

主要是对stats的分析来完成,在本实例中:

int test1()
{
    ……
 	std::vector<cv::Vec3b> colors(nccomps);
	std::vector<int> selectFlag(nccomps);
	selectFlag.assign(nccomps, 1);
	colors[0] = cv::Vec3b(0, 0, 0);
	for (int i = 1; i < nccomps; ++i)
	{
		colors[i] = cv::Vec3b(rand() % 256, rand() % 256, rand() % 256);
		if (stats.at<int>(i, cv::CC_STAT_HEIGHT) < 60 || stats.at<int>(i, cv::CC_STAT_HEIGHT) > 110
			|| stats.at<int>(i, cv::CC_STAT_WIDTH) < 30 || stats.at<int>(i, cv::CC_STAT_WIDTH) > 70)
		{
			colors[i] = cv::Vec3b(0, 0, 0);
			selectFlag[i] = 0;
		}			
	}
    ……
}

先忽略掉这段代码中关于colors的处理,这一块会在后面涂色相关的内容中讲述.

此处针对连通域进行遍历,取得stats.at(i, cv::CC_STAT_HEIGHT)stats.at(i, cv::CC_STAT_WIDTH)的高和宽. 代码段中通过selectFlag来对每一个连通域是否选中进行标记.

3.5 筛选结果提取

通过3.4小节的标记,通过以下代码生成一张筛选后的结果二值图(黑底白图),结果保存在imgBin中:

int test1()
{
    ……
	cv::Mat imgBin(src.rows, src.cols, CV_8UC1, cv::Scalar(0));
	for (int y = 0; y < imgBin.rows; ++y)
	{
		for (int x = 0; x < imgBin.cols; ++x)
		{
			int label = labels.at<int>(y, x);

			// 如果是背景连通域,或者是未通过长宽筛选的连通域
			if (0 == label || 0 == selectFlag[label])
			{
				imgBin.at<uchar>(y, x) = 0;
				continue;
			}
			CV_Assert(0 <= label && label <= nccomps);
			imgBin.at<uchar>(y, x) = 255;
		}
	}
    ……
}

label为0时,实际上表示的是背景连通域.

结果在ImageWatch中显示如下

OpenCV学习笔记(3)_OpenCV中的灰度阈值筛选和连通域分析实例_第2张图片

3.6 筛选结果涂色显示

3.5节中的结果已经能够把选中的连通域和背景区分开(二值图),但连通域之间的区别不是很明显,要想和HDevelop中那样能够对不同的连通域进行涂色,我们需要通过随机数生成颜色,再加上3.4中标记的结果来进行.

相关的代码段为

int test1()
{	……
    std::vector<cv::Vec3b> colors(nccomps);
	std::vector<int> selectFlag(nccomps);
	selectFlag.assign(nccomps, 1);
	colors[0] = cv::Vec3b(0, 0, 0);
	for (int i = 1; i < nccomps; ++i)
	{
		colors[i] = cv::Vec3b(rand() % 256, rand() % 256, rand() % 256);
		if (stats.at<int>(i, cv::CC_STAT_HEIGHT) < 60 || stats.at<int>(i, cv::CC_STAT_HEIGHT) > 110
			|| stats.at<int>(i, cv::CC_STAT_WIDTH) < 30 || stats.at<int>(i, cv::CC_STAT_WIDTH) > 70)
		{
			colors[i] = cv::Vec3b(0, 0, 0);
			selectFlag[i] = 0;
		}			
	}
	……
	cv::cvtColor(showpic, showpic, CV_GRAY2RGB);
	for (int y = 0; y < showpic.rows; ++y)
	{
		for (int x = 0; x < showpic.cols; ++x)
		{
			int label = labels.at<int>(y, x);

			// 如果是背景连通域,或者是未通过长宽筛选的连通域
			if (0 == label || 0 == selectFlag[label])
				continue;
			CV_Assert(0 <= label && label <= nccomps);
			showpic.at<cv::Vec3b>(y, x) = colors[label];
		}
	}

	cv::imshow("test1", showpic);
	cv::waitKey(0);
 	……
}

绘制的方式是逐像素绘制,通过labels中标记好的像素所归属的连通域来分配不同的颜色.

在3.4节中对不同连通域进行标记的同时,也通过随机数,给不同连通域生成不同的颜色,这是通过一个cv::Vec3b的vector实现的,每一个元素为一个三元数,表示RGB的数值. 每一个分量的数值是通过随机数的方式生成.

背景及筛选未通过的连通域使用黑色,其它的连通域使用彩色.
OpenCV学习笔记(3)_OpenCV中的灰度阈值筛选和连通域分析实例_第3张图片

你可能感兴趣的:(OpenCV+Qt,opencv,计算机视觉,图像处理)