赞
踩
学习目标
了解图像的起源
知道数字图像的表示
图像是人类视觉的基础,是自然景物的客观反映,是人类认识世界和人类本身的重要源泉。“图”是物体反射或透射光的分布,“像“是人的视觉系统所接受的图在人脑中所形版的印象或认识,照片、绘画、剪贴面、地图、书法作品、手写汉学、传真、卫星云图、影视画面、X光片、脑电图、心电图等都是图像。一姚敏,数字图像处理:机械工业出版社,2014年。
图像起源于1826年前店法国科学家Joseph Nicéphore Niépce发明的第一张可永久保存的照片,属于模拟图像。模拟图像又称连续图像,它通过某种物理量(如光、电等)的强弱变化来记录图像亮度信息,所以是连续变换的。模拟信号的特点是容易受干扰,如今已经基本全面被数字图像替代。
在第一次世界大战后,1921年美国科学家发明了Bartlane System,并从伦敦传到纽约传输了第一幅数字图像,其亮度用离散数值表示,将图片编码成5个灰度级,如下图所示,通过海底电缆进行传输。在发送端图片被编码并使用打孔带记录,通过系统传输后在接收方使用特殊的打印机恢复成图像。
1950年左右,计算机被发明,数字图像处理学科正式诞生。
计算机采用0/1编码的系统,数字图像也是利用0/1来记录信息,我们平常接触的图都是8位数图像,包含0~255灰度,其中0,代表最黑,1,表示最白。
人眼对灰度更敏感一些,在16位到32位之间。
二值图像
一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。
灰度图
每个像素只有一个采样颜色的图像,这类图像通常显示为从最暗黑色到最亮的白色的灰度,尽管理论上这个采样可以任何颜色的不同深浅,甚至可以是不同亮度上的不同颜色。灰度图像与黑白图像不同,在计算机图像领域中黑白图像只有黑色与白色两种颜色;但是,灰度图像在黑色与白色之间还有许多级的颜色深度。灰度图像经常是在单个电磁波频谱如可见光内测量每个像素的亮度得到的,用于显示的灰度图像通常用每个采样像素8位的非线性尺度来保存,这样可以有256级灰度(如果用16位,则有65536级)。
彩色图
每个像素通常是由红(R)、绿(G)、蓝(B)三个分量来表示的,分量介于(0,255)。RGB图像与索引图像一样都可以用来表示彩色图像。与索引图像一样,它分别用红(R)、绿(G)、蓝(B)三原色的组合来表示每个像素的颜色。但与索引图像不同的是,RGB图像每一个像素的颜色值(由RGB三原色表示)直接存放在图像矩阵中,由于每一像素的颜色需由R、G、B三个分量来表示,M、N分别表示图像的行列数,三个MxN的二维矩阵分别表示各个像素的R、G、B三个颜色分量。RGB图像的数据类型一般为8位无符号整形,通常用于表示和存放真彩色图像。
图像是什么
图:物体反射或透射光的分布
像:人的视觉系统所接受的图在人脑中所形版的印象或认识
模拟图像和数字图像
模拟图像:连续存储的数据
数字图像:分级存储的数据
数字图像
位数:图像的表示,常见的就是8位
分类:二值图像,灰度图像和彩色图像
学习目标
OpenCV是一款由Intel公司俄罗斯团队发起并参与和维护的一个计算机视觉处理开源软件库,支持与计算机视觉和机器学习相关的众多算法,并且正在日益扩展。
OpenCV的优势
编程语言
OpenCV基于C++实现,同时提供python,Ruby.Matlatb等语言的接口。OpenCV-Python是OpenCV的Python AP1,结合了OpenCV C++AP1和Python语言的最佳特性。
跨平台
可以在不同的系统平台上使用,包括Windows,Linux,OSX,Android和iOS,基于CUDA和OpenCL的高速GPU操作接口也在积极开发中。
活跃的开发团队
丰富的API
完善的传统计算机视觉算法,涵盖主流的机器学习算法,同时添加了对深度学习的支持。
OpenCV-Python是一个Python绑定库,旨在解决计算机视觉问题。
Python是一种由Guido van Rossum开发的通用编程语言,它很快就变得非常流行,主要是因为它的简单性和代码可读性。它使程序员能够用更少的代码行表达思想,而不会降低可读性。
与C/C++等语言相比,Python速度较慢。也就是说,Python可以使用C/C++轻松扩展,这使我们可以在C/C++中编写计算密集型代码,并创建可用作Python模块的Python包装器。这给我们带来了两个好处:首先,代码与原始C/C++代码一样快(因为它是在后台工作的实际C++代码),其次,在Python中编写代码比使用C/C++更容易。OpenCV-Python是原始OpenCVC++实现的Python包装器。
OpenCV-Python使用Numpy,这是一个高度优化的数据库操作库,具有MATLAB风格的语法。所有OpenCV数组结构都转换为Numpy数组。这也使得与使用Numpy的其他库(如SciPy和Matplotlib)集成更容易。
安装OpenCV之前需要先安装numpy,matplotlib。
创建Python虚拟环境cv,在cv中安装即可。
先安装OpenCV-Python,由于一些经典的算法被申请了版权,新版本有很大的限制,所以选用3.4.3以下的版本
首先创建Anaconda虚拟环境
然后安装相应的opencv库
pip install opencv-python==3.4.2.17 -i https://mirrors.aliyun.com/pypi/simple/ # 阿里源
pip install -U matplotlib --prefer-binary -i https://pypi.tuna.tsinghua.edu.cn/simple # 清华源
清华:https://pypi.tuna.tsinghua.edu.cn/simple
阿里云:http://mirrors.aliyun.com/pypi/simple/
中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/
华中理工大学:http://pypi.hustunique.com/
山东理工大学:http://pypi.sdutlinux.org/
豆瓣:http://pypi.douban.com/simple/
运行 import cv2
不报错即可
import cv2
# 读一个图片并进行显示(图片路径自己指定)
cat = cv2.imread("cat.jpg")
cv2.show("cat", cat)
cv2.waitKey(0)
cv2.destroyAllWindows()
pip install scipy==1.4.1 -i https://mirrors.aliyun.com/pypi/simple/
如果我们要利用SIFT和SURF等进行特征提取时,还需要安装opencv-contrib-python:
pip install opencv-contrib-python==3.4.2.17 -i https://mirrors.aliyun.com/pypi/simple/
pip install moviepy==3.4.2.17 -i https://mirrors.aliyun.com/pypi/simple/
Opencv是计算机视觉的开源库
优势:
Opencv部署方法
学习目标
下图列出的OpenCV中包含的各个模块:
其中core、highgui、imgproc是最基础的模块,该课程主要是围绕这几个模块展开的,分别介绍如下:
对于图像处理其他更高层次的方向及应用,OpenCV也有相关的模块实现
OpenCV的模块
学习目标
API
cv2.imread()
参数
要读取的图像
读取方式的标志
cv.IMREAD_COLOR:以彩色模式加载图像,任何图像的透明度将被忽略。这是默认参数1
cv.IMREAD_GRAYSCALE:以灰度模式加载图像,参数0
cv.IMREAD_UNCHANGED:包括alpha通道的加载图像模式,参数-1。
可以使用1、0或者-1来代替上面三个标志
参考代码
import numpy as np
import cv2 as cv
img = cv.imread('cat.jpg', 0)
注意:如果加载的路径有错误,不会报错,会返回一个None值
API
cv2.imshow()
参数
参考代码
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('cat.jpg')
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# matplotlib中展示
plt.imshow(img[:, :, ::-1]) # opencv图片是BGR模式,实际图片是RGB模式,此处要进行通道翻转
plt.show()
API
cv2.imwrite()
参数
参考代码:
cv2.imwrite("my_cat.jpg", img)
API
cv2.line(img, start, end, color, thickness)
参数
API
cv2.circle(img, centerpoint, r, color, thickness)
参数
API
cv2.rectangle(img, leftupper,rightdown, color, thickness)
参数
API
cv2.outText(img, station. font, fontsize, color, thickness, lintType)
参数
生成一个全黑的图像,然后在里面给制图像并添加文字
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1创建一个空白的图像 img = np.zeros((512, 512, 3), np.uint8) # 2绘制图形 cv2.line(img, (0, 0), (511, 511), (255, 0, 0), 5) # 绘制蓝色线条 cv2.rectangle(img, (384, 0), (510, 128), (0, 255, 0), 3) # 绘制绿色矩形 cv2.circle(img, (447, 63), 63, (0, 0, 255), -1) # 绘制红色实心圆形 font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(img, 'OpenCV', (10, 500), font, 4, (255, 255, 255), 2, cv2.LINE_AA) # 3图像显示 plt.imshow(img[:, :, ::-1]) plt.title('result'), plt.xticks([]), plt.yticks([]) plt.show()
运行结果
通过行和列的坐标值获取该像素的像素值,对于BGR图像,它返回一个蓝、绿、红值的数组。对于灰度图像,返回相应的灰度值。使用相同的方法对像素值进行修改。
参考代码
import cv2
import numpy as np
img = cv2.imread('cat.jpg')
# 获取某个像素点的值
px = img[100, 100] # [134,158,176]
# 仅仅获取蓝色通道的强度值
blue = img[100, 100, 0] # 134
# 修改位置的像素值
img[100:200, 100:300] = [0, 0, 0] # 将像素值修改为0
cv2.imshow('new', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果
图像属性包括行数,列数和通道数,图像数据类型,像素数等。
属性 | API |
---|---|
形状 | img.shape |
图像大小 | img.size |
数据类型 | img.dtype |
有时需要在B,G,R通道图像上单独工作,在这种情况下,需要将BGR图像分割为单个通道,或者在其他情况下,可能需要将这些单独的通道合并到BGR图像。
参考代码
import cv2 import numpy as np from matplotlib import pyplot as plt img1 = cv2.imread(r'.\cat.jpg', flags=1) # flags=1 读取彩色图像(BGR) cv2.imshow("BGR", img1) # BGR 图像 # BGR 通道拆分 bbImg, ggImg, rrImg = cv2.split(img1) # 拆分为 BGR 独立通道 print(bbImg.shape) # (614, 632) # 获取 B 通道 bImg = img1.copy() # 获取 BGR bImg[:, :, 1] = 0 # G=0 bImg[:, :, 2] = 0 # R=0 print(bImg.shape) # (614, 632, 3) # 获取 G 通道 gImg = img1.copy() # 获取 BGR gImg[:, :, 0] = 0 # B=0 gImg[:, :, 2] = 0 # R=0 # 获取 R 通道 rImg = img1.copy() # 获取 BGR rImg[:, :, 0] = 0 # B=0 rImg[:, :, 1] = 0 # G=0 # 消除 B 通道 grImg = img1.copy() # 获取 BGR grImg[:, :, 0] = 0 # B=0 # 消除 G 通道 brImg = img1.copy() # 获取 BGR brImg[:, :, 1] = 0 # G=0 # 消除 R 通道 bgImg = img1.copy() # 获取 BGR bgImg[:, :, 2] = 0 # R=0 # 打印结果 plt.subplot(331), plt.title("Only R channel"), plt.axis('off') plt.imshow(rrImg) # matplotlib 显示 单一红色通道,是灰度图 plt.subplot(332), plt.title("Only G channel"), plt.axis('off') plt.imshow(ggImg) # matplotlib 显示 单一绿色通道,是灰度图 plt.subplot(333), plt.title("Only B channel"), plt.axis('off') plt.imshow(bbImg) # matplotlib 显示 单一蓝色通道,是灰度图 plt.subplot(334), plt.title("R channel"), plt.axis('off') rImg = cv2.cvtColor(rImg, cv2.COLOR_BGR2RGB) # 图片格式转换:BGR(OpenCV) -> RGB(PyQt5) plt.imshow(rImg) # matplotlib 显示 channel B,BG channel为0 plt.subplot(335), plt.title("G channel"), plt.axis('off') gImg = cv2.cvtColor(gImg, cv2.COLOR_BGR2RGB) # 图片格式转换:BGR(OpenCV) -> RGB(PyQt5) plt.imshow(gImg) # matplotlib 显示 channel G,BR channel为0 plt.subplot(336), plt.title("B channel"), plt.axis('off') bImg = cv2.cvtColor(bImg, cv2.COLOR_BGR2RGB) # 图片格式转换:BGR(OpenCV) -> RGB(PyQt5) plt.imshow(bImg) # matplotlib 显示 channel R,GR channel为0 plt.subplot(337), plt.title("GR channel"), plt.axis('off') grImg = cv2.cvtColor(grImg, cv2.COLOR_BGR2RGB) # 图片格式转换:BGR(OpenCV) -> RGB(PyQt5) plt.imshow(grImg) # matplotLib显示 channeL GR, B channel为0 plt.subplot(338), plt.title("BR channel"), plt.axis('off') brImg = cv2.cvtColor(brImg, cv2.COLOR_BGR2RGB) # 图片格式转换:BGR(OpenCV) -> RGB(PyQt5) plt.imshow(brImg) # matplotLib显示 channeL BR, G channel为0 plt.subplot(339), plt.title("BG channel"), plt.axis('off') bgImg = cv2.cvtColor(bgImg, cv2.COLOR_BGR2RGB) # 图片格式转换:BGR(OpenCV) -> RGB(PyQt5) plt.imshow(bgImg) # matplotLib显示 channeL BG, R channel为0 plt.show() print(img1.shape, rImg.shape) cv2.waitKey(0) cv2.destroyAllWindows() # 释放所有窗口
运行结果
OpenCV中有150多种颜色空间转换方法,最广泛的转换方法有两种,BGR<–>Gray和BGR<–>HSV。
API
cv2.cvtColor(input_image,flag)
参数
参考代码
import cv2
import matplotlib.pyplot as plt
cat = cv2.imread('cat.jpg')
gray_cat = cv2.cvtColor(cat, cv2.COLOR_BGR2GRAY)
hsv_cat = cv2.cvtColor(cat, cv2.COLOR_BGR2HSV)
plt.subplot(121), plt.imshow(gray_cat, cmap='gray'), plt.title('gray_cat')
plt.subplot(122), plt.imshow(hsv_cat, cmap='hsv'), plt.title('hsv_cat')
plt.show()
运行结果
图像IO操作的API:
cv2.imread()
:读取图像
cv2.imwrite()
:保存图像
cv2.imshow()
:显示图像
在绘制上绘制几何图形
cv2.line()
:绘制直线
cv2.circle()
:绘制圆形
cv2.rectangle()
:绘制矩形
cv2.putText()
:在图像上添加文字
直接使用行列索引获取图像中的像素并进行修改
图像的属性
img.shape
:形状
img.size
图像大小
img.dtype
数据类型
拆分通道:cv2.split()
通道合并:cv2.merge()
色彩空间的改变
cv2.cvtColor(input_image, flag)
学习目标
可以使用OpenCV的cv.add0函数把两幅图像相加,或者可以简单地通过numpy操作添加两个图像,如res=img1+img2。两个图像应该具有相同的大小和类型,或者第二个图像可以是标量值。
注意:OpenCV加法和Numpy加法之间存在差异。OpenCV的加法是饱和操作,而Numpy加是模运算。
>>> x = np.uint8([250])
>>> y = np.uint8([10])
>>> print(cv.add(x,y)) # 250+10=260=>255
[[255]]
>>> print(x+y) # 250+10=260%256=4
[4]
原图
参考代码
import numpy as np import cv2 import matplotlib.pyplot as plt # 1 读取图像 img_view = cv2.imread('view.jpeg') img_rain = cv2.imread('rain.jpeg') img_rain = cv2.resize(img_rain, (444, 445)) # 2 加法操作 img_add = cv2.add(img_view, img_rain) img_jia = img_view + img_rain # 3 图像显示 fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100) axes[0].imshow(img_add[:, :, ::-1]) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0].set_title("cv中的加法") axes[1].imshow(img_jia[:, :, ::-1]) axes[1].set_title("直接相加") plt.show()
运行结果
这其实也是加法,但是不同的是两幅图像的权重不同,这就会给人一种混合或者透明的感觉。图像混合的计算公式如下:
g ( x ) = ( 1 − α ) f 0 ( x ) + α f 1 ( x ) g(x) = (1-α)f_0~(x) + αf_1(x) g(x)=(1−α)f0 (x)+αf1(x)
通过修改α的值(0→1),可以实现非常炫酷的混合。
现在我们把两幅图混合在一起。第一幅图的权重是0.7,第二幅图的权重是0.3。函数 cv2.addWeighted()
可以按下面的公式对图片进行混合操作。
参考代码
img_cat = cv2.imread('cat.jpg')
img_space = cv2.imread('space.jpeg')
print(img_cat.shape) # (701, 720, 3)
print(img_space.shape) # (521, 563, 3)
img_cat2 = cv2.resize(img_cat, (563, 521))
img_cat_in_space = cv2.addWeighted(img_cat2, 0.4, img_space, 0.6, 0) # 最后的0,gamma修正系数,不需要修正设置为0
cv2.imshow('cat_in_space', img_cat_in_space)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果
cv2.add()
cv2.addweighted()
注意:这里都要求两幅图像是相同大小的。
学习目标
缩放是对图像的大小进行调整,即使图像放大或缩小。
API
cv2.resize(src,dsize,fx=0,fy=0,interpolation=cv2.INTER_LINEAR)
参数
src:输入图像
dsize:绝对尺寸,直接指定调整后图像的大小
fx,fy:相对尺寸,将dsize设置为None,然后将fx和fy设置为比例因子即可
interpolation:插值方法
插值 | 含义 |
---|---|
cv2.INTER_LINEAR | 双线性插值 |
cv2.INTER_NEAREST | 最邻近插值 |
cv2.INTER_AREA | 像素区域重采样(默认) |
cv2.INTER_CUBIC | 双三次插值 |
参考代码
import cv2
import matplotlib.pyplot as plt
# 将狗的图片尺寸改成和猫图片尺寸大小一样
img_cat = cv2.imread('cat.jpg')
img_dog = cv2.imread('dog.jpg')
print(img_cat.shape) # (701,720,3)
print(img_dog.shape) # (481,629,3)
img_dog2 = cv2.resize(img_dog, (720, 701)) # 传入的WH值和shape值是相反的
plt.subplot(131), plt.imshow(img_cat), plt.title('original_cat')
plt.subplot(132), plt.imshow(img_dog), plt.title('original_dog')
plt.subplot(133), plt.imshow(img_dog2), plt.title('resize_dog')
plt.show()
运行结果
参考代码
import cv2 import matplotlib.pyplot as plt # 比例放大 img = cv2.imread('cat.jpg') res1 = cv2.resize(img, (0, 0), fx=1, fy=2) # y扩大两倍 res2 = cv2.resize(img, (0, 0), fx=2, fy=1) # x扩大两倍 res3 = cv2.resize(img, (0, 0), fx=3, fy=3) # 图像扩大两倍 plt.subplot(221), plt.imshow(img), plt.title('original_cat') plt.subplot(222), plt.imshow(res1), plt.title('large_y') plt.subplot(223), plt.imshow(res2), plt.title('large_x') plt.subplot(224), plt.imshow(res3), plt.title('large_image') plt.show() # 比例缩小 img = cv2.imread('dog.jpg') res = cv2.resize(img, (0, 0), fx=0.5, fy=1) plt.subplot(121), plt.imshow(img), plt.title('original_dog') plt.subplot(122), plt.imshow(res), plt.title('minify_dog') plt.show()
运行结果
图像平移将图像按照指定方向和距离,移动到相应的位置。
API
cv2.wrapAffine(img, M, dsize)
参数
img:输入图像
M:2*3移动矩阵
对于(x,y)处的像素点,要把它移动到(x+tx,y+ty)处时,M矩阵应如下设置:
M
=
[
1
0
t
1
0
1
t
y
]
\bf M=
注意:将M设置为np.float32类型的Numpy数组。
dsize:输出图像的大小
注意:输出图像的大小,他应该时(宽度, 高度)的形式。 width=列数, height=行数
参考代码
import numpy as np import cv2 as cv import matplotlib.pyplot as plt ''' 将图像的像素点移动(50,100)的距离 ''' # 1.读取图像 img = cv.imread('cat.jpg') # 2.图像平移 rows, cols = img.shape[:2] M = np.float32([[1, 0, 100], [0, 1, 50]]) # 平移矩阵 1,0是x轴, 0,1是y轴 dst = cv.warpAffine(img, M, (cols, rows)) # 3.图像显示 fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif']=['SimHei'] # 用来设置字体样式以正常显示中文标签 plt.rcParams['axes.unicode_minus']=False # 默认是使用Unicode负号,设置正常显示字符,如正常显示负号 axes[0].imshow(img[:, :, ::-1]) axes[0].set_title("原图") axes[1].imshow(dst[:, :, ::-1]) axes[1].set_title("平移后结果") plt.show()
运行结果
图像旋转是指图像按照某个位置转动一定角度的过程,旋转中图像仍保持这原始尺寸。图像旋转后图像的水平对称轴、垂直对称轴及中心坐标原点都可能会发生变换,因此需要对图像旋转中的坐标进行相应转换。(修改图像中每一个像素的坐标)
图像旋转 如下图所示:
假设图像逆时针旋转θ,则根据坐标转换可得旋转转换为:
f
(
x
)
=
{
x
′
=
r
cos
(
α
−
θ
)
y
′
=
r
sin
(
α
−
θ
)
f(x)=
其中:
r
=
x
2
+
y
2
,
sin
α
=
y
x
2
+
y
2
,
cos
α
=
x
x
2
+
y
2
r=\sqrt{x^2+y^2},\sin\alpha=\frac{y}{\sqrt{x^2+y^2}},\cos\alpha=\frac{x}{\sqrt{x^2+y^2}}
r=x2+y2
,sinα=x2+y2
y,cosα=x2+y2
x
带入上面的公式有,有:
f
(
x
)
=
{
x
′
=
x
cos
θ
+
y
sin
θ
y
′
=
−
x
sin
θ
+
y
cos
θ
f(x)=
也可以写成:
[
x
′
y
′
1
]
=
[
x
y
1
]
[
cos
θ
−
sin
θ
0
sin
θ
cos
θ
0
0
0
1
]
同时我们要修正原点的位置,因为原图像中的坐标原点在图像的左上角,经过旋转后图像的大小会有所变化,原点也需要修正。
假设在旋转的时候是以旋转中心为坐标原点的,旋转结束后还需要将坐标原点移到图像左上角,也就是还要进行一次变换。
[
x
′
′
y
′
′
1
]
=
[
x
′
y
′
1
]
[
1
0
0
0
−
1
0
l
e
f
t
t
o
p
1
]
=
[
x
y
1
]
[
cos
θ
−
sin
θ
0
sin
θ
cos
θ
0
0
0
1
]
[
1
0
0
0
−
1
0
l
e
f
t
t
o
p
1
]
在OpenCV中图像旋转首先根据旋转角度和旋转中心获取旋转矩阵,然后根据旋转矩阵进行变换,即可实现任意角度和任意中心的旋转效果。
API
cv2.getRotationMatrix2D(center, angle, scale)
参数
返回
M:旋转矩阵
调用cv2.wrapAffine完成图像的旋转
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1 读取图像 img = cv2.imread('cat.jpg') # 2 图像旋转 rows, cols = img.shape[:2] # 2.1 生成旋转坐标 M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 45, 1) # 2.2 进行旋转变换 dst = cv2.warpAffine(img, M, (cols, rows)) # 3 图像显示 fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0].imshow(img[:, :, ::-1]) axes[0].set_title("原图") axes[1].imshow(dst[:, :, ::-1]) axes[1].set_title("旋转后结果") plt.show()
运行结果
图像的仿射变换涉及到图像的形状、位置、角度的变化,是深度学习预处理中常到的功能,仿射变换主要是对图像的缩放,旋转,翻转和平移等操作的组合。
那什么是图像的仿射变换,如下图所示,图1中的点1,2和3与图二中三个点一一映射,仍然形成三角形,但形状已经大大改变,通过这样两组三点(感兴趣点)求出仿射变换,接下来我们就能把仿射变换应用到图像中所有的点中,就完成了图像的仿射变换。
在OpenCV中,仿射变换的矩阵是一个2×3的矩阵,
M
=
[
A
B
]
=
[
a
00
a
01
b
0
a
10
a
11
b
1
]
\bf M=
其中左边的2×2子矩阵
A
A
A是线性变换矩阵,右边的2×1子矩阵
B
B
B是平移项:
A
=
[
a
00
a
01
a
10
a
11
]
,
B
=
[
b
0
b
1
]
,
A=
对于图像上的任一位置(x,y),仿射变换执行的是如下的操作:
T
a
f
f
i
n
e
=
A
[
x
y
]
+
B
=
M
[
x
y
1
]
T_{affine} = A
需要注意的是,对于图像而言,宽度方向是x,高度方向是y,坐标的顺序和图像像素对应下标一致。所以原点的位置不是左下角而是左上角,y的方向也不是向上,而是向下。
在仿射变换中,原图中所有的平行线在结果图像中同样平行。为了创建这个矩阵我们需要从原图像中找到三个点以及他们在输出图像中的位置。然后 cv2.getAffineTransform()
会创建一个2×3的矩阵,最后这个矩阵会被传给函数 cv2.warpAffine()
。
参考代码
import numpy as np import cv2 import matplotlib.pyplot as plt # 1 图像读取 img = cv2.imread("dog.jpg") # 2 仿射变换 rows, cols = img.shape[:2] # 2.1 创建变换矩阵 pts1 = np.float32([[50, 50], [200, 50], [20, 200]]) pts2 = np.float32([[100, 100], [200, 50], [100, 250]]) M = cv2.getAffineTransform(pts1, pts2) # 2.2 完成仿射变换 dst = cv2.warpAffine(img, M, (cols, rows)) # 3 图像显示 fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0].imshow(img[:, :, ::-1]) axes[0].set_title("原图") axes[1].imshow(dst[:, :, ::-1]) axes[1].set_title("仿射后结果") plt.show()
运行结果
透射变换是视角变化的结果,是指利用透视中心、像点、目标点三点共线的条件,按透视旋转定律使承影面(透视面)绕迹线(透视轴)旋转某一角度,破坏原有的投影光线束,仍能保持承影面上投影几何图形不变的变换。
它的本质将图像投影到一个新的视平面,其通用变换公式为:
[
x
′
y
′
z
′
]
=
[
u
v
w
]
[
a
00
a
01
a
02
a
10
a
11
a
12
a
20
a
21
a
22
]
其中,(u,v)是原始的图像像素坐标,w取值为1,(x=x’/z’,y=y’/z’)是透射变换后的结果。后面的矩阵称为透视变换矩阵,一般情况下,我们将其分为三部分:
T
=
[
a
00
a
01
a
02
a
10
a
11
a
12
a
20
a
21
a
22
]
=
[
T
1
T
2
T
3
a
22
]
T=
其中:T1表示对图像进行线性变换,T2对图像进行平移,T3表示对图像进行投射变换,a22一般设为1.
在opencv中,我们要找到四个点,其中任意三个不共线,然后获取变换矩阵T,再进行透射变换。
通过函数 cv2.getPerspectiveTransform
找到变换矩阵,将 cv2.warpPerspective
应用于此3×3变换矩阵。
参考代码
import numpy as np import cv2 import matplotlib.pyplot as plt # 1 读取图像 img = cv2.imread("cat.jpg") # 2 透射变换 rows, cols = img.shape[:2] # 2.1 创建变换矩阵 pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]]) pts2 = np.float32([[100, 145], [300, 100], [80, 290], [310, 300]]) T = cv2.getPerspectiveTransform(pts1, pts2) # 2.2 进行变换 dst = cv2.warpPerspective(img, T, (cols, rows)) # 3图像显示 fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0].imshow(img[:, :, ::-1]) axes[0].set_title("原图") axes[1].imshow(dst[:, :, ::-1]) axes[1].set_title("透射后结果") plt.show()
运行结果
图像金字塔是图像多尺度表达的一种,最主要用于图像的分割,是一种以多分辨率来解释图像的有效但概念简单的结构
图像金字塔用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的分辨率逐步降低,且来源于同一张原始图的图像集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。
金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似,层级越高,图像越小,分辨率越低。
API
cv2.pyrUp(img)
cv2.pyrDown(img)
参考代码
import numpy as np import cv2 import matplotlib.pyplot as plt # 1 图像读取 img = cv2.imread("view.jpeg") # 2 进行图像采样 up_img = cv2.pyrUp(img) # 上采样操作 down_img = cv2.pyrDown(img) # 下采样操作 # 3 图像显示 cv2.imshow('enlarge', up_img) cv2.imshow('original', img) cv2.imshow('shrink', down_img) cv2.waitKey(0) cv2.destroyAllWindows()
运行结果
图像缩放:对图像进行放大或缩小
cv2.resize()
图像平移
指定平移矩阵后,调用 cv2.warpAffine()
平移图像
图像旋转
调用 cv2.getRotationMatrix2D()
获取旋转矩阵,然后调用 cv2.warpAffine()
进行旋转
仿射变换
调用 cv2.getAffineTransform()
将创建变换矩阵,最后该矩阵将传递给 cv2.warpAffine()
进行变换
透射变换
通过函数 cv2.getPerspectiveTransform()
找到变换矩阵,将 cv2.warpPerspective()
进行投射变换
金字塔
图像金字塔是图像多尺度表达的一种,使用的APl:
cv2.pyrUp()
:向上采样
cv2.pyrDown()
:向下采样
学习目标
在图像中,最小的单位是像素,每个像素p周围有8个邻接像素,点p的相邻像素的图像位置集称为p的邻域。
D邻域:对角上的点(x+1, y+1);(x+1, y-1);(x-1, y+1);(x-1, y-1),用**ND§**表示。
8邻域:4邻域的点+D邻域的点,用**N8§**表示。
令 V V V是用于定义邻接性的灰度值集合。
比如在二值图像中,如果我们把具有1值的像素归诸于邻接像素,则 V V V={1};当然了,在灰度图像中,也是一样的,只不过灰度图像中的V可能包含更多的元素,它可能是0-255范围的任意一个子集。
考虑三种类型的邻接:
m邻接(也称混合邻接):如果
1)q在N4( p)中,或
2)q在ND( p)中,且集合N4( p)∩N4(q)中没有来自
V
V
V中的像素,那么称这两个像素是m邻接的
连通性是描述区域和边界的重要概念,两个像素连通的两个必要条件是:
两个像素的位置是否相邻
两个像素的灰度值是否满足特定的相似性准则(或者是否相等)
形态学转换是基于图像形状的一些简单操作。它通常在二进制图像上执行。腐蚀和膨胀是两个基本的形态学运算符。然后它的变体形式如开运算,闭运算,礼帽黑帽等。
腐蚀和膨胀是最基本的形态学操作,腐蚀和膨胀都是针对白色部分(高亮部分)而言的。
膨胀就是使图像中高亮部分扩张,效果图拥有比原图更大的高亮区域;
腐蚀是原图中的高亮区域被蚕食,效果图拥有比原图更小的高亮区域。膨胀是求局部最大值的操作,腐蚀是求局部最小值的操作。
腐蚀的具体操作是:用一个结构元素扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为1,则该像素为1,否则为0。如下图所示,结构A被结构B腐蚀后:
腐蚀的作用是消除物体边界点,使目标缩小,可以消除小于结构元素的噪声点。
API
cv2.erode(img, kernel, iterations)
参数
具体操作是:用一个结构元素扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为0,则该像素为0,否则为1。如下图所示,结构A被结构B腐蚀后:
作用是将与物体接触的所有背景点合并到物体中,使目标增大,可添补目标中的孔洞。
API
cv2.dilate(img, kernel, iterations)
参数
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1 读取图片 img = cv2.imread('j.jpeg') # 2 创建核结构 kernel = np.ones((5, 5), np.uint8) # 5*5的全1阵 # 3 图像的腐蚀与膨胀 erosion = cv2.erode(img, kernel) # 腐蚀 dilate = cv2.dilate(img, kernel) # 膨胀 # 4 图像显示 fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 8), dpi=100) axes[0].imshow(img) axes[0].set_title("Original") axes[1].imshow(erosion) axes[1].set_title("Erosion") axes[2].imshow(dilate) axes[2].set_title("Dilate") plt.show()
运行结果
开运算和闭运算是将腐蚀和膨胀按照一定的次序进行处理。但这两者并不是可逆的,即先开后闭并不能得到原来的图像。
开运算是先腐蚀后膨胀。其作用是:分离物体,消除小区域。特点:消除噪点,去除小的干扰块,而不影响原来的图像。
闭运算与开运算相反,是先膨胀后腐蚀,作用是消除“闭合”物体里面的孔洞,特点:可以填充闭合区域。
API
cv2.morphologyEx(img, op, kernel)
参数
参考代码
# 1 读取图片 noise = cv2.imread('noise.jpeg') cave = cv2.imread('cave.jpeg') # 2 创建核结构 kernel = np.ones((10, 10), np.uint8) # 10*10的全1阵 # 3 图像的开闭运算 open_img = cv2.morphologyEx(noise, cv2.MORPH_OPEN, kernel) # 开运算 close_img = cv2.morphologyEx(cave, cv2.MORPH_CLOSE, kernel) # 闭运算 # 4 图像显示 fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0, 0].imshow(noise) axes[0, 0].set_title("有噪点的原图") axes[0, 1].imshow(open_img) axes[0, 1].set_title("开运算结果") axes[1, 0].imshow(cave) axes[1, 0].set_title("有空洞的原图") axes[1, 1].imshow(close_img) axes[1, 1].set_title("闭运算结果") plt.show()
运行结果
原图像与“开运算”的结果图之差,如下式计算:
dst = tophat(src, element) = src - open(src, element)
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。
礼帽运算用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。
“闭运算”的结果图与原图像之差,如下式计算:
dst = blackhat(src, element) = close(src, element) - src
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
黑帽运算用来分离比邻近点暗一些的斑块。
API
cv2.morphologyEx.(img, op, kernel)
参数
img:要处理的图像
op:处理方法
参数 | 功能 |
---|---|
cv2.MORPH_CLOSE | 闭运算 |
cv2.MORPH_OPEN | 开运算 |
cv2.MORPH_TOPHAT | 礼帽运算 |
cv2.MORPH_BLACKHAT | 黑帽运算 |
kernel:核结构
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1 读取图片 noise = cv2.imread('noise.jpeg') cave = cv2.imread('cave.jpeg') # 2 创建核结构 kernel = np.ones((10, 10), np.uint8) # 10*10的全1阵 # 3 图像的礼帽运算与黑帽运算 top_hat_img = cv2.morphologyEx(noise, cv2.MORPH_TOPHAT, kernel) # 礼帽运算 black_hat_img = cv2.morphologyEx(cave, cv2.MORPH_BLACKHAT, kernel) # 黑帽运算 # 4 图像显示 fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8), dpi=110) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0, 0].imshow(noise) axes[0, 0].set_title("有噪点的原图") axes[0, 1].imshow(top_hat_img) axes[0, 1].set_title("礼帽运算结果") axes[1, 0].imshow(cave) axes[1, 0].set_title("有空洞的原图") axes[1, 1].imshow(black_hat_img) axes[1, 1].set_title("黑帽运算结果") plt.show()
运行结果
邻域:4邻域、D邻域、8邻域
邻接:4邻接、8邻接、m邻接
连通:S是图像中的一个像素子集,若S的全部像素之间存在一个通路,则可以说S中的两个像素p和q在S中是连通的
形态学转换
腐蚀与膨胀
腐蚀:求局部最小值
膨胀:求局部最大值
开闭运算
开运算:先腐蚀后膨胀
闭运算:先膨胀后腐蚀
礼帽与黑帽运算
礼帽:原图像与开运算之差
黑帽:闭运算与原图像之差
学习目标
由于图像采集、处理、传输等过程不可避免的会受到噪声的污染,妨碍人们对图像理解及分析处理。
常见的图像噪声高斯噪声、椒盐噪声等。
椒盐噪声也称为脉冲噪声,是图像中经常见到的一种噪声,它是一种随机出现的白点或者黑点,可能是亮的区域有黑色像素或是在暗的区域有白色像素(或是两者皆有)。椒盐噪声的成因可能是影像讯号受到突如其来的强烈干扰而产生、类比数位转换器或位元传输错误等。例如失效的感应器导致像素值为最小值,饱和的感应器导致像素值为最大值。
高斯噪声是指噪声密度函数服从高斯分布的一类噪声。由于高斯噪声在空间和频域中数学上的易处理性,这种噪声(也称为正态噪声)模型经常被用于实践中。高斯随机变量z的概率密度函数由下式给出:
p
(
z
)
=
1
2
π
σ
e
−
(
z
−
μ
)
2
2
σ
2
p(z)=\frac{1}{\sqrt{2\pi}\sigma}e^{\frac{-(z-\mu)^2}{2\sigma^2}}
p(z)=2π
σ1e2σ2−(z−μ)2
其中z表示灰度值,μ表示z的平均值或期望值,σ表示z的标准差。标准差的平方σ2称为z的方差。高斯函数的曲线如图所示。
图像平滑从信号处理的角度看就是去除其中的高频信息,保留低频信息。因此我们可以对图像实施低通滤波。低通滤波可以去除图像中的噪声,对图像进行平滑。
根据滤波器的不同可分为均值滤波,高斯滤波,中值滤波,双边滤波。
采用均值滤波模板对图像噪声进行滤除。令
S
x
y
S~xy~
S xy 表示中心在(x, y)点,尺寸为mxn的矩形子图像窗口的坐标组。均值滤波器可表示为:
f
^
(
x
,
y
)
=
1
m
n
∑
(
s
,
t
)
∈
S
x
y
g
(
s
,
t
)
\hat{f}(x,y) = \frac{1}{mn}\sum_{(s, t) \in S_{xy}}g(s, t)
f^(x,y)=mn1(s,t)∈Sxy∑g(s,t)
由一个归一化卷积框完成的。它只是用卷积框覆盖区域所有像素的平均值来代替中心元素。
例如,3×3标准化的平均过滤器如下所示:
K
=
1
9
[
1
1
1
1
1
1
1
1
1
]
K = \frac{1}{9}
均值滤波的优点是算法简单,计算速度较快,缺点是在去噪的同时去除了很多细节部分,将图像变得模糊。
API
cv2.blur(scr, ksize, anchor, borderType)
参数
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1 图像读取 img = cv2.imread('noise_dog.jpeg') # 2 均值滤波 blur = cv2.blur(img, (5, 5)) # 3 图像显示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(121), plt.imshow(img[:, :, ::-1]), plt.title("原图") plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(blur[:, :, ::-1]), plt.title("均值滤波后结果") plt.xticks([]), plt.yticks([]) plt.show()
运行结果
二维高斯是构建高斯滤波器的基础,其概率分布函数如下所示:
G
(
x
,
y
)
=
1
2
π
σ
2
e
−
x
2
+
y
2
2
σ
2
G(x,y)=\frac{1}{2\pi\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}}
G(x,y)=2πσ21e−2σ2x2+y2
G
(
x
,
y
)
G(x, y)
G(x,y)的分布是一个突起的帽子的形状。这里的
σ
\sigma
σ可以看作两个值,一个是×方向的标准差
σ
x
σ_x
σx,另一个是y方向的标准差
σ
y
σ_y
σy。
当 σ x σ_x σx和 σ y σ_y σy取值越大,整个形状趋近于扁平;当 σ x σ_x σx和 σ y σ_y σy取值越小,整个形状越突起。
正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。计算平滑结果时,只需要将“中心点“作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。
高斯平滑在从图像中去除高斯噪声方面非常有效。
API
cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)
参数
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1 图像读取 img = cv2.imread('dog_noise.jpg') # 2 高斯滤波 blur = cv2.GaussianBlur(img, (5, 5), 1) # 3 图像显示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(121), plt.imshow(img[:, :, ::-1]), plt.title("原图") plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(blur[:, :, ::-1]), plt.title("高斯滤波后结果") plt.xticks([]), plt.yticks([]) plt.show()
运行结果
中值滤波是一种典型的非线性滤波技术,基本思想是用像素点邻域灰度值的中值来代替该像素点的灰度值。
中值滤波对椒盐噪声(salt-and-pepper noise)来说尤其有用,因为它不依赖于邻域内那些与典型值差别很大的值。
API
cv2.medianBlur(src, ksize)
参数
参考代码
# 1 图像读取
img = cv2.imread('dog_noise.jpeg')
# 2 中值滤波
blur = cv2.medianBlur(img, 5)
# 3 图像显示
plt.figure(figsize=(10, 8), dpi=100)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.subplot(121), plt.imshow(img[:, :, ::-1]), plt.title("原图")
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(blur[:, :, ::-1]), plt.title("中值滤波后结果")
plt.xticks([]), plt.yticks([])
plt.show()
运行结果
图像噪声
图像平滑
均值滤波:算法简单,计算速度快,在去除噪声的同时去除了很多细节部分,将图像变得模糊
cv2.blur()
高斯滤波:去除高斯噪声
cv2.GaussianBlur()
中值滤波:去除椒盐噪声
cv2.medianBlur()
学习目标
直方图是对数据进行统计的一种方法,并且将统计值组织到一系列实现定义好的bin当中。其中,bin为直方图中经常用到的一个概念,可以译为“直条”或“组距”,其数值是从数据中计算出的特征统计量,这些数据可以是诸如梯度、方向、色彩或任何其他特征。
图像直方图(Image Histogram)是用以表示数字图像中亮度分布的直方图,标绘了图像中每个亮度值的像素个数。这种直方图中,横坐标的左侧为较暗的区域,而右侧为较亮的区域。因此一张较暗图片的直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。
注意:直方图是根据灰度图进行绘制的,而不是彩色图像。假设有一张图像的信息(灰度值0-255,已知数字的范围包含256个值,于是可以按一定规律将这个范围分割成子区域(也就是bins)。如:
[
0
,
255
]
=
[
0
,
15
]
∪
[
16
,
30
]
⋯
∪
[
240
,
255
]
[0,255]=[0,15]\cup[16,30]\cdots\cup[240,255]
[0,255]=[0,15]∪[16,30]⋯∪[240,255]
然后再统计每一个bin)的像素数目。可以得到下图(其中x辅表示bin,y轴表示各个bin中的像素个数):
直方图的一些术语和细节:
直方图的意义:
使用OpenCV中的方式统计直方图,并使用matplotlab将其绘制出来
API
cv2.calcHist(images, channels, mask, ranges[, histaccumulate])
参数
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1 直接以灰度图的方式读入 img = cv2.imread('cat.jpg') gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2 统计灰度图 histr = cv2.calcHist([img], [0], None, [256], [0, 256]) # 左开右闭 # 3 绘制直方图 fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8), dpi=110) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(221), plt.imshow(img, cmap='gray'), plt.title("原图") plt.subplot(222), plt.imshow(gray_img, cmap='gray'), plt.title("灰度图") plt.subplot(212), plt.plot(histr), plt.title("直方图") plt.grid() plt.show()
运行结果
掩膜是用选定的图像、图形或物体,对要处理的图像进行遮挡,来控制图像处理的区域。
在数字图像处理中,我们通常使用二维矩阵数组进行掩膜。掩膜是由0和1组成一个二进制图像,利用该掩膜图像要处理的图像进行掩膜,其中1值的区域被处理,0值区域被屏蔽,不会处理。
掩膜的主要用途是:
掩膜在遥感影像处理中使用较多,当提取道路或者河流,或者房屋时,通过一个掩膜矩阵来对图像进行像素过滤,然后将我们需要的地物或者标志突出显示出来。
我们使用 cv.calcHist()
来查找完整图像的直方图。如果要查找图像某些区域的直方图,该怎么办?只需在要查找直方图的区域上创建一个白色的掩膜图像,否则创建黑色,然后将其作为掩码mask传递即可。
参考代码
# 1 直接以灰度图的方式读入 img = cv2.imread('luna.jpeg', 0) # 2 创建蒙版 mask = np.zeros(img.shape[:2], np.uint8) mask[50:450, 100:500] = 255 # 3 掩膜 masked_histr = cv2.bitwise_and(img, img, mask=mask) # 4 统计掩膜后图像的灰度图 mask_histr = cv2.calcHist([img], [0], mask, [256], [1, 256]) # 5 图像展示 fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10,8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0][0].imshow(img, cmap=plt.cm.gray) axes[0][0].set_title('原图') axes[0][1].imshow(mask, cmap=plt.cm.gray) axes[0][1].set_title('蒙版数据') axes[1][0].imshow(masked_histr, cmap=plt.cm.gray) axes[1][0].set_title('掩膜后数据') axes[1][1].plot(mask_histr) axes[1][1].grid() axes[1][1].set_title('灰度直方图') plt.show()
运行结果
想象一下,如果一副图像中的大多数像素点的像素值都集中在某一个小的灰度值值范围之内会怎样呢?如果一幅图像竖体很亮,那所有的像素值的取值个数应该都会很高。所以应该把它的直方图做一个横向拉伸(如下图),就可以扩大图像像素值的分布范围,提高图像的对比度,这就是直方图均衡化要做的事情。
“直方图均衡化”是把原始图像的灰度直方图从比较集中的某个灰度区间变成在更广泛灰度范围内的分布。直方图均衡化就是对图像进行非线性拉伸,重新分配图像像素值,使一定灰度范围内的像素数量大致相同。
这种方法提高图像整体的对比度,特别是有用数据的像素值分布比较接近时,在X光图像中使用广泛,可以提高骨架结构的显示,另外在曝光过度或不足的图像中可以更好的突出细节。
API
dst = cv2.equalizeHist(img)
参数
返回
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 1 直接以灰度图的方式读取图片 img = cv2.imread('luna.jpeg', 0) # 2 均衡化处理 dst = cv2.equalizeHist(img) # 3 绘制直方图 gray_img = cv2.calcHist([img], [0], None, [256], [0, 255]) gray_dst = cv2.calcHist([dst], [0], None, [256], [0, 255]) # 4 结果展示 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(221), plt.imshow(img, cmap='gray'), plt.title('原图', fontsize=15) plt.subplot(222), plt.imshow(dst, cmap='gray'), plt.title('均衡化处理后', fontsize=15) plt.subplot(223), plt.plot(gray_img), plt.title('原图直方图', fontsize=15) plt.subplot(224), plt.plot(gray_dst), plt.title('均衡化处理后直方图', fontsize=15) plt.show()
运行结果
上述的直方图均衡,我们考虑的是图像的全局对比度。在进行完直方图均衡化之后,图片背景的对比度被改变了,在许多情况下,这样做的效果并不好。如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UXXpQZB7-1652882574798)(Opencv%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E6%96%B0%E5%9B%BE%E7%89%87/%E8%87%AA%E9%80%82%E5%BA%94%E5%9D%87%E8%A1%A1%E5%8C%96.jpeg)]
为了解决这个问题,需要使用自适应的直方图均衡化。此时,整幅图像会被分成很多小块,这些小块被称为“tiles”(在OpenCV中tiles的大小默认是8×8),然后再对每一个小块分别进行直方图均衡化。所以在每一个的区域中,直方图会集中在某一个小的区域中)。如果有噪声的话,噪声会被放大。为了避免这种情况的出现要使用对比度限制。对于每个小块来说,如果直方图中的bin超过对比度的上限的话,就把其中的像素点均匀分散到其他bins中,然后在进行直方图均衡化。
最后,为了去除每一个小块之间的边界,再使用双线性差值,对每一小块进行拼接,使边界不明显。
API
cv2.createCLAHE(clipLimit, tileGridSize)
参数
参考代码
import matplotlib.pyplot as plt import numpy as np import cv2 # 1 以灰度图的方式读取图片 img = cv2.imread('luna.jpeg', 0) # 2 创建一个自适应均衡化的对象,并应用于图像 dst = cv2.equalizeHist(img) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) cl1 = clahe.apply(img) gray_img = cv2.calcHist([img], [0], None, [256], [0, 255]) gray_cl1 = cv2.calcHist([cl1], [0], None, [256], [0, 255]) gray_dst = cv2.calcHist([dst], [0], None, [256], [0, 255]) # 4 结果展示 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(231), plt.imshow(img, cmap='gray'), plt.title('原图', fontsize=15) plt.subplot(232), plt.imshow(dst, cmap='gray'), plt.title('均衡化处理后', fontsize=15) plt.subplot(233), plt.imshow(cl1, cmap='gray'), plt.title('自适应均衡化处理后', fontsize=15) plt.subplot(234), plt.plot(gray_img), plt.title('原图直方图', fontsize=15) plt.subplot(235), plt.plot(gray_dst), plt.title('均衡化处理后直方图', fontsize=15) plt.subplot(236), plt.plot(gray_cl1), plt.title('自适应均衡化处理后直方图', fontsize=15) plt.show()
运行结果
灰度直方图
直方图是图像中像素强度分布的图形表达方式。
统计了每一个强度值所具有的像素个数
不同的图像的直方图可能是相同的
掩膜
创建蒙版,透过mask进行传递,可获取感兴趣区域的直方图
直方图均衡化:增强图像对比度的一种方法
cv.equalizeHist()
:输入是灰度图像,输出是直方图均衡图像
自适应的直方图均衡化
将整幅图像分成很多小块,然后再对每一个小块分别进行直方图均衡化,最后进行拼接clahe=cv2.createCLAHE(clipLimit,tileGridSiz)
学习目标
边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。边缘的表现形式如下图所示:
图像边缘检测大幅度地减少了数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性。有许多方法用于边缘检测,它们的绝大部分可以划分为两类:基于搜索和基于零穿越。
基于零穿越:通过寻找图像二阶导数零穿越来寻找边界,代表算法是Laplacian算子。
Sobe边缘检测算法比较简单,实际应用中效率比Canny边缘检测效率要高,但是边缘不如Canny检测的准确,但是很多实际应用的场合,Sobel边缘却是首选,Sobel算子是高斯平滑与微分操作的结合体,所以其抗噪声能力很强,用途较多。尤其是效率要求较高,而对细纹理不太关心的时候。
对于不连续 的函数,一阶导数可以写作:
f
′
(
x
)
=
f
(
x
)
−
f
(
x
−
1
)
f^{'}(x)=f(x)-f(x-1)
f′(x)=f(x)−f(x−1)
或
f
′
(
x
)
=
f
(
x
+
1
)
−
f
(
x
)
f^{'}(x)=f(x+1)-f(x)
f′(x)=f(x+1)−f(x)
所以有:
f
′
(
x
)
=
f
(
x
+
1
)
−
f
(
x
−
1
)
2
f^{'}(x)=\frac{f(x+1)-f(x-1)}{2}
f′(x)=2f(x+1)−f(x−1)
假设要处理的图像为
I
I
I,在两个方向求导:
水平变化:将图像
I
I
I与奇数大小的模版进行卷积,结果为
G
x
G_x
Gx。比如,当模板大小为3时,
G
x
G_x
Gx为:
G
x
=
[
−
1
0
+
1
−
2
0
+
2
−
1
0
+
1
]
∗
I
G_x=
垂直变化:将图像
I
I
I与奇数大小的模板进行卷积,结果为
G
y
G_y
Gy。比如,当模板大小为3时,
G
y
G_y
Gy为:
G
y
=
[
−
1
−
2
−
1
0
0
0
+
1
+
2
+
1
]
∗
I
G_y=
在图像的每一点,结合以上两个结果求出:
G
=
G
x
2
+
G
y
2
G = \sqrt{{G_x^2}+{G_y^2}}
G=Gx2+Gy2
统计极大值所在的位置,就是图像的边缘。
注意:当内核大小为3时,以上Sobel可能产生比较明显的误差,为解决这一问题,我们使用Scharr函数,但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快,但结果却更加准确,其计算方法为:
G
x
=
[
−
3
0
+
3
−
10
0
+
10
−
3
0
+
3
]
∗
I
G
y
=
[
−
3
−
10
−
3
0
0
0
+
3
+
10
+
3
]
∗
I
G_x=
API
sobel_x_or_y = cv2.Sobel(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
参数
src:传入的图像
ddepth:图像的深度
dx和dy:指求导的阶数,0表示这个方向上没有求导,取值为0、1。
ksize:是Sobel算子的大小,即卷积核的大小,必须为奇数1、3、5、7,默认为3。
注重:如果ksize=-1,就演变为3×3的Scharr算子。
scale:缩放导数的比例常数,默认情况为没有伸缩系数。
borderType:图像边界的模式。默认值为cv2.BPRAER_DEFAULT。
Sobel函数求完导数后会有负值,还有会大于255的值。而原图像是uint8,即8位无符号数,所以Sobel建立的图像位数不够,会有截断。因此要使用16位有符号的数据类型,即 cv2.CV_16S
。处理完图像后,再使用 cv2.convertScaleAbs()
函数将其转回原来的uint8格式,否则图像无法显示。
Scale_abs = cv2.convertScaleAbs(x) # 格式转换函数
result = cv2.addweighted(src1,alpha,src2,beta) # 图像混合
参考代码
import cv2 import numpy as np from matplotlib import pyplot as plt # 1 读取图像 img = cv2.imread('luna.jpeg', 0) # 2 计算Sobel卷积结果 # 将计算sobel算子的部分中将ksize设为-1,就是利用Scharr算子进行边缘检测。 x1 = cv2.Sobel(img, cv2.CV_16S, 1, 0) # Sobel算子水平变换 y1 = cv2.Sobel(img, cv2.CV_16S, 0, 1) # Sobel算子垂直变换 x2 = cv2.Sobel(img, cv2.CV_16S, 0, 1, ksize=-1) # Scharr算子水平变换 y2 = cv2.Sobel(img, cv2.CV_16S, 0, 1, ksize=-1) # Scharr算子垂直变换 # 3 将数据进行转换 Sobel_absX = cv2.convertScaleAbs(x1) # convert 转换 scale 缩放 Sobel_absY = cv2.convertScaleAbs(y2) # convert 转换 scale 缩放 Scharr_absX = cv2.convertScaleAbs(x2) # convert 转换 scale 缩放 Scharr_absY = cv2.convertScaleAbs(y2) # convert 转换 scale 缩放 # 4 结果合成 result1 = cv2.addWeighted(Sobel_absX, 0.5, Sobel_absY, 0.5, 0) result2 = cv2.addWeighted(Scharr_absX, 0.5, Scharr_absY, 0.5, 0) # 5 图像显示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(131), plt.imshow(img, cmap=plt.cm.gray), plt.title('原图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(132), plt.imshow(result1, cmap=plt.cm.gray), plt.title('Sobel滤波后结果', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(133), plt.imshow(result2, cmap=plt.cm.gray), plt.title('Scharr滤波后结果', fontsize=15) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
Laplacian是利用二阶导数来检测边缘。因为图像是“2维”,我们需要在两个方向求导,如下式所示:
Δ
s
r
c
=
∂
2
s
r
c
∂
x
2
+
∂
2
s
r
c
∂
y
2
\Delta_{src}=\dfrac{\partial^2src}{\partial{x^2}}+\dfrac{\partial^2src}{\partial{y^2}}
Δsrc=∂x2∂2src+∂y2∂2src
那不连续的二阶导数是:
f
′
′
(
x
)
=
f
′
(
x
+
1
)
−
f
′
(
x
)
=
f
(
x
+
1
)
+
f
(
x
−
1
)
−
2
f
(
x
)
f^{''}(x)=f^{'}(x+1)-f^{'}(x)=f(x+1)+f(x-1)-2f(x)
f′′(x)=f′(x+1)−f′(x)=f(x+1)+f(x−1)−2f(x)
那使用的卷积核是:
k
e
r
n
e
l
=
[
0
1
0
1
−
4
1
0
1
0
]
kernel=
API
laplacian = cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
参数
参考代码
import cv2 import numpy as np from matplotlib import pyplot as plt # 1 读取图像 img = cv2.imread('luna.jpeg', 0) # 2 laplacian转换 result = cv2.Laplacian(img, cv2.CV_16S) scale_abs = cv2.convertScaleAbs(result) # 3 图像展示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(121), plt.imshow(img, cmap=plt.cm.gray), plt.title('原图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(scale_abs, cmap=plt.cm.gray), plt.title('Laplacian检测后结果', fontsize=15) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
Canny边缘检测算法是一种非常流行的边缘检测算法,是John F.Canny于1986年提出的,被认为是最优的边缘检测算法。
Canny边缘检测算法是由4步构成,分别介绍如下:
第一步:噪声去除
由于边缘检测很容易受到噪声的影响,所以首先使用5*5高斯滤波器去除噪声,在图像平滑那一章节中已经介绍过。
第二步:计算图像梯度
对平滑后的图像使用Sobel算子计算水平方向和竖直方向的一阶导数(Gx和Gy)。根据得到的这两幅梯度图(Gx和Gy)找到边界的梯度和方向,公式如下:
E
d
g
e
_
G
r
a
d
i
e
n
t
(
G
)
=
G
x
2
+
G
y
2
A
n
g
l
e
(
θ
)
=
t
a
n
−
1
(
G
y
G
x
)
Edge\_Gradient(G)=\sqrt{G^2_x+G_y^2}\\ Angle(\theta)=tan^{-1}\left(\frac{G_y}{G_x}\right)
Edge_Gradient(G)=Gx2+Gy2
Angle(θ)=tan−1(GxGy)
如果某个像素点是边缘,则其梯度方向总是垂直与边缘垂直。梯度方向被归为四类:垂直,水平,和两个对角线方向。
在获得梯度的方向和大小之后,对整幅图像进行扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。如下图所示:
A点位于图像的边缘,在其梯度变化方向,选择像素点B和C,用来检验A点的梯度是否为极大值,若为极大值,则进行保留,否则A点被抑制,最终的结果是具有“细边”的二进制图像。
现在要确定真正的边界。我们设置两个阈值:minVal和maxVal。当图像的灰度梯度高于maxVal时被认为是真的边界,低于minVal的边界会被抛弃。如果介于两者之间的话,就要看这个点是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。如下图:
如上图所示,A高于阈值maxval所以是真正的边界点,C虽然低于maxVal但高于minVal并且与A相连,所以也被认为是真正的边界点。而B就会被抛弃,因为低于maxVal而且不与真正的边界点相连。所以选择合适的maxVal和minVal 对于能否得到好的结果非常重要。
API
canny = cv2.Canny(image, threshold1, threshold2)
参数
参考代码
import cv2 import numpy as np from matplotlib import pyplot as plt # 1 读取图像 img = cv2.imread('luna.jpeg', 0) # 2 Canny边缘检测 lowThreshold = 1 max_lowThreshold = 100 canny = cv2.Canny(img, lowThreshold, max_lowThreshold) # 3 图像展示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(121), plt.imshow(img, cmap=plt.cm.gray), plt.title('原图', fontsize=20) plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(canny, cmap=plt.cm.gray), plt.title('Canny检测后结果', fontsize=20) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
边缘检测的原理
Sobel算子
基于搜索的方法获取边界
cv2.sobel()
cv2.convertScaleAbs()
cv2.addweights()
Laplacian算子
基于零穿越获取边界
cv2.Laplacian()
Canny算法
流程:
算子比较
算子 | 优缺点比较 |
---|---|
Roberts | 对具有陡峭的低噪声的图像处理效果较好,但利用Roberts算子提取边缘的结果是边缘比较粗,因此边缘定位不是很准确。 |
Sobel | 对灰度渐变和噪声较多的图像处理效果比较好,Sobel算子对边缘定位比较准确。 |
Kirsch | 对灰度渐变和噪声较多的图像处理效果较好。对灰度渐变和噪声较多的图像处理效果较好。 |
Prewitt | 对灰度渐变和噪声较多的图像处理效果较好。 |
Laplacian | 对图像中的阶跃性边缘点定位准确,对噪声非常敏感,丢失一部分边缘的方向信息,造成一些不连续的检测边缘。 |
LoG | LoG算子经常出现双边缘像素边界,而且该检测方法对噪声比较敏感,所以很少用LoG算子检测边缘,而是用来判断边缘像素是位于图像的明区还是暗区。 |
Canny | 此方法不容易受噪声的干扰,能够检测到真正的弱边缘。在edge函数中,最有效的边缘检测方法是Canny方法。该方法的优点在于使用两种不同的阈值分别检测强边缘和弱边缘,并且仅当弱边缘与强边缘相连时,才将弱边缘包含在输出图像中。因此,这种方法不容易被噪声“填充”,跟容易检测出真正的弱边缘。 |
学习目标
所谓的模板匹配,就是在给定的图片中查找和模板最相似的区域,该算法的输入包括模板和图片,整个任务的思路就是按照滑窗的思路不断的移动模板图片,计算其与图像中对应区域的匹配度,最终将匹配度最高的区域选择为最终结果。
实现流程
准备两幅图像:
原图像(I):在这幅图中,找到与模板相匹配的区域
模板(T):与原图像进行比对的图像块
滑动模板图像和原图像进行比对:
将模板块每次移动一个像素(从左往右,从上往下),在每一个位置,都计算与模板图像的相似程度。
API
res = cv2.matchTemplate(img, template, method)
参数
参考代码
示例:匹配同福客栈合照中的白展堂:
模板如下:
import cv2 import numpy as np from matplotlib import pyplot as plt # 1图像和模板读取 img = cv2.imread('wulin.jpeg') img2 = cv2.imread('wulin.jpeg') template = cv2.imread('bai.jpeg') h, w, l = template.shape # 2模板匹配 # 2.1模板匹配 res = cv2.matchTemplate(img, template, cv2.TM_CCORR) # 2.2返回图像中最匹配的位置,确定左上角的坐标,并将匹配位置绘制在图像上 min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) # 使用平方差时最小值为最佳匹配位置 # top_left=min_loc top_left = max_loc bottom_right = (top_left[0] + w, top_left[1] + h) cv2.rectangle(img, top_left, bottom_right, (0, 255, 0), 2) # 3图像显示 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(121), plt.imshow(img2[:, :, ::-1]), plt.title('匹配图像', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(img[:, :, ::-1]), plt.title('匹配结果', fontsize=15) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
拓展:模板匹配不适用于尺度变换,视角变换后的图像,这时我们就要使用关键点匹配算法,比较经典的关键点检测算法包括SIFT和SURF等,主要的思路是首先通过关键点检测算法获取模板和测试图片中的关键点;然后使用关键点匹配算法处理即可,这些关键点可以很好的处理尺度变化、视角变换、旋转变化、光照变化等,具有很好的不变性。
霍夫变换常用来提取图像中的直线和圆等几何形状,如下图所示:
在笛卡尔坐标系中,一条直线由两个点 A = ( x 1 , y 1 ) A=(x_1,y_1) A=(x1,y1)和 B = ( x 2 , y 2 ) B=(x_2,y_2) B=(x2,y2)确定,如下图所示:
将直线
y
=
k
x
+
b
y=kx+b
y=kx+b可写成关于
(
k
,
q
)
(k,q)
(k,q)的函数表达式:
{
q
=
−
k
x
1
+
y
1
q
=
−
k
x
2
+
y
2
对应的变换通过图形直观的表示如下:
变换后的空间叫做霍夫空间。即:笛卡尔坐标系中的一条直线,对应于霍夫空间中的一个点。反过来,同样成立,霍夫空间中的一条线,对应于笛卡尔坐标系中的一个点,如下所示:
A、B两个点,对应于霍夫空间的情形:
三点共线的情况:
可以看出如果在笛卡尔坐标系的点共线,那么这些点在霍夫空间中对应的直线交于一点。
如果不止存在一条直线时,如下所示:
我们选择尽可能多的直线汇成的点,上图中三条直线汇成的A、B两点,将其对应回笛卡尔坐标系中的直线:
到这里我们似乎已经完成了霍夫变换的求解。但如果像下图这种情况时:
上图中的直线是
x
=
2
x=2
x=2,那
(
k
,
q
)
(k,q)
(k,q)怎么确定呢?
为了解决这个问题,我们考虑将笛卡尔坐标系转换为极坐标。
在极坐标下是一样的,极坐标中的点对应于霍夫空间的线,这时的霍夫空间是不再是参数
(
k
,
q
)
(k,q)
(k,q)的空间,而是
(
ρ
,
θ
)
(\rho,\theta)
(ρ,θ)的空间,
ρ
\rho
ρ是原点到直线的垂直距离,
θ
\theta
θ表示直线的垂线与横轴顺时针方向的夹角,垂直线的角度为0度,水平线的角度是180度。
我们只要求得霍夫空间中的交点的位置,即可得到原坐标系下的直线。
实现流程
假设有一个大小为100*100的图片,使用霍夫变换检测图片中的直线,则步骤如下所示:
该数组的大小决定了结果的准确性,若希望角度的精度为1度,那就需要180列。对于
ρ
\rho
ρ,最大值为图片对角线的距离,如果希望精度达到像素级别,行数应该与图像的对角线的距离相等。
取直线上的第一个点 ( x , y ) (x, y) (x,y),将其带入直线在极坐标中的公式中,然后遍历 θ \theta θ的取值:0,1,2,…,180,分别求出对应的p值,如果这个数值在上述累加器中存在相应的位置,则在该位置上加1.
取直线上的第二个点,重复上述步骤,更新累加器中的值。对图像中的直线上的每个点都直线以上步骤,每次更新累加器中的值。
搜索累加器中的最大值,并找到其对应的 ( ρ , θ ) (\rho,\theta) (ρ,θ),就可将图像中的直线表示出来。
API
cv2.HoughLines(img, rho ,theta, threshold)
参数
img:检测的图像,要求是二值化图像,所以在调用霍夫变换之前首先要进行二值化,或者进行Canny边缘检测
rho、theta: ρ \rho ρ和 θ \theta θ的精度值
threshold:阈值,只有累加器中的值高于该阈值时才被认为是直线。
实现过程
霍夫线检测的流程如下图所示,这是stackflow上一个关于霍夫线变换的解释:
参考代码
示例:检测下述图像中的直线:
import cv2 import numpy as np from matplotlib import pyplot as plt # 1 加载图片,转为二值图 img = cv2.imread('calendar.jpeg') img2 = cv2.imread('calendar.jpeg') gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50, 150) # Canny边缘检测得到边缘信息 # 2 霍夫直线变换 lines = cv2.HoughLines(edges, 0.8, np.pi / 180, 150) # lines是列表 [rho, theta] # 3 将检测的线绘制在图像上(注意是极坐标) for line in lines: rho, theta = line[0] a = np.cos(theta) b = np.sin(theta) x0 = a * rho y0 = b * rho x1 = int(x0 + 1000 * (-b)) # 图像是整形,所以坐标值也是整形 y1 = int(y0 + 1000 * (a)) x2 = int(x0 - 1000 * (-b)) y2 = int(y0 - 1000 * (a)) cv2.line(img2, (x1, y1), (x2, y2), (0, 255, 0)) # 4 图像显示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(221), plt.imshow(img[:, :, ::-1]), plt.title('原图') plt.xticks([]), plt.yticks([]) plt.subplot(222), plt.imshow(gray, cmap=plt.cm.gray), plt.title('灰度图') plt.xticks([]), plt.yticks([]) plt.subplot(223), plt.imshow(edges, cmap=plt.cm.gray), plt.title('边缘图') plt.xticks([]), plt.yticks([]) plt.subplot(224), plt.imshow(img2[:, :, ::-1]), plt.title('霍夫变换线检测结果') plt.xticks([]), plt.yticks([]) plt.show()
运行结果
原理
圆的表示式是:
(
x
−
a
)
2
+
(
y
−
b
)
2
=
r
(x-a)^2+(y-b)^2=r
(x−a)2+(y−b)2=r
其中
a
a
a和
b
b
b表示圆心坐标,
r
r
r表示圆半径,因此标准的霍夫圆检测就是在这三个参数组成的三维空间累加器上进行圆形检测,此时效率就会很低,所以OpenCV中使用霍夫梯度法进行圆形的检测。
霍夫梯度法将霍夫圆检测范围两个阶段,第一阶段检测圆心,第二阶段利用圆心推导出圆半径。
原则上霍夫变换可以检测任何形状,但复杂的形状需要的参数就多,霍夫空间的维数就多,因此在程序实现上所需的内存空间以及运行效率上都不利于把标准霍夫变换应用于实际复杂图形的检测中。霍夫梯度法是霍夫变换的改进,它的目的是减小霍夫空间的维度,提高效率。
API
在OpenCV中检测图像中的圆环使用的API是:
ircles = cv2.HoughCircles(image, method, dp, minDist, param1=100, param2=100, minRadius=0, maxRadius=0)
参数
返回
参考代码
import cv2 import numpy as np import matplotlib.pyplot as plt # 1 读取图像 planets = cv2.imread("planets.jpeg") planet = cv2.imread("planets.jpeg") # 2 霍夫圆检测对噪声敏感,对图像先进行中值滤波,去噪点 median_img = cv2.medianBlur(planet, 5) # 3 转为灰度图 gray_img = cv2.cvtColor(median_img, cv2.COLOR_BGRA2GRAY) # 3 霍夫圆检测 circles = cv2.HoughCircles(gray_img, cv2.HOUGH_GRADIENT, 1, 200, param1=100, param2=30, minRadius=0, maxRadius=100) # 4 将检测结果绘制在图像上 for i in circles[0, :]: # 遍历矩阵每一行的数据 # 绘制圆形 cv2.circle(planets, (i[0], i[1]), i[2], (0, 255, 0), 2) # 绿色半径 # 绘制圆心 cv2.circle(planets, (i[0], i[1]), 2, (0, 0, 255), -1) # 红色圆心 # 5 图像显示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(221), plt.imshow(planet[:, :, ::-1]), plt.title('原图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(222), plt.imshow(median_img[:, :, ::-1]), plt.title('滤波图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(223), plt.imshow(gray_img, cmap=plt.cm.gray), plt.title('灰度图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(224), plt.imshow(planets[:, :, ::-1]), plt.title('检测图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
模板匹配
原理:在给定的图片中查找和模板最相似的区域
APl:利用 cv2.matchTemplate()
进行模板匹配,然后
使用 cv2.minMaxLoc()
搜索最匹配的位置。
霍夫线检测
原理:将要检测的内容转换到霍夫空间中,利用累加器统计最优解,将检测结果表示处理
APl:cv2.HoughLines()
注意:该方法输入是的二值化图像(边缘图像),在进行检测前要将图像进行二值化处理
霍夫圆检测
方法:霍夫梯度法
API:cv2.HoughCircles()
傅里叶
学习目标
傅里叶变换是由法国的一位数学家Joseph Fourier在18世纪提出来的,他认为:任何连续周期的信号都可以由一组适当的正弦曲线组合而成。
傅里叶变换是描述信号的需要,它能够反映信号的特征,并可以使用特征值进行量化,比如正弦波可以使用幅值和频率进行描述。下面这幅图是变压器空载电流的输入波形:
它看起来和正弦波很相近,但很难定量的描述其特征,采用傅里叶变换后,得到下述的频谱图(幅值):
从该频谱图中可以清楚的看到,主要包括3,5,7,9次谐波,我们就可以对原信号进行描述。
傅里叶变换是一种信号分析方法,它使我们能够对信号的构成和特点进行深入和定量的研究,把信号通过频谱的方式进行准确的、定量的描述。
那我们为什么要把信号分解为正弦波的组合,而不是其他波形呢?
傅里叶变换是信号的分析方法,目的就是要简化问题,而不是将其变复杂,傅里叶选择了正弦波,而没有选择其他波形,是因为正弦波有任何其他波形不具有的特点:正弦波输入至任何线性系统中,不会产生新的频率成分,输出的仍是正弦波,改变的仅仅是幅值和相位。用单位幅值的不同频率的正弦波输入至某线性系统,记录其输出正弦波的幅值和频率的关系,就得到该系统的幅频特性,记录输出正弦波的相位和频率的关系,就得到该系统的相频特性。线性系统是自动控制研究的主要对象,我们只要研究系统对正弦波的输入输出关系,就可以知道该系统对任意输入信号的响应。这是傅里叶变换的最主要的意义。
傅里叶变换是将难以处理的时域信号转换成易于分析的频域信号,那频域和时域到底是什么呢?
时域:时域是真实的世界,是唯一存在的域。从我们出生开始,所接触的这个世界就是随着时间在变化的,如花开花落,四季变换,生老病死等。以时间作为参照来分析动态世界的方法我们称其为时域分析。
比如说一段音乐,就是一个随时间变化的震动,这就是时域的表示,如下图:
频域:**频域它不是真实的,而是一个数学构造。**频域是一个遵循特定规则的数学范畴,也被一些学者称为上帝视角。结合上面对时域的理解,如果时域是运动永不停止的,那么频域就是静止的。
正弦波是频域中唯一存在的波形,这是频域中最重要的规则,即正弦波是对频域的描述,因为频域中的任何波形都可用正弦波合成。
在看上面那段音乐,我们可以将其表示成频域形式,就是一个永恒的音符。
而对于信号来说,信号强度随时间的变化规律就是时域特性,信号是由哪些单一频率的信号合成的就是频域特性,傅里叶变换实质就是是频域函数和时域函数的转换。
那频域与时域之间的关系是什么样的呢?利用正弦函数的叠加成一个矩形,不仅仅是矩形,你能想到的任何波形都是可以如此方法用正余弦波叠加起来的。如下图所示,时域是永远随着时间的变化而变化的,而频域就是装着正余弦波的空间。
从时域来看,我们会看到一个近似为矩形的波,而我们知道这个矩形的波可以拆分为一些正弦波的叠加。而从频域方向来看,我们就看到了每一个正余弦波的幅值,每两个正弦波之间都还有一条直线,那并不是分割线,而是振幅为 0 的正弦波!也就是说,为了组成特殊的曲线,有些正弦波成分是不需要的。随着叠加的递增,所有正弦波中上升的部分逐渐让原本缓慢增加的曲线不断变陡,而所有正弦波中下降的部分又抵消了上升到最高处时继续上升的部分使其变为水平线。一个矩形就这么叠加而成了。
我们看下面的动图理解,如下所示:
在傅里叶变换中怎么描述变换后的结果呢?有两个概念:频谱和相位谱。
频谱:将信号分解为若干不同频率的正弦波,那么每一个正弦波的幅度,就叫做频谱,也叫做幅度谱。
相位谱:频谱只代表了一个正弦函数的幅值,而要准确描述一个正弦函数,我们不仅需要幅值,还需要相位,不同相位决定了波的位置,所以对于频域分析,仅仅有频谱(振幅谱)是不够的,我们还需要一个相位谱
如上图所示:投影点我们用粉色点来表示,红色的点表示离正弦函数频率轴最近的一个峰值,而相位差就是粉色点和红色点水平距离。将相位差画到一个坐标轴上就形成了相位谱
根据原信号的属性,我们可以将傅里叶变换分为以下几种:
在实际应用较多的是傅里叶变换、傅里叶级数离散傅里叶变换,我们对其进行分别介绍。
任意波形都可以通过正弦波的叠加来表示,正弦波可以通过欧拉公式写成指数的形式,欧拉公式如下:
e
i
t
=
cos
(
t
)
+
i
sin
(
t
)
e^{it}=\cos(t)+i\sin(t)
eit=cos(t)+isin(t)
所以以下内容都是以指数形式进行展示。
任意的周期连续信号都可以使用正弦波叠加而成,这叫做傅里叶级数,写成指数形式如下所示:
f
(
t
)
=
∑
−
∞
∞
c
n
e
i
2
π
n
t
t
d
t
f(t) = \sum_{-\infty}^{\infty}c_ne^{i\frac{2\pi nt}t }dt
f(t)=−∞∑∞cneit2πntdt
其中
c
n
c_n
cn表示傅里叶级数:
c
n
=
1
T
∫
−
T
2
T
2
f
(
t
)
e
−
i
2
π
n
t
t
d
t
c_n=\frac{1}{T}\int_{-\frac{T}{2}}^{\frac{T}{2}}f(t)e^{-i\frac{2\pi nt}t }dt
cn=T1∫−2T2Tf(t)e−it2πntdt
对于非周期的连续信号,也可以使用正弦信号来逼近,这时我们将非周期信号看做周期无限大的周期信号,则有:
F
(
ω
)
=
∫
−
∞
∞
f
(
t
)
e
−
i
ω
t
d
t
F(\omega) = \int_{-\infty}^{\infty}f(t)e^{-i\omega t}dt
F(ω)=∫−∞∞f(t)e−iωtdt
其中,
ω
\omega
ω表示频率,t表示时间。我们叫做傅里叶变换。从中可以看出非周期信号的频谱是连续的非周期信号。
逆变换为:
f
(
t
)
=
1
2
π
∫
−
∞
∞
F
(
ω
)
e
i
ω
t
d
w
f(t) = \frac{1}{2\pi}\int_{-\infty}^{\infty}F(\omega)e^{i\omega t}dw
f(t)=2π1∫−∞∞F(ω)eiωtdw
利用上述公式就可将频域信号转换为时域信号。
由于数字信号处理是希望在计算机上实现各种运算和变换,其所涉及的变量和运算都是离散的,因此对于数字信号处理,应该找到在时域和频域都是离散的傅里叶变换,即离散傅里叶变换。
对于非周期的离散信号进行傅里叶变换就是离散傅里叶变换,其计算方法如下所示:
F
(
k
)
=
∑
n
=
0
N
−
1
f
(
n
)
e
−
i
2
π
k
n
N
F(k)= \sum_{n=0}^{N-1}f(n)e^{-i\frac{2\pi kn}{N}}
F(k)=n=0∑N−1f(n)e−iN2πkn
其中N表示傅里叶变换的点数,k表示傅里叶变换的频谱。
逆变换为:
f
(
n
)
=
1
N
∑
k
=
0
N
−
1
F
(
k
)
e
i
k
n
2
π
N
f(n) = \frac{1}{N}\sum_{k=0}^{N-1}F(k)e^{ikn\frac{2\pi}{N}}
f(n)=N1k=0∑N−1F(k)eiknN2π
图像是二维的离散信号,所以我们在对图像进行二维傅里叶变换。对于M*N的一幅图像的离散二维傅里叶变换,公式如下:
F
(
u
,
v
)
=
∑
x
=
0
M
−
1
∑
y
=
0
N
−
1
f
(
x
,
y
)
e
−
i
2
π
(
u
x
M
+
v
y
N
)
F(u,v) = \sum_{x=0}^{M-1}\sum_{y=0}^{N-1}f(x,y)e^{-i2\pi(\frac{ux}M+\frac{vy}N)}
F(u,v)=x=0∑M−1y=0∑N−1f(x,y)e−i2π(Mux+Nvy)
其中u和v确定频率,f(x,y)是灰度值,该式的意义是两个求和号对图像进行遍历,f(x,y)取出原像素的数值,当固定x时,横轴不动,对y进行遍历时,
v
y
N
\frac{vy}{N}
Nvy表示变换前像素的位置比例与变换后的位置相乘,映射到新的位置,且能够反映像素沿y方向距离的差异,越靠后的像素(y越大)
v
y
N
\frac{vy}{N}
Nvy值越大,即
v
y
N
\frac{vy}{N}
Nvy能够反映出不同位置(纵轴)像素之间的差异;前一项含义为保留像素相对位置(横轴)的信息(遍历y时为常数),2π为修正参数。
逆变换由下式给出:
f
(
x
,
y
)
=
∑
x
=
0
M
−
1
∑
y
=
0
N
−
1
F
(
u
,
v
)
e
i
2
π
(
u
x
M
+
v
y
N
)
f(x,y) = \sum_{x=0}^{M-1}\sum_{y=0}^{N-1}F(u,v)e^{i2\pi(\frac{ux}M+\frac{vy}N)}
f(x,y)=x=0∑M−1y=0∑N−1F(u,v)ei2π(Mux+Nvy)
图像的频率是表征图像中灰度变化剧烈程度的指标,是灰度在平面空间上的梯度。如:大面积的沙漠在图像中是一片灰度变化缓慢的区域,对应的频率值很低;而对于地表属性变换剧烈的边缘区域在图像中是一片灰度变化剧烈的区域,对应的频率值较高。傅里叶变换在实际中有非常明显的物理意义,从物理效果看,傅里叶变换是将图像从空间域转换到频率域,其逆变换是将图像从频率域转换到空间域。换句话说,傅里叶变换的物理意义是将图像的灰度分布函数变换为图像的频率分布函数。
傅里叶逆变换是将图像的频率分布函数变换为灰度分布函数。
我们在做DFT时是将图像的空域和频域沿x和y方向进行无限周期拓展的,如下图所示:
如果只取其中一个周期,会得到:
对一幅原图像进行傅立叶变换的结果,原点在窗口的左上角,即变换后的直流成分位于左上角,低频成分分布在窗口的四角。为了便于频域的滤波和频谱的分析,通常采用平移技术使直流成分位于窗口中心,即变换后的坐标原点移到窗口中心,因而围绕坐标中心是低频,向外是高频分量。如下图所示:
经中心化后的频谱为:
在经过频谱居中后的频谱中,中间最亮的点是最低频率,属于直流分量,越往外,频率越高,如下所示:
在OPenCV中实现图像的傅里叶变换,使用的是:
正变换:
dft = cv2.dft(src, dst=None)
参数:
返回:
逆变换:
img = cv.idft(dft)
参数:
返回:
实现:
import numpy as np import cv2 as cv from matplotlib import pyplot as plt # 1 读取图像 img = cv.imread('./image/deer.jpeg',0) # 2 傅里叶变换 # 2.1 正变换 dft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT) # 2.2 频谱中心化 dft_shift = np.fft.fftshift(dft) # 2.3 计算频谱和相位谱 mag, angle = cv.cartToPolar(dft_shift[:,:,0], dft_shift[:,:,1], angleInDegrees=True) mag=20*np.log(mag) # 3 傅里叶逆变换 # 3.1 反变换 img_back = cv.idft(dft) # 3.2 计算灰度值 img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1]) # 4 图像显示 plt.figure(figsize=(10,8)) plt.subplot(221),plt.imshow(img, cmap = 'gray') plt.title('输入图像'), plt.xticks([]), plt.yticks([]) plt.subplot(222),plt.imshow(mag, cmap = 'gray') plt.title('频谱'), plt.xticks([]), plt.yticks([]) plt.subplot(223),plt.imshow(angle, cmap = 'gray') plt.title('相位谱'), plt.xticks([]), plt.yticks([]) plt.subplot(224),plt.imshow(img_back, cmap = 'gray') plt.title('逆变换结果'), plt.xticks([]), plt.yticks([]) plt.show()
结果展示:
图像变换到频域后,就可以进行频域滤波,主要包括:高通滤波,低通滤波,带通滤波和带阻滤波。
我们知道,图像在经过傅里叶变换后,经频谱中心化后,从中间到外面,频率上依次是从低频到高频的。那么我们假设把中间规定一小部分去掉,是不是相对于把低频信号去掉了呢?这也就是相当于进行了高通滤波。
这个滤波模板如下图所示:
其中黑色部分为0,白色部分为1,我们将这个模板与图像的傅里叶变换相与就实现了高通滤波。如下所示:
import numpy as np import cv2 as cv from matplotlib import pyplot as plt # 1 读取图像 img = cv.imread('./image/deer.jpeg',0) # 2 设计高通滤波器(傅里叶变换结果中有两个通道,所以高通滤波中也有两个通道) rows,cols = img.shape mask = np.ones((rows,cols,2),np.uint8) mask[int(rows/2)-30:int(rows/2)+30,int(cols/2)-30:int(cols/2)+30,:] = 0 # 3 傅里叶变换 # 3.1 正变换 dft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT) # 3.2 频谱中心化 dft_shift = np.fft.fftshift(dft) # 3.3 滤波 dft_shift = dft_shift * mask # 3.4 频谱去中心化 dft_shift = np.fft.fftshift(dft_shift) # 3 傅里叶逆变换 # 3.1 反变换 img_back = cv.idft(dft_shift) # 3.2 计算灰度值 img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1]) plt.subplot(121),plt.imshow(img, cmap = 'gray') plt.title('输入图像'), plt.xticks([]), plt.yticks([]) plt.subplot(122),plt.imshow(img_back, cmap = 'gray') plt.title('高通滤波结果'), plt.xticks([]), plt.yticks([]) plt.show()
从结果中可以看出,高通滤波器有利于提取图像的轮廓,图像的轮廓或者边缘或者一些噪声处,灰度变化剧烈,那么在把它们经过傅里叶变换后。就会变成高频信号(高频是捕捉细节的),所以在把图像低频信号滤掉以后剩下的自然就是轮廓了。
现在我们看下低通滤波的效果,构造一个低通滤波器很简单,只要把上述模板中的1改为0,0改为1即可。把设计高通滤波器部分的代码改成如下所示:
rows,cols = img.shape
mask = np.zeros((rows,cols,2),np.uint8)
mask[int(rows/2)-30:int(rows/2)+30,int(cols/2)-30:int(cols/2)+30,:] = 1
低通滤波的效果如下图所示:
从结果中可看到低通滤波后图像轮廓变模糊了,图像的大部分信息基本上都保持了。图像的主要信息都集中在低频上,所以低通滤波器的效果是这样也是能够理解的。上述的高通、低通滤波器的构造有0,1构成的理想滤波器,也是最简单的滤波器,另一些其它的滤波器。比方说高斯滤波器,butterworth滤波器等等,如下图所示:
我们把高通和低通的一部分结合在模板中就形成了带通滤波器,它容许一定频率范围信号通过, 但减弱(或减少)频率低于於下限截止频率和高于上限截止频率的信号的通过,如下图所示:
还是以理想的带通滤波器演示如下,将构建的滤波的代码修改如下:
rows,cols = img.shape
mask1 = np.ones((rows,cols,2),np.uint8)
mask1[int(rows/2)-8:int(rows/2)+8,int(cols/2)-8:int(cols/2)+8] = 0
mask2 = np.zeros((rows,cols,2),np.uint8)
mask2[int(rows/2)-80:int(rows/2)+80,int(cols/2)-80:int(cols/2)+80] = 1
mask = mask1*mask2
结果如下所示:
这就是带通的效果,它既能保留一部分低频,也能保留一部分高频。至于保留多少,根据需求选择就可以了。
带阻滤波器减弱(或减少)一定频率范围信号, 但容许频率低于於下限截止频率和高于上限截止频率的信号的通过,如下示:
在代码中将设计滤波器部分改为如下所示:
mask = np.ones((rows,cols,2),np.uint8)
mask[int(rows/2)+80:int(rows/2)+150,int(cols/2)-150:int(cols/2)+150] = 0
mask[int(rows/2)-150:int(rows/2)-80,int(cols/2)-150:int(cols/2)+150] = 0
mask[int(rows/2)-150:int(rows/2)+150,int(cols/2)+80:int(cols/2)+150] = 0
mask[int(rows/2)-150:int(rows/2)+150,int(cols/2)-150:int(cols/2)-80] = 0
plt.imshow(mask[:,:,1],cmap=plt.cm.gray)
结果如下所示:
从结果中可看到带阻滤波器保持了原图像的大部分信息,图像的主要信息都集中在低频上,而边缘轮廓信息都在高频位置。带阻滤波器滤除了中频信息,保留了低频和高频信息,所以对图像的信息破坏是比较小的。
傅里叶变换的理解
任何连续周期的信号都可以由一组适当的正弦曲线组合而成
相关概念:
时域:以时间作为参照来分析动态世界的方法
频域:频域它不是真实的,而是一个数学构造
幅度谱:将信号分解为若干不同频率的正弦波,那么每一个正弦波的幅度,就叫做频谱,也叫做幅度谱
相位谱:每一个正弦波的相位,就叫做相位谱
傅里叶变换分类
傅里叶级数:任意的周期连续信号的傅里叶变换
傅里叶变换:非周期连续信号
离散傅里叶变换:非周期离散信号
图像中的应用
二维傅里叶变换
意义:将图像的灰度分布函数变换为图像的频率分布函数。
API:
cv.dft()
cv.idft()
滤波:高通,低通,带通,带阻
API
cv2.findContours(img, mode, method)
参数
mode。轮际检索模式
RETR EXTERNAL:只检索最外面的轮廓;RETR_LIST:检索所有的轮廓,并将其保存到一条链表当中;
·RETR_CCOMP:检索所有的轮廓,并将他们组织为两层;顶层是各部分的外部边界,第二层是空洞的边界:
·RETR_TREE:检索所有的轮廓,并构嵌套轮廓的整个层次;method:轮廊运近方法
·CHAIN_APPROX NONE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)。
·CHAIN_APPROX_SIMPLE:压缩水平的、垂言的和斜的部分,也就是,函数只保留他们的终点部分。
学习目标
大多数人都玩过拼图游戏。首先拿到完整图像的碎片,然后把这些碎片以正确的方式排列起来从而重建这幅图像。如果把拼图游戏的原理写成计算机程序,那计算机就也会玩拼图游戏了。
在拼图时,我们要寻找一些唯一的特征,这些特征要适于被跟踪,容易被比较。我们在一副图像中搜索这样的特征,找到它们,而且也能在其他图像中找到这些特征,然后再把它们拼接到一起。我们的这些能力都是天生的。
那这些特征是什么呢?我们希望这些特征也能被计算机理解。
如果我们深入的观察一些图像并搜索不同的区域,以下图为例:
在图像的上方给出了六个小图。找到这些小图在原始图像中的位置。你能找到多少正确结果呢?
A和B是平面,而且它们的图像中很多地方都存在。很难找到这些小图的准确位置。
C和D也很简单。它们是建筑的边缘。可以找到它们的近似位置,但是准确位置还是很难找到。这是因为:
沿着边缘,所有的地方都一样。所以边缘是比平面更好的特征,但是还不够好。
最后E和F是建筑的一些角点。它们能很容易的被找到。因为在角点的地方,无论你向哪个方向移动小图,结果都会有很大的不同。所以可以把它们当成一个好的特征。为了更好的理解这个概念我们再举个更简单的例子。
如上图所示,蓝色框中的区域是一个平面很难被找到和跟踪。无论向哪个方向移动蓝色框,都是一样的。对于黑色框中的区域,它是一个边缘。如果沿垂直方向移动,它会改变。但是如果沿水平方向移动就不会改变。而红色框中的角点,无论你向那个方向移动,得到的结果都不同,这说明它是唯一的。所以,我们说角点是一个好的图像特征,也就回答了前面的问题。
角点是图像很重要的特征,对图像图形的理解和分析有很重要的作用。角点在三维场景重建运动估计,目标跟踪、目标识别、图像配准与匹配等计算机视觉领域起着非常重要的作用。在现实世界中,角点对应于物体的拐角,道路的十字路口、丁字路口等
图像特征
图像特征要有区分性,容易被比较。一般认为角点,斑点等是较好的图像特征
特征检测:找到图像中的特征
特征描述:对特征及其周围的区域进行描述
学习目标
Harris角点检测的思想是通过图像的局部的小窗口观察图像,角点的特征是窗口沿任意方向移动都会导致图像灰度的明显变化,如下图所示:
将上述思想转换为数学形式,即将局部窗口向各个方向移动
(
u
,
v
)
(u,v)
(u,v)并计算所有灰度差异的总和,表达式如下:
E
(
u
,
v
)
=
∑
x
,
y
w
(
x
,
y
)
[
I
(
x
+
u
,
y
+
v
)
−
I
(
x
,
y
)
]
2
E(u,v)=\sum_{x,y}w(x,y)[I(x+u,y+v)-I(x,y)]^2
E(u,v)=x,y∑w(x,y)[I(x+u,y+v)−I(x,y)]2
其中
I
(
x
,
y
)
I(x,y)
I(x,y)是局部窗口的图像灰 度,
I
(
x
+
u
,
y
+
v
)
I(x+u,y+v)
I(x+u,y+v)是平移后的图像灰度,
w
(
x
,
y
)
w(x,y)
w(x,y)是窗口函数,可以是矩形窗口,也可以是对每一个像素赋予不同权重的高斯窗口,如下所示:
角点检测中使
E
(
u
,
v
)
E(u,v)
E(u,v)的值最大。利用一阶泰勒展开有:
I
(
x
+
u
,
y
+
v
)
=
I
(
x
,
y
)
+
I
x
u
+
I
y
v
I(x+u,y+v)=I(x,y)+I_xu+I_yv
I(x+u,y+v)=I(x,y)+Ixu+Iyv
其中
I
x
I_x
Ix和
I
y
I_y
Iy是沿x和y方向的导数,可用sobel算子计算。
推导如下:
M矩阵决定了
E
(
u
,
v
)
E(u,v)
E(u,v)的取值,下面我们利用M来求角点,M是
I
x
I_x
Ix和
I
y
I_y
Iy的二次项函数,可以表示成椭圆的形状,椭圆的长短半轴由M的特征值
λ
1
\lambda_1
λ1和
λ
2
\lambda_2
λ2决定,方向由特征矢量决定,如下图所示:
椭圆函数特征值与图像中的角点、直线(边缘)和平面之间的关系如下图所示。
共可分为三种情况:
Harris给出的角点计算方法并不需要计算具体的特征值,而是计算一个角点响应值R来判断角点。
R
R
R的计算公式为:
R
=
d
e
t
M
−
α
(
t
r
a
c
e
M
)
2
R=detM-\alpha(traceM)^2
R=detM−α(traceM)2
式中,
d
e
t
M
detM
detM为矩阵M的行列式;
t
r
a
c
e
M
traceM
traceM为矩阵M的迹;
α
\alpha
α为常数,取值范围为0.04~0.06。事实上,特征是隐含在
d
e
t
M
detM
detM和
t
r
a
c
e
M
traceM
traceM中,因为:
d
e
t
M
=
λ
1
λ
2
t
r
a
c
e
M
=
λ
1
+
λ
2
角点的判断如下图所示:
API
dst = cv2.cornerHarris(src, blockSize, ksize, k)
参数
参考代码
import cv2 import numpy as np import matplotlib.pyplot as plt # 1 读取图像,并转换成灰度图像 img_original = cv2.imread('chessboard.jpeg') img = cv2.imread('chessboard.jpeg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2 角点检测 # 2.1 输入图像必须是float32 gray = np.float32(gray) # 2.2 最后一个参数在0.04到0.05之间 dst = cv2.cornerHarris(gray, 2, 3, 0.04) # 3 设置阈值, 将角点绘制出来,阈值根据图像进行选择 img[dst > 0.001 * dst.max()] = [0, 0, 255] # 4 图像显示 plt.figure(figsize=(10, 10), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(221), plt.imshow(img_original[:, :, ::-1]), plt.title('原图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(222), plt.imshow(gray, cmap='gray'), plt.title('灰度图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(223), plt.imshow(dst, cmap=plt.cm.gray), plt.title('Harris角点检测', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(224), plt.imshow(img[:, :, ::-1]), plt.title('最终结果', fontsize=15) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
Shi-Tomasi算法是对Harris角点检测算法的改进,一般会比Harris算法得到更好的角点。Harris算法的角点响应函数是将矩阵M的行列式值与M的迹相减,利用差值判断是否为角点。后来Shi和Tomasi提出改进的方法是,若矩阵M的两个特征值中较小的一个大于阈值,则认为他是角点,即:
R
=
m
i
n
(
λ
1
,
λ
2
)
R=min(\lambda_1,\lambda_2)
R=min(λ1,λ2)
如下图所示:
从这幅图中,可以看出来只有当
λ
1
\lambda_1
λ1和
λ
2
\lambda_2
λ2都大于最小值时,才被认为是角点。
API
corners = cv2.goodFeaturesToTrack(image, maxcorners, qualityLevel, minDistance)
参数
返回
参考代码
import numpy as np import cv2 import matplotlib.pyplot as plt # 1 读取图像 img = cv2.imread('tv.jpeg') img2 = cv2.imread('tv.jpeg') img_blur = cv2.medianBlur(img, 3) gray = cv2.cvtColor(img_blur, cv2.COLOR_BGR2GRAY) # 灰度图 # 2 角点检测 corners = cv2.goodFeaturesToTrack(gray, 1000, 0.01, 10) # 3 绘制角点 for i in corners: x, y = i.ravel() # 将矩阵向量化 cv2.circle(img, (x, y), 2, (0, 0, 255), -1) # 4 图像展示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(221), plt.imshow(img2[:, :, ::-1]), plt.title('原图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(222), plt.imshow(img_blur[:, :, ::-1]), plt.title('滤波图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(223), plt.imshow(gray, cmap=plt.cm.gray), plt.title('灰度图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(224), plt.imshow(img[:, :, ::-1]), plt.title('Shi-Tomasi角点检测', fontsize=15) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
Harris算法
思想:通过图像的局部的小窗口观察图像,角点的特征是窗口沿任意方向移动都会导致图像灰度的明显
API:cv2.cornerHarris()
Shi-Tomasi算法
对Harris算法的改进,能够更好地检测角点
APl:cv2.goodFeatureToTrack()
学习目标
前面两节我们介绍了Harris和Shi-Tomasi角点检测算法,这两种算法具有旋转不变性,但不具有尺度不变性,以下图为例,在左侧小图中可以检测到角点,但是图像被放大后,在使用同样的窗口,就检测不到角点了。
所以,下面我们来介绍一种计算机视觉的算法,尺度不变特征转换即SIFT(Scale-invariant feature transform)。它用来侦测与描述影像中的局部性特征,它在空间尺度中寻找极值点,并提取出其位置、尺度、旋转不变量,此算法由 David Lowe在1999年所发表,2004年完善总结。应用范围包含物体辨识、机器人地图感知与导航、影像缝合、3D模型建立、手势辨识、影像追踪和动作比对等领域。
SIFT算法的实质是在不同的尺度空间上查找关键点(特征点),并计算出关键点的方向。SIFT所查找到的关键点是一些十分突出,不会因光照,仿射变换和噪音等因素而变化的点,如角点、边缘点、暗区的亮点及亮区的暗点等。
Lowe将SIFT算法分解为如下四步:
尺度空间极值检测:搜索所有尺度上的图像位置。通过高斯差分函数来识别潜在的对于尺度和旋转不变的关键点。
关键点定位:在每个候选的位置上,通过一个拟合精细的模型来确定位置和尺度。关键点的选择依据于它们的稳定程度。
关键点方向确定:基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而保证了对于这些变换的不变性。
关键点描述:在每个关键点周围的邻域内,在选定的尺度上测量图像局部的梯度。这些梯度作为关键点的描述符,它允许比较大的局部形状的变形或光照变化。
沿着Lowe的步骤,对SIFT算法的实现过程进行介绍:
在不同的尺度空间是不能使用相同的窗口检测极值点,对小的关键点使用小的窗口,对大的关键点使用大的窗口,为了达到上述目的,我们使用尺度空间滤波器。
高斯核是唯一可以产生多尺度空间的核函数。-《Scale-space theory:A basic tool for analysing structures at different scales》。
一个图像的尺度空间
L
(
x
,
y
,
σ
)
L(x,y,\sigma)
L(x,y,σ),定义为原始图像
l
(
x
,
y
)
l(x,y)
l(x,y)与一个可变尺度的2维高斯函数
G
(
x
,
y
,
σ
)
G(x,y,\sigma)
G(x,y,σ)卷积运算,即:
L
(
x
,
y
,
σ
)
=
G
(
x
,
y
,
σ
)
∗
I
(
x
,
y
)
L(x,y,\sigma)=G(x,y,\sigma)*I(x,y)
L(x,y,σ)=G(x,y,σ)∗I(x,y)
其中:
G
(
x
,
y
,
σ
)
=
1
2
π
σ
2
e
−
x
2
+
y
2
2
σ
2
G(x,y,\sigma)=\frac1{2\pi\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}}
G(x,y,σ)=2πσ21e−2σ2x2+y2
σ
\sigma
σ是尺度空间因子,它决定了图像的模糊的程度。在大尺度下(
σ
\sigma
σ值大)表现的是图像的概貌信息,在小尺度下(
σ
\sigma
σ值小)表现的是图像的细节信息。
在计算高斯函数的离散近似时,在大概3 σ \sigma σ距离之外的像素都可以看作不起作用,这些像素的计算也就可以忽略。所以,在实际应用中,只计算 ( 6 σ + 1 ) ∗ ( 6 σ + 1 ) (6\sigma+1)*(6\sigma+1) (6σ+1)∗(6σ+1)的高斯卷积核就可以保证相关像素影响。
下面我们构建图像的高斯金字塔,它采用高斯函数对图像进行模糊以及降采样处理得到的,高斯金字塔构建过程中,首先将图像扩大一倍,在扩大的图像的基础之上构建高斯金字塔,然后对该尺寸下图像进行高斯模糊,几幅模糊之后的图像集合构成了一个Octave,然后对该Octave下选择一幅图像进行下采样,长和宽分别缩短一倍,图像面积变为原来四分之一。这幅图像就是下一个Octave的初始图像,在初始图像的基础上完成属于这个Octave的高斯模糊处理,以此类推完成整个算法所需要的所有八度构建,这样这个高斯金字塔就构建出来了,整个流程如下图所示:
利用LoG(高斯拉普拉斯方法),即图像的二阶导数,可以在不同的尺度下检测图像的关键点信息,从而确定图像的特征点。但LoG的计算量大,效率低。所以我们通过两个相邻高斯尺度空间的图像的相减,得到DoG(高斯差分)来近似LoG。
为了计算DoG我们构建高斯差分金字塔,该金字塔是在上述的高斯金字塔的基础上构建而成的,建立过程是:在高斯金字塔中每个Octave中相邻两层相减就构成了高斯差分金字塔。如下图所示:
高斯差分金字塔的第1组第1层是由高斯金字塔的第1组第2层减第1组第1层得到的。以此类推,逐组逐层生成每一个差分图像,所有差分图像构成差分金字塔。概括为DOG金字塔的第o组第l层图像是由高斯金字塔的第o组第l+1层减第o组第层得到的。后续Sift特征点的提取都是在DOG金字塔上进行的
在DoG搞定之后,就可以在不同的尺度空间中搜索局部最大值了。对于图像中的一个像素点而言,它需要与自己周围的8邻域,以及尺度空间中上下两层中的相邻的18(2x9)个点相比。如果是局部最大值,它就可能是一个关键点。基本上来说关键点是图像在相应尺度空间中的最好代表。如下图所示:
搜索过程从每组的第二层开始,以第二层为当前层,对第二层的DoG图像中的每个点取一个3×3的立方体,立方体上下层为第一层与第三层。这样,搜索得到的极值点既有位置坐标(DoG的图像坐标),又有空间尺度坐标(层坐标)。当第二层搜索完成后,再以第三层作为当前层,其过程与第二层的搜索类似。当S=3时,每组里面要搜索3层,所以在DOG中就有S+2层,在初使构建的金字塔中每组有S+3层。
由于DoG对噪声和边缘比较敏感,因此在上面高斯差分金字塔中检测到的局部极值点需经过进一步的检验才A能精确定位为特征点。
使用尺度空间的泰勒级数展开来获得极值的准确位置,如果极值点的灰度值小于阈值(一般为0.03或0.04)就会被忽略掉。在OpenCV中这种闽值被称为contrastThreshold。
DoG算法对边界非常敏感,所以我们必须要把边界去除。Harris 算法除了可以用于角点检测之外还可以用于检测边界。从Harris角点检测的算法中,当一个特征值远远大于另外一个特征值时检测到的是边界。那在DoG算法中欠佳的关键点在平行边缘的方向有较大的主曲率,而在垂直于边缘的方向有较小的曲率,两者的比值如果高于某个阈值(在OpenCV中叫做边界阈值),就认为该关键点为边界,将被忽略,一般将该阈值设置为10。
将低对比度和边界的关键点去除,得到的就是我们感兴趣的关键点。
经过上述两个步骤,图像的关键点就完全找到了,这些关键点具有尺度不变性。为了实现旋转不变性,还需要为每个关键点分配一个方向角度,也就是根据检测到的关键点所在高斯尺度图像的邻域结构中求得一个方向基准。
对于任一关键点,我们采集其所在高斯金字塔图像以r为半径的区域内所有像素的梯度特征(幅值和幅角),半径r为:
r
=
3
×
1.5
σ
r=3×1.5\sigma
r=3×1.5σ
其中
σ
\sigma
σ是关键点所在octave的图像的尺度,可以得到对应的尺度图像。
梯度的幅值和方向的计算公式为:
m
(
x
,
y
)
=
L
(
x
+
1
,
y
)
−
L
(
x
−
1
,
y
)
2
+
(
L
(
x
,
y
+
1
)
−
L
(
x
,
y
−
1
)
)
2
θ
(
x
,
y
)
=
arctan
L
(
x
,
y
+
1
)
−
L
(
x
,
y
−
1
)
L
(
x
+
1
,
y
)
−
L
(
x
−
1
,
y
)
m(x,y)=\sqrt{L(x+1,y)-L(x-1,y)^2+(L(x,y+1)-L(x,y-1))^2}\\ \theta(x,y)=\arctan\frac{L(x,y+1)-L(x,y-1)}{L(x+1,y)-L(x-1,y)}
m(x,y)=L(x+1,y)−L(x−1,y)2+(L(x,y+1)−L(x,y−1))2
θ(x,y)=arctanL(x+1,y)−L(x−1,y)L(x,y+1)−L(x,y−1)
邻域像素梯度的计算结果如下图所示:
完成关键点梯度计算后,使用直方图统计关键点邻域内像素的梯度幅值和方向。具体做法是,将360°分为36柱,每10°为一柱,然后在以r为半径的区域内,将梯度方向在某一个柱内的像素找出来,然后将他们的幅值相加在一起作为柱的高度。因为在r为半径的区域内像素的梯度幅值对中心像素的贡献是不同的,因此还需要对幅值进行加权处理,采用高斯加权,方差为1.5
σ
\sigma
σ。如下图所示,为简化图中只画了8个方向的直方图。
每个特征点必须分配一个主方向,还需要一个或多个辅方向,增加辅方向的目的是为了增强图像匹配的鲁棒性。辅方向的定义是,当一个柱体的高度大于主方向柱体高度的80%时,则该柱体所代表的的方向就是给特征点的辅方向。
直方图的峰值,即最高的柱代表的方向是特征点邻域范围内图像梯度的主方向,但该柱体代表的角度是一个范围,所以我们还要对离散的直方图进行插值拟合,以得到更精确的方向角度值。利用抛物线对离散的直方图进行拟合,如下图所示:
获得图像关键点主方向后,每个关键点有三个信息(x,y,σ,θ):位置、尺度、方向。由此我们可以确定一个SIFT特征区域。通常使用一个带箭头的圆或直接使用箭头表示SIFT区域的三个值:中心表示特征点位置,半径表示关键点尺度,箭头表示方向。如下图所示:
通过以上步骤,每个关键点就被分配了位置,尺度和方向信息。接下来我们为每个关键点建立一个描述符,该描述符既具有可区分性,又具有对某些变量的不变性,如光照,视角等。而且描述符不仅仅包含关键点,也包括关键点周围对其有贡献的的像素点。主要思路就是通过将关键点周围图像区域分块,计算块内的梯度直方图,生成具有特征向量,对图像信息进行抽象。
描述符与特征点所在的尺度有关,所以我们在关键点所在的高斯尺度图像上生成对应的描述符。以特征点为中心,将其附近邻域划分为 d ∗ d d*d d∗d个子区域(一般取d=4),每个子区域都是一个正方形,边长为3σ,考虑到实际计算时,需进行三次线性插值,所以特征点邻域的为 3 σ ( d + 1 ) ∗ 3 σ ( d + 1 ) 3\sigma(d+1)*3\sigma(d+1) 3σ(d+1)∗3σ(d+1)的范围,如下图所示:
为了保证特征点的旋转不变性,以特征点为中心,将坐标轴旋转为关键点的主方向,如下图所示:
计算子区域内的像素的梯度,并按照σ=0.5d进行高斯加权,然后插值计算得到每个种子点的八个方向的梯度,插值方法如下图所示:
每个种子点的梯度都是由覆盖其的4个子区域插值而得的。如图中的红色点,落在第0行和第1行之间,对这两行都有贡献。对第0行第3列种子点的贡献因子为dr,对第1行第3列的贡献因子为1-dr,同理,对邻近两列的贡献因子为dc和1-dc,对邻近两个方向的贡献因子为do和1-do。则最终累加在每个方向上的梯度大小为:
w
e
i
g
h
t
=
w
∗
d
r
k
(
1
−
d
r
)
1
−
k
d
c
m
(
1
−
d
c
)
1
−
m
d
o
n
(
1
−
d
o
)
1
−
n
weight=w*dr^k(1-dr)^{1-k}dc^m(1-dc)^{1-m}do^n(1-do)^{1-n}
weight=w∗drk(1−dr)1−kdcm(1−dc)1−mdon(1−do)1−n
其中k,m,n为0或为1。如上统计
4
∗
4
∗
8
=
128
4*4*8=128
4∗4∗8=128个梯度信息即为该关键点的特征向量,按照特征点的对每个关键点的特征向量进行排序,就得到了SIFT特征描述向量。
SIFT在图像的不变特征提取方面拥有无与伦比的优势,但并不完美,仍然存在实时性不高,有时特征点较少,对边缘光滑的目标无法准确提取特征点等缺陷,自SIFT算法问世以来,人们就一直对其进行优化和改进,其中最著名的就是SURF算法。
使用SIFT算法进行关键点检测和描述的执行速度比较慢,需要速度更快的算法。2006年Bay提出了SURF (加速健壮特征,Speeded Up Robust Features)算法,是SIFT算法的增强版,它的计算量小,运算速度快,提取的特征与SIFT几乎相同,将其与SIFT算法对比如下:
SIFT | SURF | |
---|---|---|
特征点检测 | 使用不同尺度的图片与高斯函数进行卷积 | 使用不同大小的盒滤波器与原始图像做卷积,易于并行 |
方向 | 关键点邻接矩形区域内,利用梯度直方图计算 | 关键点邻接圆域内,计算x,y方向的haar小波 |
描述符生成 | 关键点邻域内划分d*d子区域,每个子区域计算采样点的子区域内计算8个方向的直方图 | 关键点邻域内划分d*d个子区域,每个子区域计算采样点的haar小波响应,记录: ∑ d x \sum{dx} ∑dx, ∑ d y , \sum{dy}, ∑dy,$\sum{ |
API
sift = cv2.xfeatures2d.SIFT_create()x
API
kp,des=sift.detectAndCompute(gray,None)
参数
返回
API
cv.drawKeypoints(image,keypoints,outputimage,color,flags)
参数
SURF算法的应用与上述流程一致
参考代码
import cv2 import numpy as np import matplotlib.pyplot as plt # 1 读取图像 img = cv2.imread('tv.jpeg') img2 = cv2.imread('tv.jpeg') img_blur = cv2.medianBlur(img, 3) gray = cv2.cvtColor(img_blur, cv2.COLOR_BGR2GRAY) # 灰度图 # 2 sift关键点检测 # 2.1 实例化sift对象 sift = cv2.xfeatures2d.SIFT_create() # 2.2 关键点检测:kp关键点信息包括方向,尺度,位置信息,des是关键点的描述符 kp, des = sift.detectAndCompute(gray, None) # 2.3 在图像上绘制关键点的检测结果 cv2.drawKeypoints(img, kp, img, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) # 3 图像展示 plt.figure(figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.subplot(221), plt.imshow(img2[:, :, ::-1]), plt.title('原图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(222), plt.imshow(img_blur[:, :, ::-1]), plt.title('滤波图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(223), plt.imshow(gray, cmap=plt.cm.gray), plt.title('灰度图', fontsize=15) plt.xticks([]), plt.yticks([]) plt.subplot(224), plt.imshow(img[:, :, ::-1]), plt.title('SIFT检测', fontsize=15) plt.xticks([]), plt.yticks([]) plt.show()
运行结果
Edward Rosten和Tom Drummond在2006年提出了FAST算法,并在2010年对其进行了修正。
FAST(全称Features from accelerated segment test)是一种用于角点检测的算法,该算法的原理是取图像中检测点,以该点为圆心的周围邻域内像素点判断检测点是否为角点,通俗的讲就是若一个像素周围有一定数量的像素与该点像素值不同,则认为其为角点。
在图像中选取一个像素点p,来判断它是不是关键点。 I p I_p Ip等于像素点p的灰度值。
以r为半径画圆,覆盖p点周围的M个像素,通常情况下,设置r=3,则M=16,如下图所示:
设置一个阈值t,如果在这16个像素点中存在n个连续像素点的灰度值都高于 I p + t I_p+t Ip+t,或者低于 I p − t I_p-t Ip−t,那么像素点p就被认为是一个角点。如上图中的虚线所示,n一般取值为12。
由于在检测特征点时是需要对图像中所有的像素点进行检测,然而图像中的绝大多数点都不是特征点,如果对每个像素点都进行上述的检测过程,那显然会浪费许多时间,因此采用一种进行非特征点判别的方法:首先对候选点的周围每个90度的点:1,9,5,13进行测试(先测试1和19,如果它们符合阈值要求再测试5和13)。如果p是角点,那么这四个点中至少有3个要符合阈值要求,否则直接剔除。对保留下来的点再继续进行测试(是否有12的点符合阈值要求)。
虽然这个检测器的效率很高,但它有以下几条缺点:
获得的候选点比较多
特征点的选取不是最优的,因为它的效果取决与要解决的问题和角点的分布情况。
进行非特征点判别时大量的点被丢弃
检测到的很多特征点都是相邻的
前3个问题可以通过机器学习的方法解决,最后一个问题可以使用非最大值抑制的方法解决。
选择一组训练图片(最好是跟最后应用相关的图片)
使用FAST算法找出每幅图像的特征点,对图像中的每一个特征点,将其周围的16个像素存储构成一个向量P。
每一个特征点的16像素点都属于下列三类中的一种
s
p
→
x
=
{
d
,
I
p
→
x
≤
I
p
−
t
(
d
a
r
k
e
r
)
s
,
I
p
−
t
<
I
p
→
x
<
I
p
+
t
(
s
i
m
i
l
a
r
)
b
,
I
p
+
t
≤
I
p
→
x
(
b
r
i
g
h
t
e
r
)
s_{p\to x}=
在筛选出来的候选角点中有很多是紧挨在一起的,需要通过非极大值抑制来消除这种影响。
为所有的候选角点都确定一个打分函数
V
V
V,
V
V
V的值可这样计算:先分别计算
I
p
I_p
Ip与圆上16个点的像素值差值,取绝对值,再将这16个绝对值相加,就得到了V的值
V
=
∑
i
16
∣
I
p
−
I
i
∣
V=\sum_i^{16}|I_p-I_i|
V=i∑16∣Ip−Ii∣
最后比较毗邻候选角点的V值,把V值较小的候选角点pass掉。
FAST算法的思想与我们对角点的直观认识非常接近,化繁为简。FAST算法比其它角点的检测算法快,但是在噪声较高时不够稳定,这需要设置合适的阈值。
OpenCV中的FAST检测算法是用传统方法实现的,
API
fast = cv2. FastFeatureDetector_create(threshold, nonmaxSuppression)
参数
返回
API
kp = fast.detect(grayImg,None)
参数
cv2.drawKeypoints(image, keypoints, outputimage, color, flags)
参考代码
# 1 读取图像 img = cv.imread('tv.jpeg') # 2 Fast角点检测 # 2.1 创建一个Fast对象,传入阈值,注意:可以处理彩色空间图像 fast = cv.FastFeatureDetector_create(threshold=30) # 2.2 检测图像上的关键点 kp = fast.detect(img, None) # 2.3 在图像上绘制关键点 img2 = cv.drawKeypoints(img, kp, None, color=(0, 0, 255)) # 2.4 输出默认参数 print("Threshold:{}".format(fast.getThreshold())) print("NonmaxSuppression:{}".format(fast.getNonmaxSuppression())) print("neighborhood:{}".format(fast.getType())) print("Total Key points with Nonmaxsuppression:{}".format(len(kp))) # 2.5 关闭非极大值抑制 fast.setNonmaxSuppression(0) kp = fast.detect(img, None) print("Total Key points without NonmaxSuppression:{}".format(len(kp))) # 2.6 绘制为进行非极大值抑制的结果 img3 = cv.drawKeypoints(img, kp, None, color=(0, 0, 255)) # 3 绘制图像 fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False axes[0].imshow(img2[:, :, ::-1]) axes[0].set_title("加入非极大值抑制") axes[1].imshow(img3[:, :, ::-1]) axes[1].set_title("未加入非极大值抑制") plt.show()
运行结果
SIFT和SURF算法是受专利保护的,在使用他们时我们是要付费的,但是ORB(Oriented Fast and Rotated Brief)不需要,它可以用来对图像中的关键点快速创建特征向量,并用这些特征向量来识别图像中的对象。
ORB算法结合了Fast和Brief算法,提出了构造金字塔,为Fast特征点添加了方向,从而使得关键点具有了尺度不变性和旋转不变性。具体流程描述如下:
构造尺度金字塔,金字塔共有n层,与SIFT不同的是,每一层仅有一幅图像。第s层的尺度为:
σ
s
=
σ
0
s
\sigma_s=\sigma_0^s
σs=σ0s
σ
0
\sigma_0
σ0是初始尺度,默认为1.2,原图在第0层。
第s层图像的的大小:
S
I
Z
E
=
(
H
∗
1
σ
s
)
×
(
W
∗
1
σ
s
)
SIZE=(H*\frac1{\sigma_s})×(W*\frac1{\sigma_s})
SIZE=(H∗σs1)×(W∗σs1)
在不同的尺度上利用Fast算法检测特征点,采用Harris角点响应函数,根据角点的响应值排序,选取前N个特征点,作为本尺度的特征点。
计算特征点的主方向,计算以特征点为圆心半径为r的圆形邻域内的灰度质心位置,将从特征点位置到质心位置的方向做特征点的主方向。
计算方法如下:
m
p
q
=
∑
x
,
y
x
p
y
q
I
(
x
,
y
)
m_{pq}=\sum_{x,y}x^py^qI(x,y)
mpq=x,y∑xpyqI(x,y)
质心位置:
C
=
(
m
10
m
00
,
m
01
m
10
)
C=(\frac{m_{10}}{m_{00}},\frac{m_{01}}{m_{10}})
C=(m00m10,m10m01)
主方向:
θ
=
arctan
(
m
01
,
m
10
)
\theta=\arctan(m_{01},m_{10})
θ=arctan(m01,m10)
BRIEF是一种特征描述子提取算法,并非特征点的提取算法,一种生成二值化描述子的算法,不提取代价低,匹配只需要使用简单的汉明距离(Hamming Distance)利用比特之间的异或操作就可以完成。因此,时间代价低,空间代价低,效果还挺好是最大的优点。
算法的步骤介绍如下:
图像滤波:原始图像中存在噪声时,会对结果产生影响,所以需要对图像进行滤波,去除部分噪声。
选取点对:以特征点为中心,取S*S的邻域窗口,在窗口内随机选取N组点对,一般N=128,256,512,默认是256,关于如何选取随机点对,提供了五种形式,结果如下图所示:
x,y方向平均分布采样
x,y均服从Gauss(0,S^2/25)各向同性采样(使用得多)
x服从Gauss(0,S^2/25),y服从Gauss(0,S^2/100)采样
x,y从网格中随机获取
x一直在(0,0),y从网络中随机选取
图中一条线段的两个端点就是一组点对,其中第二种方法的结果比较好。
构建描述符:假设x,y是某个点对的两个端点,p(x),p(y)是两点对应的像素值,则有:
t
(
x
,
y
)
=
{
1
,
p
(
x
)
>
p
(
y
)
0
,
e
l
s
e
t(x,y)=
对每一个点对都进行上述的二进制赋值,形成BRIEF的关键点的描述特征向量,该向量一般为 128-512 位的字符串,其中仅包含 1 和 0,如下图所示:
在OpenCV中实现ORB算法,使用的是:
API
orb = cv.xfeatures2d.orb_create(nfeatures)
参数
kp,des = orb.detectAndCompute(gray,None)
参数
返回
cv.drawKeypoints(image, keypoints, outputimage, color, flags)
示例:
import numpy as np import cv2 as cv from matplotlib import pyplot as plt # 1 图像读取 img = cv.imread('tv.jpeg') # 2 ORB角点检测 # 2.1 实例化ORB对象 orb = cv.ORB_create(nfeatures=500) # 2.2 检测关键点,并计算特征描述符 kp, des = orb.detectAndCompute(img, None) print(des.shape) # (500, 32) # 3 将关键点绘制在图像上 img2 = cv.drawKeypoints(img, kp, None, color=(0, 0, 255), flags=0) # 4. 绘制图像 plt.figure(figsize=(10, 8), dpi=100) plt.imshow(img2[:, :, ::-1]) plt.xticks([]), plt.yticks([]) plt.show()
Fast算法
原理:若一个像素周围有一定数量的像素与该点像素值不同,则认为其为角点
API:cv2.FastFeatureDetector_create()
ORB算法
原理:是FAST算法和BRIEF算法的结合
API:cv.ORB_create()
学习目标
在OpenCV中要获取一个视频,需要创建一个VideoCapture对象,指定要读取的视频文件:
创建读取视频的对象
API
cv2.VideoCapture可以捕获摄像头,用数字来控制不同的设备,例如0,1
如果是视频文件,直接指定好路径即可
cap = cv2.VideoCapture(filepath)
参数
视频的属性信息
2.1 获取视频的某些属性
API
retval=cap.get(propId)
参数
propid:从0到18的数字,每个数字表示视频的属性
常用属性有:
索引 | flags | 意义 |
---|---|---|
0 | cv2.CAP_PROP_POS_MSEC | 视频文件的当前位置(ms) |
1 | cv2.CAP_PROP_POS_FRAMES | 从0开始索引帧,帧位置 |
2 | cv2.CAP_PROP_POS_AVL_RATIO | 视频文件的相对位置(0表示开始,1表示结束) |
3 | cv2.CAP_PROP_FRAME_WIDTH | 视频流的帧宽度 |
4 | cv2.CAP_PROP_FRAME_HEIGHT | 视频流的帧高度 |
5 | cv2.CAP_PROP_FPS | 帧率 |
6 | cv2.CAP_PROP_FOURCC | 编解码器四字符代码 |
7 | cv2.CAP_PROP_FRAME_COUNT | 视频文件的帧 |
2.2 修改视频的某些属性
API
cap.set(propId, value)
参数
proid:属性的索引,与上面的表格相对应
value:修改后的属性值
判断图像是否读取成功
isornot = cap.isOpened()
获取视频的一帧图像
API
ret,frame=cap.read()
参数
调用 cv2.imshow()
显示图像,在显示图像时使用 cv.waitkey()
设置适当的持续时间,如果太低视频会播放的非常快,如果太高就会播放的非常慢,通常情况下我们设置25ms就可以了。
最后,调用 cap.realease()
将视频释放掉
参考代码
import cv2 import matplotlib.pyplot as plt import numpy as np # 读取视频 vc = cv2.VideoCapture('test.mp4') # 检查是否正确打开 isOpened返回一个布尔值和一帧的图像 if vc.isOpened(): open, frame = vc.read() else: open = False while open: ret, frame = vc.read() if frame is None: break if ret == True: # 读的帧没问题 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 将每一帧转换为灰度图 传入帧和处理方法 cv2.imshow('result', gray) if cv2.waitKey(1) & 0xFF == 27: # 每一帧的等待时间 break vc.release() cv2.destroyAllWindows() import numpy as np import cv2 as cv # 1 读取视频 cap = cv.VideoCapture("test.mp4") # 2 判断是否读取成功 while (cap.isOpened()): # 3 获取每一帧图像 ret, frame = cap.read() # 4是否获取成功 if ret == True: cv.imshow("frame", frame) if cv.waitKey(25)&0xFF == ord("q"): break
在OpenCV中我们保存视频使用的是VedioWriter对象,在其中指定输出文件的名称,如下所示:
创建视频写入对象
API
out=cv2.Videowriter(filename,fourcc,fps,frameSize)
参数
filename:视频保存的位置
fourcc:指定视频编解码器的4字节代码
fps:帧率
frameSize:帧大小
设置视频的编解码器,如下所示:
API
retval=cv2.Videowriter_fourcc(c1,c2,c3,c4)
参数
利用 cap.read()
获取视频中的每一帧图像,并使用 out.write()
将某一帧图像写入视频中。
使用 cap.release()
和 out.release()
释放资源。
import cv2 as cv import numpy as np # 1.读取视频 cap = cv.VideoCapture("test.mp4") # 2.获取图像的属性(宽和高),并将其转换为整数 frame_width = int(cap.get(3)) frame_height = int(cap.get(4)) # 3.创建保存视频的对象,设置编码格式,帧率,图像的宽高等 out = cv.VideoWriter('test_write.avi', cv.VideoWriter_fourcc('D', 'I', 'V', 'X'), 10, (frame_width, frame_height)) while (True): # 4.获取视频中的每一帧图像 ret, frame = cap.read() if ret == True: # 5.将每一帧图像写入到输出文件中 out.write(frame) else: break # 6.释放资源 cap.release() out.release() cv.destroyAllWindows()
读取视频:
保存视频:
保存视频:out=cv.VideoWrite()
视频写入:out.write()
资源释放:out.release()
学习目标
meanshift算法的原理很简单。假设你有一堆点集,还有一个小的窗口,这个窗口可能是圆形的,现在你可能要移动这个窗口到点集密度最大的区域当中。
如下图:
最开始的窗口是蓝色圆环的区域,命名为C1。蓝色圆环的圆心用一个蓝色的矩形标注,命名为C1_o。
而窗口中所有点的点集构成的质心在蓝色圆形点C1_r处,显然圆环的形心和质心并不重合。所以,移动蓝色的窗口,使得形心与之前得到的质心重合。在新移动后的圆环的区域当中再次寻找圆环当中所包围点集的质心,然后再次移动,通常情况下,形心和质心是不重合的。不断执行上面的移动过程,直到形心和质心大致重合结束。这样,最后圆形的窗口会落到像素分布最大的地方,也就是图中的绿色圈,命名为C2。
meanshift算法除了应用在视频追踪当中,在聚类,平滑等等各种涉及到数据以及非监督学习的场合当中均有重要应用,是一个应用广泛的算法。
图像是一个矩阵信息,如何在一个视频当中使用meanshift算法来追踪一个运动的物体呢?大致流程如下:
首先在图像上选定一个目标区域
计算选定区域的直方图分布,一般是HSV色彩空间的直方图。
对下一帧图像b同样计算直方图分布。
计算图像b当中与选定区域直方图分布最为相似的区域,使用meanshit算法将选定区域沿着最为相似的部分进行移动,直到找到最相似的区域,便完成了在图像b中的目标追踪。
重复3到4的过程,就完成整个视频目标追踪。
通常情况下我们使用直方图反向投影得到的图像和第一帧目标对象的起始位置,当目标对象的移动会反映到直方图反向投影图中,meanshift 算法就把我们的窗口移动到反向投影图像中灰度密度最大的区域了。如下图所示:
直方图反向投影的流程是:
假设我们有一张100×100的输入图像,有一张10×10的模板图像,查找的过程是这样的:
API
cv2.meanShift(probImage,window,criteria)
参数
实现Meanshift的主要流程是:
cv2.videoCapture()
参考代码
import numpy as np import cv2 # 1.获取图像 cap = cv2.VideoCapture('DOG.wmv') # 2.获取第一帧图像,并指定目标位置 ret, frame = cap.read() # 2.1目标位置(行,高,列,宽) r, h, c, w = 197, 141, 0, 208 track_window = (c, r, w, h) # 2.2指定目标的感兴趣区域 roi = frame[r:r + h, c:c + w] # 3.计算直方图 # 3.1转换色彩空间(HSV) hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 3.2 去除低亮度的值 mask = cv2.inRange(hsv_roi, np.array((0., 60., 32.)), np.array((180., 255., 255.))) # 3.3 计算直方图 roi_hist = cv2.calcHist([hsv_roi], [0], None, [180], [0, 180]) # 3.4 归一化 cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX) # 4.目标追踪 # 4.1 设置窗口搜索终止条件:最大迭代次数,窗口中心漂移最小值 term = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1) while (True): # 4.2 获取每一帧图像 ret, frame = cap.read() if ret == True: # 4.3 计算直方图的反向投影 hst = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) dst = cv2.calcBackProject([hst], [0], roi_hist, [0, 180], 1) # 4.4进行 meanShift 追踪 ret, track_window = cv2.meanShift(dst, track_window, term) # 4.5将追踪的位置绘制在视频上,并进行显示 x, y, w, h = track_window img2 = cv2.rectangle(frame, (x, y), (x + w, y + h), 255, 2) cv2.imshow('frame', img2) if cv2.waitKey(80) & 0xff == ord('q'): break # 5 资源释放 cap.release() cv2.destroyWindow()
大家认真看下上面的结果,有一个问题,就是检测的窗口的大小是固定的,而狗狗由近及远是一个逐渐变小的过程,固定的窗口是不合适的。所以我们需要根据目标的大小和角度来对窗口的大小和角度进行修正。
CamShift可以帮我们解决这个问题。
CamShift算法全称是“Continuously Adaptive Mean-Shift”(连续自适应MeanShit算法),是对MeanShift算法的改进算法,可随着跟踪目标的大小变化实时调整搜索窗口的大小,具有较好的跟踪效果。
Camshift算法首先应用meanshift,一旦meanshift收敛,它就会更新窗口的大小,还计算最佳拟合椭圆的方向,从而根据目标的位置和大小更新搜索窗口。如下图所示:
Camshift在OpenCV中实现时,只需将上述的meanshift函数改为Camshift函数即可:
将Camshift中的:
# 进行meanshift追踪
ret,track_window=cv2.meanshift(dst,track_window,term_crit)
# 将追踪的位置绘制在视频上,并进行显示
x,y,w,h=track_window
img2=cv.rectangle(frame,(x,y),(x+w,y+h),255,2)
改为:
# 进行camshift追踪
ret,track_window=cv.CamShift(dst,track_window,term_crit)
#绘制踪结果
pts=cv.boxPoints(ret)
pts=np.int0(pts)
img2=cv.polylines(frame,[pts],True,255,2)
Meanshift和camshift算法都各有优势,自然也有劣势:
meanshift
原理:一个迭代的步骤,即先算出当前点的偏移均值,移动该点到其偏移均值,然后以此为新的起始点,继续移动,直到满足一定的条件结束。
API:cv2.meanshift()
优缺点:简单,迭代次数少,但无法解决目标的遮挡问题并且不能适应运动目标的的形状和大小变化
camshift
原理:对meanshift算法的改进,首先应用meanshift,一旦meanshift收敛,它就会更新窗口的大小,还计算最佳拟合椭圆的方向,从而根据目标的位置和大小更新搜索窗口。
API:cv2.camshift()
优缺点:可适应运动目标的大小形状的改变,具有较好的跟踪效果,但当背景色和目标颜色接近时,容易使目标的区域变大,最终有可能导致目标跟踪丢失
学习目标
我们使用机器学习的方法完成人脸检测,首先需要大量的正样本图像(面部图像)和负样本图像(不含面部的图像)来训练分类器。我们需要从其中提取特征。下图中的Haar 特征会被使用,就像我们的卷积核,每一个特征是一个值,这个值等于黑色矩形中的像素值之后减去白色矩形中的像素值之和。
Haar特征值反映了图像的灰度变化情况。例如:脸部的一些特征能由矩形特征简单的描述,眼睛要比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等。
Haar特征可用于于图像任意位置,大小也可以任意改变,所以矩形特征值是矩形模版类别、矩形位置和矩形大小这三个因素的函数。故类别、大小和位置的变化,使得很小的检测窗口含有非常多的矩形特征。
得到图像的特征后,训练一个决策树构建的adaboost级联决策器来识别是否为人脸。
OpenCV中自带已训练好的检测器,包括面部,眼睛,猫脸等,都保存在XML文件中,我们可以通过以下程序找到他们:
import cv2
print(cv2.__file__)
那我们就利用这些文件来识别人脸,眼睛等。检测流程如下:
读取图片,并转换成灰度图
实例化人脸和眼睛检测的分类器对象
# 实例化级联分类器
classifier = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
# 加载分类器
classifier.load('haarcascade_frontalface_default.xml")
进行人脸和眼睛的检测
rect=classifier.detectMultiScale(gray,scaleFactor,minNeighbors,minSize,maxsize)
参数
将检测结果绘制出来。
参考代码
import cv2 as cv import matplotlib.pyplot as plt # 1.以灰度图的形式读取图片 img = cv.imread(r"D:\\pycharm\\PyCharm Community Edition 2021.1\\PythonProjects\\opencv\\tly.jpeg") gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 2.实例化OpenCV人脸和眼睛识别的分类器 face_cas = cv.CascadeClassifier(r"D:\\anaconda3\\envs\\python36\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml") face_cas.load(r"D:\\anaconda3\\envs\\python36\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml") eyes_cas = cv.CascadeClassifier(r"D:\\anaconda3\\envs\\python36\\Lib\\site-packages\\cv2\\data\\haarcascade_eye.xml") eyes_cas.load(r"D:\\anaconda3\\envs\\python36\\Lib\\site-packages\\cv2\\data\\haarcascade_eye.xml") # 3.调用识别人脸 faceRects = face_cas.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32)) for faceRect in faceRects: x, y, w, h = faceRect # 框出人脸 cv.rectangle(img, (x, y), (x + h, y + w), (0, 255, 0), 3) # 4.在识别出的人脸中进行眼睛的检测 roi_color = img[y:y + h, x:x + w] roi_gray = gray[y:y + h, x:x + w] eyes = eyes_cas.detectMultiScale(roi_gray) for (ex, ey, ew, eh) in eyes: cv.rectangle(roi_color, (ex, ey), (ex + ew, ey + eh), (0, 255, 0), 2) # 5.检测结果的绘制 plt.figure(figsize=(8, 6), dpi=100) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.imshow(img[:, :, ::-1]), plt.title('检测结果') plt.xticks([]), plt.yticks([]) plt.show()
运行结果
opencv中人脸识别的流程是:
读取图片,并转换成灰度图
实例化人脸和眼睛检测的分类器对象
# 实例化级联分类器
classifier = cv.CascadeClassifier("haarcascade_frontalface_default.xml")
# 加载分类器
classifier.load('haarcascade_frontalface_default.xml’)
进行人脸和眼睛的检测
rect=classifier.detectMultiScale(gray,scaleFactor,minNeighbors,minSize,maxsize)
绘制检测结果
字体下载 .ttf文件。Download free fonts for Windows and Mac - FontPalace.com
matplotlib中文乱码解决及plt.rcParams参数的使用_SuperSources的博客-CSDN博客
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
黑马程序员人工智能教程_10小时学会图像处理OpenCV入门教程_哔哩哔哩_bilibili
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。