本文以下OpenCV都简写成"cv2"的形式,所有img都默认为一张图片
关联文章:
OpenCV和图像处理(一)
七、边缘与轮廓
1、图像梯度
1-1:Scharr 滤波器
Scharr 滤波器:Sobel 滤波器的改良版。
S
x
=
[
−
3
0
3
−
10
0
10
−
3
0
3
]
S
y
=
[
−
3
−
10
−
3
0
0
0
3
10
3
]
计
算
梯
度
幅
值
和
方
向
S
=
S
x
2
+
S
y
2
θ
=
a
r
c
t
a
n
(
S
y
S
x
)
S_x=\left[ \begin{matrix} -3&0&3\\ -10&0&10\\ -3&0&3 \end{matrix} \right] \quad S_y=\left[ \begin{matrix} -3&-10&-3\\ 0&0&0\\ 3&10&3 \end{matrix} \right]\\ 计算梯度幅值和方向\\ S=\sqrt{S_x^2+S_y^2}\quadθ=arctan(\frac{S_y}{S_x})
Sx=⎣⎡−3−10−30003103⎦⎤Sy=⎣⎡−303−10010−303⎦⎤计算梯度幅值和方向S=Sx2+Sy2θ=arctan(SxSy)
很明显,Scharr 滤波器把 Sobel 滤波器的参数"增强"了,所以效果会更显著一些。
此外,为了加速计算,可以简化幅值:
∣
S
∣
=
∣
S
x
∣
+
∣
S
y
∣
|S|=|S_x|+|S_y|
∣S∣=∣Sx∣+∣Sy∣
在数据相对小的时候,误差也不大,但是计算比平方和再开根号就快多了。
特别地,Sx在处理横向的梯度的时候,也在纵向方向上有类似"高斯"的功能;Sy则反过来。所以
Scharr 滤波器能有"低通模糊"的功效。
# 可以对比上文 Sobel 滤波器,注意这里没有ksize参数。
scharrx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
scharry = cv2.Scharr(img, cv2.CV_64F, 0, 1)
1-2:Laplacian 滤波器
Laplacian 梯度滤波器是求图像的二阶差分, 二阶差分在亮的一边是负的,在暗的一边是正的。常数部分为零。可以用来确定边的准确位置,以及像素在亮的一侧还是暗的一侧。可假设其离散实现类似于二阶Sobel导数。事实的确如此,OpenCV在计算拉普拉斯算子时直接使用Sobel算子。
L
(
I
m
g
)
=
∂
2
I
m
g
∂
x
2
+
∂
2
I
m
g
∂
y
2
L(Img)=\frac{∂^2Img}{∂x^2}+\frac{∂^2Img}{∂y^2}
L(Img)=∂x2∂2Img+∂y2∂2Img
上面是微分的公式,先看一下一维一阶差分和二阶差分的公式:
∂
f
∂
x
i
=
f
(
x
i
+
1
)
−
f
(
x
i
)
∂
2
f
∂
x
i
2
=
f
(
x
i
+
1
)
+
f
(
x
i
−
1
)
−
2
f
(
x
i
)
\frac{∂f}{∂x_i}=f(x_{i+1})-f(x_i)\\ \frac{∂^2f}{∂x_i^2}=f(x_{i+1})+f(x_{i-1})-2f(x_i)
∂xi∂f=f(xi+1)−f(xi)∂xi2∂2f=f(xi+1)+f(xi−1)−2f(xi)
由于图像是二维的,所以(x,y)两个方向上的二阶差分分别为:
∂
2
f
∂
x
i
2
=
f
(
x
i
+
1
,
y
i
)
+
f
(
x
i
−
1
,
y
i
)
−
2
f
(
x
i
,
y
i
)
∂
2
f
∂
y
i
2
=
f
(
x
i
,
y
i
+
1
)
+
f
(
x
i
,
y
i
−
1
)
−
2
f
(
x
i
,
y
i
)
\frac{∂^2f}{∂x_i^2}=f(x_{i+1},y_i)+f(x_{i-1},y_i)-2f(x_i,y_i)\\ \frac{∂^2f}{∂y_i^2}=f(x_i,y_{i+1})+f(x_i,y_{i-1})-2f(x_i,y_i)
∂xi2∂2f=f(xi+1,yi)+f(xi−1,yi)−2f(xi,yi)∂yi2∂2f=f(xi,yi+1)+f(xi,yi−1)−2f(xi,yi)
所以 Laplacian 算子的差分形式为:
▽
2
f
(
x
i
,
y
i
)
=
f
(
x
i
+
1
,
y
i
)
+
f
(
x
i
−
1
,
y
i
)
+
f
(
x
i
,
y
i
+
1
)
+
f
(
x
i
,
y
i
−
1
)
−
4
f
(
x
i
,
y
i
)
▽^2f(x_i,y_i)=f(x_{i+1},y_i)+f(x_{i-1},y_i)+ f(x_i,y_{i+1})+f(x_i,y_{i-1})-4f(x_i,y_i)
▽2f(xi,yi)=f(xi+1,yi)+f(xi−1,yi)+f(xi,yi+1)+f(xi,yi−1)−4f(xi,yi)
写成滤波器形式(3x3)为:
L
a
p
l
a
c
i
a
n
算
子
k
e
r
n
e
l
=
[
0
1
0
1
−
4
1
0
1
0
]
Laplacian算子kernel= \left[ \begin{matrix} 0&1&0\\ 1&-4&1\\ 0&1&0 \end{matrix} \right]
Laplacian算子kernel=⎣⎡0101−41010⎦⎤
# 可以对比上文 Sobel 滤波器,这里可以只需要两个参数,并且没有xy分量了
laplacian = cv2.Laplacian(img, cv2.CV_64F)
2、Canny边缘提取算法
Canny算法首先需要经过高斯模糊去噪,减少不重要特征,然后再调用Canny方法。
img = cv2.GaussianBlur(img, (5, 5), 0)
canny = cv2.Canny(img, threshold1, threshold2)
r'''
注意到Canny有两个阈值属性:tr1和tr2
其中较大的tr2用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,
可能是断断续续的。所以这时候用较小的第一个阈值tr1用于将这些间断的边缘连接起来。
函数返回的是二值图,包含检测出的边缘
'''
2-1:非极大值抑制
这是一种边缘稀疏技术,作用在于"瘦"边。可以想象成我找了很多很多的结果,然后通过非极大值抑制,找到了相对最好的结果,而其他一般的结果我就丢掉了,达到"瘦"和"稀疏"的效果。具体如下:
1.将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较。
2.如果当前像素梯度强度与另外两个像素相比最大,则保留为边缘点;否则该像素点将被抑制。
2-2:双阈值边缘连接处理
这个是判断哪些些边缘需要保留,哪些边缘需要丢弃的。cv2.Canny中两个阈值就是梯度大小。假设有个坐标系,横轴代表像素[0-255],纵轴代表梯度大小,则两个阈值(maxVal,minVal)就相当于两条平行横轴的直线,然后连接每一个像素当前的梯度值,就形成了一条一条的梯度曲线。然后根据以下规则判断:
1.保留梯度曲线大于maxVal的部分对应的边缘。
2.如果曲线全部大于minVal,且有部分在maxVal和minVal之间,但是两头都大于maxVal,即"出头了",则保留全部曲线对应的边缘。
3、轮廓
轮廓和边缘不同,边缘厚度一般为1,即不能再薄了。可能是很多线条构成的,更有可能是断开的。而轮廓肯定是一些闭合的线条,更具有层次感。
3-1:轮廓查找和绘制
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
r'''
首先,该操作会修改原图,不想改原图需要拷贝一份
三个输入参数:输入的(二值)图像,轮廓检索方式,轮廓近似方法
=====================================================================================
1.轮廓检索方式
cv2.RETR_EXTERNAL 只检测外轮廓
cv2.RETR_LIST 检测的轮廓不建立等级关系
cv2.RETR_CCOMP 建立两个等级的轮廓,上面一层为外边界,里面一层为内孔的边界信息
cv2.RETR_TREE 建立一个等级树结构的轮廓
-------------------------------------------------------------------------------------
2.轮廓近似办法
cv2.CHAIN_APPROX_NONE 存储所有边界点
cv2.CHAIN_APPROX_SIMPLE 压缩垂直、水平、对角方向,只保留端点
cv2.CHAIN_APPROX_TX89_L1 使用teh-Chini近似算法
cv2.CHAIN_APPROX_TC89_KCOS 使用teh-Chini近似算法
=====================================================================================
返回的是轮廓的点contours和轮廓直接的从属关系hierarchy
'''
img_contour = cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
r'''
参数说明:
img 表示输入的需要画的图片
contours 表示轮廓值
-1 表示轮廓的索引,-1为全部都画
(0, 0, 255) 表示颜色(这是红色)
3 表示线条粗细,-1表示用颜色填充整个轮廓
'''
3-2:面积、周长和重心
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
M = cv2.moments(contours[0]) # 矩,参数是一个数组,此时选择一组轮廓传入,返回一个字典
cx, cy = int(M['m10'] / M['m00']), int(M['m01'] / M['m00']) # 这是公式
print("重心:", cx, cy)
area = cv2.contourArea(contours[0]) # 传入轮廓,返回面积。(上面的M['m00']的值也是面积)
print("面积:", area)
perimeter = cv2.arcLength(contours[0], True) # 传入轮廓,返回周长。参数2表示轮廓是否封闭
print("周长:", perimeter)
关于图像的"矩",这里简单介绍一下,具体可以见 @zhouzongzong 的文章:图像中矩的概念。
普通矩的计算:
m
p
q
=
∫
−
∞
+
∞
x
p
y
q
f
(
x
,
y
)
d
x
d
y
p
,
q
=
0
,
1
,
2...
m_{pq}=\int_{-∞}^{+∞}{x^py^qf(x,y)}dxdy\quad p,q=0,1,2...
mpq=∫−∞+∞xpyqf(x,y)dxdyp,q=0,1,2...
图像是二维信号,将上面公式离散化:
m
p
q
=
∑
x
∑
y
x
p
y
q
f
(
x
,
y
)
0
阶
矩
(
m
00
)
:
目
标
区
域
的
面
积
(
A
r
e
a
)
1
阶
矩
(
m
01
,
m
10
)
:
目
标
区
域
的
质
心
(
C
e
n
t
r
o
i
d
)
2
阶
矩
(
m
20
,
m
02
,
m
11
)
:
即
惯
性
矩
,
可
计
算
目
标
图
像
的
方
向
3
阶
矩
(
m
30
,
m
03
,
m
12
,
m
21
)
:
目
标
区
域
的
方
位
和
斜
度
,
反
应
目
标
的
扭
曲
.
.
.
.
.
.
m_{pq}=\sum_{x}\sum_{y}{x^py^qf(x,y)}\\ 0阶矩(m_{00}):目标区域的面积(Area)\\ 1阶矩(m_{01},m_{10}):目标区域的质心(Centroid)\\ 2阶矩(m_{20},m_{02},m_{11}):即惯性矩,可计算目标图像的方向\\ 3阶矩(m_{30},m_{03},m_{12},m_{21}):目标区域的方位和斜度,反应目标的扭曲\\ ......
mpq=x∑y∑xpyqf(x,y)0阶矩(m00):目标区域的面积(Area)1阶矩(m01,m10):目标区域的质心(Centroid)2阶矩(m20,m02,m11):即惯性矩,可计算目标图像的方向3阶矩(m30,m03,m12,m21):目标区域的方位和斜度,反应目标的扭曲......
中心矩:构造平移不变性。以目标区域的质心为中心构建中心矩,那么矩的计算时永远是目标区域中的点相对于目标区域的质心,而与目标区域的位置无关,即中心矩具备了平移不变性。
3-3:轮廓近似
得到一个大体的轮廓,需要定义一个精度参数。
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
epsilon = 30 # 设定一个精度
approx = cv2.approxPolyDP(contours[0],epsilon,True)
r'''
第一个参数是传入一个轮廓
第二个参数是精度,它是从原始轮廓到近似轮廓的最大距离,是一个准确度参数
第三个参数表示轮廓是否封闭
'''
img_contour= cv2.drawContours(img, [approx], -1, (0, 255, 0), 3)
3-4:凸包和凸性检测
和轮廓类似,但不同
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
hull = cv2.convexHull(contours[0]) # 获取当前图片轮廓的最小凸集
r'''
参数解释:[一般用到第一个参数就行]
Points:是传入的轮廓;
Hull:是输出,通常我们避免它;
Clockwise:方向标志。如果是true,则顺时针输出凸包,否则逆时针方式输出;
ReturnPoints:默认true时,返回hull点的坐标;如果是false,则返回与hull点对应的轮廓点的索引。
'''
print(cv2.isContourConvex(contours[0]), cv2.isContourConvex(hull))
r'''
1.输出当前图片轮廓是否是凸的
2.输出当前图片轮廓的最小凸集是否是凸的[肯定是True]
'''
img_contour = cv2.drawContours(img, [hull], -1, (0, 0, 255), 3)
关于边界检测,常用的有:边界矩形、最小矩形、最小外切圆等。[可用来检测碰撞]
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
# 边界矩形
x, y, w, h = cv2.boundingRect(contours[0])
img_contour = cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 最小矩形
rect = cv2.minAreaRect(contours[0])
box = cv2.boxPoints(rect)
box = np.int0(box)
img_contour = cv2.drawContours(img, [box], 0, (0, 0, 255), 2)
# 最小外切圆
(x, y), radius = cv2.minEnclosingCircle(contours[0])
center = (int(x), int(y))
radius = int(radius)
img_contour = cv2.circle(img, center, radius, (255, 0, 0), 2)
图像的最小椭圆和拟合直线:可用于方向性的判断。
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
# 椭圆拟合
ellipse = cv2.fitEllipse(contours[0])
r'''
返回的ellipse是椭圆的[中心坐标,短轴长轴(也就是2b,2a),旋转角度]
'''
cv2.ellipse(img, ellipse, (255, 0, 0), 2)
r'''
cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness, lineType, shift),可见这个方法参数是很多的,在此说明一下:
====================================================================================
img:需要绘图的图像
---------------------------------------------------------------
center:椭圆中心点坐标,int
axes:椭圆尺寸(即长短轴),int
angle:旋转角度(顺时针方向),可以为小数
---------------------[ellipse参数包含了上面三个参数]------------
startAngle:绘制的起始角度(顺时针方向)
endAngle:绘制的终止角度(如绘制整个椭圆是0,360;绘制下半椭圆就是0,180)
color:线条颜色(BGR)
thickness:线条粗细(默认值=1)
lineType:线条类型(默认值=8),本案例没设置
shift:圆心坐标点和数轴的精度(默认值=0),本案例没设置
====================================================================================
但是本文案例没有写这么多参数,经过本人测试,cv2.ellipse(img, ellipse, (255, 0, 0), 2)
自动将ellipse解包成了center, axes, angle参数并且没有报数据类型错误[全是float]
还将axes参数自动/2了,起始终止也默认设为0和360。可以说是简化了参数吧。
'''
# 直线拟合
h, w, _ = img.shape
[vx, vy, x, y] = cv2.fitLine(contours[0], cv2.DIST_L2, 0, 0.01, 0.01)
r'''
points:一组轮廓
distType:距离类型==>
-----------------------------------------------------------------------
cv2.DIST_USER: User defined distance
cv2.DIST_L1: distance = |x1-x2| + |y1-y2|
cv2.DIST_L2: 欧式距离,此时与最小二乘法相同
cv2.DIST_C: distance = max(|x1-x2|,|y1-y2|)
cv2.DIST_L12: L1-L2 metric: distance = 2(sqrt(1+x*x/2) - 1))
cv2.DIST_FAIR: distance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998
cv2.DIST_WELSCH: distance = c2/2(1-exp(-(x/c)2)), c = 2.9846
cv2.DIST_HUBER: distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345
-----------------------------------------------------------------------
param:距离参数,跟所选的距离类型有关,值可以设置为0。
reps、aeps:用于表示拟合直线所需要的径向和角度精度,通常情况下两个值均被设定为0.01
输出值:
对于二维直线,输出output为4维,前两维代表拟合出的直线的方向,后两位代表直线上的一点。
[即通常说的点斜式直线]
'''
lefty = int((-x * vy / vx) + y)
righty = int(((w - x) * vy / vx) + y)
cv2.line(img, (w - 1, righty), (0, lefty), (0, 0, 255), 2)
最后来说一些轮廓的重要性质:
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
cnt = contours[0]
注意:下面 cnt 都是从上面两步得来的某一个轮廓
-
边界矩形的宽高比
A s p e c t _ R a t i o = W i d t h H e i g h t Aspect\_Ratio=\frac{Width}{Height} Aspect_Ratio=HeightWidthx, y, w, h = cv2.boundingRect(cnt) aspect_ratio = float(w) / h -
轮廓面积与边界矩形面积的比
E x t e n t = O b j e c t _ A r e a B o u n d i n g _ R e c t a n g l e _ A r e a Extent=\frac{Object\_Area}{Bounding\_Rectangle\_Area} Extent=Bounding_Rectangle_AreaObject_Areaarea = cv2.contourArea(cnt) x, y, w, h = cv2.boundingRect(cnt) rect_area = w * h extent = float(area) / rect_area -
轮廓面积与凸包面积的比
S o l i d i t y = C o n t o u r _ A r e a C o n v e x _ H u l l _ A r e a Solidity=\frac{Contour\_Area}{Convex\_Hull\_Area} Solidity=Convex_Hull_AreaContour_Areaarea = cv2.contourArea(cnt) hull = cv2.convexHull(cnt) hull_area = cv2.contourArea(hull) extent = float(area) / hull_area -
与轮廓面积相等的圆的直径
E q u i v a l e n t _ D i a m e t e r = 4 × C o n t o u r _ A r e a π Equivalent\_Diameter=\sqrt{\frac{4×Contour\_Area}{π}} Equivalent_Diameter=π4×Contour_Areaarea = cv2.contourArea(cnt) equi_diameter = np.sqrt(4 * area / np.pi) -
图像的方向
返回椭圆长轴和短轴的长度
(x,y),(MA,ma),angle = cv2.fitEllipse(cnt) # 详见上文"图像的最小椭圆"
3-5:对象掩码
用于获取构成对象的所有像素点,即 3-1:轮廓查找和绘制 中最后参数取-1的情况
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
mask = np.zeros(imggray.shape, np.uint8) # 通过numpy生成一个和原图一样的、全为0的张量
cv2.drawContours(mask, [contours[0]], 0, 100, -1)
r'''
这里是把一组轮廓传入,并画在mask上,最后参数为-1表示是填充。
'''
pixelpoints = np.transpose(np.nonzero(mask))
# 最大值和最小值以及它们的位置
# 使用相同的掩码求一个对象的平均颜色或平均灰度 ???
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imggray, mask=mask)
3-6:形状匹配
函数 cv2.matchShape() 可以帮我们比较两个形状或轮廓的相似度。如果返回值越小,匹配越好。它是根据 Hu 矩来计算的。Hu 矩是归一化中心矩的线性组合,之所以这样做是为了能够获取代表图像的某个特征的矩函数,这些矩函数对某些变化如缩放,旋转,镜像映射具有不变形 。
img1 = cv2.imread('1.jpg', 0)
img2 = cv2.imread('2.jpg', 0)
ret, thresh1 = cv2.threshold(img1, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh1, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
cnt1 = contours[0] # 得到图1的一条轮廓
ret, thresh2 = cv2.threshold(img2, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
cnt2 = contours[0] # 得到图2的一条轮廓
ret = cv2.matchShapes(cnt1, cnt2, 1, 0.0) # 进行匹配
print(ret) # 输出匹配度[0表示完全相同,1表示完全不同]
r'''
前两个参数分别是两个图的轮廓,第三个参数是匹配模式:
-----------------------------------------------------------------------------------
CONTOURS_MATCH_I1 => 模式1
CONTOURS_MATCH_I2 => 模式2
CONTOURS_MATCH_I3 => 模式3
-----------------------------------------------------------------------------------
第四个参数是保留参数,目前无用,但是必须得写一个数:int
'''
9万+

被折叠的 条评论
为什么被折叠?



