非科班ai硕士,从零自学,先从CV开始吧
浅浅记录一下自己的学习笔记以及心得,欢迎补充和批评交流!
今天是唐宇迪老师的OpenCV第一节课,环境配置什么的之前有经验所以跳过一下
2023.3.24
import cv2
print(cv2.__version__)
我配置的是当下最新版4.7.0,唐老师说3.4.1以后的版本有些功能可能会不可用,先用着再说吧
- cv2.IMREAD_COLOR:彩色图像
- cv2.IMREAD_GRAYSCALE:灰度图像
导入彩色图片:
img=cv2.imread('cat.jpg')
此时导入的img是一个ndarray格式的矩阵,尺寸为414*500*3,dtype=‘unit-8’(0-255),3是BGR三通道,顺序与matplotlib相反,当对图像可视化时最好使用OpenCV内部函数,否则图像会很奇怪
导入灰度图:
img=cv2.imread('cat.jpg',cv2.IMREAD_GRAYSCALE)
此时导入的img是一个灰度图,尺寸为414*500
cv2.imshow('image',img)
# 等待时间,毫秒级,0表示任意键终止
cv2.waitKey(0)
cv2.destroyAllWindows()
在cv2.imshow()中传入两个参数,第一个是展示的类型,第二个是传入的变量
cv.waitKey()中传入等待时间,达到时间后执行下一步cv2.destroyAllWindows()关闭所有展示窗口,当等待时间设置为0时操作键盘任意键终止
彩色图片展示如下:
灰度图展示如下:
cv2.imwrite('mycat.png',img)
cv2.VideoCapture()可以捕捉摄像头,也可以直接导入视频文件
vc = cv2.VideoCapture('test.mp4')
此时对vc检查是否导入,以及导入的数据是否正确
if vc.isOpened():
oepn, frame = vc.read()
else:
open = False
如果导入成功则vc.isOpen()==True,如果打开的数据正确,则vc.read()返回的open==True,frame为第一帧的画面
视频逐帧展示(视频经过灰度处理,每帧之间时长为10ms,用Esc操作退出展示):
while open:
ret, frame = vc.read()
if frame is None:
break
if ret == True:
# cv2.COLOR_BGR2GRAY是灰度处理
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
cv2.imshow('result', gray)
# 27是指操作退出键Esc关闭视频
if cv2.waitKey(10) & 0xFF == 27:
break
vc.release()
cv2.destroyAllWindows()
vc.release()是用来释放空间的
提取部分尺寸的图像ROI(Region of Interest):切片即可
img=cv2.imread('cat.jpg')
cat=img[0:200,0:200]
cv_show('cat',cat)
结果如下:
其中cv_show()是前文定义的函数
提取特定通道的图像:
b,g,r = cv2.split(img)
#或
b = img[::0]
g = img[::1]
r = img[::2]
将图像分割为BGR三个通道的数据,每个的尺寸为414*500
如果已有三个通道分别的数据,要合并为完整图像,需要用一个cv2.merge()方法
img=cv2.merge((b,g,r))
对于有的场景,需要对图片进行便于填充(类似于padding):
top_size,bottom_size,left_size,right_size = (50,50,50,50)
replicate = cv2.copyMakeBorder(img,
top_size, bottom_size, left_size, right_size,
borderType=cv2.BORDER_REPLICATE)
reflect = cv2.copyMakeBorder(img,
top_size, bottom_size, left_size, right_size,
cv2.BORDER_REFLECT)
reflect101 = cv2.copyMakeBorder(img,
top_size, bottom_size, left_size, right_size,
cv2.BORDER_REFLECT_101)
wrap = cv2.copyMakeBorder(img,
top_size, bottom_size, left_size, right_size,
cv2.BORDER_WRAP)
constant = cv2.copyMakeBorder(img,
top_size, bottom_size, left_size, right_size,
cv2.BORDER_CONSTANT, value=0)
一共五种填充方式,先规定上下左右的填充尺寸,再使用cv2.copyMakeBorder()进行填充
- cv2.BORDER_REPLICATE:复制法,也就是复制最边缘像素。
- cv2.BORDER_REFLECT:反射法,对图像中的像素在两边进行复制
- cv2.BORDER_REFLECT_101:反射法,也就是以最边缘像素为轴,对称填充
- cv2.BORDER_WRAP:外包装法
- cv2.BORDER_CONSTANT:常量法,常数值填充。
填充的效果如下:
为了方便画子图展示,使用matplotlib绘制,由于matplotlib输入时RGB通道,与OpenCVBGR不同,所以效果有些不同
图像数值计算有尺寸要求,两张图片的数据相加需要其尺寸一致,否则报错
OpenCV中的图像数据结构是ndarray,因此可以直接进行四则运算,但由于图像的像素点数值只能在0-255之间,因此溢出的情况会被处理回这个范围,例如150+150=44(300%256),10-20=246(-10%256或-10+256)
另外,OpenCV还有另外一种运算,例如cv2.add(),经过这种计算的溢出的情况会被处理为最大/最小值,例如150+150=255,10-20=0
有时必须要将两个不同尺寸的图像进行运算时,使用cv2.resize()方法即可对其中一张图像进行resize操作,与numpy的reshape不同的是,这里的cv2.resize()不需要总数据个数满足新尺寸,可以更加灵活地resize(不满足时可能会产生裁剪一类的操作)
img_dog = cv2.resize(img_dog, (500, 414))
经过上面的处理,img_dog的尺寸从429*499*3变为414*500*3
同时cv2.resize()还可以对图像进行缩放:
res = cv2.resize(img, (0, 0), fx=4, fy=1)
上面的处理可以将img图像在x轴上放大4倍,结果如下图所示:
对于图像的加法运算,由于溢出机制,很难将两张图像融合在一起,例如下面情况:
为了更好地融合图像,可以使用cv2.addWeighted() 方法,例如:
res = cv2.addWeighted(img_cat, 0.4, img_dog, 0.6, 0)
即为 img_cat * 0.4 + img_dog * 0.6 + 0 的合成图像,效果如下:
ret, dst = cv2.threshold(img_gray, thresh=127, maxval=255,
type=cv2.THRESH_BINARY)
dst为阈值操作后的结果,ret为阈值,type决定了阈值操作的方法,所有type如下所示:
- cv2.THRESH_BINARY 超过阈值部分取maxval(最大值),否则取0
- cv2.THRESH_BINARY_INV THRESH_BINARY的反转
- cv2.THRESH_TRUNC 大于阈值部分设为阈值,否则不变
- cv2.THRESH_TOZERO 大于阈值部分不改变,否则设为0
- cv2.THRESH_TOZERO_INV THRESH_TOZERO的反转
经过上面的五种阈值操作,图像展示如下:
当遇到图片像素点存在噪声时,需要对图像进行平滑处理,将噪声填平,又叫滤波操作
在OpenCV中有很多滤波方式,下面依次进行介绍:
原始图像如下图所示,图像中存在一些白点作为噪声
1)均值滤波
方法是采用一个全1卷积核对图像进行卷积处理,卷积运算过程中需要取均值
blur = cv2.blur(img, (3, 3))
函数的第二个参数是卷积核的尺寸,一般为3*3或5*5
下面是效果展示:
2)方框滤波
与均值滤波基本一致,不同的是①方框滤波可以对图像输出的尺寸作规定,一般为-1表示自动将输出调整为输入的尺寸,②方框滤波可以选择是否进行归一化(即是否在卷积加和时取均值,如果为False则将溢出值截断为0-255)
box = cv2.boxFilter(img,-1,(3,3), normalize=True)
normalize=True时的效果:
normalize=False的效果:
3)高斯滤波
同样是采用卷积操作来平滑图像,不同的是此时的卷积核不是全1了,而是高斯随机数,且越接近卷积核的中央,随机数越大,这样卷积出来的结果更加关注中心点的数据
aussian = cv2.GaussianBlur(img, (5, 5), 1)
此时平滑的效果:
4)中值滤波
使用每次卷积的范围内数据中值作为卷积结果,因为噪声点的像素值多半偏大/偏小,所以中值滤波可以很大程度滤去噪声点,平滑效果最好
median = cv2.medianBlur(img, 5)
效果展示如下:
将均值、高斯、中值三种滤波方式的结果一起对比:
该展示方式是利用numpy将三个图像的数据矩阵拼接在一起,再用cv2中的函数进行展示
当图像存在一些毛刺边缘时,可以使用腐蚀操作进行改善
原图片:
经过如下腐蚀操作:
kernel = np.ones((3,3),np.uint8)
erosion = cv2.erode(img,kernel,iterations = 1)
kernel是指定卷积核尺寸,iterations是要迭代几次腐蚀操作,每次迭代都会加深腐蚀效果,上面代码处理之后的效果图:
毛刺被完美消除了,但文字也受到了腐蚀,笔画变细了
对一个圆的腐蚀效果:
原图
腐蚀效果图(从左到右依次是iterations=1,2,3):
对于被腐蚀后,或是信息较为不足的图像,可以使用膨胀操作位图像添加信息,如对于上面提到的毛刺腐蚀导致笔画变细的情况便可以使用膨胀修复(与腐蚀完全是逆运算关系):
kernel = np.ones((3,3),np.uint8)
dige_dilate = cv2.dilate(dige_erosion,kernel,iterations = 1)
腐蚀后:
膨胀后:
对圆的膨胀效果(从左到右依次是iterations=1,2,3):
开运算 = 先腐蚀 + 后膨胀
kernel = np.ones((5,5),np.uint8)
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
闭运算 = 先膨胀 + 后腐蚀
kernel = np.ones((5,5),np.uint8)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
梯度运算 = 膨胀 - 腐蚀
kernel = np.ones((7,7),np.uint8)
gradient = cv2.morphologyEx(pie, cv2.MORPH_GRADIENT, kernel)
礼帽 = 原始输入 - 开运算
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
黑帽 = 闭运算 - 原始输入
blackhat = cv2.morphologyEx(img,cv2.MORPH_BLACKHAT, kernel)
(1)高斯金字塔:
下采样:先将图像与上面卷积核进行卷积,再去除偶数行和列,最终得到原来尺寸一半的图像
up=cv2.pyrUp(img)
上采样:先将图像扩大两倍(一个点变成4个点,其余部分填充为0),再和上面卷积核进行卷积
down=cv2.pyrDown(img)
高斯上采样和下采样都会造成一定程度的信息丢失,下面展示一张图经过上采样再下采样后的图片:
(2)拉普拉斯金字塔:
使用卷积方法将图片进行低通滤波后,先下采样后上采样,再与原图片作差
down=cv2.pyrDown(img)
down_up=cv2.pyrUp(down)
l_1=img-down_up
结果展示如下:
根据卷积运算的特点,将Sobel算子作为卷积核可以有效对比卷积核扫描到区域的两侧差值,从而得到边缘。
使用上面两个算子作为卷积核,将分别检测竖直边缘和水平边缘,在OpenCV中提供了相关函数如下:
dst = cv2.Sobel(src, ddepth, dx, dy, ksize)
其中src为图像数据矩阵,ddepth为检测深度,一般取-1,dx和dy分别设置了水平梯度阶数(检测竖直边缘)和竖直梯度阶数(检测水平边缘),ksize为卷积核尺寸
接下来使用Sobel算子对一个圆形进行边缘检测:
通过以下代码将上图输入OpenCV边缘检测:
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
设置bx=1,by=0,因此是采用Gx作为卷积核,进行竖直边缘检测,令ddepth=cv2.CV_64F是为了避免梯度为负的情况,八位图像会将梯度为负的情况直接规整为0,不利于边缘检测(负梯度也是边缘,为0就显示不出来了),因此cv2.CV_64F会先将梯度转换为更高的数据类型,等后续求绝对值后再映射回八位图,才能得到完整梯度
先使用不取绝对值的处理进行边缘检测,结果展示如下 :
因为右边边缘检测时是右边的黑色减去左边的白色,因此负梯度都被规整为0了,不会显示在结果中
通过下面的取绝对值操作可以获取完整梯度:
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
经过上面代码处理后结果展示如下:
解决了梯度被规整的问题
同理进行水平边缘检测,结果展示如下:
将两个检测结果相加(0.5Gx+0.5Gy),结果展示如下:
如果直接将dx和dy都设置为1,只进行一次Sobel边缘检测会怎么样呢?
sobelxy=cv2.Sobel(img,cv2.CV_64F,1,1,ksize=3)
sobelxy = cv2.convertScaleAbs(sobelxy)
结果展示如下:
采用了一种综合算子,效果不如分开检测再相加效果好
采用Sobel算子对人像进行边缘检测:
原图像:
分别进行水平和竖直边缘检测后加权得到结果展示如下:
如果直接进行dx=1和dy=1的边缘检测就会效果不佳
与Sobel算子的计算方法类似,Scharr算子和Laplacian算子也是设计卷积核的值来完成扫描到的区域像素值比较,其中Scharr算子如下:
和Sobel算子相比,对比差异将被进一步放大
Laplacian算子如下:
与之前的边缘检测算子都不同,Laplacian算子采用一种类似于二阶梯度的方法进行计算
三种算子函数调用方法如下:
img = cv2.imread('lena.jpg',cv2.IMREAD_GRAYSCALE)
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
sobely = cv2.convertScaleAbs(sobely)
sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0)
scharrx = cv2.Scharr(img,cv2.CV_64F,1,0)
scharry = cv2.Scharr(img,cv2.CV_64F,0,1)
scharrx = cv2.convertScaleAbs(scharrx)
scharry = cv2.convertScaleAbs(scharry)
scharrxy = cv2.addWeighted(scharrx,0.5,scharry,0.5,0)
laplacian = cv2.Laplacian(img,cv2.CV_64F)
laplacian = cv2.convertScaleAbs(laplacian)
res = np.hstack((sobelxy,scharrxy,laplacian))
cv_show(res,'res')
三种算子的检测效果展示如下(从左至右依次为Sobel、Scharr、Laplacian):
可见Sobel检测效果最好,而Scharr对细小的边缘更加敏感,同时Laplacian算子的效果较差,实际应用中往往用于配合其他方法
Canny边缘检测方法来自一篇由Canny发表的论文,该方法步骤如下:
非极大值抑制:将每个点求得的梯度值与其周围点的梯度值比较,如果不是该点梯度值不是一个极大值则被抑制,这样扩大边缘点和其他区域的差距
双阈值检测:
在OpenCV中Canny边缘检测也被包装成为函数,代码如下:
v1=cv2.Canny(img, minVal, maxVal)
分别取两组minVal和maxVal进行Canny边缘检测,得到结果展示如下(左侧minVal=110,maxVal=220,右侧minVal=40,maxVal=80):
当minVal和maxVal设置较大时,较多梯度值被舍弃,保留下来的边缘简单干净,但偶尔有遗漏,反之参数设置较小时很多梯度值被保留下来,边缘更加完整,但可能会引入很多不必要的噪声
在进行轮廓检测之前先用阈值操作将图像转化为二值图像,可以提升检测的准确率
img = cv2.imread('contours.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
进行二值处理后的图像为:
OpenCV有轮廓检测函数如下:
contours, hierarchy = cv2.findContours(img,mode,method)
contours是检测得到的结果轮廓,hierarchy是轮廓的层级
mode:轮廓检索模式
method:轮廓逼近方法
接下来介绍绘制轮廓的方法:
draw_img = img.copy()
res = cv2.drawContours(draw_img, contours, i, (0, 0, 255), 1)
首先轮廓绘制是要在原图像img上绘制的,所以需要进行img.copy(),否则绘出轮廓后会改变原图像
cv2.drawContours()函数要传入5个参数,原图像、轮廓数据contours、轮廓的索引(设为-1表示绘制所有轮廓)、绘制颜色(是一个BGR数组)、绘制轮廓的线条宽度
经过轮廓检测后共得到10个轮廓(五个图形,各有内外两个轮廓),结果展示如下:
(1)轮廓周长及其包围面积的计算:
cnt = contours[0]
# 面积
area = cv2.contourArea(cnt)
# 周长,True表示闭合的
leng = cv2.arcLength(cnt,True)
计算轮廓特征时,不能直接输入轮廓数据contours,而是需要按索引拿出单个轮廓进行计算
(2)轮廓近似:
approx = cv2.approxPolyDP(cnt,epsilon,True)
会得到一个较为粗糙的,用直线近似的轮廓,epsilon是一个阈值,近似会从一条直线开始逐渐增加直线数量,当真实轮廓cnt的任意一点与近似轮廓的最短距离都小于epsilon时停止近似
效果展示如下:
(3)求外接图形:
# 外接四边形
x,y,w,h = cv2.boundingRect(cnt)
draw_img = img.copy()
draw_img = cv2.rectangle(draw_img,(x,y),(x+w,y+h),(0,255,0),2)
# 外接圆
(x,y),radius = cv2.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
draw_img = img.copy()
draw_img = cv2.circle(draw_img,center,radius,(0,255,0),2)
draw_img是用来绘制结果的,效果展示如下:
模板匹配是给出一张大图和一张子图,在大图中找出与子图最相似的区域
方法是使用子图大小的框进行滑动扫描,每次扫描到的区域与子图计算距离,寻找匹配的区域
OpenCV中可以使用函数进行模板匹配,方法如下:
res = cv2.matchTemplate(img, template, method)
其中method如下:
计算公式在官网中有说明:method计算公式
cv2.matchTemplate()得到的结果是一个得分矩阵,每个元素代表一个扫描区域与子图的匹配值
为了方便我们对匹配结果进行处理,OpenCV提供了一个索引函数:
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
利用min_loc和max_loc我们可以将结果展示如下: