day33和day34图像处理OpenCV
文章目录
- 一、图像预处理
- 12 图像梯度处理
- 12.3 Sobel算子
- 12.4 Laplacian算子
- 1.原理:
- 2.语法:
- 13 图像边缘检测思路
- 13.1 高斯滤波去噪点
- 13.2 计算图像的梯度与方向
- 13.3 非极大值抑制
- 13.4 双阈值筛选
- 13.5 Canny方法和使用
- 14 绘制图像轮廓
- 14.1 什么是轮廓
- 14.2 寻找轮廓
- 1. 语法
- 2. mode参数
- 3. method参数
- 14.3 绘制轮廓
- 15 凸包特征检测
- 15.1获取凸包
- 15.2 绘制凸包
- 16 图像轮廓特征查找
- 16.1 绘制外接矩形
- 16.2 最小外接矩形
- 16.3 最小外接圆
一、图像预处理
12 图像梯度处理
12.3 Sobel算子
-
语法:sobel_image = cv2.Sobel(src, ddepth, dx, dy, ksize)
-
src:这是输入图像,通常应该是一个灰度图像(单通道图像)
-
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
-
dx,dy:当dx=1,dy=0时,求x方向的一阶导数,即提取垂直边缘;当 dx=0,dy=1时,求y方向的一阶导数,即提取水平边缘。
-
ksize:Sobel算子的大小,可选择3、5、7,这里默认为3。
-
dx,dy如果同时为1,分别求出两个方向的梯度,最后求出总梯度:
G = G x 2 + G y 2 G={\sqrt{G x^{2}+G y^{2}}} G=Gx2+Gy2
这个计算方式和上面类似。
12.4 Laplacian算子
-
该方法直接同时提取垂直和水平边缘,效果比Sobel算子好很多。
-
Laplacian算子是二阶边缘检测的典型代表,一/二阶边缘检测各有优缺点,可自行了解。
1.原理:
要看原函数的变化率,就看一阶导数。要找边缘点,就找的一阶导数的突变点,所以用一阶导数求极值,在这些极值的地方,二阶导数为0。所以也可以通过求二阶导计算梯度:
d s t = ∂ 2 f ∂ x 2 + ∂ 2 f ∂ y 2 d s t={\frac{\partial^{2}f}{\partial x^{2}}}+{\frac{\partial^{2}f}{\partial y^{2}}} dst=∂x2∂2f+∂y2∂2f
一维的一阶和二阶差分公式分别为:
∂ f ∂ x = f ( x + 1 ) − f ( x ) {\frac{\partial f}{\partial x}}=f(x+1)-f(x) ∂x∂f=f(x+1)−f(x)
∂ 2 f ∂ x 2 = f ( x + 1 ) + f ( x − 1 ) − 2 f ( x ) {\frac{\partial^{2}f}{\partial x^{2}}}=f(x+1)+f(x-1)-2f(x) ∂x2∂2f=f(x+1)+f(x−1)−2f(x)
提取前面的系数,那么一维的Laplacian滤波核是:
k = [ 1 − 2 1 ] k=[1~~-2~~~1] k=[1 −2 1]
而对于二维函数f(x,y),两个方向的二阶差分分别是:
∂ 2 f ∂ x 2 = f ( x + 1 , y ) + f ( x − 1 , y ) − 2 f ( x , y ) {\frac{\partial^{2}f}{\partial x^{2}}}=f(x+1,y)+f(x-1,y)-2f(x,y) ∂x2∂2f=f(x+1,y)+f(x−1,y)−2f(x,y)
∂ 2 f ∂ y 2 = f ( x , y + 1 ) + f ( x , y − 1 ) − 2 f ( x , y ) {\frac{\partial^{2}f}{\partial y^{2}}}=f(x,y+1)+f(x,y-1)-2f(x,y) ∂y2∂2f=f(x,y+1)+f(x,y−1)−2f(x,y)
合在一起就是:
V 2 f ( x , y ) = f ( x + 1 , y ) + f ( x − 1 , y ) + f ( x , y + 1 ) + f ( x , y − 1 ) − 4 f ( x , y ) V^{2}f(x,y)=f(x+1,y)+f(x-1,y)+f(x,y+1)+f(x,y-1)-4f(x,y) V2f(x,y)=f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)−4f(x,y)
同样提取前面的系数,那么二维的Laplacian滤波核就是:
k = [ 0 1 0 1 − 4 1 0 1 0 ] k=\left[\begin{array}{c c c}{0}&{1}&{0}\\ {1}&{-4}&{1}\\ {0}&{1}&{0}\end{array}\right] k= 0101−41010
这就是Laplacian算子的图像卷积模板,有些资料中在此基础上考虑斜对角情况,将卷积核拓展为:
k = [ 1 1 1 1 − 8 1 1 1 1 ] k=\left[\begin{array}{c c c}{1}&{1}&{1}\\ {1}&{-8}&{1}\\ {1}&{1}&{1}\end{array}\right] k= 1111−81111
2.语法:
-
cv2.Laplacian(src, ddepth)
-
返回一个图像对象
-
src:这是输入图像
-
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
-
13 图像边缘检测思路
不单单是算子,是完整的一整套方案。
13.1 高斯滤波去噪点
边缘检测本身属于锐化操作,对噪点比较敏感,所以需要进行平滑处理。
13.2 计算图像的梯度与方向
原理:
这里使用了sobel算子来计算图像的梯度值,在上一章节中,我们了解到sobel算子其实就是一个核值固定的卷积核,如下所示:
s o b e l ( 水平方向 ) = [ − 1 0 1 − 2 0 2 − 1 0 1 ] sobel(水平方向)=\left[\begin{array}{c c c}{{-1}}&{{0}}&{{1}}\\ {{-2}}&{{0}}&{{2}}\\ {{-1}}&{{0}}&{{1}}\end{array}\right] sobel(水平方向)= −1−2−1000121
s o b e l ( 垂直方向 ) = [ − 1 − 2 − 1 0 0 0 1 2 1 ] sobel(垂直方向)=\left[\begin{array}{c c c}{{-1}}&{{-2}}&{{-1}}\\ {{0}}&{{0}}&{{0}}\\ {{1}}&{{2}}&{{1}}\end{array}\right] sobel(垂直方向)= −101−202−101
首先使用sobel算子计算中心像素点的两个方向上的梯度 G x G_{x} Gx和 G y G_{y} Gy,然后就能够得到其具体的梯度值:
G = G x 2 + G y 2 G={\sqrt{G_{x}{}^{2}+G_{y}{}^{2}}} G=Gx2+Gy2
也可以使用 G = ∣ G x + G y ∣ G=|G_{x}+G_{y}| G=∣Gx+Gy∣来代替。在OpenCV中,默认使用 G = ∣ G x + G y ∣ G=|G_{x}+G_{y}| G=∣Gx+Gy∣来计算梯度值。
然后我们根据如下公式可以得到一个角度值
G y G x = tan ( θ ) {\frac{G_{\mathrm{y}}}{G_{x}}}=\tan\,(\theta) GxGy=tan(θ)
θ = arctan ( G y G x ) \theta=\arctan\,({\frac{G_{\mathrm{y}}}{G_{x}}}) θ=arctan(GxGy)
这个角度值其实是当前边缘的梯度的方向。通过这个公式我们就可以计算出图片中所有的像素点的梯度值与梯度方向,然后根据梯度方向获取边缘的方向。
a). 并且如果梯度方向不是0°、45°、90°、135°这种特定角度,那么就要用到插值算法来计算当前像素点在其方向上进行插值的结果了,然后进行比较并判断是否保留该像素点。这里使用的是单线性插值,通过A1和A2两个像素点获得dTmp1与dTmp2处的插值,然后与中心点C进行比较(非极大值抑制)。具体的插值算法请参考图像旋转实验。
b). 得到 θ \theta θ的值之后,就可以对边缘方向进行分类,为了简化计算过程,一般将其归为四个方向:水平方向、垂直方向、45°方向、135°方向。并且:
当 θ \theta θ值为-22.5°~22.5°,或-157.5°~157.5°,则认为边缘为水平边缘;
当法线方向为22.5°~67.5°,或-112.5°~-157.5°,则认为边缘为45°边缘;
当法线方向为67.5°~112.5°,或-67.5°~-112.5°,则认为边缘为垂直边缘;
当法线方向为112.5°~157.5°,或-22.5°~-67.5°,则认为边缘为135°边缘;
13.3 非极大值抑制
在该步骤中,我们需要检查每个像素点的梯度方向上的相邻像素,并保留梯度值最大的像素,将其他像素抑制为零。假设当前像素点为(x,y),其梯度方向是0°,梯度值为G(x,y),那么我们就需要比较G(x,y)与两个相邻像素的梯度值:G(x-1,y)和G(x+1,y)。如果G(x,y)是三个值里面最大的,就保留该像素值,否则将其抑制为零。
13.4 双阈值筛选
经过非极大值抑制之后,我们还需要设置阈值来进行筛选,当阈值设的太低,就会出现假边缘,而阈值设的太高,一些较弱的边缘就会被丢掉,因此使用了双阈值来进行筛选,推荐高低阈值的比例为2:1到3:1之间,其原理如下图所示:
当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;当幅值低于最低像素时,该像素必不是边缘像素;幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘。也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。
13.5 Canny方法和使用
-
方法内部自动实现:自适应二值化、高斯滤波、梯度计算等
- 只是效果不如自己调用
-
语法:
- edges = cv2.Canny(image, threshold1, threshold2),即使读到的是彩色图也可以进行处理。
-
参数:
-
image
:输入的灰度/二值化图像数据。 -
threshold1
:低阈值,用于决定可能的边缘点。 -
threshold2
:高阈值,用于决定强边缘点。
-
-
案例
import cv2 as cv
img = cv.imread('./images/shudu.png')
# 转为灰度图
img = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 二值化处理--手动处理
ret, img_binary = cv.threshold(img,127,255,cv.THRESH_BINARY)
# 高斯滤波----手动降噪
img_binary2 = cv.GaussianBlur(img_binary,(3,3),0)# canny方法
dst_img = cv.Canny(img,100,167)
# 显示
cv.imshow('dst_img',dst_img)
cv.imshow('img_binary2',img_binary2)
cv.waitKey(0)
cv.destroyAllWindows()
14 绘制图像轮廓
14.1 什么是轮廓
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续,如下图所示。轮廓是一个闭合的、封闭的形状。
- 轮廓的作用:
- 形状分析
- 目标识别
- 图像分割
14.2 寻找轮廓
在OpenCV中,使用cv2.findContours()来进行寻找轮廓,其原理过于复杂,这里只进行一个简单的介绍,具体的实现原理可参考:
https://zhuanlan.zhihu.com/p/107257870
- 寻找轮廓我们需要对原始的图像进行灰度化、二值化的处理,令目标区域显示为白色,其他区域显示为黑色。
1. 语法
-
contours,hierarchy = cv2.findContours(image,mode,method)
-
返回值:[ 轮廓点坐标 ] 和 [ 层级关系 ]。
-
contours
:表示获取到的轮廓点的列表。检测到有多少个轮廓,该列表就有多少子列表,每一个子列表都代表了一个轮廓中所有点的坐标。 -
hierarchy
:表示轮廓之间的关系。对于第i条轮廓, h i e r a r c h y [ i ] [ 0 ] hierarchy[i][0] hierarchy[i][0], h i e r a r c h y [ i ] [ 1 ] hierarchy[i][1] hierarchy[i][1] , h i e r a r c h y [ i ] [ 2 ] hierarchy[i][2] hierarchy[i][2] ,$ hierarchy[i][3]$分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有相应的轮廓,则对应位置为-1)。该参数的使用情况会比较少。
-
-
参数:
-
image
:表示输入的二值化图像。 -
mode
:表示轮廓的检索模式。 -
method
:轮廓的表示方法。
-
2. mode参数
轮廓查找方式。返回不同的层级关系。
mode参数共有四个选项分别为:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。
- RETR_EXTERNAL
表示只查找最外层的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
- RETR_LIST
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
- RETR_CCOMP
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照成对的方式显示。
在 RETR_CCOMP
模式下,轮廓被分为两个层级:
- 层级 0:所有外部轮廓(最外层的边界)。
- 层级 1:所有内部轮廓(孔洞或嵌套的区域)。
- RETR_TREE
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。
3. method参数
轮廓存储方法。轮廓近似方法。决定如何简化轮廓点的数量。就是找到轮廓后怎么去存储这些点。
method参数有三个选项:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。
-
CHAIN_APPROX_NONE
表示将所有的轮廓点都进行存储 -
CHAIN_APPROX_SIMPLE
表示只存储有用的点,比如直线只存储起点和终点,四边形只存储四个顶点,默认使用这个方法;
对于mode和method这两个参数来说,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE这两个选项。
14.3 绘制轮廓
轮廓找出来后,其实返回的是一个轮廓点坐标的列表,因此我们需要根据这些坐标将轮廓画出来,因此就用到了绘制轮廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
-
image:原始图像,一般为单通道或三通道的 numpy 数组。
-
contours:包含多个轮廓的列表,每个轮廓本身也是一个由点坐标构成的二维数组(numpy数组)。
-
contourIdx:要绘制的轮廓索引。如果设为
-1
,则会绘制所有轮廓。根据索引找到轮廓点绘制出来。默认是-1。 -
color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。
-
thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域。
-
示例:
# ================寻找轮廓=============================
# 读取、转灰度图
img = cv.imread('./images/num.png')
num = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 二值化 === 要使用反阈值法,把内容变成 黑底白字
_, binary = cv.threshold(num,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 查找轮廓
contours, hierarchy = cv.findContours(binary,mode=cv.RETR_EXTERNAL,method=cv.CHAIN_APPROX_SIMPLE)
# 绘制轮廓
cv.drawContours(img, contours, -1, (0,255,0), 3)
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()
15 凸包特征检测
通俗的讲,凸包其实就是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的内容。
凸包检测常用在物体识别、手势识别、边界检测等领域。
凸包查找的常用方法:穷举法和QuickHull法
15.1获取凸包
- pts = cv.convexHull( )
- 返回一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。
15.2 绘制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)
-
image
:要绘制线条的目标图像,它应该是一个OpenCV格式的二维图像数组(如numpy数组)。 -
pts
:一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。 -
isClosed
:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。 -
color
:线条颜色,可以是一个三元组或四元组,分别对应BGR或BGRA通道的颜色值,或者是灰度图像的一个整数值。 -
thickness
(可选):线条宽度,默认值为1。 -
案例
img = cv.imread('./images/tu.png')
# 转为灰度图
num = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 二值化
_, binary = cv.threshold(num, 127, 255, cv.THRESH_OTSU + cv.THRESH_BINARY)
# 查找轮廓
c, h = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
# 查找凸包
pts = cv.convexHull(c[0])# 绘制凸包点
cv.polylines(img,[pts],True,(0,255,0),3,cv.LINE_AA)
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()
16 图像轮廓特征查找
- 图像轮廓特征查找其实就是他的外接轮廓。
- 应用:
- 图像分割
- 形状分析
- 物体检测与识别
- 根据轮廓点进行,所以要先找到轮廓。
- 先灰度化、二值化。目标物体白色,非目标物体黑色,选择合适的二值化方式。
16.1 绘制外接矩形
通过轮廓点就可以找到最上、最下、最左、最右的四个坐标, X m i n 、 X m a x 、 Y m i n 、 Y m a x X_{min}、X_{max}、Y_{min}、Y_{max} Xmin、Xmax、Ymin、Ymax。就可以绘制出矩形。
- 使用前面绘制矩形的方法:cv.rectangle(img,左上点, 右下点, 颜色,大小)
- 使用 x,y,w,h =
cv.boundingRect( 存放轮廓点的数组 )
找到左上和右下点
- 使用 x,y,w,h =
- 示例:
num = cv.imread("./images/num.png")
img = num.copy()
# 转灰度
img2 = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 二值化
_,new_img = cv.threshold(img2,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 查找轮廓
c,h=cv.findContours(new_img,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
# 绘制轮廓
cv.drawContours(img,c,-1,(0,255,0),3)
# 绘制外接矩形
for i in c:# 寻找每一个左上和右下坐标x,y,w,h = cv.boundingRect(i)# 绘制矩形cv.rectangle(img,(x,y),(x+w,y+h),(25,205,125),3)cv.imshow("img",img)
cv.waitKey(0)
cv.destroyAllWindows()
16.2 最小外接矩形
寻找最小外接矩形使用的算法叫做旋转卡壳法。下面简单说明一下旋转卡壳法的思路:
-
在上一章节中,我们了解到了凸包的概念,而旋转卡壳法就是基于凸包点进行的,旋转卡壳法有一个很重要的前提条件:绘制的外接矩形有一条边与凸包的一条边共线
-
以共线边为外接矩形的边,找出外接矩形,计算面积;这样计算出不同共边得到的面积,找出最小面积。
-
关键 三步 语法:
- rect = cv2.minAreaRect(cnt)
cnt
参数:一维的轮廓坐标,需要遍历找到的二维轮廓数组传入- 返回值:rect 结构通常包含中心点坐标
(x, y)
、宽度width
、高度height
和旋转角度angle
- box = cv2.boxPoints(rect).astype(np.int32)
- astype(int)方法:保证返回的数据是整数并且防止溢出
- rect参数:上面
- 返回值:一个形状为 4行2列的数组,每一行代表一个点的坐标(x, y),顺序按照逆时针或顺时针方向排列
- cv2.drawContours(image, contours, contourIdx, color, thickness)
- 返回的box也是轮廓点坐标,所以使用画凸包的方式画出矩形
- rect = cv2.minAreaRect(cnt)
-
示例
num = cv.imread("./images/num.png")
img = num.copy()
# 转灰度
img2 = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 二值化
_, new_img = cv.threshold(img2, 127, 255, cv.THRESH_OTSU + cv.THRESH_BINARY_INV)
# 查找轮廓
c, h = cv.findContours(new_img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)# 绘制最小外接矩形
for i in c: # 关键 3步rect = cv.minAreaRect(i)box = cv.boxPoints(rect).astype(np.int32)cv.drawContours(img,[box],0,(0,0,255),2,cv.LINE_AA)cv.imshow("img", img)
cv.waitKey(0)
cv.destroyAllWindows()
16.3 最小外接圆
- 语法:
- **center, radius = cv2.minEnclosingCircle(points) **
- 参数
points
:输入参数图片轮廓数据
- 返回值:
center
:圆心坐标的二元组(x, y)
。radius
:半径- 圆心和半径都不是整数,需要转化np.int_( [x,y,r] )
- cv2.circle(img, center, radius, color, thickness)
- 通过前面的画圆方法,画圆
- 示例:
img = cv.imread('./images/num.png')
# 灰度化
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 二值化
_, binary = cv.threshold(gray, 127, 255, cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 查找轮廓
c, h = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# 遍历轮廓
for i in c:# 获取最小外接圆(x, y), r = cv.minEnclosingCircle(i)# 数据取整x,y,r = np.int_([x,y,r])# 画圆cv.circle(img,(x,y),r,(0,255,0),3,cv.LINE_AA)cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()