目录
10.3 边界反射101(BORDER_REFLECT_101)
14.5 超阈值零处理(THRESH_TOZERO_INV)
1 OpenCV介绍
OpenCV(开放源代码计算机视觉库)是一个开源的计算机视觉和机器学习软件库。由一系列 C++ 类和函数构成,用于图像处理、计算机视觉领域的算法实现。
1.1 OpenCV优势
-
开源免费:完全开源,可以自由使用,降低开发成本和技术门槛。
-
多语言支持:除C++原生接口外,还支持Java、Python等编程语言。
-
跨平台:支持多种操作系统,Windows、Linux、ios、Android等,方便开发和部署。
-
丰富API:完善的传统计算机视觉算法,涵盖主流的机器学习算法,同时添加了对深度学习的支持。
1.2 OpenCV-Python
OpenCV-Python是原始OpenCV C++实现的Python包装器。它结合了 OpenCV C++ API 的高性能与 Python 语言的易用性和简洁性。通过 OpenCV-Python,开发者可以轻松地进行图像处理、计算机视觉任务以及机器学习应用。
与C / C++等语言相比,Python速度较慢。Python可以使用C / C++扩展,这使我们可以在C / C++中编写计算密集型代码,并创建可用作Python模块的Python包装器。两个好处:首先,代码与原始C / C++代码一样快(因为它是在后台工作的实际C++代码),其次,在Python中编写代码比使用C / C++更容易。
OpenCV-Python使用Numpy,这是一个高度优化的数据库操作库。所有OpenCV数组结构都转换为Numpy数组。这也使得与使用Numpy的其他库(如SciPy和Matplotlib)集成更容易。
2.环境安装
清华镜像源:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python
3 图像表示
像素是图像的基本单位,在计算机中,图像以像素的形式存在并采用二进制格式进行存储。根据图像的颜色不同,每个像素可以用不同的二进制数表示。计算机采用0/1编码的系统,数字图像也是利用0/1来记录信息,我们平常接触的图像都是8位数图像。opencv中常用的是8位图像,大多数彩色和灰度图像使用8位表示每个通道的像素值,范围从0到255,其中0,代表最黑,1,表示最白。
日常生活中最常见的是RGB三色图,也就是红绿蓝三色图,图上的每个点的颜色都可以通过这三种颜色按照不同的比例混合而成。RGB图中有三个通道,每个通道的取值是0-255,像素值不同则颜色不同。
4 图像存储
在OpenCV中,无论是读取还是创建图像,结果都是一个NumPy数组。
-
彩色图像:三维数组
-
灰度图像:二维
-
形状(Shape):图像的尺寸由其高(height)、宽(width)和通道数(channels)决定。可以通过
img.shape
属性获取这些信息。对于彩色图像(如RGB),返回的是一个包含三个值的元组 (height, width, channels)。对于灰度图像,返回的是一个包含两个值的元组 (height, width),因为灰度图像只有一个通道。 -
数据类型(dtype):图像中的每个像素值的数据类型决定了可以存储的最大值。例如,8位无符号整数(uint8)允许的范围是从0到255。
-
像素表示:单通道图像(灰度图像):每个像素由一个数值表示,代表该点的亮度。值越低(接近0),颜色越暗;值越高(接近255),颜色越亮。多通道图像(彩色图像): 在OpenCV中,默认情况下,彩色图像是以BGR(蓝-绿-红)顺序存储。
5 基本图像操作
5.1 读取图像
API:cv2.imread(path)
path:图像路径,
5.2 显示图像
API:cv2.imshow(窗口名,img)
窗口名:以字符串类型表示
img:要显示的图像
5.3保存图像
API:cv2.imwrite(path,img)
path:图片保存的路径和图片名
img:要保存的图像
5.4 创建黑白图像
API:numpy.zeros((height,width,channels),dtype=np. uint8)
使用np.zeros()
创建全黑图像,再修改像素值成为全白图像。
import cv2 as cv
import numpy as np
# 给出图像的宽高和通道数
h=300
w=450
c=3
# 利用numpy中的np.zeros创建全零矩阵,创建黑白图像
black_img=np.zeros((h,w,c),np.uint8)
print(black_img.shape)
# 显示图像
cv.imshow("black",black_img)
# 创建全白图像,改像素值为255
black_img[:,:,:]=255
cv.imshow("white",black_img)
print(black_img.shape)
# 随机创建图像,其像素值为(300,450,3)
black_img[:,:,:]=np.random.randint(0,256,(h,w,c))
cv.imshow("random",black_img)
print(black_img.shape)
cv.waitKey(0)
cv.destroyAllWindows()
5.5 图像剪裁(图像切片)
-
Opencv中,图像切片用于从图像中提取一个子区域(矩形区域)。
-
假设你有一个图像
img
,它的类型是numpy.ndarray
。img[y:y+h,x:x+w]
的含义如下:-
x:子区域左上角的x坐标
-
y:子区域左上角的y坐标
-
w:子区域的宽度
-
h:子区域的高度
-
-
切片操作
-
提取的是从
开始,高度为
,宽度为
的矩形区域import cv2 as cv # 读取图像 img_r=cv.imread("images/cat1.png") # 定义剪切出的图像起点和宽高 img=cv.resize(img_r,(480,480)) print(img.shape) x=100 y=200 #宽高 w=350 h=300 img2=img[y:y+h,x:x+w]# [200:500,100:450],300x350 cv.imshow("img",img) cv.imshow("img2",img2) print(img2.shape) cv.waitKey(0) cv.destroyAllWindows()
5.7 图像大小调整
API:cv2.resize(img,dsize)
img:需要调整大小的图像
dsize:调整图像的尺寸,是一个二元组
6 图像绘制
6.1 绘制直线
API:cv2.line(img,start,end,color,thinkness)
img:要绘制直线的图像
start、end:直线的起点和终点
color:直线的颜色(对于彩色图像,使用 BGR 格式指定颜色)
thickness:线条宽度
6.2 绘制圆形
API:cv2.circle(img,centerpoint,r,color,thinkness)
img:要绘制圆形的图片
centerpoint、r:圆心和半径
color:线条颜色
tnickness:线条宽度,为-1时生成闭合图案并填充颜色
6.3 绘制矩形
API:cv2.rectangle(img,leftupper,rightdown,color,thickness)
img:要绘制矩形的图像
leftupper、rightdown:矩形的左上角和右下角坐标
color:线条的颜色
thickness:线条的宽度
6.4 绘制文本(向图片中添加文字)
API:cv2.putText(img,text,station,font,Fontscale,color,thickness,cv2.LINE_AA)
img:要添加文字的图像
text:要写入的文本数据
station:文本的放置位置
font:字体样式
Fontscale:字体大小
thickness:字体线条宽度
cv2.LINE_AA:使用反走样技术绘制文本边框 ,反走样实现去锯齿化,使文本更平滑,清晰
import cv2 as cv
cat=cv.imread("images/cat1.png")
# 绘制直线:cv2.line(图像,起点,终点,颜色,线条粗细)(w,h)
cv.line(cat,(200,100),(400,100),(255,0,0),2)
# 绘制圆形:cv2.circle(图像,圆心坐标,半径,颜色,线条粗细(-1则填充为闭合图形))
cv.circle(cat,(300,300),100,(0,255,0),2,cv.LINE_AA)
#cv.LINE_AA:反走样技术,抗锯齿,是边缘更加平滑。默认是cv2.LINE_8
# cv.circle(cat,(300,300),100,(0,255,0),-1)
# 绘制矩形:cv2.rectangle(图像,左上坐标,右下坐标,颜色,线条粗细)
cv.rectangle(cat,(200,420),(500,600),(0,0,255),2)
# 向图像里面添加文字:cv2.putText(图像,文本,位置,字体样式,大小,颜色,线条粗细)
cv.putText(cat,"hello 赵子龙!",(400,300),cv.FONT_ITALIC,3,(255,0,0),2,cv.LINE_AA)
cv.imshow("cat",cat)
cv.waitKey(0)
cv.destroyAllWindows()
6.5 读取视频
API:cap = cv2.VideoCapture(path)
path:视频流资源路径设置为0,代表从默认摄像头捕获视频流
ret,frame = cap.read()
返回值cap调用read()方法得到一个布尔值和一帧图像,布尔值表示是否成功读取到帧,如果为False,可能是因为视频结束或读取失败,如果为True,frame则是当前帧的图像数据。
import cv2 as cv
# 创建VideoCapture对像
# cap=cv.VideoCapture("images/1.mp4")# 读取本地视频
cap=cv.VideoCapture(0)# 从摄像头读取实时视频流
# 循环读取每一帧图像:
while True:
ret, frame = cap.read()
# 判断是否读到某一帧,没有读到报错并退出循环
if not ret:
print("错了!")
break
cv.imshow("video",frame)
# cv.waitKey(40)&0xFF:等待40毫秒判断是否有按键触发,有返回ascII码,没有返回-1
if cv.waitKey(40)&0xFF==ord("w"):
break
cap.release()
cv.destroyAllWindows()
7 图像翻转(图像镜像旋转)
图像翻转以图像中心为原点进行翻转
API:cv2.flip(img,flipcode)
img: 要翻转的图像
flipcode: 指定翻转类型的标志
-
flipcode=0: 垂直翻转,图片像素点沿x轴翻转
-
flipcode>0: 水平翻转,图片像素点沿y轴翻转,一般设为1
-
flipcode<0: 水平垂直翻转,水平翻转和垂直翻转的结合一般设为-1
import cv2 as cv # 读取图像 cat=cv.imread("images/cat1.png") cat_r=cv.resize(cat,(480,480)) # 图像翻转:cv2.flip(img,翻转类型标志0、-1、1),原点在图像中心位置 # 垂直翻转:flipcode=0,沿x轴翻转,上下翻转 flip0=cv.flip(cat_r,0) # 水平翻转:flipcode=1,沿y轴翻转,左右翻转 flip1=cv.flip(cat_r,1) # 水平+垂直翻转:flipcode=-1,沿x、y都翻转 flip_1=cv.flip(cat_r,-1) cv.imshow("cat",cat_r) cv.imshow("flip0",flip0) cv.imshow("flip1",flip1) cv.imshow("flip_1",flip_1) cv.waitKey(0) cv.destroyAllWindows()
8 图像仿射变换
仿射变换是一种线性变换,基本性质有:保持直线,保持平行,比例不变性,不保持角度和长
常见仿射变换类型:
旋转:绕着某个点或轴旋转一定角度。
平移:仅改变物体的位置,不改变其形状和大小。
缩放:改变物体的大小。
剪切:使物体发生倾斜变形。
仿射变换基本原理:
线性变换
二维空间中,图像点坐标为(x,y),仿射变换的目标是将这些点映射到新的位置 (x', y')。
为了实现这种映射,通常会使用一个矩阵乘法的形式:
(类似于y=kx+b)
a,b,c,d 是线性变换部分的系数,控制旋转、缩放和剪切。
,
是平移部分的系数,控制图像在平面上的移动。
输入点的坐标被扩展为齐次坐标形式[x,y,1],以便能够同时处理线性变换和平移。
仿射变换函数:cv2.warpAffine()函数
API:cv2.warpAffine(img,M,dsize)
img:输入图像。
M:2x3的变换矩阵,类型为np.float32
。
dsize:输出图像的尺寸,形式为(width,height)
。
8.1 图像旋转
获取旋转矩阵:cv2.getRotationMatrix2D(center,angle,scale)
center:旋转中心点的坐标,格式为(x,y)
。
angle:旋转角度,单位为度,正值表示逆时针旋转负值表示顺时针旋转。
scale:缩放比例,若设为1,则不缩放。
返回值:M,2x3的旋转矩阵。
import cv2 as cv
# 读取图像
cat = cv.imread("images/cat1.png")
# 获取图像宽高
h,w,_=cat.shape
# h,w=cat.shape[:2]
# 设置旋转中心
center=(w//2,h//2)
# 旋转角度
angle=45
# 缩放比例
scale=0.6
# 获取旋转矩阵
M=cv.getRotationMatrix2D(center,angle,scale)
# 使用仿射变换函数进行仿射变换:旋转
img=cv.warpAffine(cat,M,(w,h))
# 显示图像
cv.imshow("cat",cat)
cv.imshow("new",img)
cv.waitKey(0)
cv.destroyAllWindows()
8.2 图像平移
平移操作可以将图像中的每个点沿着某个方向移动一定的距离。
假设我们有一个点 P(x,y),希望将其沿x轴方向平移t_x*个单位,沿y轴方向平移t_y个单位到新的位置P′(x′,y′),那么平移公式如下:
x′=x+tx
y′=y+ty
在矩阵形式下,该变换可以表示为:
这里的和
分别代表在x轴和y轴上的平移量。
#旋转:读取图像-》设置参数-》获取旋转矩阵-》使用仿射变换函数进行旋转-》显示图像
#平移:读取图像-》设置参数(平移量tx,ty)-》获取平移矩阵np.float32-》仿射变换函数进行平移-》显示图像
import cv2 as cv
import numpy as np
# 读取图像 (h,w,c)
cat=cv.imread("images/cat1.png")
# 定义相关参数:平移量
tx,ty=200,100
# 获取平移矩阵
M=np.float32([[1,0,tx],[0,1,ty]])
# 使用仿射变换函数进行平移 (w,h)
img=cv.warpAffine(cat,M,(cat.shape[1],cat.shape[0]))
# 显示图像
cv.imshow("cat",cat)
cv.imshow("new",img)
cv.waitKey(0)
cv.destroyAllWindows()
2.3 图像缩放
缩放操作可以改变图片的大小。
-
假设要把图像的宽高分别缩放为0.5和0.8,那么对应的缩放因子sx=0.5,sy=0.8。
-
点P(x,y)对应到新的位置P'(x',y'),缩放公式为:
-
在矩阵形式下,该变换可以表示为:
相较于图像旋转中只能等比例的缩放,图像缩放更加灵活,可以在指定方向上进行缩放。sx和sy分别表示在x轴和y轴方向上的缩放因子。
import cv2 as cv
import numpy as np
# 读图
cat=cv.imread("images/cat1.png")
cat_r=cv.resize(cat,(360,360))
# 获取宽高
h,w=cat_r.shape[:2]
# 定义缩放因子 sx*w,sy*h int()
sx,sy=0.5,0.8
# 变换后的图像宽高
w1,h1=int(sx*w),int(sy*h)
# 获取缩放矩阵
M=np.float32([[sx,0,0],[0,sy,0]])
# 使用仿射变换函数进行缩放
img=cv.warpAffine(cat_r,M,(w,h))# 输出图像尺寸是原图大小
img2=cv.warpAffine(cat_r,M,(w1,h1))# 输出图像尺寸是缩放后的大小
# 显示图像
cv.imshow("cat",cat_r)
cv.imshow("new",img)
cv.imshow("img2",img2)
cv.waitKey(0)
cv.destroyAllWindows()
8.4 图像剪切
剪切操作可以改变图形的形状,以便其在某个方向上倾斜,它将对象的形状改变为斜边平行四边形,而不改变其面积。
想象我们手上有一张矩形纸片,如果你固定纸片的一边,并沿着另一边施加一个平行于该边的力,这张纸片就会变形为一个平行四边形。这就是剪切变换的一个直观解释。
对于二维空间中的点P(x,y),对他进行剪切变换:
沿x轴剪切:
沿y轴剪切:
当需要同时沿两个方向进行剪切时,
在矩阵形式下,该变换可以表示为:
来一个图理解一下:
shy和shx分别对应沿x轴和y轴方向上的剪切因子。
可以理解为,x不变,y偏移
9 插值方法
在图像处理和计算机图形学中,插值(Interpolation)是一种通过已知数据点之间的推断或估计来获取新数据点的方法。它在图像处理中常用于处理图像的放大、缩小、旋转、变形等操作,以及处理图像中的像素值。
图像插值算法是为了解决图像缩放或者旋转等操作时,由于像素之间的间隔不一致而导致的信息丢失和图像质量下降的问题。当我们对图像进行缩放或旋转等操作时,需要在新的像素位置上计算出对应的像素值,而插值算法的作用就是根据已知的像素值来推测未知位置的像素值。
9.1 最近邻插值
cv2.INTER_NEAREST
new_img1=cv.warpAffine(img,M,(w,h),flags=cv.INTER_NEAREST)
首先给出目标点与原图像点之间坐标的计算公式:
dstX:目标图像中某点的x坐标,
dstY:目标图像中某点的y坐标,
srcWidth:原图的宽度,
dstWidth:目标图像的宽度;
srcHeight:原图的高度,
dstHeight:目标图像的高度。
而srcX和srcY:目标图像中的某点对应的原图中的点的x和y的坐标。
通俗的讲,该公式就是让目标图像中的每个像素值都能找到对应的原图中的像素值,这样才能根据不同的插值方法来获取新的像素值。根据该公式,我们就可以得到每一个目标点所对应的原图像的点,比如一个2*2的图像放大到4*4,如下图所示,其中红色的为每个像素点的坐标,黑色的则表示该像素点的像素值。
那么根据公式我们就可以计算出放大后的图像(0,0)点对应的原图像中的坐标为:
也就是原图中的(0,0)点,而最近邻插值的原则是:目标像素点的像素值与经过该公式计算出来的对应的像素点的像素值相同,如出现小数部分需要进行取整。那么放大后图像的(0,0)坐标处的像素值就是原图像中(0,0)坐标处的像素值,也就是10。接下来就是计算放大后图像(1,0)点对应的原图像的坐标,还是带入公式:
也就是原图中的(0.5,0)点,因此需要对计算出来的坐标值进行取整,取整后的结果为(0,0),也就是说放大后的图像中的(1,0)坐标处对应的像素值就是原图中(0,0)坐标处的像素值,其他像素点计算规则与此相同。
9.2 双线性插值
双线性插值是Opencv中最常用的插值方法。
双线性插值的工作原理是这样的:
-
假设要查找目标图像上坐标为
的像素值,在原图像上对应的浮点坐标为
。 -
在原图像上找到四个最接近
的像素点,通常记作,它们构成一个2x2的邻域矩阵。
-
分别在水平方向和垂直方向上做线性插值: 水平方向:根据
x
与x0
和x1
的关系计算出P00
和P10
、P01
和P11
之间的插值结果。
垂直方向:将第一步的结果与 y
与 y0
和 y1
的关系结合,再在垂直方向上做一次线性插值。
4.综合上述两次线性插值的结果,得到最终位于 处的新像素的估计值。
总结: 4乘4的图像 变成6乘6的图像 那么目标图像的(3,3)点的像素是原图中(1.8333,1.8333)的像素颜色,但是坐标必须是整数 它周围有四个像素点 该取谁呢? 按照到各自的距离比例 来分配颜色值
首先要了解线性插值,而双线性插值本质上就是在两个方向上做线性插值。还是给出目标点与原图像中点的计算公式
比如我们根据上述公式计算出了新图像中的某点所对应的原图像的点P,其周围的点分别为Q12、Q22、Q11、Q21, 要插值的P点不在其周围点的连线上,这时候就需要用到双线性插值了。首先延申P点得到P和Q11、Q21的交点R1与P和Q12、Q22的交点R2,如下图所示:
然后根据Q11、Q21得到R1的插值,根据Q12、Q22得到R2的插值,然后根据R1、R2得到P的插值即可,这就是双线性插值。以下是计算过程:
首先计算R1和R2的插值:
然后根据R1和R2计算P的插值:
这样就得到了P点的插值。注意此处如果先在y方向插值、再在x方向插值,其结果与按照上述顺序双线性插值的结果是一样的。
双线性插值的对应关系看似比较清晰,但还是有2个问题。首先是根据坐标系的不同,产生的结果不同,这张图是左上角为坐标系原点的情况,我们可以发现最左边x=0的点都会有概率直接复制到目标图像中(至少原点肯定是这样),而且就算不和原图像中的点重合,也相当于进行了1次单线性插值(带入到权重公式中会发现结果)。
下面这张图是右上角为坐标系原点的情况,我们可以发现最右面的点都会有概率直接复制到目标图像中(至少原点肯定是这样),而且就算不和原图像中的点重合,也相当于进行了1次单线性插值。那么当我们采用不同的坐标系时产生的结果是不一样的,而且无论我们采用什么坐标系,最左侧和最右侧(最上侧和最下侧)的点是“不公平的”,这是第一个问题。
第二个问题时整体的图像相对位置会发生变化。如下图所示,左侧是原图像(3,3),右侧是目标图像(5,5),原图像的几何中心点是(1,1),目标图像的几何中心点是(2,2),根据对应关系,目标图像的几何中心点对应的原图像的位置是(1.2,1.2),那么问题来了,目标图像的原点(0,0)和原始图像的原点是重合的,但是目标图像的几何中心点相对于原始图像的几何中心点偏右下,那么整体图像的位置会发生偏移,所以参与计算的点相对都往右下偏移会产生相对的位置信息损失。这是第二个问题。
因此,在OpenCV中,为了解决这两个问题,将公式进行了优化,如下所示:
使用该公式计算出原图中的对应坐标后再进行插值计算,就不会出现上面的情况了。
8.3 像素区域插值
cv2.INTER_AREA
像素区域插值主要分两种情况,缩小图像和放大图像的工作原理并不相同。
当使用像素区域插值方法进行缩小图像时,它就会变成一个均值滤波器(滤波器其实就是一个核,这里只做简单了解,后面实验中会介绍),其工作原理可以理解为对一个区域内的像素值取平均值。
当使用像素区域插值方法进行放大图像
-
如果图像放大的比例是整数倍,那么其工作原理与最近邻插值类似;
-
如果放大的比例不是整数倍,那么就会调用双线性插值进行放大。
其中目标像素点与原图像的像素点的对应公式如下所示:
8.4 双三次插值
cv2.INTER_CUBIC
与双线性插值法相同,该方法也是通过映射,在映射点的邻域内通过加权来得到放大图像中的像素值。不同的是,双三次插值法需要原图像中近邻的16个点来加权,也就是4x4的网格。
8.5 Lanczos插值
cv2.INTER_LANCZOS4
Lanczos插值方法与双三次插值的思想是一样的,不同的就是其需要的原图像周围的像素点的范围变成了8*8,并且不再使用BiCubic函数来计算权重,而是换了一个公式计算权重,64个像素点的加权叠加。
import cv2 as cv
# 读图
face=cv.imread("images/face.png")
h,w=face.shape[:2]
center=(w//2,h//2)
# 获取旋转矩阵center,angle,scale
M=cv.getRotationMatrix2D(center,0,0.5)
# 仿射变换
img1=cv.warpAffine(face,M,(w,h),cv.INTER_NEAREST)# 最近邻插值
img2=cv.warpAffine(face,M,(w,h),cv.INTER_LINEAR)# 双线性插值
img3=cv.warpAffine(face,M,(w,h),cv.INTER_AREA)# 像素区域插值:这里是缩小,相当于用均值滤波器
img4=cv.warpAffine(face,M,(w,h),cv.INTER_CUBIC)# 双三次插值,16个点
img5=cv.warpAffine(face,M,(w,h),cv.INTER_LANCZOS4)# Lanczons插值,64个点
# 显示图像
cv.imshow("face",face)
cv.imshow("NEAREST",img1)
cv.imshow("LINEAR",img2)
cv.imshow("AREA",img3)
cv.imshow("CUBIC",img4)
cv.imshow("LANCZOS4",img5)
cv.waitKey(0)
cv.destroyAllWindows()
8.6 小结
最近邻插值的计算速度最快,但是可能会导致图像出现锯齿状边缘和失真,效果较差。双线性插值的计算速度慢一点,但效果有了大幅度的提高,适用于大多数场景。双三次插值、Lanczos插值的计算速度都很慢,但是效果都很好。
在OpenCV中,关于插值方法默认选择的都是双线性插值,且一般情况下双线性插值已经能满足大部分需求。
10 边缘填充
进行仿射变换后的图可能会出现边缘缺失,所i用我们要对空出的区域进行边缘填充。
10.1 边界复制(BORDER_REPLICATE)
边界复制会将边界处的像素值进行复制,然后作为边界填充的像素值,如下图所示,可以看到四周的像素值都一样。
new_img=cv.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv.BORDER_REFLECT)
10.2 边界反射(BORDER_REFLECT)
根据原图的边缘进行反射。
new_img=cv.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv.BORDER_REFLECT)
10.3 边界反射101(BORDER_REFLECT_101)
与边界反射不同的是,不再反射边缘的像素点。
new_img=cv.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv.BORDER_REFLECT_101)
10.4 边界常数(BORDER_CONSTANT)
当选择边界常数时,还要指定常数值是多少,默认的填充常数值为0。图像边缘显示的是填充的像素值所对应的颜色。
new_img=cv.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv.BORDER_CONSTANT,borderValue=(0,0,255))
10.5 边界包裹(BORDER_WRAP)
new_img=cv.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv.BORDER_WRAP)
import cv2 as cv
face=cv.imread("images/face.png")
# 定义旋转中心
h,w=face.shape[:2]
center=(w//2,h//2)
# 获取旋转矩阵
M=cv.getRotationMatrix2D(center,45,0.2)
# 使用仿射变换矩阵进行旋转
img=cv.warpAffine(face,M,(w,h),cv.INTER_LANCZOS4)
# 边界复制
replicate=cv.warpAffine(face,M,(2*w,2*h),flags=cv.INTER_LANCZOS4,borderMode=cv.BORDER_REPLICATE)
# 边界反射
reflect=cv.warpAffine(face,M,(2*w,2*h),flags=cv.INTER_LANCZOS4,borderMode=cv.BORDER_REFLECT)
# 边界反射_101
reflect_101=cv.warpAffine(face,M,(2*w,2*h),flags=cv.INTER_LANCZOS4,borderMode=cv.BORDER_REFLECT_101)
#边界常数
constant=cv.warpAffine(face,M,(2*w,2*h),flags=cv.INTER_LANCZOS4,borderMode=cv.BORDER_CONSTANT,borderValue=(100,15,200))
# 边界包裹
wrap=cv.warpAffine(face,M,(2*w,2*h),flags=cv.INTER_LANCZOS4,borderMode=cv.BORDER_WRAP)
cv.imshow("face",face)
cv.imshow("new",img)
cv.imshow("replicate",replicate)
cv.imshow("reflect",reflect)
cv.imshow("refect_101",reflect_101)
cv.imshow("constant",constant)
cv.imshow("wrap",wrap)
cv.waitKey(0)
cv.destroyAllWindows()
11 图像矫正(透视变换)
透视变换是把一个图像投影到一个新的视平面的过程,在现实世界中,我们观察到的物体在视觉上会受到透视效果的影响,即远处的物体看起来会比近处的物体小。透视投影是指将三维空间中的物体投影到二维平面上的过程,这个过程会导致物体在图像中出现形变和透视畸变。透视变换可以通过数学模型来校正这种透视畸变,使得图像中的物体看起来更符合我们的直观感受。通俗的讲,透视变换的作用其实就是改变一下图像里的目标物体的被观察的视角。
如上图所示,图1在经过透视变换后得到了图2的结果,带入上面的话就是图像中的车道线(目标物体)的被观察视角从平视视角变成了俯视视角,这就是透视变换的作用。
假设我们有一个点 (x,y,z)在三维空间中,并且我们想要将其投影到二维平面上。我们可以先将其转换为齐次坐标, (x,y,z),然后进行透视投影,得到了经过透视投影后的二维坐标 (x′,y′)。通过将 X和Y 分别除以Z,我们可以模拟出真实的透视效果。
与仿射变换一样,透视变换也有自己的透视变换矩阵:
由此可得新的坐标的表达式为:
其中x、y是原始图像点的坐标,x^{\prime}、y^{\prime}是变换后的坐标,a11,a12,…,a33则是一些旋转量和平移量,由于透视变换矩阵的推导涉及三维的转换,所以这里不具体研究该矩阵,只要会使用就行,而OpenCV里也提供了getPerspectiveTransform()函数用来生成该3*3的透视变换矩阵。
M=getPerspectiveTransform(src,dst)
在该函数中,需要提供两个参数:
src:原图像上需要进行透视变化的四个点的坐标,这四个点用于定义一个原图中的四边形区域。
dst:透视变换后,src的四个点在新目标图像的四个新坐标。
该函数会返回一个透视变换矩阵,得到透视变化矩阵之后,使用warpPerspective()函数即可进行透视变化计算,并得到新的图像。该函
数需要提供如下参数:
cv2.warpPerspective(src, M, dsize, flags, borderMode)
src:输入图像。
M:透视变换矩阵。这个矩阵可以通过getPerspectiveTransform函数计算得到。
dsize:输出图像的大小。它可以是一个Size对象,也可以是一个二元组。
flags:插值方法的标记。
borderMode:边界填充的模式。
# 原图中卡片的四个角点:左上、右上、左下、右下
[[178, 100], [487, 134], [124, 267], [473, 308]]
import cv2 as cv
import numpy as np
# 读取图像
img=cv.imread("images/3.png")
# 获取图像尺寸
shape=img.shape
print(shape)
# 左上182,113 右上500,151 左下133,264 右下490,308
# 原图中要变换的图像的四个点坐标
pts1=np.float32([[182,113],[500,151],[133,264],[490,308]])
# 输出图像的四个点坐标
pts2=np.float32([[0,0],[shape[1],0],[0,shape[0]],[shape[1],shape[0]]])
#拷贝一份原图,绘制出要进行变换的部分
img_line=img.copy()
cv.line(img_line,pts1[0].astype(np.int64),pts1[1].astype(np.int64),(0,0,255),2)
cv.line(img_line,pts1[1].astype(np.int64),pts1[3].astype(np.int64),(0,0,255),2)
cv.line(img_line,pts1[3].astype(np.int64),pts1[2].astype(np.int64),(0,0,255),2)
cv.line(img_line,pts1[2].astype(np.int64),pts1[0].astype(np.int64),(0,0,255),2)
# 获取透视变换矩阵
M=cv.getPerspectiveTransform(pts1,pts2)
# 透视变换
new_img=cv.warpPerspective(img,M,(shape[1],shape[0]),flags=cv.INTER_LINEAR,borderMode=cv.BORDER_REFLECT_101)
cv.imshow("img",img)
cv.imshow("line",img_line)
cv.imshow("new",new_img)
cv.waitKey(0)
cv.destroyAllWindows()
12 图像色彩空间转换
图像色彩空间转换就是将图像从一种颜色表示形式转换为另一种表示形式的过程。常见的有RGB转Gray(灰度图),RGB转HSV等。色彩空间转换的作用:提高图像处理效果,节省计算资源
12.1 HSV颜色空间
HSV颜色空间指的是HSV颜色模型,这是一种与RGB颜色模型并列的颜色空间表示法。RGB颜色模型使用红、绿、蓝三原色的强度来表示颜色,是一种加色法模型,即颜色的混合是添加三原色的强度。而HSV颜色空间使用色调(Hue)、饱和度(Saturation)和亮度(Value)三个参数来表示颜色,色调H表示颜色的种类,如红色、绿色、蓝色等;饱和度表示颜色的纯度或强度,如红色越纯,饱和度就越高;亮度表示颜色的明暗程度,如黑色比白色亮度低。 HSV颜色模型是一种六角锥体模型,如下图所示:
色调H:
使用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,紫色为300°。通过改变H的值,可以选择不同的颜色
饱和度S:
饱和度S表示颜色接近光谱色的程度。一种颜色可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例越大,颜色接近光谱色的程度就越高,颜色的饱和度就越高。饱和度越高,颜色就越深而艳,光谱色的白光成分为0,饱和度达到最高。通常取值范围为0%~100%,其中0%表示灰色或无色,100%表示纯色,通过调整饱和度的值,可以使颜色变得更加鲜艳或者更加灰暗。
明度V:
明度表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0%(黑)到100%(白),通过调整明度的值,可以使颜色变得更亮或者更暗。
一般对颜色空间的图像进行有效处理都是在HSV空间进行的,然后对于基本色中对应的HSV分量需要给定一个严格的范围,下面是通过实验计算的模糊范围(准确的范围在网上都没有给出)。
H: 0— 180
S: 0— 255
V: 0— 255
此处把部分红色归为紫色范围:
RGB颜色空间转换成HSV颜色空间进行图像处理的原因:1.符合人类对颜色的感知方式 2.颜色调整更加直观 3.降维处理有利于计算。
12.2 RGB转Gray(灰度)
cv2.cvtColor
是OpenCV中的一个函数,用于图像颜色空间的转换。可以将一个图像从一个颜色空间转换为另一个颜色空间,比如从RGB到灰度图,或者从RGB到HSV的转换等。
cv2.cvtColor(img,code)
img
:输入图像,可以是一个Numpy数组绘着一个OpenCV的Mat对象
Mat
是一个核心的数据结构,主要用于存储图像和矩阵数据。在 Python 中使用 OpenCV 时,通常直接处理的是 NumPy 数组,cv2模块自动将 Mat
对象转换为 NumPy 数组。二者之间的转换是透明且自动完成的。例如,当你使用 cv2.imread()
函数读取图像时,返回的是一个 NumPy 数组,但在C++中则是 Mat
对象。
code
:指定转换的类型,可以使用预定义的转换代码。
例如cv2.COLOR_RGB2GRAY
表示从RGB到灰度图像的转换。
12.3 RGB转HSV
与上述方法类似,只是将code改为cv2.COLOR_RGB2HSV
import cv2 as cv
# 读取图像
cat=cv.imread("images/cat1.png")
# cv2.cvtColor(图像,转换方式)
# RGB转灰度图Gray
gray=cv.cvtColor(cat,cv.COLOR_BGR2GRAY)
# RGB转HSV
hsv=cv.cvtColor(cat,cv.COLOR_BGR2HSV)
# 显示效果
cv.imshow("cat",cat)
cv.imshow("gray",gray)
cv.imshow("hsv",hsv)
cv.waitKey(0)
cv.destroyAllWindows()
13 灰度实验
将彩色图像转换为灰度图像的过程称为灰度化,这种做法在图像处理和计算机视觉领域非常常见。灰度图与彩色图最大的不同就是:彩色图是由R、G、B三个通道组成,而灰度图只有一个通道,也称为单通道图像,所以彩色图转成灰度图的过程本质上就是将R、G、B三通道合并成一个通道的过程。本实验中一共介绍了三种合并方法,分别是最大值法、平均值法以及加权均值法。
13.1 灰度图
每个像素只有一个采样颜色的图像,这类图像通常显示为从最暗黑色到最亮的白色的灰度,尽管理论上这个采样可以任何颜色的不同深浅,甚至可以是不同亮度上的不同颜色。灰度图像与黑白图像不同,在计算机图像领域中黑白图像只有黑色与白色两种颜色;但是,灰度图像在黑色与白色之间还有许多级的颜色深度。灰度图像经常是在单个电磁波频谱如可见光内测量每个像素的亮度得到的,用于显示的灰度图像通常用每个采样像素8位的非线性尺度来保存,这样可以有256级灰度。
13.2 最大值法
对于彩色图像的每个像素,它会从R、G、B三个通道的值中选出最大的一个,并将其作为灰度图像中对应位置的像素值。
import cv2 as cv
import numpy as np
# 灰度化:最大值法:图像大小不变,像素点个数不变,通道数由3变为1,像素值会变,变成三个通道数的最大值
pig=cv.imread("images/pig.png")
# 获取图像大小,形状
h,w=pig.shape[:2]
# 创建一个和原图一样大小的图像,放像素,拿这张图就是灰度化后的图
gray=np.zeros((h,w),dtype=np.uint8)
# 遍历原图 取出每个像素点,拿到三个通道里的最大像素值,放进创建的图像里
# 遍历行
for i in range(h):
# 遍历列
for j in range(w):
# 取出每个像素点img[i,j]
# gray[i,j]=max(pig[i,j][0],pig[i,j][1],pig[i,j][2])
gray[i,j]=max(pig[i,j])
# 显示效果
cv.imshow("pig",pig)
cv.imshow("gray",gray)
cv.waitKey(0)
cv.destroyAllWindows()
13 .3 均值法
对于彩色图像的每个像素,它会将R、G、B三个通道的像素值全部加起来,然后再除以三,得到的平均值就是灰度图像中对应位置的像素值。
import cv2 as cv
import numpy as np
# 灰度化:最大值法:图像大小不变,像素点个数不变,通道数由3变为1,像素值会变,变成三个通道数的最大值
pig=cv.imread("images/pig.png")
# 获取图像大小,形状
h,w=pig.shape[:2]
# 创建一个和原图一样大小的图像,放像素,拿这张图就是灰度化后的图
gray=np.zeros((h,w),dtype=np.uint8)
# 遍历原图 取出每个像素点,拿到三个通道里的最大像素值,放进创建的图像里
# 遍历行
for i in range(h):
# 遍历列
for j in range(w):
# 取出每个像素点img[i,j]
gray[i,j]=np.uint8((int(pig[i,j,0])+int(pig[i,j,1])+int(pig[i,j,2]))//3)
# 显示效果
cv.imshow("pig",pig)
cv.imshow("gray",gray)
cv.waitKey(0)
cv.destroyAllWindows()
13.4 加权均值法
对于彩色图像的每个像素,它会按照一定的权重去乘以每个通道的像素值,并将其相加,得到最后的值就是灰度图像中对应位置的像素值。本实验中,权重的比例为: R乘以0.299,G乘以0.587,B乘以0.114,这是经过大量实验得到的一个权重比例,也是一个比较常用的权重比例。
所使用的权重之和应该等于1。这是为了确保生成的灰度图像素值保持在合理的亮度范围内,并且不会因为权重的比例不当导致整体过亮或过暗。
import cv2 as cv
import numpy as np
# 灰度化:最大值法:图像大小不变,像素点个数不变,通道数由3变为1,像素值会变,变成三个通道数的最大值
pig=cv.imread("images/pig.png")
# 获取图像大小,形状
h,w=pig.shape[:2]
# 定义三个通道的权重
wr,wg,wb=0.299,0.587,0.144
# 创建一个和原图一样大小的图像,放像素,拿这张图就是灰度化后的图
gray=np.zeros((h,w),dtype=np.uint8)
# 遍历原图 取出每个像素点,拿到三个通道里的最大像素值,放进创建的图像里
# 遍历行
for i in range(h):
# 遍历列
for j in range(w):
# 取出每个像素点img[i,j]
gray[i,j]=round(pig[i,j,0]*wb+pig[i,j,1]*wg+pig[i,j,2]*wr)
# 显示效果
cv.imshow("pig",pig)
cv.imshow("gray",gray)
cv.waitKey(0)
cv.destroyAllWindows()
14 图像二值化处理
将某张图像的所有像素改成只有两种值之一。
二值图像
一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。
其操作的图像也必须是灰度图。也就是说,二值化的过程,就是将一张灰度图上的像素根据某种规则修改为0和maxval(maxval表示最大值,一般为255,显示白色)两种像素值,使图像呈现黑白的效果,能够帮助我们更好地分析图像中的形状、边缘和轮廓等特征。
-
简便:降低计算量和计算需求,加快处理速度。
-
节约资源:二值图像占用空间远小于彩色图。
-
边缘检测:二值化常作为边缘检测的预处理步骤,因为简化后的图易于识别出轮廓和边界。
12.1 阈值法(THRESH_BINARY)
阈值法就是通过设置一个阈值,将灰度图中的每一个像素值与该阈值进行比较,小于等于阈值的像素就被设置为0(通常代表背景),大于阈值的像素就被设置为maxval(通常代表前景)。对于我们的8位图像(0~255)来说,通常是设置为255。
如上图所示,在灰度图中像素值较高的地方,如花瓣、花茎等地方的像素值比阈值高,那么在生成的二值化图中的对应位置的像素值就会被设置为255,也就是纯白色。
12.2 反阈值法(THRESH_BINARY_INV)
顾名思义,就是与阈值法相反。反阈值法是当灰度图的像素值大于阈值时,该像素值将会(黑),当灰度图的像素值小于等于阈值时,该像素值将会变成maxval。
如上图所示,使用反阈值法对灰度图进行二值化时,会将灰度图中像素值大于阈值的地方置为0(也就是黑),将灰度图中像素值小于阈值的地方置为255(也就是白)。
14.3 截断阈值法(THRESH_TRUNC)
截断阈值法,指将灰度图中的所有像素与阈值进行比较,像素值大于阈值的部分将会被修改为阈值,小于等于阈值的部分不变。
换句话说,经过截断阈值法处理过的二值化图中的最大像素值就是阈值。
当截断阈值为255时,如上图所示,可以看到灰度图与二值化图没有任何的区别。 使用截断阈值法进行图像二值化处理时,设置的
maxval
参数实际上是不起作用的。
14.4 低阈值零处理(THRESH_TOZERO)
低阈值零处理,字面意思,就是像素值小于等于阈值的部分被置为0(也就是黑色),大于阈值的部分不变。
如上图所示,在灰度图中较亮的部分,其像素值比阈值大,所以在二值化后其像素值并没有发生变化。而灰度图中较暗的部分,也就是像素值较低的地方,由于像素值比阈值小,就会被置为0,对应二值化图中的黑色部分。
14.5 超阈值零处理(THRESH_TOZERO_INV)
超阈值零处理就是将灰度图中的每个像素与阈值进行比较,像素值大于阈值的部分置为0(也就是黑色),像素值小于等于阈值的部分不变。
14.6 OTSU阈值法
THRESH_OTSU
本身并不是一个独立的阈值化方法,而是与 OpenCV 中的二值化方法结合使用的一个标志。具体来说,THRESH_OTSU
通常与 THRESH_BINARY
或 THRESH_BINARY_INV
结合使用。默认结合阈值法使用。也就是说,当你仅指定了 cv2.THRESH_OTSU
,实际上等同于同时指定了 cv2.THRESH_BINARY + cv2.THRESH_OTSU
。
在介绍OTSU阈值法之前,我们首先要了解一下双峰图片的概念。
双峰图片就是指灰度图的直方图上有两个峰值,直方图就是对灰度图中每个像素值的点的个数的统计图,如下图所示。
-
灰度图直方图的基础概念
-
1.灰度级:
-
在灰度图像中,每个像素的值代表其亮度,通常范围是 0 到 255(对于 8 位灰度图像)。
-
0 表示黑色,255 表示白色,中间的值表示不同程度的灰色。
-
2.直方图定义:
-
直方图是一个柱状图,其中 x 轴表示灰度级(从 0 到 255),y 轴表示对应灰度级在图像中出现的次数(频率)。
-
每个柱子的高度代表该灰度级在图像中出现的像素数量。
-
-
OTSU算法是通过一个值将这张图分前景色和背景色(也就是灰度图中小于这个值的是一类,大于这个值的是一类。例如,如果你设置阈值为128,则所有大于128的像素点可以被视作前景,而小于等于128的像素点则被视为背景。),通过统计学方法(最大类间方差)来验证该值的合理性,当根据该值进行分割时,使用最大类间方差计算得到的值最大时,该值就是二值化算法中所需要的阈值。通常该值是从灰度图中的最小值加1开始进行迭代计算,直到灰度图中的最大像素值减1,然后把得到的最大类间方差值进行比较,来得到二值化的阈值。
通过OTSU算法得到阈值之后,就可以结合上面的方法根据该阈值进行二值化,在本实验中有THRESH_OTSU和THRESH_INV_OTSU两种方法,就是在计算出阈值后结合了阈值法和反阈值法。
注意:使用OTSU算法计算阈值时,组件中的thresh参数将不再有任何作用。
14.7 自适应二值化
与二值化算法相比,自适应二值化更加适合用在明暗分布不均的图片,因为图片的明暗不均,导致图片上的每一小部分都要使用不同的阈值进行二值化处理,这时候传统的二值化算法就无法满足我们的需求了,于是就出现了自适应二值化。
自适应二值化方法会对图像中的所有像素点计算其各自的阈值,这样能够更好的保留图片里的一些信息。自适应二值化组件内容如下图所示:
cv2.adaptiveThreshold(image_np_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, 10)
其中各个参数的含义如下:
maxval
:最大阈值,一般为255
adaptiveMethod
:小区域阈值的计算方式:
ADAPTIVE_THRESH_MEAN_C
:小区域内取均值
ADAPTIVE_THRESH_GAUSSIAN_C
:小区域内加权求和,权重是个高斯核
thresholdType
:二值化方法,只能使用THRESH_BINARY、THRESH_BINARY_INV,也就是阈值法和反阈值法
blockSize
:选取的小区域的面积,如7就是7*7的小块。
c
:最终阈值等于小区域计算出的阈值再减去此值
下面介绍一下这两种方法。
1. 取均值
比如一张图片的左上角像素值如下图所示:
假如我们使用的小区域是3*3的,那么就会从图片的左上角开始(也就是像素值为162的地方)计算其邻域内的平均值,如果处于边缘地区就会对边界进行填充,填充值就是边界的像素点,如下图所示:
那么对于左上角像素值为162的这个点,161(也就是上图中括号内的计算结果,结果会进行取整)就是根据平均值计算出来的阈值,接着减去一个固定值C,得到的结果就是左上角这个点的二值化阈值了,接着根据选取的是阈值法还是反阈值法进行二值化操作。紧接着,向右滑动计算每个点的邻域内的平均值,直到计算出右下角的点的阈值为止。我们所用到的不断滑动的小区域被称之为核,比如3*3的小区域叫做3*3的核,并且核的大小都是奇数个,也就是3*3、5*5、7*7等。
自适应二值化(Adaptive Thresholding)的核心思想就是为图像中的每个像素点计算一个局部阈值。这种方法与全局阈值化不同,后者对整个图像使用同一个固定的阈值。而在自适应二值化中,每个像素的阈值是基于其周围邻域内的像素值动态确定的。
2. 加权求和
对小区域内的像素进行加权求和得到新的阈值,其权重值来自于高斯分布。
高斯分布,通过概率密度函数来定义高斯分布,一维高斯概率分布函数为:
通过改变函数中和的值,我们可以得到如下图像,其中均值为,标准差为
。
此时我们拓展到二维图像,一般情况下我们使x轴和y轴的相等并且,此时我们可以得到二维高斯函数的表达式为:
高斯概率函数是相对于二维坐标产生的,其中(x,y)为点坐标,要得到一个高斯滤波器模板,应先对高斯函数进行离散化,将得到的值作为模板的系数。例如:要产生一个3*3的高斯权重核,以核的中心位置为坐标原点进行取样,其周围的坐标如下图所示(x轴水平向右,y轴竖直向上)
将坐标带入上面的公式中,即可得到一个高斯权重核。
而在opencv里,当kernel(小区域)的尺寸为1、3、5、7并且用户没有设置sigma的时候(sigma <= 0),核值就会取固定的系数,这是一种默认的值是高斯函数的近似。
比如kernel的尺寸为3*3时,使用
进行矩阵的乘法,就会得到如下的权重值,其他的类似。
通过这个高斯核,即可对图片中的每个像素去计算其阈值,并将该阈值减去固定值得到最终阈值,然后根据二值化规则进行二值化。
而当kernels尺寸超过7的时候,如果sigma设置合法(用户设置了sigma),则按照高斯公式计算.当sigma不合法(用户没有设置sigma),则按照如下公式计算sigma的值:
某像素点的阈值计算过程如下图所示:
首先还是对边界进行填充,然后计算原图中的左上角(也就是162像素值的位置)的二值化阈值,其计算过程如上图所示,再然后根据选择的二值化方法对左上角的像素点进行二值化,之后核向右继续计算第二个像素点的阈值,第三个像素点的阈值…直到右下角(也就是155像素值的位置)为止。
当核的大小不同时,仅仅是核的参数会发生变化,计算过程与此是一样的。
import cv2 as cv
# 读图
flower_r=cv.imread("images/flower.png")
flower=cv.resize(flower_r,(360,360))
# 转灰度
gray=cv.cvtColor(flower,cv.COLOR_BGR2GRAY)
# 二值化:阈值法
_,binary=cv.threshold(gray,127,255,cv.THRESH_BINARY)
# 反阈值法
_,binary_inv=cv.threshold(gray,127,255,cv.THRESH_BINARY_INV)
# 截断阈值法
_,trunc=cv.threshold(gray,127,255,cv.THRESH_TRUNC)
# 低阈值零处理
_,zeros=cv.threshold(gray,127,255,cv.THRESH_TOZERO)
# 超阈值零处理
_,zeros_inv=cv.threshold(gray,127,255,cv.THRESH_TOZERO_INV)
# OTSU阈值法 默认结合反阈值法
thresh,otsu_binary=cv.threshold(gray,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY)
thresh,otsu_binaryinv=cv.threshold(gray,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 自适应二值化:均值法
img_mean=cv.adaptiveThreshold(gray,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,5,10)
# 自适应二值化:加权均值法
img_gauss=cv.adaptiveThreshold(gray,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY_INV,3,10)
cv.imshow("flower",flower)
cv.imshow("binary",binary)
cv.imshow("binary_inv",binary_inv)
cv.imshow("trunc",trunc)
cv.imshow("zeros",zeros)
cv.imshow("zeros_inv",zeros_inv)
cv.imshow("otsu_binary",otsu_binary)
cv.imshow("otsu_binaryinv",otsu_binaryinv)
cv.imshow("img_mean",img_mean)
cv.imshow("img_gauss",img_gauss)
cv.waitKey(0)
cv.destroyAllWindows()
15 图像掩膜
15.1 制作掩膜
掩膜(Mask)是一种在图像处理中常见的操作,它用于选择性地遮挡图像的某些部分,以实现特定任务的目标。掩膜通常是一个二值化图像,并且与原图像的大小相同,其中目标区域被设置为1(或白色),而其他区域被设置为0(或黑色),并且目标区域可以根据HSV的颜色范围进行修改,如下图就是制作红色掩膜的过程:
通过这个掩膜,我们就可以对掩膜中的白色区域所对应的原图中的区域进行处理与操作。
mask=cv.inRange(img,color_low,color_high)
cv2.inRange用于进行多通道图像(尤其是彩色图像)的阈值操作。
15.2 与运算
我们在高中时学过逻辑运算中的“与”运算,其规则是当两个命题都是真时,其结果才为真。而在图像处理中,“与”运算被用来对图像的像素值进行操作。具体来说,就是将两个图像中所有的对应像素值一一进行“与”运算,从而得到新的图像。从上面的图片我们可以看出,掩膜中有很多地方是黑色的,其像素值为0,那么在与原图像进行“与”运算的时候,得到的新图像的对应位置也是黑色的,如下图所示:
通过掩膜与原图的与运算,我们就可以提取出图像中被掩膜覆盖的区域(扣图)。
cv2.bitwise_and(src1,src2[,mask])
-
src1
:第一个输入数组。通常是输入的原始图像。 -
src2
:第二个输入数组。它可以是另一个图像、一个常数值或者与src1
相同的图像。-
当应用掩膜时,这个参数经常就是
src1
本身;即对同一个图像进行操作。 -
如果对两个不同的图像执行按位与操作(例如,将两张图片的某些部分组合在一起),可以分别将它们作为
src1
和src2
输入到cv2.bitwise_and()
函数中,创建复杂的图像效果或进行图像合成。
-
-
mask
:掩膜(可选)。输入数组元素只有在该掩膜非零时才被处理。是一个8位单通道的数组,尺寸必须与src1
和src2
相同。 -
返回值:输出数组,应用掩膜后的图像,与输入数组大小和类型相同。
15.3 颜色替换
前一个实验中,我们已经能够识别到图像中的某一种颜色,那么我们就可以对识别到的颜色进行一个操作,比如将其替换成别的颜色,其原理就是在得到原图的掩膜之后,对掩膜中的白色区域所对应的原图中的区域进行一个像素值的修改即可。
15.3.1 制作掩膜
掩膜(Mask)是一种在图像处理中常见的操作,它用于选择性地遮挡图像的某些部分,以实现特定任务的目标。掩膜通常是一个二值化图像,并且与原图像的大小相同,其中目标区域被设置为1(或白色),而其他区域被设置为0(或黑色),并且目标区域则可以根据HSV的颜色范围进行修改,如下图所示,可以选择制作不同颜色的掩膜:
通过这个掩膜,我们就可以对掩膜中的白色区域所对应的原图中的区域(也就是原图中的红色区域)进行像素值的修改,从而完成颜色替换的功能。
通过这个掩膜,我们就可以对掩膜中的白色区域所对应的原图中的区域(也就是原图中的红色区域)进行像素值的修改,从而完成颜色替换的功能。
15.3.2 颜色替换
由于掩膜与原图的大小相同,并且像素位置一一对应,那么我们就可以得到掩膜中白色(也就是像素值为255)区域的坐标,并将其带入到原图像中,即可得到原图中的红色区域的坐标,然后就以修改像素值了,这样就完成了颜色的替换,如下图所示:
import cv2 as cv
import numpy as np
# 读图
img_r=cv.imread("images/demo.png")
# 把图像变小
img=cv.resize(img_r,(480,480))
# 转为HSV图像
hsv_img=cv.cvtColor(img,cv.COLOR_BGR2HSV)
# 制作掩膜 cv2.inRange(img,min(h,s,v),max(h,s,v))
color_low=np.array([23,46,46])
color_high=np.array([34,255,255])
mask_yellow=cv.inRange(hsv_img,color_low,color_high)
# 与运算:原图与掩膜进行与运算
color_img=cv.bitwise_and(img,img,mask=mask_yellow)
# 替换颜色
img[mask_yellow==255]=(0,255,0)
# 展示图像
cv.imshow("img",img)
cv.imshow("hsv",hsv_img)
cv.imshow("yellow",mask_yellow)
cv.imshow("color",color_img)
cv.waitKey(0)
cv.destroyAllWindows()
16 ROI切割
ROI:Region of Interest,翻译过来就是感兴趣的区域。什么意思呢?比如对于一个人的照片,假如我们要检测眼睛,因为眼睛肯定在脸上,所以我们感兴趣的只有脸这部分,其他都不care,所以可以单独把脸截取出来,这样就可以大大节省计算量,提高运行速度。
还记得Numpy这个库吗?我们在使用OpenCV进行读取图像时,图像数据会被存储为Numpy数组,这也意味着我们可以使用Numpy数组的一些操作来对图像数据进行处理,比如切片。而本实验的原理也是基于Numpy数组的切片操作来完成的,因此在对应的组件中就需要填我们要切割的ROI区域的坐标来完成ROI切割操作。
注意:在OpenCV中,坐标的x轴的正方向是水平向右,y轴的正方向是垂直向下,与数学上的二维坐标并不相同。
在计算机视觉中,当我们使用OpenCV读取RGB三通道图像时,它会被转换成一个三维的Numpy数组。这个数组里的每个元素值都表示图像的一个像素值。这个三维数组的第一个维度(即轴0)通常代表图像的高度,第二个维度(即轴1)代表图像的宽度,而第三个维度(即轴2)代表图像的三个颜色通道(B、G、R,OpenCV读取到的图像以BGR的方式存储)所对应的像素值。
因此,我们可以通过指定切片的范围来选择特定的高度和宽度区域。这样,我们就能够获取这个区域内的所有像素值,即得到了这个区域的图像块,通过Numpy的切片操作,我们就完成了ROI切割的操作。这种提取ROI的方法允许我们仅获取感兴趣区域内的像素,而忽略其他不相关的部分,从而大大减少数据处理和存储的负担。
17 图像添加水印
本实验中添加水印的概念其实可以理解为将一张图片中的某个物体或者图案提取出来,然后叠加到另一张图片上。具体的操作思想是通过将原始图片转换成灰度图,并进行二值化处理,去除背景部分,得到一个类似掩膜的图像。然后将这个二值化图像与另一张图片中要添加水印的区域进行“与”运算,使得目标物体的形状出现在要添加水印的区域。最后,将得到的目标物体图像与要添加水印的区域进行相加,就完成了添加水印的操作。这样可以实现将一个图像中的某个物体或图案叠加到另一个图像上,从而实现添加水印的效果。就本实验而言,会用到两个新的组件,一个是模板输入,一个是图像融合。
17.1 模板输入(创建掩膜)
其实,该组件起到的就是图片输入的功能,只不过使用模板输入所输入的图片其实是作为要添加的水印,有了水印的彩色图之后,我们需要用它来制作一个掩模,这就用到了灰度化和二值化,即先灰度化后二值化,这就得到了带有水印图案的掩模。
17.2 与运算
有了模板的掩膜之后(也就是二值化图),我们就可以在要添加水印的图像中,根据掩膜的大小切割出一个ROI区域,也就是我们要添加水印的区域,之后让其与模板的掩膜进行与运算,我们知道,与运算的过程中,只要有黑色像素,那么得到的结果图中的对应位置也会是黑色像素。由于模板的掩膜中目标物体的像素值为黑色,所以经过与运算后,就会在ROI区域中得到模板图的形状。
17.3 图像融合(图像位与操作)
将模板的形状添加到水印区域之后,就可以将该图像与原始的模板图进行图像融合。该组件的目的就是将图像对应的数组中的对应元素进行相加(一定要注意这里的两个数组是规格相同的,也就是说要么都是灰度图,要么都是彩图),其过程如下图所示。
因此就可以让原始的模板图与添加模板图形状的ROI区域进行图像融合,得到添加水印的ROI区域。
import cv2 as cv
# 读入我们的logo和背景图,往背景里添加logo
bg=cv.imread("images/bg.png")
logo=cv.imread("images/logohq.png")
# 获取logo大小
h,w=logo.shape[:2]
# 从背景中切割出和logo一样大小的子区域
roi=bg[:h,:w]
# 将logo转灰度
gray_logo=cv.cvtColor(logo,cv.COLOR_BGR2GRAY)
# 创建掩膜:白色logo,目的是获取到logo的颜色,有logo没背景
_,white=cv.threshold(gray_logo,170,255,cv.THRESH_BINARY_INV)# 反阈值法
# 与运算,提取logo
fg1=cv.bitwise_and(logo,logo,mask=white)
# 创建掩膜:黑色的logo,目的是获取到背景颜色,有背景没logo
_,black=cv.threshold(gray_logo,170,255,cv.THRESH_BINARY)# 阈值法
# 与运算,提取背景
fg2=cv.bitwise_and(roi,roi,mask=black)
# 图像融合
# roi[:]=cv.add(fg1,fg2)
roi[:]=fg1+fg2
# 让融合后的图像赋值给切割出来的区域,这样原图中也会被修改像素值
cv.imshow("white",white)
cv.imshow("fg1",fg1)
cv.imshow("roi",roi)
cv.imshow("black",black)
cv.imshow("fg2",fg2)
cv.imshow("bg",bg)
cv.waitKey(0)
cv.destroyAllWindows()
18 图像噪点消除
首先介绍一些概念:
噪声:指图像中的一些干扰因素,通常是由图像采集设备、传输信道等因素造成的,表现为图像中随机的亮度,也可以理解为有那么一些点的像素值与周围的像素值格格不入。常见的噪声类型包括高斯噪声和椒盐噪声。高斯噪声是一种分布符合正态分布的噪声,会使图像变得模糊或有噪点。椒盐噪声则是一些黑白色的像素值分布在原图像中。
滤波器:也可以叫做卷积核,与自适应二值化中的核一样,本身是一个小的区域,有着特定的核值,并且工作原理也是在原图上进行滑动并计算中心像素点的像素值。滤波器可分为线性滤波和非线性滤波,线性滤波对邻域中的像素进行线性运算,如在核的范围内进行加权求和,常见的线性滤波器有均值滤波、高斯滤波等。非线性滤波则是利用原始图像与模板之间的一种逻辑关系得到结果,常见的非线性滤波器中有中值滤波器、双边滤波器等。
滤波与模糊联系与区别:
-
它们都属于卷积,不同滤波方法之间只是卷积核不同(对线性滤波而言)
-
低通滤波器是模糊,高通滤波器是锐化
-
低通滤波器就是允许低频信号通过,在图像中边缘和噪点都相当于高频部分,所以低通滤波器用于去除噪点、平滑和模糊图像。高通滤波器则反之,用来增强图像边缘,进行锐化处理。
注意:椒盐噪声可以理解为斑点,随机出现在图像中的黑点或白点;高斯噪声可以理解为拍摄图片时由于光照等原因造成的噪声。
本实验中共提供了五种滤波的方式,下面进行一一介绍。
18.1 均值滤波
均值滤波是一种最简单的滤波处理,它取的是卷积核区域内元素的均值,如3×3的卷积核:
在滤波算法组件中,当参数filtering_method选为均值滤波,参数component_param为ksize,代表卷积核的大小,eD:\人工智能培训ksize=3,则代表使用3×3的卷积核。比如有一张4*4的图片,现在使用一个3*3的卷积核进行均值滤波时,其过程如下所示:
对于边界的像素点,则会进行边界填充,以确保卷积核的中心能够对准边界的像素点进行滤波操作。在OpenCV中,默认的是使用BORDER_REFLECT_101的方式进行填充,下面的滤波方法中除了中值滤波使用的是BORDER_REPLICATE进行填充之外,其他默认也是使用这个方式进行填充,因此下面就不再赘述。通过卷积核在原图上从左上角滑动计算到右下角,从而得到新的4*4的图像的像素值。
18.2 方框滤波
方框滤波跟均值滤波很像,如3×3的滤波核如下:
在滤波算法组件中,当参数filtering_method选为方框滤波时,参数component_param为ksize,ddepth,normalize。下面讲解这3个参数的含义:
-
ksize:代表卷积核的大小,eD:\人工智能培训ksize=3,则代表使用3×3的卷积核。
-
ddepth:输出图像的深度,-1代表使用原图像的深度。
-
图像深度是指在数字图像处理和计算机视觉领域中,每个像素点所使用的位数(bit depth),也就是用来表示图像中每一个像素点的颜色信息所需的二进制位数。图像深度决定了图像能够表达的颜色数量或灰度级。
-
-
normalize:当normalize为True的时候,方框滤波就是均值滤波,上式中的a就等于1/9;normalize为False的时候,a=1,相当于求区域内的像素和。
其滤波的过程与均值滤波一模一样,都采用卷积核从图像左上角开始,逐个计算对应位置的像素值,并从左至右、从上至下滑动卷积核,直至到达图像右下角,唯一的区别就是核值可能会不同。
18.3 高斯滤波
-
高斯滤波是一种常用的图像处理技术,主要用于平滑图像、去除噪声。它通过使用高斯函数(正态分布)作为卷积核来对图像进行模糊处理。
前面两种滤波方式,卷积核内的每个值都一样,也就是说图像区域中每个像素的权重也就一样。高斯滤波的卷积核权重并不相同:中间像素点权重最高,越远离中心的像素权重越小。还记得我们在自适应二值化里是怎么生成高斯核的吗?这里跟自适应二值化里生成高斯核的步骤是一样的,都是以核的中心位置为坐标原点,然后计算周围点的坐标,然后带入下面的高斯公式中。
其中,x和 y 是相对于中心点的坐标偏移量,σ 是标准差,控制着高斯函数的宽度和高度。较大的 σ 值会导致更广泛的平滑效果。
卷积核通常是一个方形矩阵,其元素值根据高斯函数计算得出,并且这些值加起来等于1,近似于正态分布,以确保输出图像的亮度保持不变。
其中的值也是与自适应二值化里的一样,当时会取固定的系数,当kernel大于7并且没有设置时,会使用固定的公式进行计算sigma的值:
我们还是以3*3的卷积核为例,其核值如下所示:
得到了卷积核的核值之后,其滤波过程与上面两种滤波方式的滤波过程一样,都是用卷积核从图像左上角开始,逐个计算对应位置的像素值,并从左至右、从上至下滑动卷积核,直至到达图像右下角,唯一的区别就是核值不同。
在滤波算法组件中,当参数filtering_method选为高斯滤波,参数component_param为ksize,sigmaX。下面讲解这2个参数的含义:
ksize:代表卷积核的大小,eD:\人工智能培训ksize=3,则代表使用3×3的卷积核。
sigmaX:就是高斯函数里的值,σx值越大,模糊效果越明显。高斯滤波相比均值滤波效率要慢,但可以有效消除高斯噪声,能保留更多的图像细节,所以经常被称为最有用的滤波器。均值滤波与高斯滤波的对比结果如下(均值滤波丢失的细节更多):
18.4 中值滤波
中值又叫中位数,是所有数排序后取中间的值。中值滤波没有核值,而是在原图中从左上角开始,将卷积核区域内的像素值进行排序,并选取中值作为卷积核的中点的像素值,其过程如下所示:
中值滤波就是用区域内的中值来代替本像素值,所以那种孤立的斑点,如0或255很容易消除掉,适用于去除椒盐噪声和斑点噪声。中值是一种非线性操作,效率相比前面几种线性滤波要慢。
比如下面这张斑点噪声图,用中值滤波显然更好:
在滤波算法组件中,当参数filtering_method选为中值滤波,参数component_param为ksize,代表卷积核的大小,eD:\人工智能培训ksize=3,则代表使用3×3的卷积核。
18.5 双边滤波
模糊操作基本都会损失掉图像细节信息,尤其前面介绍的线性滤波器,图像的边缘信息很难保留下来。然而,边缘(edge)信息是图像中很重要的一个特征,所以这才有了双边滤波。
可以看到,双边滤波明显保留了更多边缘信息,下面来介绍一下双边滤波。
双边滤波的基本思路是同时考虑将要被滤波的像素点的空域信息(周围像素点的位置的权重)和值域信息(周围像素点的像素值的权重)。为什么要添加值域信息呢?是因为假设图像在空间中是缓慢变化的话,那么临近的像素点会更相近,但是这个假设在图像的边缘处会不成立,因为图像的边缘处的像素点必不会相近。因此在边缘处如果只是使用空域信息来进行滤波的话,得到的结果必然是边缘被模糊了,这样我们就丢掉了边缘信息,因此添加了值域信息。
双边滤波采用了两个高斯滤波的结合,一个负责计算空间邻近度的权值(也就是空域信息),也就是上面的高斯滤波器,另一个负责计算像素值相似度的权值(也就是值域信息),也是一个高斯滤波器。
18.6 小结
在不知道用什么滤波器好的时候,优先高斯滤波,然后均值滤波。
斑点和椒盐噪声优先使用中值滤波。
要去除噪点的同时尽可能保留更多的边缘信息,使用双边滤波。
线性滤波方式:均值滤波、方框滤波、高斯滤波(速度相对快)。
非线性滤波方式:中值滤波、双边滤波(速度相对慢)。
import cv2 as cv
# 读取图像
img_r=cv.imread("images/lvbo2.png")
img_jy=cv.imread("images/lvbo3.png")
# 图像太大,将图像变小
img=cv.resize(img_r,(480,480))
# 均值滤波 cv2.blur(img,ksize)
img_blur=cv.blur(img,(3,3))
# 高斯滤波 cv2.GaussianBlur(img,ksize,sigmaX)
img_gauss=cv.GaussianBlur(img,(3,3),1)
# 方框滤波 cv2.boxFilter(img,d,ksize.nomalize)
img_box1=cv.boxFilter(img,-1,(3,3),normalize=True)# 相当于均值滤波
img_box2=cv.boxFilter(img,-1,(3,3),normalize=False)# 把区域内的像素值全部相加
# 中值滤波 cv2.medianBlur(img,ksize)
img_median=cv.medianBlur(img_jy,3)
# 双边滤波
img_bilate=cv.bilateralFilter(img,9,100,100)
# 显示图像
cv.imshow("img",img)
cv.imshow("guass",img_gauss)
cv.imshow("blur",img_blur)
cv.imshow("box1",img_box1)
cv.imshow("box2",img_box2)
cv.imshow("median",img_median)
cv.imshow("bilate",img_bilate)
cv.waitKey(0)
cv.destroyAllWindows()
19 图像梯度处理
19.1 图像梯度
如果你还记得高数中用一阶导数来求极值的话,就很容易理解了:把图片想象成连续函数,因为边缘部分的像素值是与旁边像素明显有区别的,所以对图片局部求极值,就可以得到整幅图片的边缘信息了。不过图片是二维的离散函数,导数就变成了差分,这个差分就称为图像的梯度。
19.2 垂直边缘提取
滤波是应用卷积来实现的,卷积的关键就是卷积核,我们来考察下面这个卷积核:
这个核是用来提取图片中的垂直边缘的,怎么做到的呢?看下图:
当前列左右两侧的元素进行差分,由于边缘的值明显小于(或大于)周边像素,所以边缘的差分结果会明显不同,这样就提取出了垂直边缘。
同理,把上面那个矩阵转置一下,就是提取水平边缘。这种差分操作就称为图像的梯度计算:
cv2.filter2D(src, ddepth, kernel)
filter2D函数是用于对图像进行二维卷积(滤波)操作。它允许自定义卷积核(kernel)来实现各种图像处理效果,如平滑、锐化、边缘检测等
-
src
: 输入图像,一般为numpy
数组。 -
ddepth
: 输出图像的深度,可以是负值(表示与原图相同)、正值或其他特定值(常用-1 表示输出与输入具有相同的深度)。 -
kernel
: 卷积核,一个二维数组(通常为奇数大小的方形矩阵),用于计算每个像素周围邻域的加权和。 -
先用数组模拟一下
import cv2 as cv
import numpy as np
img5=np.array([[100,102,109,110,98,20,19,18,21,21],
[110,104,105,100,104,23,20,18,20,21],
[98,100,104,104,100,17,19,22,21,25],
[110,104,105,100,104,23,20,18,20,21],
[98,100,104,104,100,17,19,22,21,25],
[100,102,109,110,98,20,19,18,21,21]],dtype=np.float32)
kernel=np.array([[-1,0,1],
[-2,0,2],
[-1,0,1]],np.float32)
img2=cv.filter2D(img5,-1,kernel=kernel)
print(img2)
img=cv.imread("images/shudu.png")
# 垂直边缘提取
img3=cv.filter2D(img,-1,kernel=kernel)
# 水平边缘提取
kernel2=kernel.T
img4=cv.filter2D(img,-1,kernel=kernel2)
pig=cv.imread("images/pig.png",cv.IMREAD_GRAYSCALE)
pig1=cv.filter2D(pig,-1,kernel=kernel)
cv.imwrite("./images/pig_d.png",pig1)
cv.imshow("img",img)
cv.imshow("img3",img3)
cv.imshow("img4",img4)
cv.imshow("f",pig1)
cv.waitKey(0)
cv.destroyAllWindows()
19.3 Sobel算子
上面的两个卷积核都叫做Sobel算子,只是方向不同,它先在垂直方向计算梯度:
再在水平方向计算梯度:
最后求出总梯度:
在梯度处理方式这个组件中,当参数filter_method选择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
# cv.Sobel(img(灰度图).ddepth.dx.dy.ksize)一阶边缘检测
img=cv.imread("images/shudu.png",cv.IMREAD_GRAYSCALE)
# 提取垂直边缘
dst_x=cv.Sobel(img,-1,1,0,ksize=3)
# 提取水平边缘
dst_y=cv.Sobel(img,-1,0,1,ksize=3)
# 图像边缘,水平垂直边缘都提取 dxy=|dx+dy|因为dx,dy有方向,即矢量相加
dst_xy=cv.Sobel(img,-1,1,1,ksize=3)
# 展示图像
cv.imshow("img",img)
cv.imshow("x",dst_x)
cv.imshow("y",dst_y)
cv.imshow("xy",dst_xy)
cv.waitKey(0)
cv.destroyAllWindows()
19.4 Laplacian算子
高数中用一阶导数求极值,在这些极值的地方,二阶导数为0,所以也可以通过求二阶导计算梯度:
一维的一阶和二阶差分公式分别为:
提取前面的系数,那么一维的Laplacian滤波核是:
而对于二维函数f(x,y),两个方向的二阶差分分别是:
合在一起就是:
同样提取前面的系数,那么二维的Laplacian滤波核就是:
这就是Laplacian算子的图像卷积模板,有些资料中在此基础上考虑斜对角情况,将卷积核拓为:
cv2.Laplacian(src, ddepth)
src:这是输入图像
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
import cv2 as cv
# 读取图像 将它转为灰度图
img=cv.imread("images/shudu.png",cv.IMREAD_GRAYSCALE)
#拉普拉斯算子 用二阶导
dst=cv.Laplacian(img,-1)
# 展示图像
cv.imshow("img",img)
cv.imshow("dst",dst)
cv.waitKey(0)
cv.destroyAllWindows()
20 图像边缘检测
不是一个算子,是完整的一整套方案。
20.1 高斯滤波
就是去除噪点!
边缘检测本身属于锐化操作,对噪点比较敏感,所以需要进行平滑处理。这里使用的是一个5*5的高斯核对图像进行消除噪声。上一个实验中已经介绍了高斯滤波的具体过程,这里就不再过多叙述,只展示一下用到的5*5的高斯核:
20.2 计算图像的梯度与方向
这里使用了sobel算子来计算图像的梯度值,在上一章节中,我们了解到sobel算子其实就是一个核值固定的卷积核,这里用到了水平方向和垂直方向的两个卷积核。
首先使用sobel算子计算中心像素点的两个方向上的梯度G_{x}和G_{y},然后就能够得到其具体的梯度值:
也可以使用来代替。在OpenCV中,默认使用
来计算梯度值。
然后我们根据如下公式可以得到一个角度值:
这个角度值其实是当前边缘的梯度的方向。通过这个公式我们就可以计算出图片中所有的像素点的梯度值与梯度方向,然后根据梯度方向获取边缘的方向。
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°边缘;
20.3 非极大值抑制(细化边缘)
得到每个边缘的方向之后,其实把它们连起来边缘检测就算完了,但是为什么还有这一步与下一步呢?是因为经过第二步得到的边缘不经过处理是没办法使用的,因为高斯滤波的原因,边缘会变得模糊,导致经过第二步后得到的边缘像素点非常多,因此我们需要对其进行一些过滤操作,而非极大值抑制就是一个很好的方法,它会对得到的边缘像素进行一个排除,使边缘尽可能细一点。
在该步骤中,我们需要检查每个像素点的梯度方向上的相邻像素,并保留梯度值最大的像素,将其他像素抑制为零。假设当前像素点为(x,y),其梯度方向是0°,梯度值为G(x,y),那么我们就需要比较G(x,y)与两个相邻像素的梯度值:G(x-1,y)和G(x+1,y)。如果G(x,y)是三个值里面最大的,就保留该像素值,否则将其抑制为零。
20.4 双阈值筛选
经过非极大值抑制之后,我们还需要设置阈值来进行筛选,当阈值设的太低,就会出现假边缘,而阈值设的太高,一些较弱的边缘就会被丢掉,因此使用了双阈值来进行筛选,推荐高低阈值的比例为2:1到3:1之间,其原理如下图所示:
当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;当幅值低于最低像素时,该像素必不是边缘像素;幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘。也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。
20.5 API和使用
edges = cv2.Canny(image, threshold1, threshold2),即使读到的是彩色图也可以进行处理。
-
image
:输入的灰度/二值化图像数据。 -
threshold1
:低阈值,用于决定可能的边缘点。 -
threshold2
:高阈值,用于决定强边缘点。import cv2 as cv # 读取图像及灰度化处理 img=cv.imread("images/lvbo3.png",cv.IMREAD_GRAYSCALE) # 二值化处理 ret,img_binary=cv.threshold(img,127,255,cv.THRESH_BINARY) # Canny边缘检测 cv2.Canny(原图像,低阈值,高阈值) dst_img=cv.Canny(img_binary,30,70) cv.imshow("dst",dst_img) cv.waitKey(0) cv.destroyAllWindows()
21 绘制图像轮廓
21.1 什么是轮廓
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续,如下图所示。轮廓是一个闭合的、封闭的形状。
-
轮廓的作用:
-
形状分析
-
目标识别
-
图像分割
-
21.2 寻找轮廓
在OpenCV中,使用cv2.findContours()来进行寻找轮廓,其原理过于复杂,这里只进行一个简单的介绍,具体的实现原理可参考:
https://zhuanlan.zhihu.com/p/107257870
寻找轮廓需要将图像做一个二值化处理,并且根据图像的不同选择不同的二值化方法来将图像中要绘制轮廓的部分置为白色,其余部分置为黑色。也就是说,我们需要对原始的图像进行灰度化、二值化的处理,令目标区域显示为白色,其他区域显示为黑色,如下图所示。
之后,对图像中的像素进行遍历,当一个白色像素相邻(上下左右及两条对角线)位置有黑色像素存在或者一个黑色像素相邻(上下左右及两条对角线)位置有白色像素存在时,那么该像素点就会被认定为边界像素点,轮廓就是有无数个这样的边界点组成的。
下面具体介绍一下cv2.findContours()函数,其函数原型为:
contours,hierarchy = cv2.findContours(image,mode,method)
-
返回值:[ 轮廓点坐标 ] 和 [ 层级关系 ]。
-
contours:表示获取到的轮廓点的列表。检测到有多少个轮廓,该列表就有多少子列表,每一个子列表都代表了一个轮廓中所有点的坐标。
-
hierarchy:表示轮廓之间的关系。对于第i条轮廓,hierarchy[i][0], hierarchy[i][1] , hierarchy[i][2] , hierarchy[i][3]分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有相应的轮廓,则对应位置为-1)。该参数的使用情况会比较少。
-
image:表示输入的二值化图像。
-
mode:表示轮廓的检索模式。
-
method:轮廓的表示方法。
21.2.1 mode参数
轮廓查找方式。返回不同的层级关系。
mode参数共有四个选项分别为:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。
-
RETR_EXTERNAL(用的最多)
表示只查找最外层的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
2.3.4.会查找所有轮廓,但会有层级关系。
-
RETR_LIST
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
RETR_CCOMP
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照成对的方式显示。
在 RETR_CCOMP
模式下,轮廓被分为两个层级:
-
层级 0:所有外部轮廓(最外层的边界)。
-
层级 1:所有内部轮廓(孔洞或嵌套的区域)。
RETR_TREE
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。
21.2.2 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这两个选项。
21.3 绘制轮廓
轮廓找出来后,其实返回的是一个轮廓点坐标的列表,因此我们需要根据这些坐标将轮廓画出来,因此就用到了绘制轮廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
-
image:原始图像,一般为单通道或三通道的 numpy 数组。
-
contours:包含多个轮廓的列表,每个轮廓本身也是一个由点坐标构成的二维数组(numpy数组)。
-
contourIdx:要绘制的轮廓索引。如果设为
-1
,则会绘制所有轮廓。根据索引找到轮廓点绘制出来。默认是-1。 -
color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。
-
thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域
import cv2 as cv
# 读图,转灰度
img=cv.imread("images/num.png")
number=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 二值化
ret,binary=cv.threshold(number,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 查找轮廓 cv2.findContours(img,mode(如何找),method(怎么存))
contours,h=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
# 绘制轮廓 cv.drawContours(img(图像要为原图,不能为二值化图像,即不能为binary,contours,-1(要绘制的索引,表示全部查找),color,线条宽度)
cv.drawContours(img,contours,-1,(0,255,0),2)
# 显示图像
cv.imshow("img",img)
cv.waitKey(0)
cv.destroyAllWindows()
print(contours)
print(h)
22 凸包特征检测
在进行凸包特征检测之前,首先要了解什么是凸包。通俗的讲,凸包其实就是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的内容。
一般来说,凸包都是伴随着某类点集存在的,也被称为某个点集的凸包。
对于一个点集来说,如果该点集存在凸包,那么这个点集里面的所有点要么在凸包上,要么在凸包内。
凸包检测常用在物体识别、手势识别、边界检测等领域。
-
穷举法
-
QuickHull法
1.穷举法
-
将集中的点进行两两配对,并进行连线,对于每条直线,检查其余所有的点是否处于该直线的同一侧,如果是,那么说明构成该直线的两个点就是凸包点,其余的线依次进行计算,从而获取所有的凸包点。
-
用向量的思想,点都是有坐标的,连起来就可以构成一个向量。再以其中一个点,连接另一个点,构成另一个向量,让两个向量做外积,就是叉积。也就是std=|向量a|*|向量b|*sin(\theta),能控制std的正负的只能是\theta,如果计算出来的std的正负都相同,说明这些点都在这条直线的同一侧,那么这两个点就是凸包的边界点。然后换两个点,就是说换一条直线,换一个向量,继续进行检测,直到找到凸包的所有的边界点。
-
缺点:时间复杂度高,不断使用for循环,耗时。
2.QuickHull法
-
将所有点放在二维坐标系中,找到横坐标最小和最大的两个点P_1和P_2并连线。此时整个点集被分为两部分,直线上为上包,直线下为下包。
-
以上包为例,找到上包中的点距离该直线最远的点P_3,连线并寻找直线P1P3左侧的点和P2P3右侧的点,然后重复本步骤,直到找不到为止。对下包也是这样操作。
我们以点集来举例,假如有这么一些点,其分布如下图所示:
那么经过凸包检测并绘制之后,其结果应该如下图所示:
可以看到,原图像在经过凸包检测之后,会将最外围的几个点进行连接,剩余的点都在这些点的包围圈之内。那么凸包检测到底是怎么检测出哪些点是最外围的点呢?
我们还是以上面的点集为例,假设我们知道这些点的坐标,那么我们就可以找出处于最左边和最右边的点,如下图所示:
接着将这两个点连接,并将点集分为上半区和下半区,我们以上半区为例:
找到上面这些点离直线最远的点,其中,这条直线由于有两个点的坐标,所以其表示的直线方程是已知的,并且上面的点的坐标也是已知的,那么我们就可以根据点到直线的距离公式来进行计算哪个点到直线的距离最远,假设直线的方程为:A x+B y+C=0,那么点到直线的距离公式为:
然后我们就可以得到距离这条线最远的点,将其与左右两点连起来,并分别命名为y1和y2,如下图所示:
然后分别根据点的坐标求出y1和y2的直线方程,之后将上半区的每个点的坐标带入下面公式中:
当d=0时,表明该点在直线上;当d>0时,表明点在直线的上方,在这里就代表该点在上图所围成的三角形的外面,也就意味着该三角形并没有完全包围住上半区的所有点,需要重新寻找凸包点;当d<0时,表明点在直线的下方,在这里就代表该点在上图所围成的三角形的里面,也就代表着这类点就不用管了。
当出现d>0时,我们需要将出现这种结果的两个计算对象:某点和y1或y2这条线标记,并在最后重新计算出现这种现象的点集到y1或y2的距离来获取新的凸包点的坐标。在本例子中,也就是如下图所示的点和y2这条直线:
由于本例子中只有这一个点在这个三角形之外,所以毫无疑问的它就是一个凸包点,因此直接将它与y2直线的两个端点相连即可。当有很多点在y2直线外时,就需要计算每个点到y2的距离,然后找到离得最远的点与y2构建三角形,并重新计算是否还有点在该三角形之外,如果没有,那么这个点就是新的凸包点,如果有,那就需要重复上面的步骤,直到所有的点都能被包围住,那么构建直线的点就是凸包点。这是上半区寻找凸包点的过程,下半区寻找凸包点的思路与此一模一样,只不过是需要筛选d<0(也就是点在直线的下方)的点,并重新构建三角形,寻找新的凸包点。
上面的过程都是基于我们知道点的坐标进行的,实际上,对于未经处理的图像,我们无法直接获取点的坐标。特别是对于彩色图像,我们需要将其转换为二值图像,并使用轮廓检测技术来获取轮廓边界的点的坐标。然后,我们才能进行上述寻找凸包点的过程。因此,在处理图像时,我们需要将彩色图像转换为二值图像,并通过轮廓检测技术来获取轮廓边界的点的坐标,然后才能进行凸包点的寻找过程。
22.1 获取凸包点
cv2.convexHull(points)
-
points
:输入参数,图像的轮廓22.2 绘制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)
-
image
:要绘制线条的目标图像,它应该是一个OpenCV格式的二维图像数组(如numpy数组)。 -
pts
:一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。 -
isClosed
:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。 -
color
:线条颜色,可以是一个三元组或四元组,分别对应BGR或BGRA通道的颜色值,或者是灰度图像的一个整数值。 -
thickness
(可选):线条宽度,默认值为1。
import cv2 as cv
# 读图
img=cv.imread("images/tu.png")
# 转灰度
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 二值化
ret,binary=cv.threshold(gray,127,255,cv.THRESH_OTSU)# OTSU+阈值法,阈值法不用写,默认为阈值法
# 查找轮廓
contours,h=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
# 寻找凸包 cv2.convexHull(points)
# hull=cv.convexHull(contours[0])# 只有一个外轮廓的时候可以用索引,有多个轮廓就用for循环
for i in range(len(contours)):
hull=cv.convexHull(contours[i])
# 绘制凸包 cv2.polylines(image, pts, isClosed, color, thickness=1)
cv.polylines(img,[hull],True,(0,0,255),2,cv.LINE_AA)
# 显示图像
cv.imshow("img",img)
cv.waitKey(0)
cv.destroyAllWindows()
23 图像轮廓特征查找
图像轮廓特征查找其实就是他的外接轮廓。
-
应用:
-
图像分割
-
形状分析
-
物体检测与识别
-
-
根据轮廓点进行,所以要先找到轮廓。
-
先灰度化、二值化。目标物体白色,非目标物体黑色,选择合适的二值化方式。
-
-
有了轮廓点就可以找到最上、最下、最左、最右的四个坐标。就可以绘制出矩形。
23.1 外接矩形
cv2.boundingRect(轮廓点)
传入轮廓点坐标,返回x,y,h,w
形状的外接矩形有两种,如下图,绿色的叫外接矩形,表示不考虑旋转并且能包含整个轮廓的矩形。其中,外接矩形可根据获得到的轮廓坐标中最上、最下、最左、最右的点的坐标来绘制外接矩形,也就是下图中的绿色矩形。蓝色的是最小外接矩形,会考虑面积。
import cv2 as cv
# 读图
num=cv.imread("images/num.png")
# 拷贝
img=num.copy()
# 灰度化
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 二值化处理:目标物呈白色
ret,binary=cv.threshold(gray,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 查找轮廓
cnts,h=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
# 绘制轮廓
cv.drawContours(num,cnts,-1,(0,0,255),2,cv.LINE_AA)
cv.imshow("num",num)
# 在原图上显示
# cv.waitKey(0)
# cv.destroyAllWindows()
# 绘制外接矩形 x,y,w,h=cv2.boundingRect(轮廓点)
# 循环遍历轮廓列表 这是因为它有多个轮廓
for cnt in cnts:
# x,y坐上坐标 w,h是宽高
x,y,w,h=cv.boundingRect(cnt)
cv.rectangle(num,(x,y),(x+w,y+h),(0,255,0),2,cv.LINE_AA)
# 显示效果,在原图上显示
cv.imshow("num2",num)
cv.waitKey(0)
cv.destroyAllWindows()
23.2 最小外接矩形(外接矩形面积最小)
最小外接矩形就是上图所示的蓝色矩形,寻找最小外接矩形使用的算法叫做旋转卡壳法。下面简单说明一下旋转卡壳法的思路:
在上一章节中,我们了解到了凸包的概念,凸包就是一个点集的凸多边形,它是这个点集所有点的凸壳,点集中所有的点都处于凸包里,构成凸包的点我们叫做凸包点。而旋转卡壳法就是基于凸包点进行的,
旋转卡壳法有一个很重要的前提条件:对于多边形P的一个外接矩形存在一条边与原多边形的边共线。
假设某凸包图如下所示:
根据前提条件,上面的凸多边形的最小外接矩形与凸多边形的某条边是共线的。因此我们只需要以其中的一条边为起始边,然后按照逆时针方向计算每个凸包点与起始边的距离,并将距离最大的点记录下来。
如上图所示,我们首先以a、b两点为起始边,并计算出e点离起始边最远,那么e到起始边的距离就是一个矩形的高度,因此我们只需要再找出矩形的宽度即可。对于矩形的最右边,以向量为基准,然后分别计算凸包点在向量
上的投影的长度,投影最长的凸包点所在的垂直于起始边的直线就是矩形最右边所在的直线。
如上图所示,d点就是在向量
上投影最长的凸包点,那么通过d点垂直于直线ab的直线就是矩形的右边界所在的直线。矩形的左边界的也是这么计算的,不同的是使用的向量不是
而是
。
如上图所示,h点垂直于ab的直线就是以ab为起始边所计算出来的矩形所在的左边界所在的直线。其中矩形的高就是e点到直线ab的距离,矩形的宽是h点在向量上
的投影加上d点在向量
上
的投影减去ab的长度,即:
于是我们就有了以ab为起始边所构成的外接矩形的宽和高,这样就可以得到该矩形的面积。然后再以bc为起始边,并计算其外接矩形的面积。也就是说凸多边形有几个边,就要构建几次外接矩形,然后找到其中面积最小的矩形作为该凸多边形的最小外接矩形。
在OpenCV中,可以直接使用cv2.minAreaRect()来获取最小外接矩形,该函数只需要输入一个参数,就是凸包点的坐标,然后会返回最小外接矩形的中心点坐标、宽高以及旋转角度。通过返回的内容信息,即可绘制凸多边形的的最小外接矩形。
需要使用到的API说明:
-
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,则绘制轮廓点;如果是负数,则填充轮廓内部区域。
import cv2 as cv
import numpy as np
# 读图
num=cv.imread("images/num.png")
# 灰度化
gray=cv.cvtColor(num,cv.COLOR_BGR2GRAY)
# 二值化
ret,binary=cv.threshold(gray,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 查找轮廓
cnts,h=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
# rect=cv2.minAreaRect(cnt),rect:(x,y),w,h,angle
# 循环遍历轮廓列表
for cnt in cnts:
# 绘制外接矩形
x, y, w, h = cv.boundingRect(cnt)
cv.rectangle(num, (x, y), (x + w, y + h), (0, 0, 255), 2, cv.LINE_AA)
hull=cv.convexHull(cnt)
# 绘制凸包
cv.polylines(num,[hull],True,(255,0,0),2,cv.LINE_AA)
rect=cv.minAreaRect(hull)
print(rect)
# cv2.boxPoints(rect),box:返回4行2列数组,每一行代表一个左边的(x,y)
box=cv.boxPoints(rect).astype(np.int32)
print(box)
cv.drawContours(num,[box],-1,(0,255,0),2,cv.LINE_AA)
cv.imshow("num",num)
cv.waitKey(0)
cv.destroyAllWindows()
23.3 最小外接圆
寻找最小外接圆使用的算法是Welzl算法。Welzl算法基于一个定理:希尔伯特圆定理表明,对于平面上的任意三个不在同一直线上的点,存在一个唯一的圆同时通过这三个点,且该圆是最小面积的圆(即包含这三个点的圆中半径最小的圆,也称为最小覆盖圆)。
进一步推广到任意 n 个不在同一圆上的点,总存在一个唯一的最小覆盖圆包含这 n 个点。
若已经存在平面上互不共线(或共圆)的 n 个点,并确定了它们的最小覆盖圆,那么添加第 n+1 个点,并且要求这个点不在原来的最小覆盖圆内(即在圆外),为了使新的包含 n+1 个点的最小覆盖圆的半径增大,新加入的点必须位于由原 n 个点确定的最小覆盖圆的边界上(即圆周上)。这是因为,如果新点在原最小覆盖圆的内部,显然不会影响最小覆盖圆;如果新点在原最小覆盖圆之外但不在圆周上,那么通过新点和至少两个原有圆上的点可以构造出一个更大的圆,这个圆必然比原最小覆盖圆更大,因此不是包含所有 n+1 个点的最小覆盖圆。所以,按照这一逻辑,当第 n+1 个点在原 n 个点的最小覆盖圆外时,确实这个点会位于包含所有 n+1 个点的新最小覆盖圆的圆周上。
有了这个定理,就可以先取3个点建立一个圆(不共线的三个点即可确定一个圆,如果共线就取距离最远的两个点作为直径建立圆),然后遍历剩下的所有点,对于遍历到的点P来说:
如果该点在圆内,那么最小覆盖圆不变。
如果该点在圆外,根据上述定理,该点一定在想要求得的最小覆盖圆的圆周上,又因为三个点才能确定一个圆,所以需要枚举P点之前的点来找其余的两个点。当找到与P点组成的圆能够将所有点都包含在圆内或圆上,该圆就是这些点的最小外接圆。
在OpenCV中,可以直接使用cv2.minEnclosingCircle()来获取最小外接圆,该函数只需要输入一个参数,就是要绘制最小外接圆的点集的坐标,然后会返回最小外接圆的圆心坐标与半径。通过该函数返回的内容信息即可绘制某点集的最小外接圆。如下图所示:
需要使用的API说明
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
# 读图
num=cv.imread("images/num.png")
# 灰度化
gray=cv.cvtColor(num,cv.COLOR_BGR2GRAY)
# 二值化
ret,binary=cv.threshold(gray,127,255,cv.THRESH_OTSU+cv.THRESH_BINARY_INV)
# 查找轮廓
cnts,h=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
# 遍历轮廓列表
for cnt in cnts:
# 获取最小外接圆相关信息:cv2.minEnclosingCircle(cnt),返回值:x,y,radius
(x,y),radius=cv.minEnclosingCircle(cnt)
# 数据取整 tuple没有astype()
x,y,radius=np.int_(x),np.int_(y),np.int_(radius)
# 绘制圆
cv.circle(num,(x,y),radius,(0,255,0),2,cv.LINE_AA)
# 显示图像
cv.imshow("num",num)
cv.waitKey(0)
cv.destroyAllWindows()
24 直方图均衡化
24.1 什么是直方图
直方图是对数据进行统计的一种方法,并且将统计值组织到一系列实现定义好的 bin 当中。其中, bin 为直方图中经常用到的一个概念,可以译为 “直条” 或 “组距”,其数值是从数据中计算出的特征统计量,这些数据可以是诸如梯度、方向、色彩或任何其他特征。
直方图:反映图像像素分布的统计图,横坐标就是图像像素的取值,纵坐标是该像素的个数。也就是对一张图像中不同像素值的像素个数的统计。
增加对比度:黑的更黑,白的更白。
24.2 绘制直方图
就是以像素值为横坐标,像素值的个数为纵坐标绘制一个统计图。
hist=cv2.calcHist(images, channels, mask, histSize, ranges)
-
images
:输入图像列表,可以是一幅或多幅图像(通常是灰度图像或者彩色图像的各个通道)。 -
channels
:一个包含整数的列表,指示在每个图像上计算直方图的通道编号。如果输入图像是灰度图,它的值就是 [0];如果是彩色图像的话,传入的参数可以是 [0],[1],[2] 它们分别对应着通道 B,G,R。 -
mask
(可选):一个与输入图像尺寸相同的二值掩模图像,其中非零元素标记了参与直方图计算的区域,None为全部计算。 -
histSize
:一个整数列表,也就是直方图的区间个数(BIN 的数目)。用中括号括起来,例如:[256]。 -
ranges
:每维数据的取值范围,它是一个二维列表,每一维对应一个通道的最小值和最大值,例如对灰度图像可能是[0, 256]
。
返回值hist 是一个长度为255的数组,数组中的每个值表示图像中对应灰度等级的像素计数
minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(hist)
获取直方图的最小值、最大值及其对应最小值的位置索引、最大值的位置索引
cv2.line(img, pt1, pt2, color, thickness)
-
img:原始图像,即要在上面画线的numpy数组(一般为uint8类型)。
-
pt1 和 pt2:分别为线段的起点和终点坐标,它们都是元组类型,例如
(x1, y1)
和(x2, y2)
分别代表线段两端的横纵坐标。 -
color:线段的颜色,通常是一个包含三个元素的元组
(B, G, R)
表示BGR色彩空间的像素值,也可以是灰度图像的一个整数值。 -
thickness:线段的宽度,默认值是1,如果设置为负数,则线宽会被填充。
import cv2 as cv import numpy as np def draw(image,color): # hist=cv2.calcHist(images, channels, mask, histSize, ranges) # 获取像素值和像素点个数的相关信息 hist = cv.calcHist([image], [0], None, [256], [0, 256]) # print(image,image[0]) # print(hist,len(hist),type(hist)) # minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(hist) # 获取像素点个数的最小和最大值及其位置索引 minval, maxval, minloc, maxloc = cv.minMaxLoc(hist) # 使用np.zeros创建一幅全黑图像,绘制直方图 hist_img = np.zeros((256, 256, 3), np.uint8) # 限制直方图的高,直方图的高最大为背景的90% hpt = int(0.9 * 256) # 使用for循环,遍历像素值对应的像素点的个数,遍历hist,绘制直方图 for h in range(256): print(hist[h],hist[h].item()) intensity = int(hist[h].item()/ maxval * hpt) # print(intensity) # 绘制直方图:使用cv2.line,需要传入起点终点 cv.line(hist_img, (h, 256), (h, 256 - intensity), color, 1) return hist_img if __name__ == "__main__": img = cv.imread("./images/lvbo.png") img2 = cv.imread("images/face.png") dst = draw(img, (0, 0, 255)) dst2 = draw(img2, (0, 255, 0)) cv.imshow("hist", dst) cv.imshow("hist2", dst2) cv.waitKey(0) cv.destroyAllWindows()
24.3 直方图均衡化
一副效果好的图像通常在直方图上的分布比较均匀,直方图均衡化就是用来改善图像的全局亮度和对比度。
如果一幅图像整体很亮,那所有的像素值的取值个数应该都会很高,所以应该把它的直方图做一个横向拉伸,就可以扩大图像像素值的分布范围,提高图像的对比度
通俗的讲,就是遍历图像的像素统计出灰度值的个数、比例与累计比例,并重新映射到0-255范围(也可以是其他范围)内,其实从观感上就可以发现,下面两幅图中前面那幅图对比度不高,偏灰白。
-
直方图均衡化作用:
-
增强对比度
-
提高图像质量
-
可以看到均衡化后图片的亮度和对比度效果明显好于原图。
import cv2 as cv
# 读图
img=cv.imread("images/zhifang.png")
# 灰度化
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 直方图均值化
img_gray=cv.equalizeHist(gray)
cv.imshow("gray",gray)
cv.imshow("img_gray",img_gray)
cv.waitKey(0)
cv.destroyAllWindows()
24.3.1 自适应直方图均衡化
自适应直方图均衡化(Adaptive Histogram Equalization, AHE),通过调整图像像素值的分布,使得图像的对比度和亮度得到改善。
具体过程如下所示:
假设有一个3*3的图像,其灰度图的像素值如上图所示,现在我们要对其进行直方图均衡化,首先就是统计其每个像素值的个数、比例以及其累计比例。如下图所示。
接下来我们就要进行计算,就是将要缩放的范围(通常是缩放到0-255,所以就是255-0)乘以累计比例,得到新的像素值,并将新的像素值放到对应的位置上,比如像素值为50的像素点,将其累计比例乘以255,也就是0.33乘以255得到84.15,取整后得到84,并将84放在原图像中像素值为50的地方,像素值为100、210、255的计算过程类似,最终会得到如下图所示的结果,这样就完成了最基本的直方图均衡化的过程。
dst = cv.equalizeHist(imgGray)
imgGray为需要直方图均衡化的灰度图,返回值为处理后的图像
该方法适用于图像的灰度分布不均匀,且灰度分布集中在更窄的范围,图像的细节不够清晰且对比度较低的情况,然而,传统的直方图均衡化方法会引入噪声,并导致图像中出现过度增强的区域。这是因为直方图均衡化方法没有考虑到图像的局部特征和全局对比度的差异。如下图:
24.3.2 对比度受限的自适应直方图均衡化
很明显,因为全局调整亮度和对比度的原因,脸部太亮,大部分细节都丢失了。自适应均衡化就是用来解决这一问题的:它在每一个小区域内(默认8×8)进行直方图均衡化。当然,如果有噪点的话,噪点会被放大,需要对小区域内的对比度进行了限制,所以这个算法全称叫:对比度受限的自适应直方图均衡化(Contrast Limited Adaptive Histogram Equalization, CLAHE)。
其主要步骤为:
-
图像分块(Tiling):
-
图像首先被划分为多个不重叠的小块(tiles)。这样做的目的是因为在全局直方图均衡化中,单一的直方图无法反映图像各个局部区域的差异性。通过局部处理,AHE能够更好地适应图像内部的不同光照和对比度特性。(tiles 的 大小默认是 8x8)
-
-
计算子区域直方图:
-
对于每个小块,独立计算其内部像素的灰度直方图。直方图反映了该区域内像素值的分布情况。
-
-
子区域直方图均衡化:
-
对每个小块的直方图执行直方图均衡化操作。这涉及重新分配像素值,以便在整个小块内更均匀地分布。均衡化过程会增加低频像素的数量,减少高频像素的数量,从而提高整个小块的对比度。
-
-
对比度限制(Contrast Limiting):
-
如果有噪声的话,噪声会被放大。为了防止过大的对比度增强导致噪声放大,出现了限制对比度自适应直方图均衡化(CLAHE)。CLAHE会在直方图均衡化过程中引入一个对比度限制参数。当某一小块的直方图在均衡化后出现极端值时,会对直方图进行平滑处理(使用线性或非线性的钳制函数),确保对比度增强在一个合理的范围内。
-
-
重采样和邻域像素融合:
-
由于小块之间是不重叠的,直接拼接经过均衡化处理的小块会产生明显的边界效应。因此,在CLAHE中通常采用重采样技术来消除这种效应,比如通过双线性插值将相邻小块的均衡化结果进行平滑过渡,使最终图像看起来更为自然和平滑。
-
-
合成输出图像:
-
将所有小块均衡化后的结果整合在一起,得到最终的自适应直方图均衡化后的图像。
-
clahe = cv2.createCLAHE(clipLimit=None, tileGridSize=None)
-
clipLimit(可选):对比度限制参数,用于控制直方图均衡化过程中对比度增强的程度。如果设置一个大于1的值(如2.0或4.0),CLAHE会限制对比度增强的最大程度,避免过度放大噪声。如果不设置,OpenCV会使用一个默认值。
-
tileGridSize(可选):图像分块的大小,通常是一个包含两个整数的元组,如
(8, 8)
,表示将图像划分成8x8的小块进行独立的直方图均衡化处理。分块大小的选择会影响到CLAHE的效果以及处理速度。
创建CLAHE对象后,可以使用 .apply()
方法对图像进行CLAHE处理:
img=clahe.apply(image)
-
image:要均衡化的图像。
-
img均衡后的图像
import cv2 as cv
from Draw import draw
# 读图
img=cv.imread("images/zhifang.png")
# 灰度化
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 给上面的灰度图绘制直方图
dst_gray=draw(gray,(0,255,0))
# 直方图均衡化
eqh=cv.equalizeHist(gray)
#为上面均衡化的图绘制直方图
dst_eqh=draw(eqh,(0,255,0))
# 对比度受限的自适应直方图均衡化
clahe=cv.createCLAHE(clipLimit=2,tileGridSize=(8,8))
img_clahe=clahe.apply(gray)
dst_clahe=draw(img_clahe,(0,255,0))
# 显示效果
cv.imshow("gray",gray)# 灰度图
cv.imshow("dst_gray",dst_gray)# 灰度图的直方图
cv.imshow("eqh",eqh)# 直方图均衡化
cv.imshow("dst_eqh",dst_eqh)# 直方图均衡化后的直方图
cv.imshow("img_clahe",img_clahe)# 对比度受限的自适应直方图均衡化
cv.imshow("dst_clahe",dst_clahe)# 对比度受限的自适应直方图均衡化的直方图
cv.waitKey(0)
cv.destroyAllWindows()
25 模块匹配
25.1模板匹配
模板匹配就是用模板图(通常是一个小图)在目标图像(通常是一个比模板图大的图片)中不断的滑动比较,通过某种比较方法来判断是否匹配成功,找到模板图所在的位置。
-
不会有边缘填充。
-
类似于卷积,滑动比较,挨个比较象素。
-
返回结果大小是:目标图大小-模板图大小-1。
25.2 匹配方法
res=cv2.matchTemplate(image, templ, method)
-
image:原图像,这是一个灰度图像或彩色图像(在这种情况下,匹配将在每个通道上独立进行)。
-
templ:模板图像,也是灰度图像或与原图像相同通道数的彩色图像。
-
method:匹配方法,可以是以下之一:
-
cv2.TM_CCOEFF
-
cv2.TM_CCOEFF_NORMED
-
cv2.TM_CCORR
-
cv2.TM_CCORR_NORMED
-
cv2.TM_SQDIFF
-
cv2.TM_SQDIFF_NORMED
-
这些方法决定了如何度量模板图像与原图像子窗口之间的相似度。
-
-
返回值res
函数在完成图像模板匹配后返回一个结果矩阵,这个矩阵的大小与原图像相同。矩阵的每个元素表示原图像中相应位置与模板图像匹配的相似度。
匹配方法不同,返回矩阵的值的含义也会有所区别。以下是几种常用的匹配方法及其返回值含义:
-
cv2.TM_SQDIFF
或cv2.TM_SQDIFF_NORMED
:返回值越接近0,表示匹配程度越好。最小值对应的最佳匹配位置。
-
cv2.TM_CCORR
或cv2.TM_CCORR_NORMED
:返回值越大,表示匹配程度越好。最大值对应的最佳匹配位置。
-
cv2.TM_CCOEFF
或cv2.TM_CCOEFF_NORMED
:返回值越大,表示匹配程度越好。最大值对应的最佳匹配位置。
-
25.2.1 平方差匹配
cv2.TM_SQDIFF
以模板图与目标图所对应的像素值使用平方差公式来计算,其结果越小,代表匹配程度越高,计算过程举例如下。
注意:模板匹配过程皆不需要边缘填充,直接从目标图像的左上角开始计算。
25.2.2 归一化平方差匹配
cv2.TM_SQDIFF_NORMED
与平方差匹配类似,只不过需要将值统一到0到1,计算结果越小,代表匹配程度越高,计算过程举例如下。
25.2.4 归一化相关匹配
cv2.TM_CCORR_NORMED
与相关匹配类似,只不过是将其值统一到0到1之间,值越大,代表匹配程度越高,计算过程举例如下。
25.2.6 归一化相关系数匹配
cv2.TM_CCOEFF_NORMED
也是将相关系数匹配的结果统一到0到1之间,值越接近1代表匹配程度越高,计算过程举例如下。
25.3 绘制轮廓
找的目标图像中匹配程度最高的点,我们可以设定一个匹配阈值来筛选出多个匹配程度高的区域。
-
loc=np.where(array > 0.8) #loc包含array中所有大于0.8的元素索引的数组
np.where(condition) 是 NumPy 的一个函数,当条件为真时,返回满足条件的元素的索引。
-
zip(*loc)
-
*loc
是解包操作,将loc
中的多个数组拆开,作为单独的参数传递给zip
。 -
zip
将这些数组按元素一一配对,生成一个迭代器,每个元素是一个元组,表示一个坐标点。x=list([[1,2,3,4,3],[23,4,2,4,2]]) print(list(zip(*x)))#[(1, 23), (2, 4), (3, 2), (4, 4), (3, 2)]
-
import numpy as np
import cv2 as cv
# 读图
img=cv.imread("./images/game.png")# 目标图
temp=cv.imread("./images/temp.png")# 模板图
# 转灰度
img_gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)# 目标图转灰度
temp_gray=cv.cvtColor(temp,cv.COLOR_BGR2GRAY)# 模板图转灰度
# 模板匹配,拿到匹配结果,返回匹配程度矩阵
# res=cv2.matchTemplate(img,temp,method)
res=cv.matchTemplate(img_gray,temp_gray,cv.TM_CCOEFF_NORMED)
# print(res)
# 设置阈值,使用np.where获取符合条件的坐标
threshold=0.8
loc=np.where(res>=0.8)# 会获取匹配程度大于等于阈值的结果的索引位置 [[y1,y2,...,yn],[x1,x2,...,xn]]
# print(loc,type(loc),res[loc[0][0],loc[1][0]])
# print(zip(*loc))
# 获取模板宽高
h,w=temp.shape[:2]
# 解包,拿坐标
for pt in zip(*loc):# pt===》(y,x)
left_upper=pt[::-1]# (x,y)
right_bottom=(pt[1]+w,pt[0]+h) # (x+w,y+h)
# 绘制出匹配的部分,框出来
cv.rectangle(img,left_upper,right_bottom,(0,255,0),2,cv.LINE_AA)
cv.imshow("img",img)
cv.waitKey(0)
cv.destroyAllWindows()
26 霍夫变换
26.1 理解霍夫变换
-
霍夫变换是图像处理的一种技术,主要用于检测图像中的直线、圆等几何形状。基本思想就是将图像空间中的点映射到参数空间中,通过在参数空间中寻找累计最大值实现对特定形状的检测。
26.2 霍夫直线变换
对于一条直线(不垂直于x轴的直线),都可以用y=k x+b来表示,此时,x和y是横纵坐标,k和b是一个固定参数。当我们换一种方式来看待这个式子,我们就可以得到:
此时,以k和b 为横纵坐标,x和y为固定参数,也就是说k和b成了自变量和因变量,变换如下图所示:
从上图可以看出,在直角坐标系下的一个直线,在变换后的空间中仅仅表示为一点,对于变换后的空间,我们称其为霍夫空间,也就是参数空间。也就是说,直角坐标系下的一条直线对应了霍夫空间中的一个点。类似的,霍夫空间中的一条直线也对应了直角坐标系中的一个点,如下图所示:
那么对于一个二值化后的图形来说,其中的每一个目标像素点(这里假设目标像素点为白色像素点)都对应了霍夫空间的一条直线,当霍夫空间中有两条直线相交时,就代表了直角坐标系中某两个点所构成的直线。而当霍夫空间中有很多条线相交于一点时,说明直角坐标系中有很多点能构成一条直线,也就意味着这些点共线,因此我们就可以通过检测霍夫空间中有最多直线相交的交点来找到直角坐标系中的直线。
然而对于x=1这种直线(垂直于x轴)来说,y已经不存在了,斜率无穷大,无法映射到霍夫空间中去,那么就没办法使用上面的方法进行检测了,为了解决这个问题,我们就将直角坐标系转化为极坐标系,然后通过极坐标系与霍夫空间进行相互转化。
在极坐标系下是一样的,极坐标中的点对于霍夫空间中的线,霍夫空间中的点对应极坐标中的直线。并且此时的霍夫空间不再是以k为横轴、b为纵轴,而是以为θ横轴、ρ(上图中的r)为纵轴。上面的公式中,x、y是直线上某点的横纵坐标(直角坐标系下的横纵坐标),和是极坐标下的坐标,因此我们只要知道某点的x和y的坐标,就可以得到一个关于θ-ρ的表达式,如下图所示:
根据上图,霍夫空间在极坐标系下,一点可以产生一条三角函数曲线,而多条这样的曲线可能会相交于同一点。因此,我们可以通过设定一个阈值,来检测霍夫空间中的三角函数曲线相交的次数。如果一个交点的三角函数曲线相交次数超过阈值,那么这个交点所代表的直线就可能是我们寻找的目标直线。
使用API
lines=cv2.HoughLines(image, rho, theta, threshold)
-
image
:输入图像,通常为二值图像,其中白点表示边缘点,黑点为背景。经过边缘检测后的图像 -
rho
:r的精度,以像素为单位,表示霍夫空间中每一步的距离增量, 值越大,考虑越多的线。 -
theta
:角度θ的精度,通常以弧度为单位,表示霍夫空间中每一步的角度增量。值越小,考虑越多的线。 -
threshold
:累加数阈值,只有累积投票数超过这个阈值的候选直线才会被返回。
返回值:cv2.HoughLines
函数返回一个二维数组,每一行代表一条直线在霍夫空间中的参数 (rho, theta)
。
import cv2 as cv
import numpy as np
# 读图
img=cv.imread("images/huofu.png")
# 转灰度图
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 边缘检测
dst=cv.Canny(gray,30,70)
# 霍夫直线变换:lines=cv2.HoughLines(image, rho, theta, threshold)
lines=cv.HoughLines(dst,0.8,np.pi/180,90)
print(lines)
#循环遍历lines,去取每条直线的rho和theta
for line in lines:
# print(line)
rho,theta=line[0]
# print(rho,theta)
sin_theta=np.sin(theta)
cos_theta=np.cos(theta)
x1,x2=0,img.shape[1]
y1=int((rho-x1*cos_theta)/sin_theta)
y2=int((rho-x2*cos_theta)/sin_theta)
cv.line(img,(x1,y1),(x2,y2),(0,255,0))
cv.imshow("img",img)
cv.imshow("dst",dst)
cv.waitKey(0)
cv.destroyAllWindows()
26.3 统计概率霍夫直线变换
前面的方法又称为标准霍夫变换,它会计算图像中的每一个点,计算量比较大,另外它得到的是整一条线(r和θ),并不知道原图中直线的端点。所以提出了统计概率霍夫直线变换(Probabilistic Hough Transform),是一种改进的霍夫变换,它在获取到直线之后,会检测原图中在该直线上的点,并获取到两侧的端点坐标,然后通过两个点的坐标来计算该直线的长度,通过直线长度与最短长度阈值的比较来决定该直线要不要被保留。
使用API
lines=cv2.HoughLinesP(image, rho, theta, threshold, lines=None, minLineLength=0, maxLineGap=0)
-
image
:输入图像,通常为二值图像,其中白点表示边缘点,黑点为背景。 -
rho
:极径分辨率,以像素为单位,表示极坐标系中的距离分辨率。 -
theta
:极角分辨率,以弧度为单位,表示极坐标系中角度的分辨率。 -
threshold
:阈值,用于过滤掉弱检测结果,只有累计投票数超过这个阈值的直线才会被返回。 -
lines
(可选):一个可初始化的输出数组,用于存储检测到的直线参数。 -
minLineLength
(可选):最短长度阈值,比这个长度短的线会被排除。 -
maxLineGap
(可选):同一直线两点之间的最大距离。当霍夫变换检测到一系列接近直角的线段时,这些线段可能是同一直线的不同部分。maxLineGap
参数指定了在考虑这些线段属于同一直线时,它们之间最大可接受的像素间隔。
返回值lines:cv2.HoughLinesP
函数返回一个二维数组,每个元素是一个包含4个元素的数组,分别表示每条直线的起始点和结束点在图像中的坐标(x1, y1, x2, y2)。
import cv2 as cv
import numpy as np
# 读图
img=cv.imread("images/huofu.png")
# 转灰度化
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 边缘检测
dst=cv.Canny(gray,30,70)
# 统计概率霍夫直线检测 lines=cv2.HoughLinesP(image, rho, theta, threshold, lines=None, minLineLength=0, maxLineGap=0)
lines=cv.HoughLinesP(dst,0.8,np.pi/180,90,minLineLength=50,maxLineGap=10)
# for循环遍历所有lines,取出每个直线的x,y
for line in lines:
x1,y1,x2,y2=line[0]
cv.line(img,(x1,y1),(x2,y2),(0,255,0),1,cv.LINE_AA)
cv.imshow("img",img)
cv.waitKey(0)
cv.destroyAllWindows()
26.4 霍夫圆变换
霍夫圆变换跟直线变换类似,它可以从图像中找出潜在的圆形结构,并返回它们的中心坐标和半径。只不过线是用(r,θ)表示,圆是用(x_center,y_center,r)来表示,从二维变成了三维,数据量变大了很多;所以一般使用霍夫梯度法减少计算量。
使用API
circles=cv2.HoughCircles(image, method, dp, minDist, param1, param2)
-
image
:输入图像,通常是灰度图像。 -
method
:使用的霍夫变换方法:霍夫梯度法,可以是cv2.HOUGH_GRADIENT
,这是唯一在OpenCV中用于圆检测的方法。 -
dp
:累加器分辨率与输入图像分辨率之间的降采样比率,用于加速运算但不影响准确性。设置为1表示霍夫梯度法中累加器图像的分辨率与原图一致 -
minDist
:检测到的圆心之间的最小允许距离,以像素为单位。在霍夫变换检测圆的过程中,可能会检测到许多潜在的圆心。minDist
参数就是为了过滤掉过于接近的圆检测结果,避免检测结果过于密集。当你设置一个较小的minDist
值时,算法会尝试找出尽可能多的圆,即使是彼此靠得很近的圆也可能都被检测出来。相反,当你设置一个较大的minDist
值时,算法会倾向于只检测那些彼此间存在一定距离的独立的圆。 -
param1
和param2
:这两个参数是在使用cv2.HOUGH_GRADIENT
方法时的特定参数,分别为:-
param1
(可选):阈值1,决定边缘强度的阈值。 -
param2
:阈值2,控制圆心识别的精确度。较大的该值会使得检测更严格的圆。param2
通常被称为圆心累积概率的阈值。在使用霍夫梯度方法时,param2
设置的是累加器阈值,它决定了哪些候选圆点集合被认为是有效的圆。较高的param2
值意味着对圆的检测更严格,只有在累加器中积累了足够高的响应值才认为是真实的圆;较低的param2
值则会降低检测的门槛,可能会检测到更多潜在的圆,但也可能包含更多的误检结果。
-
返回值:cv2.HoughCircles
返回一个二维numpy数组,包含了所有满足条件的圆的参数。
import cv2 as cv
import numpy as np
# 读图
img=cv.imread("images/huofu.png")
# 转灰度
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# 边缘检测
dst=cv.Canny(gray,30,70)
# 霍夫圆变换
circles=cv.HoughCircles(dst,cv.HOUGH_GRADIENT,1,20,param2=30)
# 将circles中的数据整数化,np.around四舍五入,np.int整数化
circles=np.int_(np.around(circles))
# for循环遍历,得到圆心和半径
for circle in circles:
x,y,r=circle[0]
cv.circle(img,(x,y),r,(0,255,0),1,cv.LINE_AA)
cv.imshow("img",img)
cv.waitKey(0)
cv.destroyAllWindows()
27 图像亮度变换
我们知道,图像都是由一个个像素值组成的,图像的亮度变换实际上还是图像像素值的变换。
27.1 亮度变换
在讲解亮度时,需要和对比度一起来进行解释。
对比度调整:图像暗处像素强度变低,图像亮处像素强度变高,从而拉大中间某个区域范围的显示精度。
亮度调整:图像像素强度整体变高或者变低。
上图中,(a)把亮度调高,就是图片中的所有像素值加上了一个固定值;(b)把亮度调低,就是图片中的所有像素值减去了一个固定值;(c)增大像素对比度(白的地方更白,黑的地方更黑);(d)减小像素对比度(整幅图都趋于一个颜色);
OpenCV调整图像对比度和亮度时,公式为:。但是不能浅显的讲
是控制对比度,
是控制亮度的。
对比度:需要通过、
一起控制。
亮度:通过控制。
import cv2 as cv
import numpy as np
# 读图
img=cv.imread("images/1.jpg")
# 使用cv2.addWeighted(src1,alpha,src2,beta,gamma)
dst=cv.addWeighted(img,1,np.zeros_like(img),1,0)
# 显示效果
cv.imshow("img",img)
cv.imshow("dst",dst)
cv.waitKey(0)
cv.destroyAllWindows()
27.3 直接像素值修改
如果只需要增加或减少固定的亮度值,可以直接遍历图像像素并对每个像素值进行加减操作。
使用的API:
numpy.clip(a, a_min, a_max)
用于对数组中的元素进行限定,将超出指定范围的元素值截断至指定的最小值和最大值之间
-
a
:输入数组。 -
a_min
:指定的最小值,数组中所有小于a_min
的元素将被替换为a_min
。 -
a_max
:指定的最大值,数组中所有大于a_max
的元素将被替换为a_max
。
import cv2 as cv
import numpy as np
'''
原图:img
调整后的:dst=img+p(p>0 p<=0)
防止溢出:np.clip(dst,0,255)p由滑条控制,p就相当于滑条中的value需要把滑条中的值映射到[-255,255]
用一个滑条来控制p :cv2.createTrackbar(trackbar_name,window_name,value,max_val,def) min_val=0
'''
# 给滑条创建窗口
window_name="Trackbar"
cv.namedWindow(window_name)
# 写一个改变图像亮度的方法
def change(p):
# 读图
img=cv.imread("images/1.jpg")
# 把滑条从[0,255]映射到[-255,255]
p=p/255*(255-(-255))-255
# 亮度变换
dst=np.uint8(np.clip(img.astype(int)+p,0,255))
cv.imshow("img",img)
cv.imshow("dst",dst)
#创建滑条,设置参数
max_val=255#滑条最大值
trackbar_name="p_value"#滑条名
trackbar_value=150# 滑条初始值
change(trackbar_value)
cv.createTrackbar(trackbar_name,window_name,trackbar_value,255,change)
cv.waitKey(0)
cv.destroyAllWindows()
28 形态学变换
形态学变换(Morphological Transformations)是一种基于形状的简单变换,它的处理对象通常是二值化图像。形态学变换有两个输入,一个输出:输入为原图像、核(结构化元素),输出为形态学变换后的图像。其基本操作有腐蚀和膨胀,这两种操作是相反的,即较亮的像素会被腐蚀和膨胀。下面我们来说一下核、腐蚀与膨胀的概念。
28.1 核
自适应二值化中,我们已经接触过核了,还记得吗?就是那个在原图中不断滑动计算的3*3的小区域,那其实就是一个3*3的核。
核(kernel)其实就是一个小区域,通常为3*3、5*5、7*7大小,有着其自己的结构,比如矩形结构、椭圆结构、十字形结构,如下图所示。通过不同的结构可以对不同特征的图像进行形态学操作的处理。
28.2 腐蚀
腐蚀操作就是使用核在原图(二值化图)上进行从左到右、从上到下的滑动(也就是从图像的左上角开始,滑动到图像的右下角)。在滑动过程中,令核值为1的区域与被核覆盖的对应区域进行相乘,得到其最小值,该最小值就是卷积核覆盖区域的中心像素点的新像素值,接着继续滑动。由于操作图像为二值图,所以不是黑就是白,这就意味着,在被核值为1覆盖的区域内,只要有黑色(像素值为0),那么该区域的中心像素点必定为黑色(0)。这样做的结果就是会将二值化图像中的白色部分尽可能的压缩,如下图所示,该图经过腐蚀之后,“变瘦”了。
在腐蚀操作的详细流程中,遍历图像的过程如下:
-
初始化:
-
设置一个起始位置(通常从图像的左上角开始)。
-
准备好结构元素(structuring element),它是一个小的矩阵,大小通常是奇数,并且有一个明确的中心点。
-
-
逐像素处理: 对于输入图像中的每一个像素,执行以下步骤:
a. 定位: 将结构元素移动到当前待处理像素的位置,使得结构元素的中心与该像素对齐。
b. 区域覆盖: 结构元素会覆盖图像上的一个局部邻域,这个邻域由结构元素的尺寸决定。
c. 条件检查: 检查结构元素覆盖区域内所有图像像素的颜色。对于二值图像来说,就是看这些像素是否都是白色(前景像素)。如果所有被结构元素覆盖的像素均为白色,则继续下一个步骤;否则,跳过此步骤,将中心像素视为背景像素。
d. 侵蚀决策: 如果结构元素覆盖的所有像素都是白色,则原图像中的中心像素保持不变(在输出图像中仍为白色);否则,将中心像素变为黑色(在输出图像中变为背景色)。
-
迭代移动: 结构元素沿着图像从左到右、从上到下逐行逐列地移动,重复上述过程,直到整个图像都被结构元素遍历过。
-
循环处理: 如果指定了多个迭代次数,那么在整个图像完成一次遍历后,再次从头开始进行同样的遍历和侵蚀决策,直到达到指定的迭代次数。
通过这样的遍历方式,腐蚀操作能够逐步收缩目标物体边界,消除孤立的噪声像素以及细化连续的前景区域。
总结:二值图腐蚀后白色像素(非0)变少了
28.3 膨胀
膨胀与腐蚀刚好相反,膨胀操作就是使用核在原图(二值化图)上进行从左到右、从上到下的滑动(也就是从图像的左上角开始,滑动到图像的右下角),在滑动过程中,令核值为1的区域与被核覆盖的对应区域进行相乘,得到其最大值,该最大值就是核覆盖区域的中心像素点的新像素值,接着继续滑动。由于操作图像为二值图,所以不是黑就是白,这就意味着,在卷积核覆盖的区域内,只要有白色(像素值为255),那么该区域的中心像素点必定为白色(255)。这样做的结果就是会将二值化图像中的白色部分尽可能的扩张,如下图所示,该图经过膨胀之后,“变胖”了。
在膨胀操作的详细流程中,遍历图像的过程如下:
-
初始化:
-
设置一个起始位置(通常从图像的左上角开始)。
-
准备好结构元素(structuring element),它是一个小的矩阵,大小通常是奇数,并且有一个明确的中心点。
-
-
逐像素处理: 对于输入图像中的每一个像素,执行以下步骤:
a. 定位: 将结构元素移动到当前待处理像素的位置,使得结构元素的中心与该像素对齐。
b. 区域覆盖: 结构元素会覆盖图像上的一个局部邻域,这个邻域由结构元素的尺寸决定。
c. 条件检查: 检查结构元素覆盖区域内是否存在白色(前景)像素。对于二值图像来说,如果有任何一个被结构元素覆盖的像素是白色的,则继续下一步;否则,将中心像素保持原样(黑色或非目标物体像素不变)。
d. 膨胀决策: 如果在结构元素覆盖的范围内找到了至少一个白色像素,则无论原中心像素是什么颜色,都将输出图像中的该中心像素设置为白色(前景色)。这表示即使原中心像素可能是背景像素,但只要其周围有白色像素存在,就认为该位置也应属于前景区域。
e. 更新输出: 根据上述判断结果更新输出图像对应位置的像素值。
-
迭代移动: 结构元素沿着图像从左到右、从上到下逐行逐列地移动,重复上述过程,直到整个图像都被结构元素遍历过。
-
循环处理: 如果指定了多个迭代次数,那么在整个图像完成一次遍历后,再次从头开始进行同样的遍历和膨胀决策,直到达到指定的迭代次数。
通过这样的遍历方式,膨胀操作能够逐步扩大目标物体边界,连接断裂的前景部分,并填充内部空洞,使得物体轮廓更加明显且连续。
注意 此案例代表灰度图
总结:二值图膨胀后白色像素变多了
28.4 开运算
开运算是先腐蚀后膨胀,其作用是:分离物体,消除小区域。特点:消除噪点,去除小的干扰块,而不影响原来的图像
28.5 闭运算
闭运算与开运算相反,是先膨胀后腐蚀,作用是消除/“闭合”物体里面的孔洞,特点:可以填充闭合区域
28.6 礼帽运算
原图像与“开运算“的结果图之差,因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。
礼帽运算用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用礼帽运算进行背景提取
28.7 黑帽运算
黑帽运算为”闭运算“的结果图与原图像之差,
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
黑帽运算用来分离比邻近点暗一些的斑块
28.8 形态学梯度
形态学梯度是一个基于结构元素的图像处理方法,它通过比较原图像与膨胀图和腐蚀图之间的差异来突出图像边缘特征。具体来说,对于图像中的每个像素点,其形态学梯度值是该像素点在膨胀后的图像值与其在腐蚀后的图像值之差。这样得到的结果通常能够强化图像的边缘信息,并且对噪声有一定的抑制作用
import cv2 as cv
import numpy as np
long=cv.imread("../day12_code/images/long.png")
car1=cv.imread("../day12_code/images/car.png")
_,car=cv.threshold(car1,127,255,cv.THRESH_BINARY_INV)
#定义一个5x5的卷积核
kernel=np.ones((5,5),np.uint8)
# print(kernel)
# 腐蚀操作:cv2.erode(img,kernel,iterations=1)iterations:迭代次数,默认值为1
erosion=cv.erode(car,kernel=kernel,iterations=1)
# erosion2=cv.erode(long,kernel=kernel,iterations=2)
# cv.imshow("long",long)
cv.imshow("erosion",erosion)
# cv.imshow("erosion2",erosion2)
# 膨胀操作:cv2.dilate(img,kernel,iteration=1)
dilation=cv.dilate(car,kernel=kernel,iterations=1)
cv.imshow("dilation",dilation)
# cv.morphologyEx(img,method,kernl)
# 开运算(先腐蚀 后膨胀): cv.morphologyEx(img,cv.MORPH_OPEN,kernl)
opening=cv.morphologyEx(car,cv.MORPH_OPEN,kernel=kernel)
cv.imshow("car",car)
# cv.imshow("open",opening)
# 闭运算(先膨胀后腐蚀):cv.morphologyEx(img,cv.MORPH_CLOSE,kernl)
closing=cv.morphologyEx(car,cv.MORPH_CLOSE,kernel=kernel)
# cv.imshow("clolse",closing)
# 礼帽运算(原图和开运算的差):cv.morphologyEx(img,cv.MORPH_TOPHAT,kernl)
tophat=cv.morphologyEx(car,cv.MORPH_TOPHAT,kernel=kernel)
# cv.imshow("top",tophat)
# 黑帽运算(闭运算和原图之差):cv.morphologyEx(img,cv.MORPH_BLACKHAT,kernl)
blackhat=cv.morphologyEx(car,cv.MORPH_BLACKHAT,kernel=kernel)
# cv.imshow("black",blackhat)
# 形态学梯度(膨胀和腐蚀的差):cv.morphologyEx(img,cv.MORPH_GRADIENR,kernl)
gradient=cv.morphologyEx(car,cv.MORPH_GRADIENT,kernel=kernel)
cv.imshow("gradient",gradient)
cv.waitKey(0)
cv.destroyAllWindows()