【OpenCV(02)】图像颜色处理,灰度化,二值化,仿射变换
【OpenCV(03)】插值方法,边缘填充,透视变换,水印制作,噪点消除
目录
图形化理解

对图像灰度化出来后,Z=f(X,Y) 即灰度值,在灰度值曲面上任取一点为例:
1.黄线:梯度方向,指向灰度值变化最大的方向,由水平梯度方向和垂直梯度方向共同决定,两个矢量共同组成一个矢量,想象一个石子在凹凸不平的地面上总有两个力牵引它往更低的地方
2.白线:边缘方向,指向灰度值变化最小的方向,边缘两边的梯度过大,以0填充像素灰度值,表示边缘线的走向
3.红线:f(x) ,水平方向上的灰度值变化函数,求导得Gx,即水平梯度
4.蓝线,f(y) ,垂直方向是的灰度值变化函数,求导得Gy,即垂直梯度
梯度方向和边缘方向总是垂直的
图像梯度处理
图像梯度
import cv2 as cv
import numpy as np
# 模拟一张图像,灰度图
img=np.array([
[100,0,109,110,98,20,19,18,21,22],
[109,0,98,108,102,20,21,19,20,21],
[109,0,105,108,98,20,22,19,19,18],
[109,0,102,108,102,20,23,19,20,22],
[109,0,105,108,98,20,22,19,20,18],
[100,0,108,110,98,20,19,18,21,22],
[109,0,98,108,102,20,22,19,20,21],
[109,0,108,108,98,20,22,19,19,18],
],dtype=np.uint8)
# 定义卷积核,
kernel=np.array([[-1,0,1],
[-2,0,2],
[-1,0,1]],dtype=np.float32)
# 二维卷积操作
img2=cv.filter2D(img,-1,kernel)
# 打印卷积后的图
print(img2)
输出:
[[ 0 0 255 0 0 0 0 2 12 0]
[ 0 0 255 0 0 0 0 0 7 0]
[ 0 0 255 0 0 0 0 0 3 0]
[ 0 0 255 0 0 0 0 0 4 0]
[ 0 0 255 0 0 0 0 0 5 0]
[ 0 1 255 0 0 0 0 0 9 0]
[ 0 0 255 0 0 0 0 0 7 0]
[ 0 0 255 0 0 0 0 0 2 0]]
图像边缘
cv.filter2D(src, ddepth, kernel)
filter2D函数是用于对图像进行二维卷积(滤波)操作。它允许自定义卷积核(kernel)来实现各种图像处理效果,如平滑、锐化、边缘检测等
-
src
: 输入图像,一般为numpy
数组。 -
ddepth
: 输出图像的深度,可以是负值(表示与原图相同)、正值或其他特定值(常用-1 表示输出与输入具有相同的深度)。 -
kernel
: 卷积核,一个二维数组(通常为奇数大小的方形矩阵),用于计算每个像素周围邻域的加权和。import cv2 as cv
import numpy as np#读图
shu = cv.imread('E:\hqyj\code\opencv\images\shudu.png')
#定义卷积核
#垂直边缘提取
kernel=np.array([[-1,0,1],
[-2,0,2],
[-1,0,1]],dtype=np.float32)
#水平边缘提取 kernel1.T
dst2 = cv.filter2D(shu,-1,kernel.T)
#卷积
dst1 = cv.filter2D(shu,-1,kernel)
dst3 = dst1 + dst2
dst4 = cv.add(dst1,dst2)
cv.imshow('dst4',dst4) #饱和运算
cv.imshow('dst3',dst3) #加法,溢出失真
cv.imshow('shu',shu)
cv.imshow('dst1',dst1)
cv.imshow('dst2',dst2)
cv.waitKey(0)
cv.destroyAllWindows()
Sobel算子
sobel_image = cv2.Sobel(src, ddepth, dx, dy, ksize)
-
src:这是输入图像,通常应该是一个灰度图像(单通道图像),因为 Sobel 算子是基于像素亮度梯度计算的。在彩色图像的情况下,通常需要先将其转换为灰度图像。
-
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
-
dx,dy:当组合为dx=1,dy=0时求x方向的一阶导数,在这里,设置为1意味着我们想要计算图像在水平方向(x轴)的梯度。当组合为 dx=0,dy=1时求y方向的一阶导数(如果同时为1,通常得不到想要的结果,想两个方向都处理的比较好 学习使用后面的算子)
-
ksize:Sobel算子的大小,可选择3、5、7,默认为3。
import cv2 as cv
import numpy as np#读图
shu = cv.imread('E:\hqyj\code\opencv\images\shudu.png',cv.IMREAD_GRAYSCALE)
#Sobel算子
#dx=1,dy=0 求x方向梯度
dst1 = cv.Sobel(shu,-1,1,0,ksize = 3)
#dy=1,dx=0 求y方向梯度
dst2 = cv.Sobel(shu,-1,0,1,ksize = 3)
cv.imshow('shu',shu)
cv.imshow('dst',dst1)
cv.imshow('dst2',dst2)
cv.waitKey(0)
cv.destroyAllWindows()
上面的两个卷积核都叫做Sobel算子,只是方向不同,它先在垂直方向计算梯度:
公式
G x = k 1 × s r c G_{x}=k_{1}\times s r c Gx=k1×src
再在水平方向计算梯度:
G y = k 2 × s r c G_{y}=k_{2}\times s r c Gy=k2×src
最后求出总梯度:
G = G x 2 + G y 2 G={\sqrt{G x^{2}+G y^{2}}} G=Gx2+Gy2
Laplacian算子
cv2.Laplacian(src, ddepth)
-
src:这是输入图像
-
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
Laplacian算子是二阶边缘检测的典型代表,一/二阶边缘检测各有优缺点,大家可自行了解。
import cv2 as cv
import numpy as np
#读图
shu = cv.imread('E:\hqyj\code\opencv\images\\shudu.png',cv.IMREAD_GRAYSCALE)
#Laplacian算子
dst = cv.Laplacian(shu,-1,ksize = 3)
cv.imshow('shudu',shu)
cv.imshow('Laplacian',dst)
cv.waitKey(0)
cv.destroyAllWindows
高数中用一阶导数求极值,在这些极值的地方,二阶导数为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
图像边缘检测
import cv2 as cv
#读图
shu = cv.imread('E:\hqyj\code\opencv\images\\shudu.png',cv.IMREAD_GRAYSCALE)
#二值化处理
_,binary = cv.threshold(shu,127,255,cv.THRESH_BINARY)
#使用canny边缘检测
dst = cv.Canny(binary,30,70)
#显示效果
cv.imshow('shudu',dst)
cv.waitKey(0)
cv.destroyAllWindows()
首先使用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)
这个角度值其实是当前边缘的梯度的方向 。通过这个公式我们就可以计算出图片中所有的像素点的梯度值与梯度方向,然后根据梯度方向获取边缘的方向。
梯度方向(gradient direction)表示图像灰度变化最大的方向,而边缘方向(edge direction)则与梯度方向垂直,因为边缘是沿着灰度变化最小的方向(即沿着边缘的方向)。
双阈值筛选
经过非极大值抑制之后,我们还需要设置阈值来进行筛选,当阈值设的太低,就会出现假边缘,而阈值设的太高,一些较弱的边缘就会被丢掉,因此使用了双阈值来进行筛选,推荐高低阈值的比例为2:1到3:1之间,其原理如下图所示:
当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;当幅值低于最低像素时,该像素必不是边缘像素;幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘。也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。
图像轮廓查找
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:轮廓的表示方法。
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里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。
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这两个选项。import cv2 as cv
#读图
img = cv.imread('E:\hqyj\code\opencv\images\num.png')
#灰度化
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
#二值化
_,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY_INV)
#查找轮廓
contours,hierarchy = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
print('轮廓数量:',len(contours))
print('=' * 50)
print('层级关系:\n',hierarchy)
#绘制轮廓
cv.drawContours(img,contours,-1,(255,0,0),2)
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()
凸包特征检测
穷举法
1. 枚举所有点对:从给定的点集中选择所有可能的点对。
2. 检查点对:对于每一对点,检查其余的点是否都在这两点形成的直线的同侧。
3. 确定凸包点:如果所有其他点都在直线的同侧,则这对点是凸包上的点。
QuickHull法
1. 找到最远的点:从点集中找到距离初始直线最远的点。
2. 创建三角形:将找到的点与初始直线的两个端点连接,形成两个新的三角形。
3. 递归构建:对每个三角形的两条新边,重复上述过程,直到所有点都被包含在凸包内。
import cv2 as cv
#读图
tu = cv.imread('E:\hqyj\code\opencv\images\\tu.png')
#灰度化
grey = cv.cvtColor(tu,cv.COLOR_BGR2GRAY)
#二值化
_,binary = cv.threshold(grey,127,255,cv.THRESH_BINARY)
#查找轮廓
conts,th = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
#获取凸包点
hull = [cv.convexHull(c) for c in conts]
cv.polylines(tu,hull,-1,(0,255,255),2)
#显示
cv.imshow('tu',tu)
cv.waitKey(0)
cv.destroyAllWindows()
获取凸包点
cv2.convexHull(points)
points
:输入参数,图像的轮廓
绘制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)
image
:要绘制线条的目标图像,它应该是一个OpenCV格式的二维图像数组(如numpy数组)。pts
:一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。isClosed
:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。color
:线条颜色,可以是一个三元组或四元组,分别对应BGR或BGRA通道的颜色值,或者是灰度图像的一个整数值。thickness
(可选):线条宽度,默认值为1。
图像轮廓特征查找
-
boundingRect(轮廓点)
-
contours, hierarchy = cv2.findContours(image_np_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours为二值图像上查找所有的外部轮廓 -
rect = cv2.minAreaRect(cnt)
传入的cnt参数为contours中的轮廓,可以遍历contours中的所有轮廓,然后计算出每个轮廓的小面积外接矩形 -
rect 是计算轮廓最小面积外接矩形:rect 结构通常包含中心点坐标
(x, y)
、宽度width
、高度height
和旋转角度angle
-
cv2.boxPoints(rect).astype(int)
cv2.boxPoints(rect)返回 是一个形状为 4行2列的数组,每一行代表一个点的坐标(x, y),顺序按照逆时针或顺时针方向排列,将最小外接矩形转换为边界框的四个角点,并转换为整数坐标 -
cv2.drawContours(image, contours, contourIdx, color, thickness)
image
:原图像,一般为 numpy 数组,通常为灰度或彩色图像。
contours
:一个包含多个轮廓的列表,可以用上一个api得到的 [box]
contourIdx
:要绘制的轮廓索引。如果设置为-1
,则绘制所有轮廓。
color
:轮廓的颜色,可以是 BGR 颜色格式的三元组,例如(0, 0, 255)
表示红色。
thickness
:轮廓线的粗细,如果是正数,则绘制实线;如果是 0,则绘制轮廓点;如果是负数,则填充轮廓内部区域。 -
cv2.minEnclosingCircle(points) -> (center, radius)
points
:输入参数图片轮廓数据
center
:一个包含圆心坐标的二元组(x, y)
。
radius
:浮点数类型,表示计算得到的最小覆盖圆的半径。 -
cv2.circle(img, center, radius, color, thickness)
img
:输入图像,通常是一个numpy数组,代表要绘制圆形的图像。
center
:一个二元组(x, y)
,表示圆心的坐标位置。
radius
:整型或浮点型数值,表示圆的半径长度。
color
:颜色标识,可以是BGR格式的三元组(B, G, R)
,例如(255, 0, 0)
表示红色。
thickness
:整数,表示圆边框的宽度。如果设置为-1
,则会填充整个圆。import cv2 as cv
import numpy as np#读图
tu = cv.imread('E:\hqyj\code\opencv\images\num.png')#灰度化
grey = cv.cvtColor(tu,cv.COLOR_BGR2GRAY)#二值化
_,binary = cv.threshold(grey,200,255,cv.THRESH_BINARY_INV)#查找轮廓
conts,th = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)#获取外接矩形点
rect = [cv.boundingRect(c) for c in conts] #遍历轮廓表,获取每条轮廓的外接矩形(x,y,w,h)
print(rect)
rect_min_list = [cv.minAreaRect(c) for c in conts] #获取外接矩形的最小外接矩形
print('rect_min_list===============================((中心坐标),(宽,高),旋转角度)============================')
print(rect_min_list)
box_list = [cv.boxPoints(rect).astype(int) for rect in rect_min_list] #获取外接矩形的四个顶点
print('box_list====================================((左上),(右上),(左下),(右下))=============================')
print(box_list)
cv.drawContours(tu,box_list,-1,(0,0,255),2)i = 1
for box in box_list: #遍历外接矩形的四个顶点
print(f'box{i}======================================((左上),(右上),(左下),(右下))========================')
print(box)
cv.drawContours(tu,[box],-1,(0,0,255),2)
i += 1
for cont in rect: #遍历外接矩形
x,y,w,h = cont
cv.rectangle(tu,(x,y),(x+w,y+h),(255,0,0),2)#显示
cv.imshow('tu',tu)
cv.waitKey(0)
cv.destroyAllWindows()