OpenCV计算机视觉
文章目录
- OpenCV计算机视觉
- 10、Canny边缘检测
- 11、图像金字塔
- 12、图像轮廓:cv2.findContours()
- 13、直方图
- 14、傅里叶变换
- 15、项目实战:信用卡数字识别
- 16、项目实战:利用OpenCV工具包进行文档扫描OCR识别
- 17、图像特征-harris角点检测cv2.cornerHarris()
- 18、图像特征-sift(Scale Invariant Feature Transform),即平移不变性的特征匹配算法
- 19、特征匹配
- 20、项目实战-停车场车位识别
附带:《停车场车位智能识别》项目
项目最终效果:
本文使用到的图片请自行下载:
https://github.com/Sjenrey/learningOpenCV
1、参考文档
2、环境详情
-
MacOS-10.14.6
-
Python3.9
-
numpy-1.22.4
- NumPy(Numerical Python)是Python的一种开源的数值计算扩展。这种工具可用来存储和处理大型矩阵,比Python自身的嵌套列表(nested list structure)结构要高效的多(该结构也可以用来表示矩阵(matrix)),支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库。
-
matplotlib-3.5.2
- Matplotlib是一个 Python 的 2D绘图库,它以各种硬拷贝格式和跨平台的交互式环境生成出版质量级别的图形。
-
opencv-python-3.4.11.45
- opencv是用于快速处理图像处理、计算机视觉问题的工具,支持多种语言进行开发如c++、python、java等。
-
opencv-contrib-python-3.4.11.45
- opencv-contrib-python包含了主要模块以及扩展模块,扩展模块主要是包含了一些带专利的收费算法(如shift特征检测)以及一些在测试的新的算法(稳定后会合并到主要模块)。相当于加了一些额外的扩展,比如特征提取的一些算法,这些是OpenCV中没有的。
3、安装
安装opencv-python
pip3 install opencv-python
报错install pyproject.toml-based projects
- 访问opencv的镜像文件的网站,下载whl文件安装
https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/opencv-python/
- 找到适合自己的whl文件
我是MacOS10.14.6,Python3.9所以直接用下面链接下载即可:
https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1f/e0/187bf6941ad68e1c12d4e11d473d0841257aa388e3e88f1541f1f0c9a5dd/opencv_python-3.4.11.45-cp39-cp39-macosx_10_13_x86_64.whl#sha256=87015ea21e3f2faa7923cc3505e671b5e99b791fc812630f5d5ca4474387b242
- 下载完成后,进入opencv_python-3.4.11.45-cp39-cp39-macosx_10_13_x86_64.whl文件所在目录执行安装
pip3 install opencv_python-3.4.11.45-cp39-cp39-macosx_10_13_x86_64.whl
Successfully installed numpy-1.22.4 opencv-python-3.4.11.45
- 验证是否可以在Python中使用OpenCV
import cv2
安装opencv-contrib-python
注意需要和opencv-python版本号相同!
pip3 install opencv-contrib-python==3.4.11.45
Successfully installed opencv-contrib-python-3.4.11.45
到此为止,OpenCV和opencv-contrib都装好了。
4、图像基本操作
计算机是由每一个小格构成像素点来组成图像的。
像素点就是一个值,是在0~255之间的值,共计256个值,表示该点的亮度,0代表黑的,255代表最亮的。
RGB是三元色,叫图像的颜色通道。
黑白图也叫灰度图,只有一个通道,来表示亮度就足够了。
读取图像
这里用到的cat.jpg 是h=414,w=500,分辨率=72
# import matplotlib需要先安装,执行下面命令
pip3 install matplotlib
Successfully installed cycler-0.11.0 fonttools-4.33.3 kiwisolver-1.4.3 matplotlib-3.5.2 packaging-21.3 pillow-9.1.1 pyparsing-3.0.9
- 读取成彩色图像:
cv2.imread('cat.jpg', cv2.IMREAD_COLOR)
- 读取成灰度图像:
cv2.imread('cat.jpg', cv2.IMREAD_GRAYSCALE)
import cv2 # opencv默认读取的格式是BGR,不是RGB
img = cv2.imread('cat.jpg') # 读取彩色图像
# print(type(img)) # <class 'numpy.ndarray'>
print(img)
[[[142 151 160] # 从左上角开始,第一行,B G R
[146 155 164]
[151 160 170]
...
[156 172 185]
[155 171 184]
[154 170 183]]
[[108 117 126] # 第二行
[112 123 131]
[118 127 137]
...
[155 171 184]
[154 170 183]
[153 169 182]]
...
[[140 164 176] # 第n-1行
[147 171 183]
[139 163 175]
...
[169 187 188]
[125 143 144]
[106 124 125]]
[[154 178 190] # 第n行
[154 178 190]
[121 145 157]
...
[183 198 200]
[128 143 145]
[127 142 144]]]
# 读取灰度图
img = cv2.imread("cat.jpg", cv2.IMREAD_GRAYSCALE)
print(img)
[[153 157 162 ... 174 173 172]
[119 124 129 ... 173 172 171]
[120 124 130 ... 172 171 170]
...
[187 182 167 ... 202 191 170]
[165 172 164 ... 185 141 122]
[179 179 146 ... 197 142 141]]
图像展示,cv2.imshow()
# 图像的显示,也可以创建多个窗口
cv2.imshow('image', img) # image是窗口的名字
# 等待时间,毫秒级(1000ms=1s),0表示任意键终止
cv2.waitKey(1000)
cv2.destroyAllWindows()
获取图像hwc三个属性
# 获取图像的hwc三个属性,h=height,w=width,c=3是RGB
print(img.shape) # (414, 500, 3)
图像保存,cv2.imwrite()
cv2.imwrite("mycat.png", img)
计算像素点个数
print(img.size) # 207000
数据类型
print(img.dtype) # uint8
截取部分图像数据
# ROI(region of interest,感兴趣区域)
cat = img[0:50, 0:200] # 从左上角:截取h=50,w=200像素点的区域
cv2.imshow('image', cat)
颜色通道提取,cv2.split()
b, g, r = cv2.split(img)
print(b) # 打印每一个像素点中的blue
# print(g)
# print(r)
# print(b.shape) # (414, 500)
[[142 146 151 ... 156 155 154] # 从左上角开始,第一行所有像素点中全部的Blue值
[108 112 118 ... 155 154 153] # 第二行
...
[140 147 139 ... 169 125 106] # 第n-1行
[154 154 121 ... 183 128 127]] # 第n行
# 只保留R
cur_img = img.copy()
cur_img[:,:,0]=0
cur_img[:,:,1]=0
颜色通道组合,cv2.merge()
# 接上提取操作后,把b、g、r组合成一张图片
img = cv2.merge((b, g, r)) # 这里的结果img还是原图
拷贝图像
cur_img = img.copy()
只保留单通道颜色
# 只保留R
cur_img = img.copy()
# []里是hwc三个属性,:代表取所有,h=height,w=width,c=0是B c=1是G c=2是R
cur_img[:, :, 0] = 0 # 设置B为0
cur_img[:, :, 1] = 0 # 设置G为0
cv2.imshow('image', cur_img) # image是窗口的名字
# 等待时间,毫秒级(1000ms=1s),0表示任意键终止
cv2.waitKey(1000)
cv2.destroyAllWindows()
# 只保留G
cur_img = img.copy()
cur_img[:, :, 0] = 0 # 设置B为0
cur_img[:, :, 2] = 0 # 设置R为0
# 只保留B
cur_img = img.copy()
cur_img[:, :, 1] = 0 # 设置G为0
cur_img[:, :, 2] = 0 # 设置R为0
边界填充,cv2.copyMakeBorder()
- ORIGINAL:原图
- BORDER_REPLICATE:复制法,也就是复制最边缘像素。
- BORDER_REFLECT:反射法,对感兴趣的图像中的像素在两边进行复制,边界重复,例如:fedcba|abcdefgh|hgfedcb
- BORDER_REFLECT_101:反射法,也就是以最边缘像素为轴,对称,边界不重复,gfedcb|abcdefgh|gfedcba
- BORDER_WRAP:外包装法cdefgh|abcdefgh|abcdefg
- BORDER_CONSTANT:常量法,常数值填充。
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.imshow('image', constant) # image是窗口的名字
# 等待时间,毫秒级(1000ms=1s),0表示任意键终止
cv2.waitKey(10000)
cv2.destroyAllWindows()
每一个像素点加10
print(img)
img2 = img + 10
print(img2)
array([[142, 146, 151, ..., 156, 155, 154],
[107, 112, 117, ..., 155, 154, 153],
[108, 112, 118, ..., 154, 153, 152],
[139, 143, 148, ..., 156, 155, 154],
[153, 158, 163, ..., 160, 159, 158]], dtype=uint8)
# 加10以后
array([[152, 156, 161, ..., 166, 165, 164],
[117, 122, 127, ..., 165, 164, 163],
[118, 122, 128, ..., 164, 163, 162],
[149, 153, 158, ..., 166, 165, 164],
[163, 168, 173, ..., 170, 169, 168]], dtype=uint8)
两个相同hw的图相加
如果相加超过255,则需要减去256,因为是0~255,还有个0,所以要减256
print(img + img2)
array([[ 38, 46, 56, ..., 66, 64, 62],
[224, 234, 244, ..., 64, 62, 60],
[226, 234, 246, ..., 62, 60, 58],
[ 32, 40, 50, ..., 66, 64, 62],
[ 60, 70, 80, ..., 74, 72, 70]], dtype=uint8)
cv2.add()
和直接+运算不同的是,如果相加超过255,就取255
print(cv2.add(img, img2))
array([[255, 255, 255, ..., 255, 255, 255],
[224, 234, 244, ..., 255, 255, 255],
[226, 234, 246, ..., 255, 255, 255],
[255, 255, 255, ..., 255, 255, 255],
[255, 255, 255, ..., 255, 255, 255]], dtype=uint8)
图像拉伸,重新设置hw,cv2.resize()
print(img.shape) # (414, 500, 3)
# 设置shape值,相当于是图像拉伸操作。
img_cat = cv2.resize(img, (1000, 814))
print(img_cat.shape) # (814,1000,3)
# 以倍数拉伸
print(img.shape) # (414, 500, 3)
res = cv2.resize(img, (0, 0), fx=0.4, fy=2) # x轴即w拉伸0.4倍,y轴即h拉伸2倍
print(res.shape) # (828, 200, 3)
图像融合,cv2.addWeighted()
把两个图像叠加成一张图像。
注意:如果两张图片的shape值是不同的,是不可以直接用+运算的。
图像融合公式: R = α x 1 + β x 2 + b R=\alpha x_1+\beta x_2+b R=αx1+βx2+b
α \alpha α: x 1 x_1 x1的权重
β \beta β: x 2 x_2 x2的权重
b b b:偏置项
x 1 x_1 x1:图像1
x 2 x_2 x2:图像2
print(img_cat + img_dog)
ValueError: operands could not be broadcast together with shapes (414,500,3) (429,499,3)
# 第一步:调整两张图片shape值为相等。
print(img_cat.shape) # (414,500,3)
print(img_dog.shape) # (429,499,3)
# 设置shape值,相当于是图像拉伸操作。
img_dog = cv2.resize(img_dog, (500, 414))
print(img_dog.shape) # (414,500,3)
# 第二步:根据融合公式进行融合
res = cv2.addWeighted(img_cat, 0.3, img_dog, 0.7, 0)
cv2.imshow('image', res) # image是窗口的名字
# 等待时间,毫秒级(1000ms=1s),0表示任意键终止
cv2.waitKey(10000)
cv2.destroyAllWindows()
5、视频基本操作
视频是由很多帧组成的,每一帧都可以当作是静止的图像,把很多张静止的图像连在一起就形成视频。
帧(frame);视频都是由一帧一帧组成的,每一帧其实是一个包含有音频视频信息的基本单位。
每秒传输的图片帧数,也可以理解为图形处理器每秒刷新的次数,通常以fps(Frames Per Second)表示。
读取视频,cv2.VideoCapture()
-
cv2.VideoCapture
可以捕获摄像头,用数字来控制不同的设备,例如0,1 -
如果是视频文件,直接指定好路径即可。
import cv2 # opencv默认读取的格式是BGR,不是RGB
# cv2.VideoCapture可以捕获摄像头,用数字来控制不同的设备,例如0,1
# 如果是视频文件,直接指定好路径即可。
vc = cv2.VideoCapture("test.mp4")
# 检查是否打开正确
if vc.isOpened():
"""
is_open: bool,True/False
frame: 当前这一帧图像的像素点值,<class 'numpy.ndarray'>
read(): 读取视频的每一帧,可以写循环就读取所有帧了
"""
is_open, frame = vc.read()
else:
is_open = False
# 读取每一帧实现读取视频的效果
while is_open:
ret, frame = vc.read()
if frame is None:
break
if ret is True:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 转化成灰度图
cv2.imshow('result', gray) # 图像展示
"""cv2.waitKey(1)只能是integer,1代表PC能处理多快就多快,即处理完一帧要等少毫秒,所以视频是加速播放的。
27为ESC键,我们可以按ESC退出
13为ENTER键
9为TAB
20为Caps Lock键
"""
if cv2.waitKey(1) & 0xFF == 27:
break
vc.release()
cv2.destroyAllWindows()
6、图像阈值
首先,我们要求图像为灰度图,像素点值越接近255该点越亮。
其次,图像是由像素点组成,每一个像素点都是灰度的值,对每一个值进行判断,如果该值大于阈值,我们要怎么处理,小于要怎么处理。
# 灰度图的每一个像素点的list
[[153 157 162 ... 174 173 172]
[119 124 129 ... 173 172 171]
[120 124 130 ... 172 171 170]
...
[187 182 167 ... 202 191 170]
[165 172 164 ... 185 141 122]
[179 179 146 ... 197 142 141]]
阈值处理函数,cv2.threshold()
ret, dst = cv2.threshold(src, thresh, maxval, type)
-
src: 输入图,只能输入单通道图像,通常来说为灰度图
-
dst: 输出图
-
thresh: 阈值,不是百分比,是在0~255之前确定的值,比较常见的是127
-
maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值,最大的值也就是255
-
type:二值化操作的类型,包含以下5种类型: cv2.THRESH_BINARY; cv2.THRESH_BINARY_INV; cv2.THRESH_TRUNC; cv2.THRESH_TOZERO;cv2.THRESH_TOZERO_INV
- cv2.THRESH_BINARY
- 超过阈值部分取maxval(最大值),否则取0
- cv2.THRESH_BINARY_INV
- THRESH_BINARY的反转
- cv2.THRESH_TRUNC
- 大于阈值部分设为阈值,否则不变。相当于指定一个截断值。
- cv2.THRESH_TOZERO
- 大于阈值部分不改变,否则设为0
- cv2.THRESH_TOZERO_INV
- THRESH_TOZERO的反转
- cv2.THRESH_BINARY
-
ret:你设置的thresh阈值的值,一般来说是用不到该返回变量的。
import cv2 # opencv默认读取的格式是BGR,不是RGB
import matplotlib.pyplot as plt
img = cv2.imread("cat.jpg", cv2.IMREAD_GRAYSCALE)
# BINARY,亮点的全为白,暗点的全为黑
ret, thresh1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
print(ret) # 127.0
# BINARY_INV,是BINARY的反转,即:亮点的全为黑,暗点的全为白
ret, thresh2 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
# TRUNC,大于阈值部分设为阈值,否则不变,相当于指定一个截断值
ret, thresh3 = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
# TOZERO,大于阈值则不变,小于阈值全为黑
ret, thresh4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
# TOZERO_INV,是TOZERO的反转,大于阈值全为黑,小于阈值则不变
ret, thresh5 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)
titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2, 3, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
7、图像平滑
四种滤波:
- 均值滤波:cv2.blur()
- 方框滤波: cv2.boxFilter()
- 高斯滤波:cv2.GaussianBlur()
- 中值滤波:cv2.medianBlur()
图像与滤波
图像其实是一种波,可以用波的算法处理图像。
我们知道,图像由像素组成。下图是一张 400 x 400 的图片,一共包含了 16 万个像素点。
每个像素的颜色,可以用红、绿、蓝、透明度四个值描述,大小范围都是0 ~ 255
,比如黑色是[0, 0, 0, 255]
,白色是[255, 255, 255, 255]
。通过 Canvas API 就可以拿到这些值。
如果把每一行所有像素(上例是400个)的红、绿、蓝的值,依次画成三条曲线,就得到了下面的图形。
可以看到,每条曲线都在不停的上下波动。有些区域的波动比较小,有些区域突然出现了大幅波动(比如 54 和 324 这两点)。
对比一下图像就能发现,曲线波动较大的地方,也是图像出现突变的地方。
这说明波动与图像是紧密关联的。图像本质上就是各种色彩波的叠加。
两种常见的滤波器:
lowpass
使得图像的高频区域变成低频,即色彩变化剧烈的区域变得平滑,也就是出现模糊效果。
highpass
正好相反,过滤了低频,只保留那些变化最快速最剧烈的区域,也就是图像里面的物体边缘,所以常用于边缘识别。
图像平滑:即对图像进行各种滤波操作。
下图即lenaNoise.png图片的原图,可以看到图片有一些白色的噪点。
现在我们想通过滤波或者是平滑处理操作,来尽可能去掉这些噪点。。
假设上图的像素点的矩阵为下图所示:
均值滤波
下述代码中的(3, 3)
代表着上图黄色的区域(称为“核”,核一般是奇数,每3x3要进行一次计算),即处理值为204这个像素点的值,应该是和其周围像素点的值是比较相关的。所以,要使用该区域的9个像素点的均值来作为204这个像素点处理完后的值。
即: 121 + 75 + 78 + 24 + 204 + 113 + 154 + 104 + 235 9 \frac{121+75+78+24+204+113+154+104+235}{9} 9121+75+78+24+204+113+154+104+235
import cv2
img = cv2.imread("lenaNoise.png")
# 均值滤波
# 简单的平均卷积操作
blur = cv2.blur(img, (3, 3))
cv2.imshow('blur', blur)
cv2.waitKey(0)
cv2.destroyAllWindows()
方框滤波
和均值滤波一样,可以把boxFilter()和blur()当成一个,只不过boxFilter()多了一个参数。
import cv2
img = cv2.imread("lenaNoise.png")
# 方框滤波
# -1不用管是固定的,normalize=True代表做归一化操作(除9),此时和均值滤波一模一样
box = cv2.boxFilter(img,-1,(3,3), normalize=True)
cv2.imshow('box', box)
cv2.waitKey(0)
cv2.destroyAllWindows()
import cv2
img = cv2.imread("lenaNoise.png")
# 方框滤波
# normalize=False不做归一化操作(不除9),容易越界(值超过255则取255当作结果)
box = cv2.boxFilter(img,-1,(3,3), normalize=False)
cv2.imshow('box', box)
cv2.waitKey(0)
cv2.destroyAllWindows()
高斯滤波
高斯函数的意思就是越接近均值的时候,可能性越大。越接近x=0的时候y的值越大,即下图所示。
所以,要处理204这个像素点的值,那么离204点越近的点,可能性越大,即75、24、113、104点可能性更大,121、78、154、235点可能性小。
import cv2
img = cv2.imread("lenaNoise.png")
# 高斯滤波
# 高斯模糊的卷积核里的数值是满足高斯分布,相当于更重视中间的
aussian = cv2.GaussianBlur(img, (5, 5), 1)
cv2.imshow('aussian', aussian)
cv2.waitKey(0)
cv2.destroyAllWindows()
中值滤波
中值就是中间的值,所以需要从小到大排序,取中间的值。
即:24、75、78、104、113、121、154、204、235
所以,值为204的像素点经过中值滤波处理后的值就是113了。
所以,最后经过中值滤波处理的图像就没有噪点了。
import cv2
img = cv2.imread("lenaNoise.png")
# 中值滤波
# 相当于用中值代替
median = cv2.medianBlur(img, 5)
cv2.imshow('median', median)
cv2.waitKey(0)
cv2.destroyAllWindows()
展示所有结果
import cv2
import numpy as np
img = cv2.imread("lenaNoise.png")
# 均值滤波
blur = cv2.blur(img, (3, 3))
# 方框滤波
box1 = cv2.boxFilter(img, -1, (3, 3), normalize=True)
box2 = cv2.boxFilter(img, -1, (3, 3), normalize=False)
# 高斯滤波
aussian = cv2.GaussianBlur(img, (5, 5), 1)
# 中值滤波
median = cv2.medianBlur(img, 5)
# 展示所有的
# hstack是横向拼接展示;vstack是纵向拼接展示
res = np.hstack((blur, box1, box2, aussian, median))
# print(res)
cv2.imshow('median vs average', res)
cv2.waitKey(0)
cv2.destroyAllWindows()
8、形态学morphology
腐蚀操作,cv2.erode()
前提条件:图像一般都是2值的数据图像。
现在想把下图文字上的毛刺去掉。
缺点就是图像有价值的信息越来越少,即下图所示经过腐蚀操作后白色线条变细了。
场景:当我们的图像中有一些细线条的时候,我们可以用腐蚀操作去掉,再用膨胀操作复原。
import cv2
import numpy as np
img = cv2.imread('dige.png')
# 设置3x3的核心
kernel = np.ones((3, 3), np.uint8)
# iterations迭代次数,次数越多,白色字条越细,甚至消失。
erosion = cv2.erode(img, kernel, iterations=1)
res = np.hstack((img, erosion))
cv2.imshow('res', res)
cv2.waitKey(0)
cv2.destroyAllWindows()
膨胀操作,cv2.dilate()
可以用来弥补腐蚀操作后图像中线条变细的情况。
import cv2
import numpy as np
img = cv2.imread('dige.png')
# 腐蚀操作
kernel = np.ones((3, 3), np.uint8)
erosion = cv2.erode(img, kernel, iterations=1)
# 膨胀操作
kernel = np.ones((3, 3), np.uint8)
dilate = cv2.dilate(erosion, kernel, iterations=1)
res = np.hstack((img, erosion, dilate))
cv2.imshow('res', res)
cv2.waitKey(0)
cv2.destroyAllWindows()
开运算与闭运算
其实就是把腐蚀和膨胀总结成了一个方法cv2.morphologyEx()
import cv2
import numpy as np
# 开:先腐蚀,再膨胀
img = cv2.imread('dige.png')
kernel = np.ones((5, 5), np.</