文章目录
前言
本篇文章是作者在学习OpenCV图像与视频分析的笔记,或许还会有视频部分。本笔记包括了大部分的图像分析案例,包括图像分割,轮廓操作,霍夫检测API和图像的形态学操作。
本文所例举的17个部分,包括了OpenCV图像处理相关API的详细解读和使用例子。
图像部分
1. 图像的五种阈值分割方法
OpenCV提供了5种阈值分割的方法,一般的,我们对图像进行二值分割比较多,所以前面两种阈值分割的方法用的比较多,需要注意的是,自定义阈值T的分割法支持多通道,以通道为单位操作。
//阈值法1,二值化,if > T 255 else 0
threshold(gray, binary1, 127, 255, THRESH_BINARY);
//阈值法2,二值化,if < T 255 else 0
threshold(gray, binary2, 127, 255, THRESH_BINARY_INV); //invert 反转
//阈值法3,if < T 原值 else T
threshold(gray, binary3, 127, 255, THRESH_TRUNC); //truncate 截断
//阈值法4,if < T 0 else 原值
threshold(gray, binary4, 127, 255, THRESH_TOZERO);
//阈值法5,if < T 原值 else 0
threshold(gray, binary5, 127, 255, THRESH_TOZERO_INV);
2. 图像的全局阈值分割法
利用OpenCV提供的API确定阈值T,然后基于T进行二值分割,常见的全局阈值分割法有:均值法,OTSU阈值法,三角阈值法。
//均值法,自己求灰度图像的像素均值
Scalar s = mean(gray);
double t1 = threshold(gray, binary1, s[0], 255,THRESH_BINARY); //返回阈值T
//OTSU阈值,求出每个像素点的最小类内(以像素点为基准,将图像像素点分为两类)方差,作为阈值
double t2 = threshold(gray, binary2, 0, 255, THRESH_BINARY | THRESH_OTSU);
//三角阈值,原理基于最多像素值(只对单峰图像效果较好,如医学生物类图像)和最大像素值
double t3 = threshold(gray, binary3, 0, 255, THRESH_BINARY | THRESH_TRIANGLE);
3. 自适应阈值(局部阈值)分割法
自适应阈值分割法特适用于光照不均匀或背景复杂的图像,在这些情况下,全局阈值分割法可能无法有效地分割目标和背景。
/*
自适应阈值分割法函数原型
src: 单通道图像
dst: 输出的目标图像
maxValue: 大于0的最大像素值
adaptiveMethod: 使用的自适应阈值方法,常见的有 ADAPTIVE_THRESH_GAUSSIAN_C 和 ADAPTIVE_THRESH_MEAN_C
thresholdType: 所使用阈值的方法
blockSize: 像素邻居的块大小,可以理解为局部块的大小
C: 常数C,为整数,计算出局部块的阈值之后,所减去的值
*/
void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C);
/*
使用例子
*/
4. 图像中联通组件的发现
在OpenCV的图像中,联通组件可以简单的理解为是图像中每一个元素(比如一个风景图像,花,树木等都属于一个联通组件),一般图像的背景也是一个联通组件,在检测联通组件的时候,背景一般是第一个联通组件,通常被赋值为0。
/*
联通组件发现函数原型1(第2个原型在第五部分介绍)
param image: input image.
param labels: output result, storage the label value of connected components.
param connectivity: can be understood as the method of finding connected components, normal value is 8 or 4.
param ltype: output image label type. Currently CV_32S and CV_16U are supported.
param ccltype: connected components algorithm type, normal value is CCL_DEFAULT.
return: the number of connected components.
*/
int connectedComponents(InputArray image, OutputArray labels, int connectivity, int ltype, int ccltype);
/*
使用例子
binary1:一个二值图像,调用联通组件API前,应该先将图像进行二值化
labels:存储了联通组件信息,同一个联通组件在图像中覆盖的位置,值相同,其中,背景的值为0
*/
Mat labels = Mat::zeros(binary1.size(), CV_32S);
int num_labels = connectedComponents(binary1, labels, 8, CV_32S, CCL_DEFAULT); //返回联通组件个数
5. 续4
在OpenCV中,提供的另一个联通组件发现的API,connectedComponentsWithStats
,该API不仅可以实现联通组件的发现,还可以对每一个联通组件的状态信息(联通组件的中心位置,联通组件的矩形范围)进行输出。
/*
联通组件发现函数原型2
param image: input image.
param labels: output result, storage the label value of connected components.
param stats: the status of connected component, including backgroud label.
param centroids: the central position of connected component, including x and y axis
param connectivity: can be understood as the method of finding connected components, normal value is 8 or 4.
param ltype: output image label type. Currently CV_32S and CV_16U are supported.
param ccltype: connected components algorithm type, normal value is CCL_DEFAULT.
return: the number of connected components.
*/
int connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids, int connectivity, int ltype, int ccltype);
/*
使用例子
labels:每一个联通组件的取值
stats:每一个联通组件的矩形范围信息,比如最左上角点的信息,矩形的宽和高,矩形面积等
centroids:每一个联通组件的中心位置
*/
Mat labels = Mat::zeros(binary.size(), CV_32S); //存储联通组件标签,同一个联通组件的像素标签相同
Mat stats, centroids; //stats存储了每个联通组件的矩形范围,centroids存储了每个联通组件中心位置
int num_labels = connectedComponentsWithStats(binary, labels, stats, centroids, 8, CV_32S, CCL_DEFAULT); //CV_32S: 32位有符号整数
6. 轮廓发现和绘制
在OpenCV中,图像轮廓的定义很好理解,就是每一个图像元素的边缘(比如风景图,花的边缘和树的边缘等),轮廓发现和绘制就是通过OpenCV提供的API,将图像所有的元素轮廓进行检测,并且绘制出来,其中,轮廓发现是基于二值图的。
/*
轮廓发现函数原型
param image: 输入的二值图像
param contours: 存储检测到的每一个轮廓点集合(vector<vector<cv::Point>>).
param heirarchy: 可选参数vector(vector<cv::Vec4i>), 存储内外轮廓之间的嵌套关系,hierarchy[i][0]~hierarchy[i][3]分别存储了后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的索引编号。
mode: 轮廓检测的模式, 常见的两种模式为RETR_TREE(检测所有轮廓,并且构建所有轮廓的拓扑关系)和RETR_EXTERNAL(只检测外部轮廓).
method: 轮廓逼近的方法,常见的取值为CHAIN_APPROX_SIMPLE(只取能体现轮廓的少部分点)和CHAIN_APPROX_NONE(取轮廓所有的点,效率较慢)
param offset: 默认即可.
*/
void findContours( InputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point());
/*
使用例子
binary:二值图
*/
vector<vector<Point>> contours; //存储每一个轮廓所构成的点集合
vector<Vec4i> hierachy; //存储图像的层次结构
findContours(binary, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
/*
轮廓绘制函数原型,解释一些比较关键的参数
image:与上面findContours()不同的是,这里image是多通道图像,很好理解,我们绘制的轮廓一般是带颜色的
contourIdx:轮廓数组contours的索引,大于0时绘制一个轮廓,小于零时绘制轮廓数组contours的所有轮廓
hierarchy:存储图像轮廓层次的数组,默认即可
*/
void drawContours( InputOutputArray image, InputArrayOfArrays contours,int contourIdx, const Scalar& color, int thickness = 1, int lineType = LINE_8, InputArray hierarchy = noArray(), int maxLevel = INT_MAX, Point offset = Point());
/*
使用例子
src:需要绘制轮廓的多通道图像
第三个参数为-1表示绘制所有轮廓,也可以使用循环一个一个轮廓的绘制
*/
drawContours(src, contours, -1, Scalar(0, 255, 255), 2, LINE_AA); //第三个参数为负数,绘制所有轮廓
for (size_t i = 0; i < contours.size(); ++i) {
drawContours(src, contours, i, Scalar(0, 0, 255), 2, LINE_AA); //第三个参数为正数,绘制一个轮廓
}
7. 续6. 轮廓面积与周长,最大 or 最小外接矩形
如6所示,当我们进行轮廓发现之后,会得到轮廓数组contours,通过该轮廓数组,利用OpenCV提供的一些轮廓相关的API,可以输出轮廓相关的信息。
/*
计算轮廓面积函数原型
contour:轮廓的点集合
oriented:是否返回带有方向的值(矢量),默认false返回标量即可
*/
double contourArea( InputArray contour, bool oriented = false);
/*
计算轮廓周长函数原型
curve:轮廓点集合
closed:曲线(轮廓)是否闭合,findContours返回的轮廓一般是闭合的,这里true即可
*/
double arcLength( InputArray curve, bool closed);
/*
轮廓最大外接矩形函数原型
array:轮廓的点集合
返回值为矩形,可以绘制
*/
Rect boundingRect( InputArray array);
/*
轮廓最小外接矩形函数原型
points:轮廓的点集合
返回值为旋转矩形,可以绘制
*/
RotatedRect minAreaRect( InputArray points);
/*
续6,例子,当我们进行轮廓发现之后,得到contours数组,对该数组进行遍历
*/
for (size_t i = 0; i < contours.size(); ++i) {
double area = contourArea(contours[i]); //计算轮廓面积
double size = arcLength(contours[i], true); //计算轮廓周长
cout << area << ", " << size << endl;
//绘制最大ROI矩形区域,矩形规则摆放
Rect box = boundingRect(contours[i]);
rectangle(src, box, Scalar(0, 0, 255), 2, 8);
//绘制最小ROI矩形区域,矩形不是规则摆放的
RotatedRect rotatedRect = minAreaRect(contours[i]);
vector<Point2f> pts(4);
rotatedRect.points(pts); //旋转矩形的四个点,提供了绘制不规则矩形的方法
for (int i = 0; i < 4; ++i) {
line(src, pts[i], pts[(i + 1) % 4], Scalar(150, 75, 0), 2, 8);
}
cout << rotatedRect.angle << endl;
//绘制椭圆形状的ROI区域
ellipse(src, rotatedRect, Scalar(0, 150, 75), 2, 8);
}
8. 轮廓匹配
在OpenCV中,提供了API对两幅图像的相同轮廓进行匹配,轮廓匹配的基本原理是基于图像的矩实现的,那么,什么是矩呢?其实,矩可以理解为用数学公式的方式来表达图像的几何信息。矩又分为很多种,几何矩、中心矩、归一化中心矩和Hu不变矩,通过对矩的理解,才能知道OpenCV进行轮廓匹配的原理。
几何矩是基于图像像素强度的统计量,用于描述图像的几何特性。对于一个二值图像或灰度图像,其几何矩由下式定义:
G
e
o
m
e
t
r
i
c
M
o
m
e
n
t
s
(
p
,
q
)
=
∑
x
=
0
X
−
1
∑
y
=
0
Y
−
1
x
p
y
q
f
(
x
,
y
)
Geometric Moments_{(p,q)} = \sum_{x=0}^{X-1} \sum_{y=0}^{Y-1} x^{p} y^{q} f(x,y)
GeometricMoments(p,q)=x=0∑X−1y=0∑Y−1xpyqf(x,y)
其中,
(
p
+
q
)
(p+q)
(p+q)表示图像矩的阶数,
f
(
x
,
y
)
f(x,y)
f(x,y)表示图像中某一像素点的灰度值(二值图像的
f
(
x
,
y
)
=
0
o
r
1
f(x,y) = 0 or 1
f(x,y)=0or1),在图像处理中,比较常用的是0阶矩、一阶矩和二阶矩。
- 0阶矩 ( p + q = 0 ) (p+q=0) (p+q=0)对于图像处理,有什么几何特性呢?由二值图像可知,我们的图像像素灰度值不是0就是1,所以,将其带入到0阶矩的公式中,为0的像素灰度值表示背景,为1的像素灰度值表示前景轮廓,0阶矩公式即表示为所有轮廓的像素灰度值和(轮廓的总面积)。
- 同理,对于一阶矩 ( p + q = 1 ) (p + q = 1) (p+q=1)的几何特性, G e o m e t r i c M o m e n t s ( 1 , 0 ) GeometricMoments_{(1,0)} GeometricMoments(1,0)表示图像灰度值中心点(质心)在x方向上的值, G e o m e t r i c M o m e n t s ( 0 , 1 ) GeometricMoments_{(0,1)} GeometricMoments(0,1)表示图像质心在y方向上的值。
- 二阶矩的几何意义是,描述图像的惯性,也称为惯性矩,它描述了图像的形状,方向和对称信息,二阶矩有三个 G e o m e t r i c M o m e n t s ( 2 , 0 ) GeometricMoments_{(2,0)} GeometricMoments(2,0)、 G e o m e t r i c M o m e n t s ( 0 , 2 ) GeometricMoments_{(0,2)} GeometricMoments(0,2)和 G e o m e t r i c M o m e n t s ( 1 , 1 ) GeometricMoments_{(1,1)} GeometricMoments(1,1), G e o m e t r i c M o m e n t s ( 2 , 0 ) GeometricMoments_{(2,0)} GeometricMoments(2,0)为x轴方向上的惯性矩,描述了图像在x轴上的扩散程度, G e o m e t r i c M o m e n t s ( 0 , 2 ) GeometricMoments_{(0,2)} GeometricMoments(0,2)为y轴方向上的惯性矩,描述了图像在y轴上的扩散程度,其中,这两个较大的值称为长轴,较小的值称为短轴(想象一下椭圆的长短轴)。 G e o m e t r i c M o m e n t s ( 1 , 1 ) GeometricMoments_{(1,1)} GeometricMoments(1,1)为x轴和y轴之间的联合惯性矩,它衡量了图像在两个正交方向上的耦合程度,即图像是否倾向于某个特定的角度,如果该值较大,就说明图像存在明显的倾斜(非对称)。
中心矩:在介绍Hu不变矩之前,先理解一下中心矩与归一化中心矩,因为Hu不变矩是基于归一化中心矩计算得到的。中心矩增加了图像的质心,使矩的值不随图像轮廓位置的改变而改变。这个好理解,举个例子,在图像的变换中,原图像轮廓的所在位置,在新图像上发生了变化,根据几何矩的计算公式,像素点所在位置
(
x
,
y
)
(x,y)
(x,y)发生了改变,矩的值也就随之发生了改变,但是,中心矩的计算方式使矩的值不会因轮廓位置的改变而改变。
C
e
n
t
r
a
l
M
o
m
e
n
t
s
(
p
,
q
)
=
∑
x
=
0
X
−
1
∑
y
=
0
Y
−
1
(
x
−
x
ˉ
)
p
(
y
−
y
ˉ
)
q
f
(
x
,
y
)
CentralMoments_{(p,q)} = \sum_{x=0}^{X-1} \sum_{y=0}^{Y-1} (x-\bar{x}) ^{p} (y-\bar{y})^{q} f(x,y)
CentralMoments(p,q)=x=0∑X−1y=0∑Y−1(x−xˉ)p(y−yˉ)qf(x,y)
归一化中心距:就是在中心距的基础上,除以一个适当的尺度因子(通常是零阶矩的幂次),对比中心矩,归一化中心矩保证了矩的值在图像缩放的时候也保持不变。
N
o
r
m
a
l
i
z
e
d
C
e
n
t
r
a
l
M
o
m
e
n
t
s
(
p
,
q
)
=
C
e
n
t
r
a
l
M
o
m
e
n
t
s
(
p
,
q
)
G
e
o
m
e
t
r
i
c
M
o
m
e
n
t
s
(
0
,
0
)
r
,
r
=
p
+
q
+
2
2
NormalizedCentralMoments_{(p,q)}= \frac{CentralMoments_{(p,q)}}{GeometricMoments_{(0,0)}^{r}}, r= \frac{p+q+2}{2}
NormalizedCentralMoments(p,q)=GeometricMoments(0,0)rCentralMoments(p,q),r=2p+q+2
Hu不变矩:Hu不变矩通过对归一化中心矩的线性组合,构成了7个值,保证了图像矩的值在图像旋转时也保持不变,它是归一化中心矩的进一步优化,使得Hu不变矩能够成为最好的图像几何信息数学表达方式,在图像进行平移、缩放和旋转时,都能够保证矩的值不变,下列公式,用
N
N
N表示归一化中心矩,
M
M
M表示Hu不变矩。
KaTeX parse error: Unknown column alignment: e at position 17: …\begin{array}{le̲ft} M_{1}=N_{20…
通过对几何矩、中心矩和归一化中心矩的介绍,我们应该对图像的矩有一个清晰的认识。不同阶数的几何矩,它能表达图像中不同的几何信息,但是,随着图像的变化(如平移,缩放,旋转),其矩的值也会变化。所以,就出现了中心矩,中心距在几何矩的基础上,解决了平移导致图像距变化的问题,归一化中心矩在中心矩的基础上,解决了缩放导致图像矩变化的问题,Hu不变矩在归一化中心矩的基础上,解决了旋转导致图像矩变化的问题。
/*
轮廓匹配函数原型
contour1: 待匹配轮廓的Hu不变矩
contour2:匹配轮廓的Hu不变矩
method:主要的取值有CONTOURS_MATCH_I1、CONTOURS_MATCH_I2和CONTOURS_MATCH_I3,分别表示使用Hu不变矩的欧几里得距离作为相似度度量、使用Hu不变矩的曼哈顿距离作为相似度度量和使用Hu不变矩的切比雪夫距离作为相似度度量
return: 返回值表示两个轮廓之间的形状相似度,这个值越小,表示两个轮廓越相似
*/
double matchShapes( InputArray contour1, InputArray contour2,int method, double parameter );
/*
使用例子
*/
void contours_find_info(Mat& image, vector<vector<Point>>& contours) {
Mat gray;
Mat binary;
//灰度化 & 二值化
cvtColor(image, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
}
void QuickDemo::ContoursMatchDemo() {
Mat src1 = imread("D:\\Photo\\images\\abc.png");
Mat src2 = imread("D:\\Photo\\images\\a.png");
if (src1.empty() || src2.empty()) {
return;
}
//src1图像中有ABC三个字母,src2图像中只有A字母,需要通过src2匹配src1中的A字母轮廓,并且绘制
imshow("src1", src1);
imshow("src2", src2);
//轮廓发现
vector<vector<Point>> contours1; //存储src1图像轮廓
vector<vector<Point>> contours2; //存储src2图像轮廓
contours_find_info(src1, contours1);
contours_find_info(src2, contours2);
//计算src2图像中A字母轮廓的几何距
Moments mm2 = moments(contours2[0]);
//计算src2图像中A字母轮廓的Hu距
Mat Hu2;
HuMoments(mm2, Hu2);
//遍历src1图像中的所有字母轮廓,与src2图像中的A字母轮廓进行匹配
for (int i = 0; i < contours1.size(); ++i) {
//计算src1图像中轮廓的几何距
Moments mm1 = moments(contours1[i]);
//通过质心和像素总和求出轮廓的中心位置
cout << mm1.m00 << endl; //得出0阶距的值就是轮廓的像素总和
double cx = mm1.m10 / mm1.m00;
double cy = mm1.m01 / mm1.m00;
cout << cx << " " << cy << endl;
//通过src1图像中字母轮廓的几何距,计算Hu距
Mat Hu1;
HuMoments(mm1, Hu1);
//通过Hu距进行轮廓匹配
double dist = matchShapes(Hu1, Hu2, CONTOURS_MATCH_I1, 0);
if (dist < 1) {
cout << dist << endl;
//在src1图像中绘制出从src2图像匹配到的轮廓
drawContours(src1, contours1, i, Scalar(255, 0, 255), 2, LINE_AA);
}
}
imshow("contours_match_demo", src1);
}
9. 轮廓逼近
在谈轮廓逼近之前,想先聊一下什么是边缘检测和轮廓检测:
边缘检测主要是通过一些手段检测数字图像中明暗变化剧烈(即梯度值比较大)的像素点,偏向于图像的像素点变化。如canny边缘检测,结果通常保存在和源图像一样尺寸和类型的边缘图中。轮廓检测指检测图像中的对象边界,更偏向于关注上层语义对象。如OpenCV中的findContours()
函数, 它会得到每一个轮廓并以点向量方式存储。图像的边缘检测更加注重于图像的像素点,而图像的轮廓检测更加注重于图像中的对象。
轮廓逼近,就是通过对轮廓外形无限逼近,删除非关键点、得到轮廓的关键点,不断逼近轮廓的真实形状。
/*
轮廓逼近函数原型
curve: 需要进行逼近的轮廓
approxCurve:存储轮廓逼近的结果,源码对这个参数的要求是,类型需要和curve一致
epsilon:逼近精度,表示原始轮廓与近似轮廓之间的最大允许距离(最大误差)
closed:轮廓是否闭合,选择默认true即可
*/
void approxPolyDP( InputArray curve,OutputArray approxCurve,double epsilon, bool closed );
/*
使用例子,通过轮廓逼近API找到轮廓的关键点,并且绘制出结果
*/
void QuickDemo::ContoursApproxDemo() {
Mat src = imread("D://Photo//images//contours.png",IMREAD_COLOR);
Mat gray;
Mat binary;
cvtColor(src, gray, COLOR_BGR2GRAY); //转灰度图像
threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU); //转二值图像
//轮廓发现
vector<vector<Point>> contours; //存储每一个轮廓的信息
vector<Vec4i> hierarchy; //存储轮廓层次
findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
//轮廓逼近
for (size_t i = 0; i < contours.size(); ++i) {
vector<Point> results;
approxPolyDP(contours[i], results, 4, true); //轮廓逼近
//绘制轮廓逼近的结果
for (auto result: results) {
circle(src, result, 2, Scalar(255, 200, 0), 2, LINE_AA);
}
imshow("src", src);
imshow("binary", binary);
}
10. 轮廓拟合
生成最相似的圆或者椭圆,读者可以对轮廓拟合、轮廓的最小外接矩形(续7.)三个方法进行对比,实现的需求是差不多的,这里对比一下轮廓拟合和轮廓最小外接矩形(ROI)的区别。
- 轮廓拟合:轮廓拟合的主要目标是用一个简单的几何形状描述轮廓的形状特征,更多的用于形状分析和识别,OpenCV提供了三个轮廓拟合的API,
fitEllipse();
、fitLine();
和approxPolyDP();
,分别使用椭圆、直线和多边形进行拟合(多边形拟合也叫轮廓逼近)。 - 最小外接矩形:最小外接矩形侧重于定义轮廓的边界和位置,主要用于ROI区域的定义和图像预处理阶段(减少图像分析的范围),OpenCV提供了一个轮廓最小外接矩形的API,
RotatedRect minAreaRect( InputArray points);
/*
椭圆轮廓拟合原函数
points: 轮廓的点集合,通常是Mat对象或者是vector<Point>对象
return: 返回椭圆对应的旋转矩阵
*/
RotatedRect fitEllipse( InputArray points );
/*
直线轮廓拟合原函数
points: 轮廓的点集合
line: 拟合输出的的结果,是包含四个元素的向量,(vx, vy, x0, y0),(vx, vy)表示直线对应的归一化向量,(x0, y0)表示直线通过的点
distType: 计算点到直线距离的方式,常见的有最小二乘法、最小中值误差或最小均值误差法,详细可以见DistanceTypes类
param: 额外参数,这里使用0即可,让fitLine()自动匹配最优的参数
reps: 半径精度,表示轮廓点到拟合直线的距离
aeps: 角度精度,OpenCV官方建议将reps和aeps的值设置为0.1
*/
void fitLine( InputArray points, OutputArray line, int distType,double param, double reps, double aeps);
/*
多边形轮廓拟合原函数
curve: 拟合轮廓的点集合,vector<Point>或者Mat对象
approxCurve:轮廓拟合的结果,参数类型与curve一致
epsilon: 拟合精度,表示原始轮廓与近似轮廓之间的最大允许距离(最大误差)
closed: 轮廓是否闭合,默认true即可
*/
void approxPolyDP( InputArray curve,OutputArray approxCurve,double epsilon, bool closed);
/*
这里主要演示椭圆轮廓拟合例子
*/
void QuickDemo::ContoursFittingDemo() {
Mat src = imread("D://Photo//images//self_circle.png", IMREAD_COLOR);
Mat gray;
Mat binary;
cvtColor(src, gray, COLOR_BGR2GRAY); //转灰度图像
threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU); //转二值图像
//轮廓发现
vector<vector<Point>> contours;
vector<Vec4i> hierarchy; //存储轮廓拓扑信息
findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
//轮廓拟合
for (size_t i = 0; i < contours.size(); ++i) {
RotatedRect rrt = fitEllipse(contours[i]); //椭圆轮廓拟合,基于二值图
Point center = rrt.center; //旋转矩阵的中心
circle(src, center, 2, Scalar(0, 0, 255), 2, 8);
ellipse(src, rrt, Scalar(255, 0, 0), 2, 8); //根据旋转矩阵绘制椭圆
}
imshow("src", src);
imshow("binary", binary);
}
11. 霍夫直线检测HoughLines()
霍夫直线检测,通俗的理解就是,对图像中的点集合进行直线检测,其基本原理是:将图像看成一个直角坐标系,像素点 ( x , y ) (x,y) (x,y)看作已知参数,组成极坐标方程 ρ ( θ ) = x ⋅ c o s ( θ ) + y ⋅ s i n ( θ ) \rho(\theta) = x \cdot cos(\theta) + y \cdot sin(\theta) ρ(θ)=x⋅cos(θ)+y⋅sin(θ),此时就构成了一个, θ \theta θ作为自变量, ρ \rho ρ作为因变量的三角函数曲线。同理,对其他的 n − 1 n-1 n−1个像素点进行该变换,就组成了 n n n条不同的 ρ i ( θ ) = x ⋅ c o s ( θ ) + y ⋅ s i n ( θ ) , i ⊆ [ 1 , n ] , θ ⊆ [ 0 , π ) \rho_{i}(\theta) = x \cdot cos(\theta) + y \cdot sin(\theta),i\subseteq[1,n],\theta\subseteq[0,\pi) ρi(θ)=x⋅cos(θ)+y⋅sin(θ),i⊆[1,n],θ⊆[0,π)三角函数曲线,其中, θ \theta θ的取值范围跟图像中直线与x轴正向的夹角有关。霍夫检测认为,如果这 n n n个点作为已知参数构成的 n n n条三角函数曲线相交于一个点,那么这 n n n个点就属于同一条直线上, n n n越多,就说明越多的点在一条直线上,直线就越平滑,至此,霍夫对一条直线的检测就完成了。
/*
霍夫直线检测HoughLines()函数原型
image: 输入的单通道二值图像
lines:存储检测到的直线几何信息,vector<Vec3f>或者Mat对象存储,其中,lines[i][0]存储图像原点到直线的距离;lines[i][1]存储直线的法线(方向指向lines[i]),与x轴正向的夹角;lines[i][3]存储图像中有多少个像素点在lines[i]中
rho:直线法线的像素精度,默认取1即可
theta:直线法线(方向指向lines[i]),与x轴正向夹角的角度精度,如果以度数为精度,默认取值CV_PI/180.0
threshold:检测直线的阈值,只有超过threshold个像素单位才认为是一条直线
srn:rho方向上的高斯滤波器的标准差,默认0即可
stn:theta方向上的高斯滤波器的标准差,默认0即可
min_theta:指定theta的最小值,可以过滤一些不需要检测的直线
max_theta:指定theta的最大值,同上
*/
void HoughLines( InputArray image, OutputArray lines,double rho, double theta, int threshold,double srn = 0, double stn = 0,double min_theta = 0, double max_theta = CV_PI);
/*
霍夫直线检测HoughLines()使用例子
*/
void QuickDemo::HoughLinesDemo() {
Mat src = imread("D://Photo//images//lines1.png", IMREAD_COLOR);
//二值化
Mat gray;
Mat binary;
GaussianBlur(src, src, Size(3, 3), 0);
cvtColor(src, gray, COLOR_BGR2GRAY); //转灰度图像
threshold(gray,binary,0,255,THRESH_BINARY|THRESH_OTSU); //转二值图像
vector<Vec3f> lines;
HoughLines(binary, lines, 1, (CV_PI / 180.0), 200, 0, 0, 0);
//绘制直线
for (size_t i = 0; i < lines.size(); ++i) {
float rho = lines[i][0]; //距离,左上角为起始点
float theta = lines[i][1]; //角度
float acc = lines[i][2]; //转换为极坐标曲线,经过同一个点的曲线个数
double x0, y0; //存储直角坐标系上原始的点
x0 = rho * cos(theta);
y0 = rho * sin(theta);
//输出lines[i]里面的内容
printf("rho: %.2lf, theta: %.2lf, acc: %.2lf\n", rho, theta, acc);
printf("x0 = %.2lf, y0 = %.2lf\n", x0, y0);
Point pt1, pt2; //找到直线上延长的两个点
float new_rho = CV_PI - theta;
pt1.x = cvRound(x0 + 1000 * sin(new_rho));
pt1.y = cvRound(y0 + 1000 * cos(new_rho));
pt2.x = cvRound(x0 - 1000 * sin(new_rho));
pt2.y = cvRound(y0 - 1000 * cos(new_rho));
line(src, pt1, pt2, Scalar(255, 150, 0), 2, LINE_AA);
}
imshow("src", src);
imshow("binary", binary);
}
12. 霍夫直线检测HoughLinesP()
与11的霍夫检测直线不同的是,OpenCV提供的第二个霍夫直线检测API,HoughLinesP()
更加注重于图像中直线段的检测(从API的参数也可以看出来),跟传统的霍夫检测HoughLines()
不同的是,HoughLinesP()
只对局部的像素点进行统计,保证准确率的同时,直线检测的效率也提高了。
/*
霍夫直线检测HoughLinesP()函数原型
image:输入的单通道二值图像
lines:存储检测到直线的几何信息,<vector<Vec4i>>或者Mat数据类型,lines[i][0]、lines[i][1]、lines[i][2]和lines[i][3]分别表示直线起始坐标(x1, y1, x2, y2)
rho:直线法线(方向指向直线)的像素精度(也叫步长或者距离分辨率),默认取1
theta:直线法线(方向指向直线)与x轴正向夹角的角度精度(也叫角度分辨率),默认CV_PI/180.0
threshold:检测直线的阈值,只有超过threshold个像素单位才认为是一条直线
minLineLength:检测直线的最小线段长度
maxLineGap:检测到的两条直线最大允许间隙(单位像素,直线距离)
*/
void HoughLinesP( InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = 0, double maxLineGap = 0)
/*
霍夫直线检测HoughLinesP()使用例子
*/
void QuickDemo::HoughLinesPDemo() {
Mat src = imread("D://Photo//images//morph01.png", IMREAD_COLOR);
Mat gray;
Mat binary;
GaussianBlur(src, src, Size(3, 3), 0); //高斯模糊
cvtColor(src, gray, COLOR_BGR2GRAY);
Canny(gray, binary, 80, 160, 3, false); //利用边缘检测进行二值化
vector<Vec4i> lines; //存储四个值为整数的向量
HoughLinesP(binary, lines, 1, CV_PI / 180.0, 80, 200, 10);
Mat result = Mat::zeros(src.size(), src.type());
for (size_t i = 0; i < lines.size(); ++i) {
int x1 = lines[i][0];
int y1 = lines[i][1];
int x2 = lines[i][2];
int y2 = lines[i][3];
line(result, Point(x1, y1), Point(x2, y2), Scalar(255, 0, 0), 2, LINE_AA); //gap 间隔
}
imshow("src", src);
imshow("result", result);
imshow("binary", binary);
}
13. 霍夫圆检测
霍夫圆检测,和霍夫直线检测原理类似,对于 X − Y X-Y X−Y坐标系下(对应于我们的图像空间)的圆方程 ( x − a ) 2 + ( y − b ) 2 = r 2 (x-a)^{2}+(y-b)^{2} = r^{2} (x−a)2+(y−b)2=r2,我们进行霍夫空间变换到 a − b a-b a−b坐标系中,将 x x x、 y y y和 r r r看作已知参数, a − b a-b a−b坐标系下,新圆的方程为 ( a − x ) 2 + ( b − y ) 2 = r 2 (a-x)^{2}+(b-y)^{2} = r^{2} (a−x)2+(b−y)2=r2,同理,对 X − Y X-Y X−Y坐标系下圆的 n n n个点转换到 a − b a-b a−b坐标系中,就能得到半径为 r r r的 n n n个圆,着 a − b a-b a−b坐标系下的 n n n个圆必相交于一个点,至此,霍夫圆检测就完成了。对图像中所有的边缘点,设定 r r r的范围,就能检测一定条件下所有的点了。
/*
霍夫圆检测函数原型
image:经过灰度化和边缘检测处理的单通道八位图像
circles:存储检测到的圆的几何信息,vector<Vec4f>,circles[i][0]表示检测到第i个圆圆心的x值,circles[i][1]表示检测到第i个圆圆心的y值,circles[i][2]表示检测到第i个圆的半径,circles[i][3]表示检测到第i个圆所拥有的像素单位个数
method:检测圆的方法,常见的两个检测方法是HOUGH_GRADIENT和HOUGH_GRADIENT_ALT
dp:这个参数很重要,说一下,dp = 图像分辨率 / 累计数组分辨率,图像分辨率好理解,累计数组是什么呢?霍夫圆检测算法运行的过程中,霍夫参数空间为(a, b, r),累计数组的作用就是,记录每一对参数空间(a0, b0, r0)中有多少个曲线H(a, b, r)经过,统计累计数组的局部最大值,就能检测到一个圆了(累计数组局部最大值就是检测圆边缘点的个数)。知道了累计数组是什么,累计数组分辨率就好理解了,指的就是a,b,r的分辨率,a,b代表圆心,r代表半径。dp值越接近1,说明a,b,r的分辨率越接近图像分辨率,检测的精度就越高,dp值越大,检测精度就越低。所以,dp最好的取值范围是[1,2],dp不可能小于1,因为图像局部的分辨率不可能超过整个图像的分辨率
minDist:检测到的各个圆之间,其圆心的最小距离
param1:边缘检测的高阈值,默认100即可
param2:累加器阈值,类似于HoughLines()的threshold,只有累计数组中向量(a0, b0, r0)的累加值高于param2,才会被认为(a0, b0, r0)是检测到的一个圆
minRadius:检测圆的最小半径,限制r,提高检测效率
maxRadius:检测圆的最大半径,限制r,提高检测效率
*/
void HoughCircles( InputArray image, OutputArray circles,int method, double dp, double minDist, double param1 = 100, double param2 = 100, int minRadius = 0, int maxRadius = 0 );
/*
霍夫圆检测使用例子
*/
void QuickDemo::HoughCircleDemo() {
Mat src = imread("D://Photo//images//coins.jpg", IMREAD_COLOR);
Mat gray;
Mat binary;
GaussianBlur(src, src, Size(9, 9), 2); //高斯模糊
cvtColor(src, gray, COLOR_BGR2GRAY);
Canny(gray, binary, 40, 160, 3, false); //利用边缘检测进行二值化
vector<Vec4f> circles;
int dp = 2; //图像分辨率与累加器数组circles分辨率的ratio
double minDist = 40; //检测到的圆心最小距离,正常来说是最小半径的两倍
double minRadius = 20; //检测圆半径的最小值
double maxRadius = 70; //检测圆半径的最大值
HoughCircles(binary, circles, HOUGH_GRADIENT, dp, minDist, 100, 100, minRadius, maxRadius);
//绘制检测到的圆
for (size_t i = 0; i < circles.size(); ++i) {
Point center(circles[i][0], circles[i][1]); //圆心
int radius = cvRound(circles[i][2]); //半径
cout << circles[i][3] << endl; //像素单位个数
circle(src, center, radius, Scalar(0, 0, 255), 4, LINE_AA, 0); //绘制圆
circle(src, center, 2, Scalar(255, 0, 0), 2, LINE_AA, 0); //绘制圆心
}
imshow("src", src);
imshow("gray", gray);
imshow("binary", binary);
}
14. 图像形态学操作,腐蚀(erosion)与膨胀(dilate)
图像的腐蚀(erosion)和膨胀(dilation)是形态学操作的基础,常用于图像处理和分析中,特别是在二值图像(前景一般为白色,背景一般为黑色)中用于改变图像中对象的形状和大小。
腐蚀,一般用来缩小图像的前景区域(将轮廓加以腐蚀),具体的,定义一个核(结构元素),然后将核沿着图像滑动(有点类似于卷积操作),当核每经过图像局部区域,都会将局部区域像素最小值替代核中的某一个像素值(具体位置在定义核的时候表明)。
膨胀,膨胀与腐蚀刚好相反,一般用来放大图像的前景区域(将轮廓加以膨胀),膨胀操作与腐蚀操作的原理几乎一致,只是像素最大值替代核中某一个像素值。
/*
腐蚀操作函数原型
src:任意通道数的图像,常见的BGR, GRAY, BINARY图像都可以进行腐蚀操作
dst:腐蚀操作输出的目标图像
kernel:腐蚀操作用到的核(结构元素)
anchor:腐蚀操作替代核中像素值的位置,Point(-1, -1)表示默认替代核的中心位置像素值
iterations:表示进行腐蚀操作的迭代次数,默认1即可
borderType:处理边界的类型,默认即可,具体类型可以在BorderTypes枚举中查看
borderValue:指定边界的填充值,默认即可
*/
void erode( InputArray src, OutputArray dst, InputArray kernel, oint anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());
/*
膨胀操作函数原型
src:任意通道数的图像,常见的BGR, GRAY, BINARY图像都可以进行膨胀操作
dst:膨胀操作输出的目标图像
kernel:膨胀操作用到的核(结构元素)
anchor:膨胀操作替代核中像素值的位置,Point(-1, -1)表示默认替代核的中心位置像素值
iterations:表示进行膨胀操作的迭代次数,默认1即可
borderType:处理边界的类型,默认即可,具体类型可以在BorderTypes枚举中查看
borderValue:指定边界的填充值,默认即可
*/
void dilate( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());
/*
通常对图像进行形态学操作的时候,我们会提前定义好核(结构元素),定义核的函数原型如下
shape:核的形状,OpenCV提供了三种核的形状,分别是矩形(MORPH_RECT = 0, 八领域),十字形(MORPH_CROSS = 1, 四领域),椭圆形(MORPH_ELLIPSE = 2)
ksize:size()数据类型,表示核的大小
anchor:替代核中的像素值位置,Point(-1, -1)默认替代核的中心位置
*/
Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1));
/*
腐蚀与膨胀操作使用例子
*/
void QuickDemo::ErodeAndDilateDemo() {
Mat src = imread("D://Photo//images//cells.png", IMREAD_COLOR);
Mat gray;
Mat binary;
//二值化
GaussianBlur(src, src, Size(9, 9), 2); //高斯模糊
cvtColor(src, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
//定义腐蚀与膨胀的核,最后一个参数-1表示代替中心位置的像素
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
Mat result1 = Mat::zeros(src.size(), src.type());
Mat result2 = Mat::zeros(src.size(), src.type());
//对二值图像的腐蚀操作
erode(binary, result1, kernel); //erode:腐蚀,缩小轮廓
//对二值图像的膨胀操作
dilate(binary, result2, kernel); //dilate:膨胀,扩大轮廓
imshow("binary", binary);
imshow("erode result1", result1);
imshow("dilate result2", result2);
}
15. 图像形态学操作,开操作(erosion)与闭操作(dilate)
开操作,开操作是先腐蚀,再膨胀,开操作的目的是用来消除小物体、在纤细点处分离物体、平滑较大物体的边界,这个很好理解,因为先进行的腐蚀操作(缩小轮廓),会去除轮廓的一些小凸起,然后进行膨胀操作,会对较大物体的轮廓进行放大,使得看起来更加平滑。
闭操作,闭操作是先膨胀,再腐蚀,闭操作的目的是将图像中靠近的图块进行连通,这个也很好理解,因为先进行的膨胀操作,靠的较近的图块由于轮廓会被放大,这时,两个图块的轮廓就会重合,达到图块连通的目的,然后再进行腐蚀操作,将连通部分的突起进行缩小。
/*
OpenCV提供了形态学操作的API,morphologyEx,该API可以执行erode, dilate, open, close等形态学操作,函数原型如下
src:任意通道的图像,常见的BGR, GRAY, BINARY图像都可以进行形态学操作
dst:形态学操作输出的结果,图像大小、类型和src一致
op: 形态学操作,MorphTypes提供了8种形态学操作,这里先知道四种,后续篇幅会有其他四种的介绍,MORPH_OPEN = 2, MORPH_CLOSE = 3, MORPH_ERODE = 0, MORPH_DILATE = 1
kernel:形态学操作用到的核(结构元素)
anchor:形态学操作替代核中像素值的位置,Point(-1, -1)表示默认替代核的中心位置像素值
iterations:形态学操作迭代次数
borderType:处理边界的类型,默认即可,具体类型可以在BorderTypes枚举中查看
borderValue:指定边界的填充值,默认即可
*/
void morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());
/*
开操作和闭操作使用例子
*/
void QuickDemo::MorphologyOpenAndCloseDemo() {
Mat src = imread("D://Photo//images//fill.png", IMREAD_COLOR);
Mat gray, binary;
//灰度且二值化
cvtColor(src, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
Mat dst1, dst2;
//定义开闭操作的卷积核,第一个参数为卷积核的形状
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
//开操作,删除小的干扰块,一定要进行二值化,并且使背景通道值为0,因为腐蚀是用通道小的值替代
morphologyEx(binary, dst1, MORPH_OPEN, kernel, Point(-1, -1), 1);
//闭操作,填充闭合区域
morphologyEx(binary, dst2, MORPH_CLOSE, kernel, Point(-1, -1), 1);
imshow("src", src);
imshow("binary", binary);
imshow("dst1_open", dst1);
imshow("dst2_close", dst2);
}
16. 图像形态学梯度
形态学梯度,形态学梯度的主要目的是将图像中图块的轮廓提取出来,为什么梯度可以提取出轮廓呢?这个很好理解,还记得我们在进行腐蚀和膨胀操作的时候,就是对图块边缘轮廓进行缩小或放大。这样,腐蚀的结果、膨胀的结果和原图的像素差别就是轮廓部分的大小不一样,又因为梯度就是在这三者之间做像素差,所以结果就把轮廓给提取出来了。具体的有基本梯度,内梯度,外梯度三种。
- 基本梯度:膨胀减去腐蚀之后的结果,
MorphTypes
种提供了MORPH_GRADIENT
实现 - 内梯度:原图减去腐蚀之后的结果,需要自己做像素差
- 外梯度:膨胀减去原图之后的结果,需要自己做像素差
/*
形态学梯度原函数也是和上述15一样,用OpenCV提供的morphologyEx执行即可,将第三个参数op赋值为MORPH_GRADIENT即可实现基本梯度的需求
*/
void morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());
/*
形态学梯度使用例子
细心的读者会发现,我这里形态学操作跟上面不同的是,使用GRAY类型的图像,这也验证了,形态学操作支持任意类型的图像
*/
void QuickDemo::MorphologyGradientDemo() {
Mat src = imread("D://Photo//images//LinuxLogo.jpg", IMREAD_COLOR);
Mat gray, binary;
//灰度且二值化
cvtColor(src, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
//定义形态学操作的卷积核
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3), Point(-1, -1));
Mat basic_grad, inter_grad, exter_grad;
//基本梯度
morphologyEx(gray, basic_grad, MORPH_GRADIENT, kernel, Point(-1, -1), 1);
Mat dst1, dst2;
erode(gray, dst1, kernel); //腐蚀
dilate(gray, dst2, kernel); //膨胀
//内梯度
subtract(gray, dst1, inter_grad);
//外梯度
subtract(dst2, gray, exter_grad);
imshow("src", src);
imshow("gray", gray);
imshow("binary", binary);
imshow("basic_grad", basic_grad);
imshow("inter_grad", inter_grad);
imshow("exter_grad", exter_grad);
}
17. 图像形态学操作,顶帽、黑帽
顶帽,顶帽操作的主要目的是提取图像中的噪声,突出原图像中,局部区域比周围亮的部分,它是原图减去开操作的结果。这个好理解,我们知道,开操作的主要目的是用来消除小物体、在纤细点处分离物体、平滑较大物体的边界,用原图减去开操作之后,我们就能提取出那些消除的小物体(噪声),以及大物体没有平滑的部分(局部区域比周围亮的部分)。
黑帽,黑帽操作的主要目的是提取图像中,局部区域比周围暗的部分,它是闭操作减去原图的结果。这个也好理解,我们知道,闭操作的主要目的是将两个靠近的图块进行连通,原图这两个图块之间本来是不连通的(局部区域比周围暗的部分),所以用闭操作减去原图,就能把这个比周围暗的区域提取出来。
/*
顶帽与黑帽操作,直接通过OpenCV提供的形态学操作API执行即可,第三个参数 op = MORPH_TOPHAT为顶帽操作,op = MORPH_BLACKHAT为黑帽操作
*/
void morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue());
/*
顶帽与黑帽操作使用例子
*/
void QuickDemo::OtherMorphologyDemo() {
Mat src = imread("D://Photo//images//cross.png", IMREAD_COLOR);
Mat gray, binary;
//灰度且二值化
cvtColor(src, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
//定义形态学操作卷积核
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(15, 15), Point(-1, -1));
//顶帽
Mat dst1;
morphologyEx(binary, dst1, MORPH_TOPHAT, kernel, Point(-1, -1), 1);
//黑帽
Mat dst2;
morphologyEx(binary, dst2, MORPH_BLACKHAT, kernel, Point(-1, -1), 1);
imshow("gray", gray);
imshow("binary", binary);
imshow("top_hat demo", dst1);
imshow("black_hat demo", dst2);
}