本篇学习笔记主要内容在图像的Canny 边缘检测、图像金字塔、轮廓检测内容。
获取更多可以查看本栏目其他文章。
往期内容:
OpenCV-Python图像处理学习笔记(一)——认识、安装、环境测试
OpenCV-Python图像处理学习笔记(二)——图像/视频读取保存、分割及边界填充
OpenCV-Python图像处理学习笔记(三)——数值运算、图像阈值、图像平滑(滤波)
OpenCV-Python图像处理学习笔记(四)——形态学操作、图像梯度
OpenCV-Python图像处理学习笔记(五)——Canny 边缘检测、图像金字塔、轮廓检测(一)OpenCV-Python图像处理学习笔记(六)——轮廓检测(二)、模板匹配
OpenCV-Python图像处理学习笔记(七)——直方图、图像变换
目录
导入必要Python包
import cv2
import numpy as np
import matplotlib.pyplot as plt
1 Canny 边缘检测
Canny 边缘检测是一种非常流行的边缘检测算法,是 John F.Canny 在 1986 年提出的,它是一个有很多步构成的算法。
边缘检测流程
- 使用高斯滤波器,平滑图像,去除噪声;
- 计算图像中每个像素点的梯度强度和方向;
- 应用非极大值抑制(Non-Maximum Suooression),消除边缘检测带来的杂散响应;
- 应用双阈值(Double-Threshold)检测来确定真实的和潜在的边缘;
- 通过一直孤立的弱边缘最终完成边缘检测。
1.1 梯度强度和方向
平滑操作在之前的博文中讲到过,这里不再重复。这里使用 Sobel 算子计算水平方向和竖直方向的一阶导数(图像梯度)(Gx 和 Gy)。根据得到的这两幅梯度图(Gx 和 Gy)找到边界的梯度和方向,公式如下:
梯度的方向一般总是与边界垂直。梯度方向被归为四类:垂直,水平,和 两个对角线。
1.2 非极大值抑制
在获得梯度的方向和大小之后,应该对整幅图像做一个扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。如下图所示:
通俗理解为比较当前点梯度值与周围点梯度值大小,保存最大值的点,抑制非最大的点。
1.3 双阈值检测
双阈值检测是确定那些边界才是真正的边界。需要设置两个阈值:
minVal 和 maxVal
这里需要判断图像的灰度梯度与 maxVal 和 minVal 的大小:
- 梯度值 > maxVal : 视为边界
- minVal < 梯度值 < maxVal : 是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃
- 梯度值 < minVal :舍弃
例如下图举例说明:
A 高于阈值 maxVal 所以是真正的边界点,C 虽然低于 maxVal 但高于 minVal 并且与 A 相连,所以也被认为是真正的边界点。而 B 就会被抛弃,因为他不仅低于 maxVal 而且不与真正的边界点相连。所以选择合适的 maxVal 和 minVal 对于能否得到好的结果非常重要。 在这一步一些小的噪声点也会被除去,因为已经假设边界都是一些长的线段。
1.4 边缘检测代码实现
需要用到 cv2.Canny() 函数来完成边缘检测。
img = cv2.imread('.jpg', 0)
edges = cv2.Canny(img, 100, 200)
plt.subplot(121)
plt.imshow(img, cmap = 'gray')
plt.title('Original Image')
plt.xticks([])
plt.yticks([])
plt.subplot(122)
plt.imshow(edges, cmap = 'gray')
plt.title('Edge Image')
plt.xticks([])
plt.yticks([])
plt.show()
其中 cv2.Canny() 函数涉及到的参数如下:
- 第一个参数是输入图像;
- 第二和第三个分别是 minVal 和 maxVal;
- 第三个参数用来计算图像梯度的 Sobel 卷积核的大小,默认值为 3;
- 第四个参数是 L2gradient,用来设定求梯度大小的方程。如果设为 True,就会使用我们上面提到过的方程,否则使用方程:代替,默认值为 False。
2 图像金字塔
一般情况下,我们要处理是一副具有固定分辨率的图像。但是有些情况下, 我们需要对同一图像的不同分辨率的子图像进行处理。比如,我们要在一幅图 像中查找某个目标,比如脸,我们不知道目标在图像中的尺寸大小。这种情况 下,我们需要创建创建一组图像,这些图像是具有不同分辨率的原始图像。我 们把这组图像叫做图像金字塔(简单来说就是同一图像的不同分辨率的子图集 合)。如果我们把最大的图像放在底部,最小的放在顶部,看起来像一座金字塔,故而得名图像金字塔。
有两类图像金字塔:高斯金字塔和拉普拉斯金字塔。
高斯金字塔的顶部是通过将底部图像中的连续的行和列去除得到的。顶部图像中的每个像素值等于下一层图像中 5 个像素的高斯加权平均值。这样操作一次一个M x N的图像就变成了一个M/2 x N/2 的图像。所以这幅图像的面积就变为原来图像面积的四分之一。
2.1 高斯金字塔
向下采样方法(缩小)
- 将与高斯内核卷积
- 将所有偶数行和列去除
向上采样方法(放大)
通过下列图像进行代码操作示例:
原图像大小为(442,340,3)
img = cv2.imread('.jpg')
# 上采样
up = cv2.pyrUp(img)
# 下采样
down = cv2.pyrDown(img)
分别进行上采样和下采样原图像大小分别变为如下:
(884,680,3)和(221,170,3)
使用cv2.pyrDown(),图像的分辨率就会降低,信息就会被丢失。
2.2 拉普拉斯金字塔
拉普拉斯金字塔可以有高斯金字塔计算得来,公式如下:
拉普拉金字塔的图像看起来就像边界图,其中很多像素都是 0。经常被用在图像压缩中。
具体计算方式如下:
down = cv2.pyrDown(img)
down_up = cv2.pyrUp(down)
l_1 = img - down_up
cv_show(l_1, 'l_1')
3 图像轮廓检测
轮廓可以简单认为成将连续的点(连着边界)连在一起的曲线,具有相同的颜色或者灰度。
- 为了更加准确,要使用二值化图像。在寻找轮廓之前,要进行阈值化处理或者 Canny 边界检测。
- 查找轮廓的函数会修改原始图像。
- 在 OpenCV 中,查找轮廓就像在黑色背景中找白色物体。要找的物体应该是白色,而背景应该是黑色。
通过cv2.findContours()函数遭到图像轮廓,具体常用参数如下:
mode:轮廓检索模式
- RETR_EXTERNAL:只检索最外面的轮廓;
- RETR_LIST:检索所有的轮廓,并将其保存到一条链表当中;
- RETR_CCOMP:检索所有的轮廓,并将他们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界;
- RETR_TREE:检索所有的轮廓,并重构嵌套轮廓的整个层次;
method:轮廓近似方法
- CHAIN_APPROX_NONE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列);
- CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分;
通过cv2.drawContours()函数遭到图像轮廓,具体常用参数如下:
- 第一个参数:图像;
- 第二个参数:轮廓本身,在Python中是一个list;
- 第三个参数:指定绘制轮廓list中的哪条轮廓,如果是-1,则绘制其中的所有轮廓;
- 其他参数:包括线条粗细、颜色等。
更详细说明可参考:OpenCV: Modules
3.1 绘制图像轮廓
通过以下代码找到并绘制图像轮廓:
im = cv2.imread('test.jpg')
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
# 二值图像
ret, thresh = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
# 找到图像轮廓
image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
'''
image:二值结果图像
contours:图像轮廓点,list
hierarchy:轮廓结果层级信息
'''
# 尽量进行copy
img = imgray .copy()
# 绘制图像轮廓
# img = cv2.drawContour(img, contours, -1, (0, 255, 0), 3)
img = cv2.drawContours(img, contours, 3, (0, 255, 0), 3)
'''
(0, 255, 0):bgr颜色
3:线条宽度
'''
3.2 轮廓近似方法
轮廓是一个形状具有相同灰度值的边界,它会存储形状边界上所有的(x,y)坐标。实际上我们不需要所有的点,当需要直线时,找到两个端点即可。cv2.CHAIN_APPROX_SIMPLE可以实现。它会将轮廓上的冗余点去掉,压缩轮廓,从而节省内存开支。
下面第一个显示使用cv2.CHAIN_APPROX_NONE的效果,一共734个点,第二个图是使用cv2.CHAIN_APPROX_SIMPLE的结果,只有4个点。
3.3 轮廓特征
轮廓的不同特征,例如面积,周长,重心,边界框等。
3.3.1 矩
图像的矩可以帮助我们计算图像的质心,面积等。
函数 cv2.moments() 会将计算得到的矩以一个字典的形式返回:
img = cv2.imread('.jpg', 0)
ret, thresh = cv2.threshold(img, 127, 255, 0)
image, contours, hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
M = cv2.moments(cnt)
print(M)
根据这些矩值,可以计算出图像的重心:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
3.3.2 轮廓面积
可以使用函数cv2.contourArea()计算得到,也可以用矩(0阶矩),M['m00']。
area = cv2.contourArea(cnt)
3.3.3 轮廓周长
也被称为弧长。可以使用函数cv2.arcLength()计算得到。这个函数的第二参数可以用来指定对象的形状是闭合的(True),还是打开的(一条曲线)。
perimeter = cv2.arcLength(cnt, True)
3.3.4 轮廓近似
将轮廓形状近似到另外一种由更少点组成的轮廓形状,新轮廓的点的数目 由我们设定的准确度来决定,使用的Douglas-Peucker算法。
如下图所示,现在可以使用这个函数来近似这个形状。cv2.approxPolyDP()函数的第二个参数叫 epsilon,它是从原始轮廓到近似轮廓的最大距离。它是一个准确度参数。
epsilon = 0.1 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
下边,第二幅图中的绿线是当 epsilon = 10% 时得到的近似轮廓,第三幅 图是当 epsilon = 1% 时得到的近似轮廓。第三个参数设定弧线是否闭合。
3.3.5 凸包
函数 cv2.convexHull() 可以用来检测一个曲线是否具有凸性缺陷,并能纠正缺陷。一般来说,如果图像有地方凸出或凹陷就被叫做凸性缺陷。例如下图中。红色曲线显示了凸性缺陷被双箭头标出来了。
hull = cv2.convexHull(points, hull, clockwise, returnPoints)
'''
points:要传入的轮廓
hull:输出,通常不需要
clockwise:方向标志,如果设置为True,输出的凸包是顺时针方向的,否则为逆时针方向。
returnPoints:默认值为True。它会返回凸包上点的坐标,如果设置为False,就会返回与凸包点对应的轮廓上的点。
'''
# 寻找凸包,得到凸包的角点
img = cv2.imread('.jpg', 0)
ret, thresh = cv2.threshold(img, 127, 255, 0)
image, contours, hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
hull = cv2.convexHull(cnt)
# 绘制凸包
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
cv2.polylines(image, [hull], True, (0, 255, 0), 2)
3.3.6 凸性检测
函数cv2.isContourConvex()可以检测一个曲线是不是凸的。它只能返回True或者False。
k = cv2.isContourConvex(cnt)
3.3.7 边界矩形
有两类边界矩形:直边界矩形和旋转的边界矩形
直边界矩形是一个直矩形(就是没有旋转的矩形)。它不会考虑对象是否旋转。 所以边界矩形的面积不是最小的。可以使用函数 cv2.boundingRect() 得到。
(x,y)为矩形左上角的坐标,(w,h)是矩形的宽和高。
x, y, w, h = cv2.boundingRect(cnt)
# 绘制矩形框
img = cv2.rectangle(img,
(x,y),
(x + w, y + h),
(0, 255, 0),
2)
'''
参数表示依次为:(图片,长方形框左上角坐标, 长方形框右下角坐标,字体颜色,字体粗细)
'''
旋转的边界矩形是面积最小的,因为它考虑了对象的旋转。用函数cv2.minAreaRect()。返回的是一个 Box2D 结构,其中包含矩形左上角角点的坐标(x,y),矩形的宽和高(w,h),以及旋转角度。但是要绘制这个矩形需要矩形的 4 个角点,可以通过函数 cv2.boxPoints() 获得。
# 得到最小外接矩形的(中心(x,y), (宽,高), 旋转角度)
rect = cv2.minAreaRect(contours[0])
# 获取最小外接矩形的4个顶点坐标
box = cv2.boxPoints(rect)
# 画出来
cv2.drawContours(img, [box], 0, (0, 0, 255), 5)
plt.imshow(img)
plt.show()
下图其中绿色的为直矩形,红的为旋转矩形。
3.3.8 最小外接圆
函数 cv2.minEnclosingCircle() 可以找到一个对象的外切圆。 它是所有能够包括对象的圆中面积最小的一个。
(x, y), radius = cv2.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
# 绘制圆形
img = cv2.circle(img, center, radius, (0, 255, 0), 2)
'''
img:输入的图片data
center:圆心位置
radius:圆的半径
color:圆的颜色
thickness:圆形轮廓的粗细(如果为正)。负厚度表示要绘制实心圆。
lineType: 圆边界的类型。
shift:中心坐标和半径值中的小数位数。
'''
得到下列结果:
3.3.9 椭圆拟合
使用的函数为 cv2.ellipse(),返回值是旋转边界矩形的内切圆。
# 提取轮廓椭圆拟合后的椭圆参数
ellipse = cv2.fitEllipse(cnt)
'''
返回值:ellipse = [ (x, y) , (a, b), angle ]
(x, y):代表椭圆中心点的位置
(a, b):代表长短轴长度,应注意a、b为长短轴的直径,而非半径
angle:代表了中心旋转的角度
'''
# 绘制拟合的椭圆
im = cv2.ellipse(im, ellipse, (0, 255, 0), 2)
'''
im:原始图像
ellipse:拟合的椭圆信息
(0, 255, 0):颜色,服从BGR的规律
2:所绘制轮廓的宽度
'''
得到如下结果:
3.3.10 直线拟合
可以根据一组点拟合出一条直线,同样我们也可以为图像中的白色点拟合出一条直线。
# 取彩色图片的长、宽
rows, cols = img.shape[:2]
# 拟合直线
[vx, vy, x, y] = cv2.fitLine(cnt, cv2.DIST_L2, 0, 0.01, 0.01)
'''
cnt: 待拟合的直线的集合,必须是矩阵形式;这里是选定的轮廓
cv2.DIST_L2: 距离类型。fitline为距离最小化函数,拟合直线时,要使输入点到拟合直线的距离和最小化。类型有以下几种:
cv2.DIST_USER: 自定义
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:第5/6个参数用于表示拟合直线所需要的径向和角度精度,通常情况下两个值均被设定为1e-2.
返回值:对于二维直线,输出为4维,前两维代表拟合出的直线的方向,后两位代表直线上的一点。
'''
lefty = int((x * vy / vx) + y)
righty = int(((cols - x) * vy / vx) + y)
img = cv2.line(img, (cols-1, righty), (0, lefty), (0, 255, 0), 2)
'''
img: 背景图
(cols-1, righty): 直线起点坐标
(0, lefty): 直线终点坐标
(0, 255, 0): 当前绘画的颜色。如在BGR模式下,传递(255,0,0)表示蓝色画笔。灰度图下,只需要传递亮度值即可。
2: 画笔的粗细,线宽。若是-1表示画封闭图像,如填充的圆。默认值是1.
lineType: 线条的类型,如8-connected类型、anti-aliased线条(反锯齿),默认情况下是8-connected样式ide,cv2.LINE_AA表示反锯齿线条,在曲线的时候视觉效果更佳。
'''
得到如下结果:
4 小结
下期将继续同步OpenCV轮廓性质部分内容以及模板匹配等部分内容。