OpenCV Usage
本文最后更新于:2021年10月27日 晚上
This is OpenCV basic usage.
OpenCV 是一款由 Intel 公司俄罗斯团队发起并参与和维护的一个计算机视觉处理开源软件库,支持与计算机视觉和机器学习相关的众多算法。
1. opencv 包安装
pip install opencv-contrib-python -i https://mirrors.aliyun.com/pypi/simple/
官方文档:https://opencv-python-tutroals.readthedocs.io/en/latest/
2. opencv 简单图像处理
2.1 图像像素存储形式
首先得了解下图像在计算机中存储形式:(为了方便画图,每列像素值都写一样了)
对于只有黑白颜色的灰度图,为单通道,一个像素块对应矩阵中一个数字,数值为 0 到 255, 其中 0 表示最暗(黑色) ,255 表示最亮(白色)
对于采用 RGB 模式的彩色图片,为三通道图,Red、Green、Blue 三原色,按不同比例相加,一个像素块对应矩阵中的一个向量, 如[24,180, 50],分别表示三种颜色的比列, 即对应深度上的数字,如下图所示:
注意:由于历史遗留问题,opencv 采用 BGR 模式,而不是 RGB
2.2 图像读取和写入
cv.imread()
cv.imread(img_path, flag) 读取图片,返回图片对象
img_path: 图片的路径,即使路径错误也不会报错,但打印返回的图片对象为None
flag:cv.IMREAD_COLOR,读取彩色图片,图片透明性会被忽略,为默认参数,也可以传入1
cv.IMREAD_GRAYSCALE,按灰度模式读取图像,也可以传入0
cv.IMREAD_UNCHANGED,读取图像,包括其alpha通道,也可以传入-1
cv.imshow()
cv.imshow(window_name, img):显示图片,窗口自适应图片大小
window_name: 指定窗口的名字
img:显示的图片对象
可以指定多个窗口名称,显示多个图片
cv.waitKey(millseconds) 键盘绑定事件,阻塞监听键盘按键,返回一个数字(不同按键对应的数字不同)
millseconds: 传入时间毫秒数,在该时间内等待键盘事件;传入0时,会一直等待键盘事件
cv.destroyAllWindows(window_name)
window_name: 需要销毁的窗口名字,不传入时关闭所有窗口
cv.imwrite()
cv.imwrite(img_path_name, img)
img_path_name:保存的文件名
img:文件对象
cv.cvtColor()
cv.cvtColor()
参数:
img: 图像对象
code:
cv.COLOR_RGB2GRAY: RGB转换到灰度模式
cv.COLOR_RGB2HSV: RGB转换到HSV模式(hue,saturation,Value)
cv.inRange()
参数:
img: 图像对象/array
lowerb: 低边界array, 如lower_blue = np.array([110,50,50])
upperb:高边界array, 如 upper_blue = np.array([130,255,255])
mask = cv.inRange(hsv, lower_green, upper_green)
使用示例:
import cv2 as av
img = cv.imread(r"./image/kids.jpg")
# print(img.shape)
# 将图片灰度处理
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, img_threshold = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)
cv.imshow("img", img)
cv.imshow("thre", img_threshold)
key = cv.waitKey(0)
cv.imwrite(r"./image/kids1.jpg", img_threshold)
2.3 绘制图形和文字
绘制图形
# 绘制直线
cv.line(img, start, end, color, thickness)
img:要绘制直线的图像
Start,end: 直线的起点和终点
color: 线条的颜色
Thickness: 线条宽度
# 绘制圆形
cv.circle(img, centerpoint, r, color, thickness)
mg:要绘制圆形的图像
Centerpoint, r: 圆心和半径
color: 线条的颜色
Thickness: 线条宽度,为-1时生成闭合图案并填充颜色
# 绘制矩形
cv.rectangle(img,leftupper,rightdown,color,thickness)
img:要绘制矩形的图像
Leftupper, rightdown: 矩形的左上角和右下角坐标
color: 线条的颜色
Thickness: 线条宽度
向图像中添加文字
# 向图像中添加文字
cv.putText(img, text, station, font, fontsize, color, thickness, cv.LINE_AA)
img: 图像
text:要写入的文本数据
station:文本的放置位置
font:字体
Fontsize :字体大小
使用示例:
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 创建一个空白的图像
img = np.zeros((512, 512, 3), np.uint8)
# 2 绘制图形
cv.line(img, (0, 0), (511, 511), (255, 0, 0), 5)
cv.rectangle(img, (384, 0), (510, 128), (0, 255, 0), 3)
cv.circle(img, (447, 63), 63, (0, 0, 255), -1)
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img, 'OpenCV', (10, 500), font, 4, (255, 255, 255), 2, cv.LINE_AA)
# 3 图像展示
plt.imshow(img)
plt.title('匹配结果'), plt.xticks([]), plt.yticks([])
plt.show()
2.4 图像像素获取和裁剪
获取并修改图像中的像素点
import numpy as np
import cv2 as cv
img = cv.imread('./image/kids.jpg')
# 获取某个像素点的值
px = img[100, 100]
print(px)
# 仅获取蓝色通道的强度值
blue = img[100, 100, 0]
print(blue)
# 修改某个位置的像素值
img[100, 100] = [255, 255, 255]
[142 187 190]
142
图片性质
图像属性包括行数,列数和通道数,图像数据类型,像素数等。
import cv2 as cv
img = cv.imread(r"./image/kids.jpg")
# rows, cols, channels
img.shape #返回(280, 450, 3), 宽280(rows),长450(cols),3通道(channels)
# size
img.size #返回378000,所有像素数量,=280*450*3
# type
img.dtype #dtype('uint8')
ROI 截取(Range of Interest)
有时需要在 B,G,R 通道图像上单独工作。在这种情况下,需要将 BGR 图像分割为单个通道。或者在其他情况下,可能需要将这些单独的通道合并到 BGR 图像。你可以通过以下方式完成。
#ROI,Range of instrest
roi = img[100:200,300:400] #截取100行到200行,列为300到400列的整块区域
img[50:150,200:300] = roi #将截取的roi移动到该区域 (50到100行,200到300列)
b = img[:,:,0] #截取整个蓝色通道
# 通道拆分
b, g, r = cv.split(img)
# 通道合并
img = cv.merge((b, g, r))
2.5 添加边界(padding)
cv.copyMakeBorder()
参数:
img:图像对象
top, bottom, left, right: 上下左右边界宽度,单位为像素值
borderType:
cv.BORDER_CONSTANT, 带颜色的边界,需要传入另外一个颜色值
cv.BORDER_REFLECT, 边缘元素的镜像反射做为边界
cv.BORDER_REFLECT_101/cv.BORDER_DEFAULT
cv.BORDER_REPLICATE, 边缘元素的复制做为边界
CV.BORDER_WRAP
value: borderType为cv.BORDER_CONSTANT时,传入的边界颜色值,如[0,255,0]
使用示例:
import cv2 as cv
import matplotlib.pyplot as plt
img2 = cv.imread(r"./image/dog.jpg")
img = cv.cvtColor(img2, cv.COLOR_BGR2RGB) # matplotlib的图像为RGB格式
constant = cv.copyMakeBorder(img, 20, 20, 20, 20, cv.BORDER_CONSTANT, value=[0, 255, 0]) # 绿色
reflect = cv.copyMakeBorder(img, 20, 20, 20, 20, cv.BORDER_REFLECT)
reflect01 = cv.copyMakeBorder(img, 20, 20, 20, 20, cv.BORDER_REFLECT_101)
replicate = cv.copyMakeBorder(img, 20, 20, 20, 20, cv.BORDER_REPLICATE)
wrap = cv.copyMakeBorder(img, 20, 20, 20, 20, cv.BORDER_WRAP)
titles = ["constant", "reflect", "reflect01", "replicate", "wrap"]
images = [constant, reflect, reflect01, replicate, wrap]
for i in range(5):
plt.subplot(2, 3, i + 1), plt.imshow(images[i]), plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
2.6 像素算术运算
cv.add() 相加的两个图片,应该有相同的大小和通道
你可以使用 OpenCV 的 cv.add()函数把两幅图像相加,或者可以简单地通过 numpy 操作添加两个图像,如 res = img1 + img2。两个图像应该具有相同的大小和类型,或者第二个图像可以是标量值。
注意:OpenCV 加法和 Numpy 加法之间存在差异。OpenCV 的加法是饱和操作,而 Numpy 添加是模运算。
cv.add()
参数:
img1:图片对象1
img2:图片对象2
mask:None (掩膜,一般用灰度图做掩膜,img1和img2相加后,和掩膜与运算,从而达到掩盖部分区域的目的)
dtype:-1
注意:图像相加时应该用cv.add(img1,img2)代替img1+img2
>>> x = np.uint8([250])
>>> y = np.uint8([10])
>>> print cv.add(x,y) # 250+10 = 260 => 255 #相加,opencv超过255的截取为255
[[255]]
>>> print x+y # 250+10 = 260 % 256 = 4 #相加,np超过255的会取模运算 (uint8只能表示0-255,所以取模)
[4]
使用示例:图一无掩膜,图二有掩膜
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img1 = cv.imread("./image/view.jpg")
img2 = cv.imread("./image/rain.jpg")
# 2 加法操作
img3 = cv.add(img1, img2) # cv中的加法
img4 = img1 + img2 # 直接相加
# 3 图像显示
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100)
axes[0].imshow(img3[:, :, ::-1])
axes[0].set_title("cv中的加法")
axes[1].imshow(img4[:, :, ::-1])
axes[1].set_title("直接相加")
plt.show()
结果如下所示:
cv.addWeight(): 两张图片相加,分别给予不同权重,实现图片融合和透明背景等效果
这其实也是加法,但是不同的是两幅图像的权重不同,这就会给人一种混合或者透明的感觉。图像混合的计算公式如下:
g(x) = (1−α)f0(x) + αf1(x)
通过修改 α 的值(0 → 1),可以实现非常炫酷的混合。
现在我们把两幅图混合在一起。第一幅图的权重是 0.7,第二幅图的权重是 0.3。函数 cv2.addWeighted()可以按下面的公式对图片进行混合操作。
dst = α⋅img1 + β⋅img2 + γ
这里 γ 取为零。
cv.addWeighted() 两张图片相加,分别给予不同权重,实现图片融合和透明背景等效果
参数:
img1:图片对象1
alpha:img1的权重
img2:图片对象2
beta:img1的权重
gamma:常量值,图像相加后再加上常量值
dtype:返回图像的数据类型,默认为-1,和img1一样
(img1*alpha+img2*beta+gamma)
使用示例:
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img1 = cv.imread("./image/view.jpg")
img2 = cv.imread("./image/rain.jpg")
# 2 图像混合
img3 = cv.addWeighted(img1, 0.7, img2, 0.3, 0)
# 3 图像显示
plt.figure(figsize=(8, 8))
plt.imshow(img3[:, :, ::-1])
plt.show()
2.7 图像位运算
btwise_and(), bitwise_or(), bitwise_not(), bitwise_xor()
cv.btwise_and(): 与运算
参数:
img1:图片对象1
img2:图片对象2
mask:掩膜
cv.bitwise_or():或运算
参数:
img1:图片对象1
img2:图片对象2
mask:掩膜
cv.bitwise_not(): 非运算
img1:图片对象1
mask:掩膜
cv.bitwise_xor():异或运算,相同为1,不同为0(1^1=0,1^0=1)
img1:图片对象1
img2:图片对象2
mask:掩膜
使用示例:将 logo 图片移动到足球图片中,需要截取 logo 图片的前景和足球图片 ROI 的背景,然后叠加,效果如下:
import cv2 as cv
import matplotlib.pyplot as plt
img1 = cv.imread(r"./image/logo.png")
rows,cols = img1.shape[0:2]
img2 = cv.imread(r"./image/Messi.jpg")
roi = img2[0:rows, 0:cols]
img1_gray = cv.cvtColor(img1,cv.COLOR_BGR2GRAY)
ret,img1_thres = cv.threshold(img1_gray, 200, 255,cv.THRESH_BINARY_INV)
img1_fg =cv.add(img1, img1, mask=img1_thres) #拿到logo图案的前景
img1_thres_inv = cv.bitwise_not(img1_thres)
roi_bg = cv.add(roi, roi,mask=img1_thres_inv) #拿到roi图案的背景
img_add = cv.add(img1_fg, roi_bg) #背景和前景相加
img2[0:rows, 0:cols] = img_add
cv.imshow("gray", img1_gray)
cv.imshow("thres", img1_thres)
cv.imshow("fg", img1_fg)
cv.imshow("tinv", img1_thres_inv)
cv.imshow("roi_bg", roi_bg)
cv.imshow("img_add", img_add)
cv.imshow("img2", img2)
cv.waitKey(0)
cv.destroyAllWindows()
2.8 性能评价
cv.getTickCount(): 获得时钟次数
cv.getTickFrequency():获得时钟频率 (每秒振动次数)
img1 = cv.imread('messi5.jpg')
e1 = cv.getTickCount()
for i in xrange(5,49,2):
img1 = cv.medianBlur(img1,i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
print(t)
2.9 绑定 trackbar 到图像
cv.createTrackbar()
cv.getTrackbarPos()
cv.createTrackbar() 为窗口添加trackbar
参数:
trackbarname: trackbar的名字
winname: 窗口的名字
value: trackbar创建时的值
count:trackbar能设置的最大值,最小值总为0
onChange:trackbar值发生变化时的回调函数,trackbar的值作为参数传给onchange
cv.getTrackbarPos() 获取某个窗口中trackbar的值
参数:
trackbarname: trackbar的名字
winname: 窗口的名字
使用示例:通过改变 trackbar 的值,来寻找最优的 mask 范围,从而识别出图片中蓝色的瓶盖
import cv2 as cv
import numpy as np
def nothing(args):
pass
img = cv.imread(r"./image/frame.png")
img_hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
cv.namedWindow('tracks')
cv.createTrackbar("LH","tracks",0,255,nothing)
cv.createTrackbar("LS","tracks",0,255,nothing)
cv.createTrackbar("LV","tracks",0,255,nothing)
cv.createTrackbar("UH","tracks",255,255,nothing)
cv.createTrackbar("US","tracks",255,255,nothing)
cv.createTrackbar("UV","tracks",255,255,nothing)
# switch = "0:OFF \n1:ON"
# cv.createTrackbar(switch,"tracks",0,1,nothing)
while True:
l_h = cv.getTrackbarPos("LH","tracks")
l_s = cv.getTrackbarPos("LS","tracks")
l_v = cv.getTrackbarPos("LV","tracks")
u_h = cv.getTrackbarPos("UH","tracks")
u_s = cv.getTrackbarPos("US","tracks")
u_v = cv.getTrackbarPos("UV","tracks")
lower_b = np.array([l_h,l_s,l_v])
upper_b = np.array([u_h,u_s,u_v])
mask = cv.inRange(img_hsv,lower_b,upper_b)
res = cv.add(img,img,mask=mask)
cv.imshow("img",img)
cv.imshow("mask",mask)
cv.imshow("res",res)
k = cv.waitKey(1)
if k==27:
break
cv.destroyAllWindows()
再来一个例子
import cv2 as cv
import numpy as np
def empty(a):
pass
def stackImages(scale, imgArray):
rows = len(imgArray)
cols = len(imgArray[0])
rowsAvailable = isinstance(imgArray[0], list)
width = imgArray[0][0].shape[1]
height = imgArray[0][0].shape[0]
if rowsAvailable:
for x in range(0, rows):
for y in range(0, cols):
if imgArray[x][y].shape[:2] == imgArray[0][0].shape[:2]:
imgArray[x][y] = cv.resize(imgArray[x][y], (0, 0), None, scale, scale)
else:
imgArray[x][y] = cv.resize(imgArray[x][y], (imgArray[0][0].shape[1], imgArray[0][0].shape[0]), None,
scale, scale)
if len(imgArray[x][y].shape) == 2: imgArray[x][y] = cv.cvtColor(imgArray[x][y], cv.COLOR_GRAY2BGR)
imageBlank = np.zeros((height, width, 3), np.uint8)
hor = [imageBlank] * rows
hor_con = [imageBlank] * rows
for x in range(0, rows):
hor[x] = np.hstack(imgArray[x])
ver = np.vstack(hor)
else:
for x in range(0, rows):
if imgArray[x].shape[:2] == imgArray[0].shape[:2]:
imgArray[x] = cv.resize(imgArray[x], (0, 0), None, scale, scale)
else:
imgArray[x] = cv.resize(imgArray[x], (imgArray[0].shape[1], imgArray[0].shape[0]), None, scale, scale)
if len(imgArray[x].shape) == 2: imgArray[x] = cv.cvtColor(imgArray[x], cv.COLOR_GRAY2BGR)
hor = np.hstack(imgArray)
ver = hor
return ver
path = 'Resources/lambo.png'
cv.namedWindow("TrackBars")
cv.resizeWindow("TrackBars", 640, 240)
cv.createTrackbar("Hue Min", "TrackBars", 0, 179, empty)
cv.createTrackbar("Hue Max", "TrackBars", 19, 179, empty)
cv.createTrackbar("Sat Min", "TrackBars", 110, 255, empty)
cv.createTrackbar("Sat Max", "TrackBars", 240, 255, empty)
cv.createTrackbar("Val Min", "TrackBars", 153, 255, empty)
cv.createTrackbar("Val Max", "TrackBars", 255, 255, empty)
while True:
img = cv.imread(path)
imgHSV = cv.cvtColor(img, cv.COLOR_BGR2HSV)
h_min = cv.getTrackbarPos("Hue Min", "TrackBars")
h_max = cv.getTrackbarPos("Hue Max", "TrackBars")
s_min = cv.getTrackbarPos("Sat Min", "TrackBars")
s_max = cv.getTrackbarPos("Sat Max", "TrackBars")
v_min = cv.getTrackbarPos("Val Min", "TrackBars")
v_max = cv.getTrackbarPos("Val Max", "TrackBars")
print(h_min, h_max, s_min, s_max, v_min, v_max)
lower = np.array([h_min, s_min, v_min])
upper = np.array([h_max, s_max, v_max])
mask = cv.inRange(imgHSV, lower, upper)
imgResult = cv.bitwise_and(img, img, mask=mask)
# cv.imshow("Original",img)
# cv.imshow("HSV",imgHSV)
# cv.imshow("Mask", mask)
# cv.imshow("Result", imgResult)
# stackImages图片拼接
imgStack = stackImages(0.6, ([img, imgHSV], [mask, imgResult]))
cv.imshow("Stacked Images", imgStack)
cv.waitKey(1)
3. 图像阈值化
cv.threshold()
cv.adaptiveThreshold()
cv.threshold():
参数:
img:图像对象,必须是灰度图
thresh:阈值
maxval:最大值
type:
cv.THRESH_BINARY: 小于阈值的像素置为0,大于阈值的置为maxval
cv.THRESH_BINARY_INV: 小于阈值的像素置为maxval,大于阈值的置为0
cv.THRESH_TRUNC: 小于阈值的像素不变,大于阈值的置为thresh
cv.THRESH_TOZERO 小于阈值的像素置0,大于阈值的不变
cv.THRESH_TOZERO_INV 小于阈值的不变,大于阈值的像素置0
返回两个值
ret:阈值
img:阈值化处理后的图像
cv.adaptiveThreshold() 自适应阈值处理,图像不同部位采用不同的阈值进行处理
参数:
img: 图像对象,8-bit单通道图
maxValue:最大值
adaptiveMethod: 自适应方法
cv.ADAPTIVE_THRESH_MEAN_C :阈值为周围像素的平均值
cv.ADAPTIVE_THRESH_GAUSSIAN_C : 阈值为周围像素的高斯均值(按权重)
threshType:
cv.THRESH_BINARY: 小于阈值的像素置为0,大于阈值的置为maxValuel
cv.THRESH_BINARY_INV: 小于阈值的像素置为maxValue,大于阈值的置为0
blocksize: 计算阈值时,自适应的窗口大小,必须为奇数 (如3:表示附近3个像素范围内的像素点,进行计算阈值)
C: 常数值,通过自适应方法计算的值,减去该常数值
(mean value of the blocksize*blocksize neighborhood of (x, y) minus C)
使用示例:
import cv2 as cv
import matplotlib.pyplot as plt
img = cv.imread(r"C:./image/maze.png", 0)
ret, thre1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
adaptive_thre1 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 7, 2)
adaptive_thre2 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 7, 2)
titles = ["img", "thre1", "adaptive_thre1", "adaptive_thre2"]
imgs = [img, thre1, adaptive_thre1, adaptive_thre2]
for i in range(4):
plt.subplot(2, 2, i + 1), plt.imshow(imgs[i], "gray")
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
奥斯二值化(Otsu’s Binarization)
对于一些双峰图像,奥斯二值化能找到两峰之间的像素值作为阈值,并将其返回。适用于双峰图像的阈值化,或者通过去噪而产生的双峰图像。
官网使用示例:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('noisy2.png',0)
# global thresholding
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
# Otsu's thresholding
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# Otsu's thresholding after Gaussian filtering
blur = cv.GaussianBlur(img,(5,5),0)
ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# plot all the images and their histograms
images = [img, 0, th1,
img, 0, th2,
blur, 0, th3]
titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
'Original Noisy Image','Histogram',"Otsu's Thresholding",
'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]
for i in xrange(3):
plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
plt.show()
4. 图像形状变换
4.1 cv.resize() 图像缩放
缩放是对图像的大小进行调整,即使图像放大或缩小。
cv.resize(src, dsize, fx=0, fy=0, interpolation=cv2.INTER_LINEAR) 放大和缩小图像
参数:
src: 输入图像对象
dsize:输出矩阵/图像的大小,为0时计算方式如下:dsize = Size(round(fx*src.cols),round(fy*src.rows))
fx: 水平轴的缩放因子,为0时计算方式: (double)dsize.width/src.cols
fy: 垂直轴的缩放因子,为0时计算方式: (double)dsize.heigh/src.rows
interpolation:插值算法
cv.INTER_NEAREST : 最近邻插值法
cv.INTER_LINEAR 默认值,双线性插值法
cv.INTER_AREA 基于局部像素的重采样(resampling using pixel area relation)。对于图像抽取(image decimation)来说,这可能是一个更好的方法。但如果是放大图像时,它和最近邻法的效果类似。
cv.INTER_CUBIC 基于4x4像素邻域的3次插值法
cv.INTER_LANCZOS4 基于8x8像素邻域的Lanczos插值
cv.INTER_AREA 适合于图像缩小, cv.INTER_CUBIC (slow) & cv.INTER_LINEAR 适合于图像放大
官网示例:
import cv2 as cv
from matplotlib import pyplot as plt
# 1. 读取图片
img1 = cv.imread("./image/kids.jpg")
# 2.图像缩放
# 2.1 绝对尺寸
rows, cols = img1.shape[:2]
res = cv.resize(img1, (2 * cols, 2 * rows), interpolation=cv.INTER_CUBIC)
# 2.2 相对尺寸
res1 = cv.resize(img1, None, fx=0.5, fy=0.5)
# 3 图像显示
# 3.1 使用opencv显示图像(不推荐)
cv.imshow("orignal", img1)
cv.imshow("enlarge", res)
cv.imshow("shrink)", res1)
cv.waitKey(0)
# 3.2 使用matplotlib显示图像
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 8), dpi=100)
axes[0].imshow(res[:, :, ::-1])
axes[0].set_title("绝对尺度(放大)")
axes[1].imshow(img1[:, :, ::-1])
axes[1].set_title("原图")
axes[2].imshow(res1[:, :, ::-1])
axes[2].set_title("相对尺度(缩小)")
plt.show()
4.2 cv.warpAffine() 仿射变换
仿射变换(从二维坐标到二维坐标之间的线性变换,且保持二维图形的“平直性”和“平行性”。仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切)
cv.warpAffine() 仿射变换(从二维坐标到二维坐标之间的线性变换,且保持二维图形的“平直性”和“平行性”。仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切)
参数:
img: 图像对象
M:2*3 transformation matrix (转变矩阵)
dsize:输出矩阵的大小,注意格式为(cols,rows) 即width对应cols,height对应rows
flags:可选,插值算法标识符,有默认值INTER_LINEAR,
如果插值算法为WARP_INVERSE_MAP, warpAffine函数使用如下矩阵进行图像转dst(x,y)=src(M11*x+M12*y+M13,M21*x+M22*y+M23)
borderMode:可选, 边界像素模式,有默认值BORDER_CONSTANT
borderValue:可选,边界取值,有默认值Scalar()即0
常用插值算法:
仿射变换的本质:即一个矩阵 A 和向量 B 共同组成的转变矩阵,和原图像坐标相乘来得到新图像的坐标,从而实现图像移动,旋转等。如下矩阵 A 和向量 B 组成的转变矩阵 M,用来对原图像的坐标(x,y)进行转变,得到新坐标向量 T
矩阵 A 和向量 B
仿射变换(矩阵计算):变换前坐标(x,y)
变换结果:变换后坐标(a00x+a01 y+b00, a10x+a11y+b10)
4.2.1 平移变换
了解了仿射变换的概念,平移变换只是采用了一个如下的转变矩阵(transformation matrix): 从(x,y)平移到(x+tx, y+ty)
官网使用示例:向左平移 100,向下平移 50
cv.warpAffine(img, M, dsize)
img: 输入图像
M: 2*∗3移动矩阵
dsize: 输出图像的大小
注意:输出图像的大小,它应该是(宽度,高度)的形式。请记住,width=列数,height=行数
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1. 读取图像
img1 = cv.imread("./image/kids.jpg")
# 2. 图像平移
rows, cols = img1.shape[:2]
M = M = np.float32([[1, 0, 100], [0, 1, 50]]) # 平移矩阵
dst = cv.warpAffine(img1, M, (cols, rows))
# 3. 图像显示
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100)
axes[0].imshow(img1[:, :, ::-1])
axes[0].set_title("原图")
axes[1].imshow(dst[:, :, ::-1])
axes[1].set_title("平移后结果")
plt.show()
4.2.2 放大和缩小
放大和缩小指相对于原坐标(x,y),变换为了(ax, by),即水平方向放大了 a 倍,水平方向放大了 b 倍,其对应的转变矩阵如下:
4.2.3 旋转变换
图像旋转是指图像按照某个位置转动一定角度的过程,旋转中图像仍保持这原始尺寸。图像旋转后图像的水平对称轴、垂直对称轴及中心坐标原点都可能会发生变换,因此需要对图像旋转中的坐标进行相应转换。
将(x,y),以坐标原点为中心,顺时针方向旋转 α 得到(x1,y1), 有如下关系 x1 = xcosα-ysinα, y1 =xsinα+ycosα; 因此可以构建对应的转变矩阵如下:
opencv 将其扩展到,任意点 center 为中心进行顺时针旋转 α,放大 scale 倍的,转变矩阵如下:
通过 getRotationMatrix2D()能得到转变矩阵
cv.getRotationMatrix2D() 返回2*3的转变矩阵(浮点型)
参数:
center:旋转的中心点坐标
angle:旋转角度,单位为度数,证书表示逆时针旋转
scale:同方向的放大倍数
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread("./image/kids.jpg")
# 2 图像旋转
rows, cols = img.shape[:2]
# 2.1 生成旋转矩阵
M = cv.getRotationMatrix2D((cols / 2, rows / 2), 90, 1)
# 2.2 进行旋转变换
dst = cv.warpAffine(img, M, (cols, rows))
# 3 图像展示
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100)
axes[0].imshow(img[:, :, ::-1])
axes[0].set_title("原图")
axes[1].imshow(dst[:, :, ::-1])
axes[1].set_title("旋转后结果")
plt.show()
4.2.4 仿射变换矩阵的计算
通过上述的平移,缩放,旋转的组合变换即实现了仿射变换,上述多个变换的变换矩阵相乘即能得到组合变换的变换矩阵。同时该变换矩阵中涉及到六个未知数(2*3 的矩阵),通过变换前后对应三组坐标,也可以求出变换矩阵,opencv 提供了函数 getAffineTransform()来计算变化矩阵
- 矩阵相乘:将平移,旋转和缩放的变换矩阵相乘,最后即为仿射变换矩阵
- getAffineTransform():根据变换前后三组坐标计算变换矩阵
cv.getAffineTransform() 返回2*3的转变矩阵
参数:
src:原图像中的三组坐标,如np.float32([[50,50],[200,50],[50,200]])
dst: 转换后的对应三组坐标,如np.float32([[10,100],[200,50],[100,250]])
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 图像读取
img = cv.imread("./image/kids.jpg")
# 2 仿射变换
rows, cols = img.shape[:2]
# 2.1 创建变换矩阵
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[100, 100], [200, 50], [100, 250]])
M = cv.getAffineTransform(pts1, pts2)
# 2.2 完成仿射变换
dst = cv.warpAffine(img, M, (cols, rows))
# 3 图像显示
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100)
axes[0].imshow(img[:, :, ::-1])
axes[0].set_title("原图")
axes[1].imshow(dst[:, :, ::-1])
axes[1].set_title("仿射后结果")
plt.show()
4.3 透视变换(persperctive transformation)
仿射变换都是在二维空间的变换,透视变换(投影变换)是在三维空间中发生了旋转。需要前后四组坐标来计算对应的转变矩阵,opencv 提供了函数 getPerspectiveTransform()来计算转变矩阵,cv2.wrapPerspective()函数来进行透视变换。其对应参数如下:
cv.getPerspectiveTransform() 返回3*3的转变矩阵
src:原图像中的四组坐标,如 np.float32([[56,65],[368,52],[28,387],[389,390]])
dst: 转换后的对应四组坐标,如np.float32([[0,0],[300,0],[0,300],[300,300]])
cv.wrapPerspective()
src: 图像对象
M:3*3 transformation matrix (转变矩阵)
dsize:输出矩阵的大小,注意格式为(cols,rows) 即width对应cols,height对应rows
flags:可选,插值算法标识符,有默认值INTER_LINEAR,如果插值算法为WARP_INVERSE_MAP, warpAffine函数使用如下矩阵进行图像转dst(x,y)=src(M11*x+M12*y+M13,M21*x+M22*y+M23)
borderMode:可选, 边界像素模式,有默认值BORDER_CONSTANT
borderValue:可选,边界取值,有默认值Scalar()即0
使用示例:
import cv2 as cv
import numpy as np
# 1 读取图像
img = cv.imread("./image/poker.jpg")
width, height = 250, 350
# 2 创建变换矩阵
# 找到扑克牌中的四个点
pts1 = np.float32([[111, 219],[287, 188],[154, 482],[352, 440]])
pts2 = np.float32([[0, 0], [width, 0], [0, height], [width, height]])
matrix = cv.getPerspectiveTransform(pts1, pts2)
# 3 进行变换
imgOutput = cv.warpPerspective(img, matrix, (width, height))
# 4 图像显示
cv.imshow("Image", img)
cv.imshow("Output", imgOutput)
cv.waitKey(0)
从上图中可以透视变换的一个应用,如果能找到原图中纸张的四个顶点,将其转换到新图中纸张的四个顶点,能将歪斜的 roi 区域转正,并进行放大;如在书籍,名片拍照上传后进行识别时,是一个很好的图片预处理方法。
5. 形态学操作
形态学转换是基于图像形状的一些简单操作。它通常在二进制图像上执行。腐蚀和膨胀是两个基本的形态学运算符。然后它的变体形式如开运算,闭运算,礼帽黑帽等。
5.1 腐蚀和膨胀
腐蚀和膨胀是最基本的形态学操作,腐蚀和膨胀都是针对白色部分(高亮部分)而言的。
膨胀就是使图像中高亮部分扩张,效果图拥有比原图更大的高亮区域;腐蚀是原图中的高亮区域被蚕食,效果图拥有比原图更小的高亮区域。膨胀是求局部最大值的操作,腐蚀是求局部最小值的操作。
- 腐蚀
具体操作是:用一个结构元素扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为 1,则该像素为 1,否则为 0。如下图所示,结构 A 被结构 B 腐蚀后:
腐蚀的作用是消除物体边界点,使目标缩小,可以消除小于结构元素的噪声点。
API:
cv.erode(img,kernel,iterations)
img: 要处理的图像
kernel: 核结构
iterations: 腐蚀的次数,默认是1
- 膨胀
具体操作是:用一个结构元素扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为 0,则该像素为 0,否则为 1。如下图所示,结构 A 被结构 B 腐蚀后:
膨胀的作用是将与物体接触的所有背景点合并到物体中,使目标增大,可添补目标中的孔洞。
API:
cv.dilate(img,kernel,iterations)
img: 要处理的图像
kernel: 核结构
iterations: 腐蚀的次数,默认是1
- 示例
我们使用一个 5*5 的卷积核实现腐蚀和膨胀的运算:
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread("./image/letter.png")
# 2 创建核结构
kernel = np.ones((5, 5), np.uint8)
# 3 图像腐蚀和膨胀
erosion = cv.erode(img, kernel) # 腐蚀
dilate = cv.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("原图")
axes[1].imshow(erosion)
axes[1].set_title("腐蚀后结果")
axes[2].imshow(dilate)
axes[2].set_title("膨胀后结果")
plt.show()
5.2 开闭运算
开运算和闭运算是将腐蚀和膨胀按照一定的次序进行处理。 但这两者并不是可逆的,即先开后闭并不能得到原来的图像。
- 开运算
开运算是先腐蚀后膨胀,其作用是:分离物体,消除小区域。特点:消除噪点,去除小的干扰块,而不影响原来的图像。 - 闭运算
闭运算与开运算相反,是先膨胀后腐蚀,作用是消除/“闭合”物体里面的孔洞,特点:可以填充闭合区域。
- API
cv.morphologyEx(img, op, kernel)
img: 要处理的图像
op: 处理方式:若进行开运算,则设为cv.MORPH_OPEN,若进行闭运算,则设为cv.MORPH_CLOSE
Kernel: 核结构
- 示例
使用 10*10 的核结构对卷积进行开闭运算的实现。
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img1 = cv.imread("./image/letter1.png")
img2 = cv.imread("./image/letter2.png")
# 2 创建核结构
kernel = np.ones((10, 10), np.uint8)
# 3 图像的开闭运算
cvOpen = cv.morphologyEx(img1, cv.MORPH_OPEN, kernel) # 开运算
cvClose = cv.morphologyEx(img2, cv.MORPH_CLOSE, kernel) # 闭运算
# 4 图像展示
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8))
axes[0, 0].imshow(img1)
axes[0, 0].set_title("原图")
axes[0, 1].imshow(cvOpen)
axes[0, 1].set_title("开运算结果")
axes[1, 0].imshow(img2)
axes[1, 0].set_title("原图")
axes[1, 1].imshow(cvClose)
axes[1, 1].set_title("闭运算结果")
plt.show()
5.3 礼帽和黑帽
- 礼帽运算
原图像与“开运算“的结果图之差,如下式计算:
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。
礼帽运算用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。 - 黑帽运算
为”闭运算“的结果图与原图像之差。数学表达式为:
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
黑帽运算用来分离比邻近点暗一些的斑块。 - API
cv.morphologyEx(img, op, kernel)
img: 要处理的图像
op: 处理方式:
Kernel: 核结构
- 参数:
- 示例
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img1 = cv.imread("./image/letter.png")
img2 = cv.imread("./image/letter.png")
# 2 创建核结构
kernel = np.ones((10, 10), np.uint8)
# 3 图像的礼帽和黑帽运算
cvOpen = cv.morphologyEx(img1, cv.MORPH_TOPHAT, kernel) # 礼帽运算
cvClose = cv.morphologyEx(img2, cv.MORPH_BLACKHAT, kernel) # 黑帽运算
# 4 图像显示
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8))
axes[0, 0].imshow(img1)
axes[0, 0].set_title("原图")
axes[0, 1].imshow(cvOpen)
axes[0, 1].set_title("礼帽运算结果")
axes[1, 0].imshow(img2)
axes[1, 0].set_title("原图")
axes[1, 1].imshow(cvClose)
axes[1, 1].set_title("黑帽运算结果")
plt.show()
6. 图像平滑
6.1 图像噪声
由于图像采集、处理、传输等过程不可避免的会受到噪声的污染,妨碍人们对图像理解及分析处理。常见的图像噪声有高斯噪声、椒盐噪声等。
6.1.1 椒盐噪声
椒盐噪声也称为脉冲噪声,是图像中经常见到的一种噪声,它是一种随机出现的白点或者黑点,可能是亮的区域有黑色像素或是在暗的区域有白色像素(或是两者皆有)。椒盐噪声的成因可能是影像讯号受到突如其来的强烈干扰而产生、类比数位转换器或位元传输错误等。例如失效的感应器导致像素值为最小值,饱和的感应器导致像素值为最大值。
6.1.2 高斯噪声
高斯噪声是指噪声密度函数服从高斯分布的一类噪声。由于高斯噪声在空间和频域中数学上的易处理性,这种噪声(也称为正态噪声)模型经常被用于实践中。高斯随机变量 z 的概率密度函数由下式给出:
其中 z 表示灰度值,μ 表示 z 的平均值或期望值,σ 表示 z 的标准差。标准差的平方\sigma^{2}_σ_2 称为 z 的方差。高斯函数的曲线如图所示。
6.2 图像平滑简介
图像平滑从信号处理的角度看就是去除其中的高频信息,保留低频信息。因此我们可以对图像实施低通滤波。低通滤波可以去除图像中的噪声,对图像进行平滑。
根据滤波器的不同可分为均值滤波,高斯滤波,中值滤波, 双边滤波。
6.2.1 均值滤波
采用均值滤波模板对图像噪声进行滤除。令 表示中心在(x, y)点,尺寸为 m×n 的矩形子图像窗口的坐标组。 均值滤波器可表示为:
由一个归一化卷积框完成的。它只是用卷积框覆盖区域所有像素的平均值来代替中心元素。
例如,3x3 标准化的平均过滤器如下所示:
均值滤波的优点是算法简单,计算速度较快,缺点是在去噪的同时去除了很多细节部分,将图像变得模糊。
API:
cv.blur(src, ksize, anchor, borderType)
src:输入图像
ksize:卷积核的大小
anchor:默认值 (-1,-1) ,表示核中心
borderType:边界类型
示例:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 1 图像读取
img = cv.imread('./image/dogsp.jpeg')
# 2 均值滤波
blur = cv.blur(img, (5, 5))
# 3 图像显示
plt.figure(figsize=(10, 8), dpi=100)
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()
6.2.2 高斯滤波
二维高斯是构建高斯滤波器的基础,其概率分布函数如下所示:
G(x,y)的分布是一个突起的帽子的形状。这里的 σ 可以看作两个值,一个是 x 方向的标准差\sigmaxσx,另一个是 y 方向的标准差\sigma_yσy_。
正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。计算平滑结果时,只需要将”中心点”作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。
高斯平滑在从图像中去除高斯噪声方面非常有效。
高斯平滑的流程:
- 首先确定权重矩阵
假定中心点的坐标是(0,0),那么距离它最近的 8 个点的坐标如下:
更远的点以此类推。
为了计算权重矩阵,需要设定 σ 的值。假定 σ=1.5,则模糊半径为 1 的权重矩阵如下:
这 9 个点的权重总和等于 0.4787147,如果只计算这 9 个点的加权平均,还必须让它们的权重之和等于 1,因此上面 9 个值还要分别除以 0.4787147,得到最终的权重矩阵。
- 计算高斯模糊
有了权重矩阵,就可以计算高斯模糊的值了。
假设现有 9 个像素点,灰度值(0-255)如下:
每个点乘以对应的权重值:
得到
将这 9 个值加起来,就是中心点的高斯模糊的值。
对所有点重复这个过程,就得到了高斯模糊后的图像。如果原图是彩色图片,可以对 RGB 三个通道分别做高斯平滑。
API:
cv2.GaussianBlur(src,ksize,sigmaX,sigmay,borderType)
src: 输入图像
ksize:高斯卷积核的大小,注意 : 卷积核的宽度和高度都应为奇数,且可以不同
sigmaX: 水平方向的标准差
sigmaY: 垂直方向的标准差,默认值为0,表示与sigmaX相同
borderType:填充边界类型
示例:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 1 图像读取
img = cv.imread('./image/dogGasuss.jpeg')
# 2 高斯滤波
blur = cv.GaussianBlur(img, (3, 3), 1)
# 3 图像显示
plt.figure(figsize=(10, 8), dpi=100)
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()
6.2.3 中值滤波
中值滤波是一种典型的非线性滤波技术,基本思想是用像素点邻域灰度值的中值来代替该像素点的灰度值。
中值滤波对椒盐噪声(salt-and-pepper noise)来说尤其有用,因为它不依赖于邻域内那些与典型值差别很大的值。
API:
cv.medianBlur(src, ksize)
src:输入图像
ksize:卷积核的大小
示例:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 1 图像读取
img = cv.imread('./image/dogsp.jpeg')
# 2 中值滤波
blur = cv.medianBlur(img, 5)
# 3 图像展示
plt.figure(figsize=(10, 8), dpi=100)
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()
7. 直方图
7.1 灰度直方图
7.1.1 原理
直方图是对数据进行统计的一种方法,并且将统计值组织到一系列实现定义好的 bin 当中。其中, bin 为直方图中经常用到的一个概念,可以译为 “直条” 或 “组距”,其数值是从数据中计算出的特征统计量,这些数据可以是诸如梯度、方向、色彩或任何其他特征。
图像直方图(Image Histogram)是用以表示数字图像中亮度分布的直方图,标绘了图像中每个亮度值的像素个数。这种直方图中,横坐标的左侧为较暗的区域,而右侧为较亮的区域。因此一张较暗图片的直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。
注意:直方图是根据灰度图进行绘制的,而不是彩色图像。 假设有一张图像的信息(灰度值 0 - 255,已知数字的范围包含 256 个值,于是可以按一定规律将这个范围分割成子区域(也就是 bins)。如:
然后再统计每一个 bin(i) 的像素数目。可以得到下图(其中 x 轴表示 bin,y 轴表示各个 bin 中的像素个数):
直方图的一些术语和细节:
- dims:需要统计的特征数目。在上例中,dims = 1 ,因为仅仅统计了灰度值。
- bins:每个特征空间子区段的数目,可译为 “直条” 或 “组距”,在上例中, bins = 16。
- range:要统计特征的取值范围。在上例中,range = [0, 255]。
直方图的意义:
- 直方图是图像中像素强度分布的图形表达方式。
- 它统计了每一个强度值所具有的像素个数。
- 不同的图像的直方图可能是相同的
7.1.2 直方图的计算和绘制
我们使用 OpenCV 中的方法统计直方图,并使用 matplotlib 将其绘制出来。
API:
cv.calcHist(images,channels,mask,histSize,ranges[,hist[,accumulate]])
images: 原图像。当传入函数时应该用中括号 [] 括起来,例如:[img]。
channels: 如果输入图像是灰度图,它的值就是 [0];如果是彩色图像的话,传入的参数可以是 [0],[1],[2] 它们分别对应着通道 B,G,R。
mask: 掩模图像。要统计整幅图像的直方图就把它设为 None。但是如果你想统计图像某一部分的直方图的话,你就需要制作一个掩模图像,并使用它。(后边有例子)
histSize:BIN 的数目。也应该用中括号括起来,例如:[256]。
ranges: 像素值范围,通常为 [0,256]
示例:
如下图,绘制相应的直方图
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 1 直接以灰度图的方式读入
img = cv.imread('./image/cat.jpeg', 0)
# 2 统计灰度图
histr = cv.calcHist([img], [0], None, [256], [0, 256])
# 3 绘制灰度图
plt.figure(figsize=(10, 6), dpi=100)
plt.plot(histr)
plt.grid()
plt.show()
7.1.3 掩膜的应用
掩膜是用选定的图像、图形或物体,对要处理的图像进行遮挡,来控制图像 处理的区域。
在数字图像处理中,我们通常使用二维矩阵数组进行掩膜。掩膜是由 0 和 1 组成一个二进制图像,利用该掩膜图像要处理的图像进行掩膜,其中 1 值的区域被处理,0 值区域被屏蔽,不会处理。
掩膜的主要用途是:
- 提取感兴趣区域:用预先制作的感兴趣区掩模与待处理图像进行”与“操作,得到感兴趣区图像,感兴趣区内图像值保持不变,而区外图像值都为 0。
- 屏蔽作用:用掩模对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
- 结构特征提取:用相似性变量或图像匹配方法检测和提取图像中与掩模相似的结构特征。
- 特殊形状图像制作
掩膜在遥感影像处理中使用较多,当提取道路或者河流,或者房屋时,通过一个掩膜矩阵来对图像进行像素过滤,然后将我们需要的地物或者标志突出显示出来。
我们使用 cv.calcHist()来查找完整图像的直方图。 如果要查找图像某些区域的直方图,该怎么办? 只需在要查找直方图的区域上创建一个白色的掩膜图像,否则创建黑色, 然后将其作为掩码 mask 传递即可。
示例:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 1. 直接以灰度图的方式读入
img = cv.imread('./image/cat.jpeg', 0)
# 2. 创建蒙版
mask = np.zeros(img.shape[:2], np.uint8)
mask[400:650, 200:500] = 255
# 3.掩模
masked_img = cv.bitwise_and(img, img, mask=mask)
# 4. 统计掩膜后图像的灰度图
mask_histr = cv.calcHist([img], [0], mask, [256], [1, 256])
# 5. 图像展示
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8))
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_img, 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()
7.2 直方图均衡化
7.2.1 原理与应用
想象一下,如果一副图像中的大多数像素点的像素值都集中在某一个小的灰度值值范围之内会怎样呢?如果一幅图像整体很亮,那所有的像素值的取值个数应该都会很高。所以应该把它的直方图做一个横向拉伸(如下图),就可以扩大图像像素值的分布范围,提高图像的对比度,这就是直方图均衡化要做的事情。
“直方图均衡化”是把原始图像的灰度直方图从比较集中的某个灰度区间变成在更广泛灰度范围内的分布。直方图均衡化就是对图像进行非线性拉伸,重新分配图像像素值,使一定灰度范围内的像素数量大致相同。
这种方法提高图像整体的对比度,特别是有用数据的像素值分布比较接近时,在 X 光图像中使用广泛,可以提高骨架结构的显示,另外在曝光过度或不足的图像中可以更好的突出细节。
使用 opencv 进行直方图统计时,使用的是:
API:
cv.equalizeHist(img)
img: 灰度图像
返回:
- dst : 均衡化后的结果
示例:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 1. 直接以灰度图的方式读入
img = cv.imread('./image/cat.jpeg', 0)
# 2. 均衡化处理
dst = cv.equalizeHist(img)
# 3. 结果展示
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8), dpi=100)
axes[0].imshow(img, cmap=plt.cm.gray)
axes[0].set_title("原图")
axes[1].imshow(dst, cmap=plt.cm.gray)
axes[1].set_title("均衡化后结果")
plt.show()
7.2.2 自适应的直方图均衡化
上述的直方图均衡,我们考虑的是图像的全局对比度。 的确在进行完直方图均衡化之后,图片背景的对比度被改变了,在猫腿这里太暗,我们丢失了很多信息,所以在许多情况下,这样做的效果并不好。如下图所示,对比下两幅图像中雕像的画面,由于太亮我们丢失了很多信息。
为了解决这个问题, 需要使用自适应的直方图均衡化。 此时, 整幅图像会被分成很多小块,这些小块被称为“tiles”(在 OpenCV 中 tiles 的 大小默认是 8x8),然后再对每一个小块分别进行直方图均衡化。 所以在每一个的区域中, 直方图会集中在某一个小的区域中)。如果有噪声的话,噪声会被放大。为了避免这种情况的出现要使用对比度限制。对于每个小块来说,如果直方图中的 bin 超过对比度的上限的话,就把 其中的像素点均匀分散到其他 bins 中,然后在进行直方图均衡化。
最后,为了 去除每一个小块之间的边界,再使用双线性差值,对每一小块进行拼接。
API:
cv.createCLAHE(clipLimit, tileGridSize)
clipLimit: 对比度限制,默认是40
tileGridSize: 分块的大小,默认为8*88∗8
示例:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
# 1. 以灰度图形式读取图像
img = cv.imread('./image/cat.jpeg', 0)
# 2. 创建一个自适应均衡化的对象,并应用于图像
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
cl1 = clahe.apply(img)
# 3. 图像展示
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100)
axes[0].imshow(img, cmap=plt.cm.gray)
axes[0].set_title("原图")
axes[1].imshow(cl1, cmap=plt.cm.gray)
axes[1].set_title("自适应均衡化后的结果")
plt.show()
8. 边缘检测
8.1 原理
边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。边缘的表现形式如下图所示:
图像边缘检测大幅度地减少了数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性。有许多方法用于边缘检测,它们的绝大部分可以划分为两类:基于搜索和基于零穿越。
- 基于搜索:通过寻找图像一阶导数中的最大值来检测边界,然后利用计算结果估计边缘的局部方向,通常采用梯度的方向,并利用此方向找到局部梯度模的最大值,代表算法是 Sobel 算子和 Scharr 算子。
- 基于零穿越:通过寻找图像二阶导数零穿越来寻找边界,代表算法是 Laplacian 算子。
8.2 Sobel 检测算子
Sobel 边缘检测算法比较简单,实际应用中效率比 canny 边缘检测效率要高,但是边缘不如 Canny 检测的准确,但是很多实际应用的场合,sobel 边缘却是首选,Sobel 算子是高斯平滑与微分操作的结合体,所以其抗噪声能力很强,用途较多。尤其是效率要求较高,而对细纹理不太关心的时候。
8.2.1 方法
对于不连续的函数,一阶导数可以写作:
或
所以有:
假设要处理的图像为 II,在两个方向求导:
- 水平变化: 将图像 II 与奇数大小的模版进行卷积,结果为 Gx_Gx。比如,当模板大小为 3 时, Gx_Gx为:
- 垂直变化: 将图像 II与奇数大小的模板进行卷积,结果为 Gy_Gy。比如,当模板大小为 3 时, Gy_Gy为:
在图像的每一点,结合以上两个结果求出:
统计极大值所在的位置,就是图像的边缘。
注意:当内核大小为 3 时, 以上 Sobel 内核可能产生比较明显的误差, 为解决这一问题,我们使用 Scharr 函数,但该函数仅作用于大小为 3 的内核。该函数的运算与 Sobel 函数一样快,但结果却更加精确,其计算方法为:
8.2.2 应用
利用 OpenCV 进行 sobel 边缘检测的 API 是:
Sobel_x_or_y = cv.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,就演变成为3x3的Scharr算子。
scale:缩放导数的比例常数,默认情况为没有伸缩系数。
borderType:图像边界的模式,默认值为cv2.BORDER_DEFAULT。
Sobel 函数求完导数后会有负值,还有会大于 255 的值。而原图像是 uint8,即 8 位无符号数,所以 Sobel 建立的图像位数不够,会有截断。因此要使用 16 位有符号的数据类型,即 cv2.CV_16S。处理完图像后,再使用 cv2.convertScaleAbs()函数将其转回原来的 uint8 格式,否则图像无法显示。
Sobel 算子是在两个方向计算的,最后还需要用 cv2.addWeighted( )函数将其组合起来
Scale_abs = cv.convertScaleAbs(x) # 格式转换函数
result = cv.addWeighted(src1, alpha, src2, beta) # 图像混合
示例:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 1 读取图像
img = cv.imread('./image/horse.jpg', 0)
# 2 计算Sobel卷积结果
x = cv.Sobel(img, cv.CV_16S, 1, 0)
y = cv.Sobel(img, cv.CV_16S, 0, 1)
# 3 将数据进行转换
Scale_absX = cv.convertScaleAbs(x) # convert 转换 scale 缩放
Scale_absY = cv.convertScaleAbs(y)
# 4 结果合成
result = cv.addWeighted(Scale_absX, 0.5, Scale_absY, 0.5, 0)
# 5 图像显示
plt.figure(figsize=(10, 8), dpi=100)
plt.subplot(121), plt.imshow(img, cmap=plt.cm.gray), plt.title('原图')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(result, cmap=plt.cm.gray), plt.title('Sobel滤波后结果')
plt.xticks([]), plt.yticks([])
plt.show()
将上述代码中计算 sobel 算子的部分中将 ksize 设为-1,就是利用 Scharr 进行边缘检测。
x = cv.Sobel(img, cv.CV_16S, 1, 0, ksize = -1)
y = cv.Sobel(img, cv.CV_16S, 0, 1, ksize = -1)
8.3 Laplacian 算子
Laplacian 是利用二阶导数来检测边缘 。 因为图像是 “2 维”, 我们需要在两个方向求导,如下式所示:
那不连续函数的二阶导数是:
那使用的卷积核是:
API:
cv.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
Src: 需要处理的图像
Ddepth: 图像的深度,-1表示采用的是原图像相同的深度,目标图像的深度必须大于等于原图像的深度
ksize:算子的大小,即卷积核的大小,必须为1,3,5,7
示例:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 1 读取图像
img = cv.imread('./image/horse.jpg', 0)
# 2 laplacian转换
result = cv.Laplacian(img, cv.CV_16S)
Scale_abs = cv.convertScaleAbs(result)
# 3 图像展示
plt.figure(figsize=(10, 8), dpi=100)
plt.subplot(121), plt.imshow(img, cmap=plt.cm.gray), plt.title('原图')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(Scale_abs, cmap=plt.cm.gray), plt.title('Laplacian检测后结果')
plt.xticks([]), plt.yticks([])
plt.show()
8.4 Canny 边缘检测
Canny 边缘检测算法是一种非常流行的边缘检测算法,是 John F. Canny 于 1986 年提出的,被认为是最优的边缘检测算法。
8.4.1 原理
Canny 边缘检测算法是由 4 步构成,分别介绍如下:
- 第一步:噪声去除
由于边缘检测很容易受到噪声的影响,所以首先使用$5*5$高斯滤波器去除噪声,在图像平滑那一章节中已经介绍过。 - 第二步:计算图像梯度
对平滑后的图像使用 Sobel 算子计算水平方向和竖直方向的一阶导数(Gx 和 Gy)。根据得到的这两幅梯度图(Gx 和 Gy)找到边界的梯度和方向,公式如下:
如果某个像素点是边缘,则其梯度方向总是垂直与边缘垂直。梯度方向被归为四类:垂直,水平,和两个对角线方向。
- 第三步:非极大值抑制
在获得梯度的方向和大小之后,对整幅图像进行扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。如下图所示:
A 点位于图像的边缘,在其梯度变化方向,选择像素点 B 和 C,用来检验 A 点的梯度是否为极大值,若为极大值,则进行保留,否则 A 点被抑制,最终的结果是具有“细边”的二进制图像。
- 第四步:滞后阈值
现在要确定真正的边界。 我们设置两个阈值: minVal 和 maxVal。 当图像的灰度梯度高于 maxVal 时被认为是真的边界, 低于 minVal 的边界会被抛弃。如果介于两者之间的话,就要看这个点是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。如下图:
如上图所示,A 高于阈值 maxVal 所以是真正的边界点,C 虽然低于 maxVal 但高于 minVal 并且与 A 相连,所以也被认为是真正的边界点。而 B 就会被抛弃,因为低于 maxVal 而且不与真正的边界点相连。所以选择合适的 maxVal 和 minVal 对于能否得到好的结果非常重要。
8.4.2 应用
在 OpenCV 中要实现 Canny 检测使用的 API:
canny = cv.Canny(image, threshold1, threshold2)
image:灰度图
threshold1: minval,较小的阈值将间断的边缘连接起来
threshold2: maxval,较大的阈值检测图像中明显的边缘
示例:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 1 图像读取
img = cv.imread('./image/horse.jpg', 0)
# 2 Canny边缘检测
lowThreshold = 0
max_lowThreshold = 100
canny = cv.Canny(img, lowThreshold, max_lowThreshold)
# 3 图像展示
plt.figure(figsize=(10, 8), dpi=100)
plt.subplot(121), plt.imshow(img, cmap=plt.cm.gray), plt.title('原图')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(canny, cmap=plt.cm.gray), plt.title('Canny检测后结果')
plt.xticks([]), plt.yticks([])
plt.show()
9. 模版匹配和霍夫变换
9.1 模板匹配
9.1.1 原理
所谓的模板匹配,就是在给定的图片中查找和模板最相似的区域,该算法的输入包括模板和图片,整个任务的思路就是按照滑窗的思路不断的移动模板图片,计算其与图像中对应区域的匹配度,最终将匹配度最高的区域选择为最终的结果。
实现流程:
- 准备两幅图像: 1.原图像(I):在这幅图中,找到与模板相匹配的区域 2.模板(T):与原图像进行比对的图像块
- 滑动模板图像和原图像进行比对:
将模板块每次移动一个像素 (从左往右,从上往下),在每一个位置,都计算与模板图像的相似程度。
- 对于每一个位置将计算的相似结果保存在结果矩阵(R)中。如果输入图像的大小(WxH)且模板图像的大小(wxh),则输出矩阵 R 的大小为(W-w + 1,H-h + 1)将 R 显示为图像,如下图所示:
- 获得上述图像后,查找最大值所在的位置,那么该位置对应的区域就被认为是最匹配的。对应的区域就是以该点为顶点,长宽和模板图像一样大小的矩阵。
9.1.2 实现
我们使用 OpenCV 中的方法实现模板匹配。
API:
res = cv.matchTemplate(img,template,method)
img: 要进行模板匹配的图像
Template :模板
method:实现模板匹配的算法,主要有:
a. 平方差匹配(CV_TM_SQDIFF):利用模板与图像之间的平方差进行匹配,最好的匹配是0,匹配越差,匹配的值越大。
b. 相关匹配(CV_TM_CCORR):利用模板与图像间的乘法进行匹配,数值越大表示匹配程度较高,越小表示匹配效果差。
c. 利用相关系数匹配(CV_TM_CCOEFF):利用模板与图像间的相关系数匹配,1表示完美的匹配,-1表示最差的匹配。
完成匹配后,使用cv.minMaxLoc()方法查找最大值所在的位置即可。如果使用平方差作为比较方法,则最小值位置是最佳匹配位置。
示例:
在该案例中,载入要搜索的图像和模板,图像如下所示:
模板如下所示:
通过 matchTemplate 实现模板匹配,使用 minMaxLoc 定位最匹配的区域,并用矩形标注最匹配的区域。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 1 图像和模板读取
img = cv.imread('./image/wulin2.jpeg')
template = cv.imread('./image/wulin.jpeg')
h, w, l = template.shape
# 2 模板匹配
# 2.1 模板匹配
res = cv.matchTemplate(img, template, cv.TM_CCORR)
# 2.2 返回图像中最匹配的位置,确定左上角的坐标,并将匹配位置绘制在图像上
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
# 使用平方差时最小值为最佳匹配位置
# top_left = min_loc
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
cv.rectangle(img, top_left, bottom_right, (0, 255, 0), 2)
# 3 图像显示
plt.imshow(img[:, :, ::-1])
plt.title('匹配结果'), plt.xticks([]), plt.yticks([])
plt.show()
拓展:模板匹配不适用于尺度变换,视角变换后的图像,这时我们就要使用关键点匹配算法,比较经典的关键点检测算法包括 SIFT 和 SURF 等,主要的思路是首先通过关键点检测算法获取模板和测试图片中的关键点;然后使用关键点匹配算法处理即可,这些关键点可以很好的处理尺度变化、视角变换、旋转变化、光照变化等,具有很好的不变性。
9.2 霍夫变换
霍夫变换常用来提取图像中的直线和圆等几何形状,如下图所示:
9.2.1 霍夫线检测
在 OpenCV 中做霍夫线检测是使用的 API 是:
cv.HoughLines(img, rho, theta, threshold)
img: 检测的图像,要求是二值化的图像,所以在调用霍夫变换之前首先要进行二值化,或者进行Canny边缘检测
rho、theta: \rhoρ 和\thetaθ的精确度
threshold: 阈值,只有累加器中的值高于该阈值时才被认为是直线。
霍夫线检测的整个流程如下图所示,这是在 stackflow 上一个关于霍夫线变换的解释:
示例:
检测下述图像中的直线:
import numpy as np
import random
import cv2 as cv
import matplotlib.pyplot as plt
# 1.加载图片,转为二值图
img = cv.imread('./image/rili.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray, 50, 150)
# 2.霍夫直线变换
lines = cv.HoughLines(edges, 0.8, np.pi / 180, 150)
# 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))
cv.line(img, (x1, y1), (x2, y2), (0, 255, 0))
# 4. 图像显示
plt.figure(figsize=(10, 8), dpi=100)
plt.imshow(img[:, :, ::-1]), plt.title('霍夫变换线检测')
plt.xticks([]), plt.yticks([])
plt.show()
9.2.2 霍夫圆检测[了解]
- 原理圆的表示式是:
其中 aa和 bb表示圆心坐标,rr表示圆半径,因此标准的霍夫圆检测就是在这三个参数组成的三维空间累加器上进行圆形检测,此时效率就会很低,所以 OpenCV 中使用霍夫梯度法进行圆形的检测。霍夫梯度法将霍夫圆检测范围两个阶段,第一阶段检测圆心,第二阶段利用圆心推导出圆半径。
- 圆心检测的原理:圆心是圆周法线的交汇处,设置一个阈值,在某点的相交的直线的条数大于这个阈值就认为该交汇点为圆心。
- 圆半径确定原理:圆心到圆周上的距离(半径)是相同的,确定一个阈值,只要相同距离的数量大于该阈值,就认为该距离是该圆心的半径。
原则上霍夫变换可以检测任何形状,但复杂的形状需要的参数就多,霍夫空间的维数就多,因此在程序实现上所需的内存空间以及运行效率上都不利于把标准霍夫变换应用于实际复杂图形的检测中。霍夫梯度法是霍夫变换的改进,它的目的是减小霍夫空间的维度,提高效率。
API 在 OpenCV 中检测图像中的圆环使用的是 API 是:
circles = cv.HoughCircles(image, method, dp, minDist, param1=100, param2=100, minRadius=0,maxRadius=0 )
image:输入图像,应输入灰度图像
method:使用霍夫变换圆检测的算法,它的参数是CV_HOUGH_GRADIENT
dp:霍夫空间的分辨率,dp=1时表示霍夫空间与输入图像空间的大小一致,dp=2时霍夫空间是输入图像空间的一半,以此类推
minDist为圆心之间的最小距离,如果检测到的两个圆心之间距离小于该值,则认为它们是同一个圆心
param1:边缘检测时使用Canny算子的高阈值,低阈值是高阈值的一半。
param2:检测圆心和确定半径时所共有的阈值
minRadius和maxRadius为所检测到的圆半径的最小值和最大值
返回:
- circles:输出圆向量,包括三个浮点型的元素——圆心横坐标,圆心纵坐标和圆半径
由于霍夫圆检测对噪声比较敏感,所以首先对图像进行中值滤波。
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
# 1 读取图像,并转换为灰度图
planets = cv.imread("./image/star.jpeg")
gay_img = cv.cvtColor(planets, cv.COLOR_BGRA2GRAY)
# 2 进行中值模糊,去噪点
img = cv.medianBlur(gay_img, 7)
# 3 霍夫圆检测
circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT, 1, 200, param1=100, param2=30, minRadius=0, maxRadius=100)
# 4 将检测结果绘制在图像上
for i in circles[0, :]: # 遍历矩阵每一行的数据
# 绘制圆形
cv.circle(planets, (i[0], i[1]), i[2], (0, 255, 0), 2)
# 绘制圆心
cv.circle(planets, (i[0], i[1]), 2, (0, 0, 255), 3)
# 5 图像显示
plt.figure(figsize=(10, 8), dpi=100)
plt.imshow(planets[:, :, ::-1]), plt.title('霍夫变换圆检测')
plt.xticks([]), plt.yticks([])
plt.show()
10. 视频操作
10.1 视频读写
10.1.1 从文件中读取视频并播放
在 OpenCV 中我们要获取一个视频,需要创建一个 VideoCapture 对象,指定你要读取的视频文件:
- 创建读取视频的对象
cap = cv.VideoCapture(filepath)
filepath: 视频文件路径
视频的属性信息 2.1. 获取视频的某些属性,
retval = cap.get(propId)
propId: 从0到18的数字,每个数字表示视频的属性
常用属性有:
修改视频的属性信息
cap.set(propId,value)
proid: 属性的索引,与上面的表格相对应
value: 修改后的属性值
判断图像是否读取成功
isornot = cap.isOpened()
- 若读取成功则返回 true,否则返回 False
获取视频的一帧图像
ret, frame = cap.read()
ret: 若获取成功返回True,获取失败,返回False
Frame: 获取到的某一帧的图像
- 调用 cv.imshow()显示图像,在显示图像时使用 cv.waitkey()设置适当的持续时间,如果太低视频会播放的非常快,如果太高就会播放的非常慢,通常情况下我们设置 25ms 就可以了。
- 最后,调用 cap.realease()将视频释放掉
示例:
import numpy as np
import cv2 as cv
# 1.获取视频对象
cap = cv.VideoCapture('./image/DOG.wmv')
# 2.判断是否读取成功
while (cap.isOpened()):
# 3.获取每一帧图像
ret, frame = cap.read()
# 4. 获取成功显示图像
if ret == True:
cv.imshow('frame', frame)
# 5.每一帧间隔为25ms
if cv.waitKey(25) & 0xFF == ord('q'):
break
# 6.释放视频对象
cap.release()
cv.destoryAllwindows()
10.1.2 保存视频
在 OpenCV 中我们保存视频使用的是 VedioWriter 对象,在其中指定输出文件的名称,如下所示:
- 创建视频写入的对象
out = cv.VideoWriter(filename,fourcc, fps, frameSize)
filename:视频保存的位置
fourcc:指定视频编解码器的4字节代码
fps:帧率
frameSize:帧大小
设置视频的编解码器,如下所示,
retval = cv.VideoWriter_fourcc( c1, c2, c3, c4 )
c1,c2,c3,c4: 是视频编解码器的4字节代码,在fourcc.org中找到可用代码列表,与平台紧密相关,常用的有:
在Windows中:DIVX(.avi)
在OS中:MJPG(.mp4),DIVX(.avi),X264(.mkv)
- 利用 cap.read()获取视频中的每一帧图像,并使用 out.write()将某一帧图像写入视频中。
- 使用 cap.release()和 out.release()释放资源。
示例:
import cv2 as cv
import numpy as np
# 1. 读取视频
cap = cv.VideoCapture("./image/DOG.wmv")
# 2. 获取图像的属性(宽和高,),并将其转换为整数
frame_width = int(cap.get(3))
frame_height = int(cap.get(4))
# 3. 创建保存视频的对象,设置编码格式,帧率,图像的宽高等
out = cv.VideoWriter('outpy.avi', cv.VideoWriter_fourcc('M', 'J', 'P', 'G'), 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()
10.2 视频追踪
10.2.1.meanshift
原理
meanshift 算法的原理很简单。假设你有一堆点集,还有一个小的窗口,这个窗口可能是圆形的,现在你可能要移动这个窗口到点集密度最大的区域当中。
如下图:
最开始的窗口是蓝色圆环的区域,命名为 C1。蓝色圆环的圆心用一个蓝色的矩形标注,命名为 C1_o。
而窗口中所有点的点集构成的质心在蓝色圆形点 C1_r 处,显然圆环的形心和质心并不重合。所以,移动蓝色的窗口,使得形心与之前得到的质心重合。在新移动后的圆环的区域当中再次寻找圆环当中所包围点集的质心,然后再次移动,通常情况下,形心和质心是不重合的。不断执行上面的移动过程,直到形心和质心大致重合结束。 这样,最后圆形的窗口会落到像素分布最大的地方,也就是图中的绿色圈,命名为 C2。
meanshift 算法除了应用在视频追踪当中,在聚类,平滑等等各种涉及到数据以及非监督学习的场合当中均有重要应用,是一个应用广泛的算法。
图像是一个矩阵信息,如何在一个视频当中使用 meanshift 算法来追踪一个运动的物体呢? 大致流程如下:
- 首先在图像上选定一个目标区域
- 计算选定区域的直方图分布,一般是 HSV 色彩空间的直方图。
- 对下一帧图像 b 同样计算直方图分布。
- 计算图像 b 当中与选定区域直方图分布最为相似的区域,使用 meanshift 算法将选定区域沿着最为相似的部分进行移动,直到找到最相似的区域,便完成了在图像 b 中的目标追踪。
- 重复 3 到 4 的过程,就完成整个视频目标追踪。
通常情况下我们使用直方图反向投影得到的图像和第一帧目标对象的起始位置,当目标对象的移动会反映到直方图反向投影图中,meanshift 算法就把我们的窗口移动到反向投影图像中灰度密度最大的区域了。如下图所示:
直方图反向投影的流程是:
假设我们有一张 100x100 的输入图像,有一张 10x10 的模板图像,查找的过程是这样的:
- 从输入图像的左上角(0,0)开始,切割一块(0,0)至(10,10)的临时图像;
- 生成临时图像的直方图;
- 用临时图像的直方图和模板图像的直方图对比,对比结果记为 c;
- 直方图对比结果 c,就是结果图像(0,0)处的像素值;
- 切割输入图像从(0,1)至(10,11)的临时图像,对比直方图,并记录到结果图像;
- 重复 1 ~ 5 步直到输入图像的右下角,就形成了直方图的反向投影。
实现
在 OpenCV 中实现 Meanshift 的 API 是:
cv.meanShift(probImage, window, criteria)
probImage: ROI区域,即目标的直方图的反向投影
window: 初始搜索窗口,就是定义ROI的rect
criteria: 确定窗口搜索停止的准则,主要有迭代次数达到设置的最大值,窗口中心的漂移值大于某个设定的限值等。
实现 Meanshift 的主要流程是:
- 读取视频文件:cv.videoCapture()
- 感兴趣区域设置:获取第一帧图像,并设置目标区域,即感兴趣区域
- 计算直方图:计算感兴趣区域的 HSV 直方图,并进行归一化
- 目标追踪:设置窗口搜索停止条件,直方图反向投影,进行目标追踪,并在目标位置绘制矩形框。
示例:
import numpy as np
import cv2 as cv
# 1.获取图像
cap = cv.VideoCapture('./image/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 = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
# 3.2 去除低亮度的值
# mask = cv.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.)))
# 3.3 计算直方图
roi_hist = cv.calcHist([hsv_roi], [0], None, [180], [0, 180])
# 3.4 归一化
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)
# 4. 目标追踪
# 4.1 设置窗口搜索终止条件:最大迭代次数,窗口中心漂移最小值
term_crit = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1)
while (True):
# 4.2 获取每一帧图像
ret, frame = cap.read()
if ret == True:
# 4.3 计算直方图的反向投影
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
# 4.4 进行meanshift追踪
ret, track_window = cv.meanShift(dst, track_window, term_crit)
# 4.5 将追踪的位置绘制在视频上,并进行显示
x, y, w, h = track_window
img2 = cv.rectangle(frame, (x, y), (x + w, y + h), 255, 2)
cv.imshow('frame', img2)
if cv.waitKey(60) & 0xFF == ord('q'):
break
else:
break
# 5. 资源释放
cap.release()
cv.destroyAllWindows()
下面是三帧图像的跟踪结果:
10.2.2 Camshift
大家认真看下上面的结果,有一个问题,就是检测的窗口的大小是固定的,而狗狗由近及远是一个逐渐变小的过程,固定的窗口是不合适的。所以我们需要根据目标的大小和角度来对窗口的大小和角度进行修正。CamShift 可以帮我们解决这个问题。
CamShift 算法全称是“Continuously Adaptive Mean-Shift”(连续自适应 MeanShift 算法),是对 MeanShift 算法的改进算法,可随着跟踪目标的大小变化实时调整搜索窗口的大小,具有较好的跟踪效果。
Camshift 算法首先应用 meanshift,一旦 meanshift 收敛,它就会更新窗口的大小,还计算最佳拟合椭圆的方向,从而根据目标的位置和大小更新搜索窗口。如下图所示:
Camshift 在 OpenCV 中实现时,只需将上述的 meanshift 函数改为 Camshift 函数即可:
将 Camshift 中的:
# 4.4 进行meanshift追踪
ret, track_window = cv.meanShift(dst, track_window, term_crit)
# 4.5 将追踪的位置绘制在视频上,并进行显示
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)
10.2.3 算法总结
Meanshift 和 camshift 算法都各有优势,自然也有劣势:
- Meanshift 算法:简单,迭代次数少,但无法解决目标的遮挡问题并且不能适应运动目标的的形状和大小变化。
camshift 算法:可适应运动目标的大小形状的改变,具有较好的跟踪效果,但当背景色和目标颜色接近时,容易使目标的区域变大,最终有可能导致目标跟踪丢失。
- Tutorial:https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_trackbar/py_trackbar.html#trackbar
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!