OpenCV学习笔记(3)_OpenCV中的灰度阈值筛选和连通域分析实例
文章目录
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 <iostream>
#include "opencv2/opencv.hpp"
#include <string>
#include <vector>
#include <time.h>
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;
}
结果展示一下:
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中,通过此函数可完成
- 连通域划分
- 面积计算
- 宽 高 计算
- 中心点坐标计算
函数原型为
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<int>(i, cv::CC_STAT_HEIGHT)
和stats.at<int>(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中显示如下
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的数值. 每一个分量的数值是通过随机数的方式生成.
背景及筛选未通过的连通域使用黑色,其它的连通域使用彩色.