目录
一、 OpenCV的Python接口
二、 OpenCV基础知识
2.1 读取和写入图像
2.2 颜色空间
2.3 显示图像及结果
三、处理视频
3.1 视频输入
3.2 视频读取到Numpy数组中
四、 跟踪
4.1 光流
4.2 Lucas-Kanade算法
引言
这里主要学习如何通过Python接口使用流行的计算机视觉库OpenCV。OpenCV是一个C++库,用于实时处理计算机视觉问题。
OpenCV是一个C++库,包含了计算机视觉领域的很多模块。除了C++和C,Python作为一种简洁的脚本语言,C++代码基础上的python接口得到了越来越广泛的支持。目前,OpenCV的python接口仍在发展,不过并不是所有的OpenCV组件都提供了相应的Python接口。
OpenCV自带读取、写入图像函数以及矩阵操作和数学库。
打印出图像大小,对图像进行转换并保存为.png格式:
import cv2
# 读取图像
im = cv2.imread('D:\\picture\\test5.jpg')
h,w = im.shape[:2]
print(h,w)
# 保存图像
cv2.imwrite('D:\\picture\\test5.png', im)
函数imread()返回图像为一个标准的Numpy数组,并且该函数能够处理很多不同格式的图像。函数imwrite()会根据文件后缀自动转换图像。结果如下:
在OpenCV中,图像不是按传统的RGB颜色通道,而是按BGR顺序(即RGB的倒序)存储的。读取图像时默认的是BGR,但是还有一些可用的转换函数。颜色空间的转换可以用函数cvColor()来实现。可以通过下面的方式将原图像转换成灰度图像:
# 读取图像
im = cv2.imread('D:\\picture\\test5.jpg')
# 创建灰度图像
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
在读取原图像之后,紧接其后的是OpenCV颜色转换代码:
cv2.COLOR_BGR2GRAY
cv2.COLOR_BGR2RGB
cv2.COLOR_GRAY2BGR
上面的转换代码中,转换的图像颜色通道数与对应的转换代码相匹配,对于灰度图像只有一个通道,对于RGB和BGR图像则有三个通道。cv2.COLOR_GRAY2BGR是将灰度图像转换成BGR彩色图像。
从文件中读取图像,并创建一个整数图像表示:
import cv2
# 读取图像
im = cv2.imread('D:\\picture\\test5.jpg')
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
# 计算积分图像
intim = cv2.integral(gray)
# 归一化并保存
intim = (255.0 * intim) / intim.max()
cv2.imwrite('D:\\picture\\result.jpg',intim)
在读取图像后,将其转换为灰度图像,函数integral()创建一幅图像,这个图像的每个像素值是原图上方和左边强度值相加的结果。
下面两张图分别为原图以及代码运行后输出的图:
这里是从一个种子像素进行泛洪填充:
import cv2
from numpy import *
# 读取图像
filename = 'D:\\picture\\test5.jpg'
im = cv2.imread(filename)
h,w = im.shape[:2] # 三通道
# 泛洪填充
diff = (6,6,6)
mask = zeros((h+2,w+2), uint8)
cv2.floodFill(im,mask,(10,10),(255,255,0),diff,diff)
# opencv窗口显示结果
cv2.imshow('flood fill', im)
cv2.waitKey()
在这个例子里对图像应用泛洪填充并在OpenCV窗口中显示结果。waitkey()函数则一直处于暂停状态,直到有按键按下窗口才会自动关闭。floodfill()函数获取图像、一个掩膜、一个种子像素以及新的颜色值来替代下限和上限阈值差的泛洪像素。泛洪填充以种子像素为起始只要能在阈值的差异范围内添加新的像素泛洪填充就会持续扩展,不同阈值差异又元组(R,G,B)给出。
单纯用Python处理视频有些困难因为要考虑诸多因素的影响。目前并没有针对python的视频库,使用OpenCV的python接口是唯一的选择。
OpenCV能够很好的支持从摄像头读取视频,下面给出一个捕获视频帧并在OpenCV窗口显示这些视频帧的代码:
import cv2
# 设置视频捕获
cap = cv2.VideoCapture(0)
while True:
ret, im = cap.read()
cv2.imshow('video.test', im)
key = cv2.waitKey(10)
if key == 27:
break
if key == ord(' '):
cv2.imwrite('vid_result.jpg', im)
捕获对象VideoCapture从摄像头或头文件捕或视频,通过一个整数进行初始化,这个整数九三视频设备的id;当仅有一个摄像头与计算机连接时,那么摄像头的id为0.read()方法解码并返回到下一个视频帧。在运行后,按下esc,退出应用,按下空格键,保存视频帧。
下面给出通过摄像头采集数据,并进行模糊处理的例子:
import cv2
# 设置视频捕获
cap = cv2.VideoCapture(0)
while True:
ret, im = cap.read()
blur = cv2.GaussianBlur(im, (0, 0), 5)
cv2.imshow('camera blur', blur)
if cv2.waitKey(10) == 27:
break
这里的每一帧视频都会传递给GaussianBlur()函数,对其进行模糊处理。
OpenCV可以从一个文件中读取视频帧并转换为Numpy数组。下面为代码:
import cv2
from numpy import *
# 设置视频捕获
cap = cv2.VideoCapture(0)
frames = []
# 获取帧,存储到数组中
while True:
ret, im = cap.read()
cv2.imshow('video', im)
frames.append(im)
if cv2.waitKey(10) == 27:
break
frames = array(frames)
# 检查尺寸
print(im.shape)
print(frames.shape)
将每一个视频帧数组添加到列表中,知道捕获结束最终会得到有帧数、帧高、帧宽的以及颜色通道的数组。
这里就记录了148帧。
跟踪是指图像序列或视频对其中目标进行跟踪的过程。
光流是目标、场景或摄像机在连续两帧图像间运动时造成的目标的运动。是图像在平移过程中的二维矢量场。光流法主要依赖三个假设
1、亮度恒定:图像中目标的像素强度在连续帧之间不会发生变化。
2、时间规律:相邻帧之间的时间足够短,以至于在考虑运行变化时可以忽略它们之间的差异.该假设用于导出下面的核心方程。
3、空间一致性:相邻像素具有相似的运动。
不过在很多情况下这些假设并不成立,对于相邻帧间的小运动以及短时间跳跃,这就是一个很好的模型了。假设一个目标像素在t时刻亮度为,在时刻运动后与t时刻具有相同的亮度,即,对该约束用泰勒公式进行一阶展开求关于t的偏导,得到光流方程:
v=[u,v]是运动矢量,是时间偏导。在Lucas-Kanade算法中将使用这些假设。
运行脚本:
import cv2
from numpy import *
def draw_flow(im, flow, step=16):
""" 在间隔分开的像素采样点处绘制光流 """
h,w = im.shape[:2]
y,x = mgrid[step/2:h:step, step/2:w:step].reshape(2, -1)
fx, fy = flow[y, x].T
# 创建线的终点
lines = vstack([x, y, x + fx, y + fy]).T.reshape(-1, 2, 2)
lines = int32(lines)
# 创建图像并绘制
vis = cv2.cvtColor(im, cv2.COLOR_GRAY2BGR)
for (x1, y1), (x2, y2) in lines:
cv2.line(vis, (x1, y1), (x2, y2), (0, 255, 0), 1)
cv2.circle(vis, (x1, y1), 1, (0, 255, 0), -1)
return vis
# 设置视频捕获
cap = cv2.VideoCapture(0)
ret, im = cap.read()
prev_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
while True:
# 获取灰度图像
ret, im = cap.read()
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
# 计算流
flow = cv2.calcOpticalFlowFarneback(prev_gray, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0)
prev_gray = gray
# 画出流失量
cv2.imshow('Optical flow', draw_flow(gray, flow))
if cv2.waitKey(10) == 27:
break
这里摄像头捕获图像,并对每个连续图像进行光流估计,由calcOpticalFlowFarneback()返回的运动光流矢量保存在双通道图像变量flow中。除了需要获取前一帧和当前帧,该函数还需要一系列参数。draw_flow()会在图像均匀间隔的点处绘制光流矢量。并用变量step()控制流样本的间距。
这里若用书上的代码运行时会出现:
arrays used as indices must be of integer (or boolean) type的报错
这是由于代码中的x和y是numpy.array,而这两个是float64型的array。因此想要成功运行需要将
y,x = mgrid[step/2:h:step, step/2:w:step].reshape(2, -1)
改成:
y,x = mgrid[step/2:h:step, step/2:w:step].reshape(2, -1).astype('int64')
此时运行结果,这里每每个15个像素点采样一次:
跟踪最基本的形式是跟随兴趣点,如角点。Lucas-Kanada跟踪算法利用了稀疏光流算法。这个算法可以应用于任何一种特征,但通常使用一些角点进行跟踪,如Harris角点。
goodFeaturesToTrack()函数采用了Shi和Tomasi设计的算法检测角点。
如果基于每一个像素考虑,该光流方程组是欠定方程,每个方程中含有很多未知变量,利用相邻像素有相同运动这一假设,可以将方程写成如下系统方程:
系统方程的优势是,方程的数目多于未知变量,并且可以用最小二乘法解出该系统方程,对于周围像素的贡献可以进行加权处理,使越远的像素影响越小;高斯权重是一种常见的选择。上式可以变换为:
这个超定方程组可以使用最小二乘法求解,得到的运动矢量为:
v只有在可逆时才是可解的。标准的Lucas-Kanada跟踪适用于小位移;为处理较大位移,要采用分层的方法,该情形下光流可以通过对图像由粗到精采样计算得到。
创建一个lktrack.py的文件:
import cv2
from numpy import *
# 一些常见及默认的参数
lk_params = dict(winSize=(15, 15), maxLevel=2,criteria=(cv2.TERM_CRITERIA_EPS|cv2.TERM_CRITERIA_COUNT,10,0.03))
subpix_params = dict(zeroZone=(-1, -1),winSize=(10, 10), criteria=(cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, 20, 0.03))
feature_params = dict(maxCorners=500,qualityLevel=0.01,minDistance=10)
class LKTracker(object):
""" 用金字塔光流Lucas-Kanade跟踪类 """
def __init__(self, imnames):
""" 使用图像名称列表初始化"""
self.imnames = imnames
self.features = []
self.tracks = []
self.current_frame = 0
def detect_points(self):
""" 利用子像素精确度在当前帧中检测”利于跟踪的好的特征“ """
# 载入图像并创建灰度图像
self.image = cv2.imread(self.imnames[self.current_frame])
self.gray = cv2.cvtColor(self.image,cv2.COLOR_BGR2GRAY)
# 搜素好的特征点
features = cv2.goodFeaturesToTrack(self.gray,**feature_params)
# 提炼角点位置
cv2.cornerSubPix(self.gray,features,**subpix_params)
self.features = features
self.tracks = [[p] for p in features.reshape((-1,2))]
self.prev_gray = self.gray
def track_points(self):
""" 跟踪检测到的特征 """
if self.features != []:
self.step() # 移动到下一帧
# 载入图像并创建灰度图像
self.image = cv2.imread(self.imnames[self.current_frame])
self.gray = cv2.cvtColor(self.image,cv2.COLOR_BGR2GRAY)
# reshape()操作以适应输入格式
tmp = float32(self.features).reshape(-1,1,2)
# 计算光流
features,status,track_error = cv2.calcOpticalFlowPyrLK(self.prev_gray,self.gray,tmp,None,**lk_params)
# 去除丢失的点
self.features = [p for (st, p) in zip(status,features) if st]
# 从丢失的点清楚跟踪轨迹
features = array(features).reshape((-1,2))
for i,f in enumerate(features):
self.tracks[i].append(f)
ndx = [i for (i,st) in enumerate(status) if not st]
ndx.reverse() # 从后面移除
for i in ndx:
self.tracks.pop(i)
self.prev_gray = self.gray
def step(self, framenbr=None):
""" 移动到下一帧。如果没有给定参数,直接移到下一帧 """
if framenbr is None:
self.current_frame = (self.current_frame + 1) % len(self.imnames)
else:
self.current_frame = framenbr % len(self.imnames)
def draw(self):
""" 用OpenCV的画图函数画出当前图像及跟踪点,按任意键关闭 """
# 绿色圆圈画出跟踪点
for point in self.features:
cv2.circle(self.image,(int(point[0][0]), int(point[0][1])), 3, (0, 255, 0), -1)
cv2.imshow('LKtrack', self.image)
cv2.waitKey()
用文件名列表对跟踪对象进行初始化,变量features和tracks分别保存角点和对这些角点进行跟踪的位置,同时利用一个变量对当前帧进行跟踪,定义三个字典变量用于特征提取、跟踪和亚像素特征点的提炼。对载入图像进行灰度变换,这个利用goodFeaturesToTrack()方法完成。
cornerSubPix()提炼角点位置,并保存在成员变量features和tracks中。该函数会清除跟踪历史。利用calc0pticalFlowPyrLK()找出这些点运动到何处,最后清除这些包含跟踪点的列表。可以用上述track_points()方法完成。
辅助函数step()用于移动到下一视频帧,最后draw()方法绘出图像。
实现一个完整的独立的跟踪系统:
使用跟踪器
import lktrack
imnames = ['D:\\picture\\bt\\bt.003.pgm', 'D:\\picture\\bt\\bt.002.pgm', 'D:\\picture\\bt\\bt.001.pgm',
'D:\\picture\\bt\\bt.000.pgm']
# 创建跟踪对象
lkt = lktrack.LKTracker(imnames)
# 在第一帧进行检测,跟踪剩下的帧
lkt.detect_points()
lkt.draw()
for i in range(len(imnames)-1):
lkt.track_points()
lkt.draw()
使用发生器
import lktrack
from pylab import *
imnames = ['D:\\picture\\viff\\viff.000.ppm', 'D:\\picture\\viff\\viff.001.ppm', 'D:\\picture\\viff\\viff.002.ppm',
'D:\\picture\\viff\\viff.003.ppm', 'D:\\picture\\viff\\viff.004.ppm']
# LKTracker发生器进行跟踪
lkt = lktrack.LKTracker(imnames)
for im, ft in lkt.track():
print('tracking %d features' % len(ft))
# 画出轨迹
figure()
imshow(im)
for p in ft:
plot(p[0],p[1],'bo')
for t in lkt.tracks:
plot([p[0] for p in t], [p[1] for p in t])
axis('off')
show()
这里使用了track()方法,将其添加到LKTracker类中。
def track(self):
""" 发射器,用于遍历整个序列 """
for i in range(len(self.imnames)):
if self.features == []:
self.detect_points()
else:
self.track_points()
# 创建一份RGB副本
f = array(self.features).reshape(-1, 2)
im = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
yield im,f
发生器使前面定义的跟踪类的使用变得非常容易,并完全向用户隐藏了OpenCV里的函数,结果如下: