OpenCV为Open Source Computer Vision的缩写,是一个免费的计算机视觉库,可以通过处理图像和视频来完成各种任务,包括显示摄像头输入的信号以及使机器人识别现实生活中的物体。
1、两个库
Numpy:提供数值计算函数,包括高效的矩阵计算函数。
Scipy:科学计算库,方便的处理OpenCV的图像数据。
2、基本I/O脚本
大多数CV程序:输入和输出:图像。
交互式CV程序:输入源为摄像头,窗口显示输出。
其他输入和输出:图像文件、视频文件、原始字节等。
2.1 读/写图像文件
OpenCV的imread函数和imwrite函数支持各种静态图像文件格式。
不同系统支持的文件格式不一样,但都支持BMP格式,通常还支持PNG,JPEG和TIFF格式。
无论哪种格式,每个像素都会有一个值,但不同格式表示像素的方式有所不同。
例子如下:
我们通过二维Numpy数组创建一个黑色的正方形图像:
img = np.zeros((3, 3), dtype=np.uint8)
print(type(img))
print(img.dtype)
print(img)
输出为:
<class 'numpy.ndarray'>
uint8
[[0 0 0][0 0 0][0 0 0]]
每个像素由一个8位整数来表示,即每个像素值的范围为0~255。
现在利用cv2.cvtColor函数将该图像转换成Blue-green-red(BGR)格式:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
print(type(img))
print(img.dtype)
print(img)
输出:
<class 'numpy.ndarray'>
uint8
[[[0 0 0][0 0 0][0 0 0]][[0 0 0][0 0 0][0 0 0]][[0 0 0][0 0 0][0 0 0]]]
可见,每个像素由一个三元数组表示,每个整型向量分别表示一个B、G和R通道。
其他色彩空间(如HSV)也已同样的方式来表示像素,只是取值范围和通道数目不同(HSV色彩空间的色度值范围为0~180)
看图像的结构:shape属性。返回行和列,如果有一个以上的通道,还会返回通道数。
img = np.zeros((3, 3), dtype=np.uint8)
print(img.shape)
输出:
(3, 3)
如果将图像转化成BGR格式,shape会返回(3,3,3),表明每个像素存在三个通道。
可读取一种格式的图像文件,将其保存为另一种格式。
例子:
将PNG文件转换为JPEG格式。
image = cv2.imread('pic.png')
cv2.imwrite('pic.jpg', image)
默认情况下,即使图像文件为灰度格式,imread函数也会返回BGR格式的图像。
BGR与RGB所表示的色彩空间相同,但字节顺序相反。
imread函数的参数:
IMREAD_ANYCOLOR = 4
IMREAD_ANYDEPTH = 2
IMREAD_COLOR = 1
IMREAD_GRAYSCALE = 0
IMREAD_LOAD_GDAL = 8
IMREAD_UNCHANGED = -1
我们将加载的png文件作为灰度图像并保存。
image = cv2.imread('pic.png', cv2.IMREAD_GRAYSCALE)
# 或者下面这句话。 根据上面的参数,我们可以用0代替。
image = cv2.imread('pic.png', 0)
cv2.imwrite('PicGray.png', image)
imread函数会删除所有alpha通道的信息(透明度)。
imwrite函数要求图像为BGR或灰度格式,并且每个通道要有一定的位,输出格式要支持这些通道。
如:bmp格式要求每个通道有8位,png允许每个通道有8位或16位。
2.2 图像与原始字节之间的转换
一个字节能表示0~255的整数,一个像素由每个通道的一个字节表示。
一个OpenCV图像是一个.array类型的二维或三维数组。
8位的灰度图像是一个含有字节值二维数组,24位的BGR图像是一个三维数组,它也包含了字节值。
可使用表达式访问这些值,例如image[0, 0]或image[0, 0, 0],第一个值代表行,第二个值代表列,第三个值代表颜色通道。
若一幅图像的每个通道为8位,则可将其显式转换为标准的一维bytearray格式:
byteArray = bytearray(image)
反之,bytearray若含有恰当顺序的字节,可以通过显式转换和重构,得到numpy.array形式的图像。
grayImage = np.array(graybyteArray).reshape(height, width)
bgrImage = np.array(bgrByteArray).reshape(height, width, 3)
下面介绍一个详细的例子,将含有随机字节的bytearray转换为灰度图像和BGR图像:
import cv2
import numpy
import os# 创建包含120000个随机数字的数组,使用os.urandom()函数可随机生成原始字节
randomByteArray = bytearray(os.urandom(120000))
flatNumpyArray = numpy.array(randomByteArray)
print(flatNumpyArray.sum())# 将数组转化成300 * 400的灰度图像
grayImage = flatNumpyArray.reshape(300, 400)
cv2.imwrite('RandimGray.png', grayImage)# 将数组转化成100 * 400 * 3 的bgr图像
bgrImage = flatNumpyArray.reshape(100, 400, 3)
cv2.imwrite('RandomColer.png', bgrImage)
2.3 使用numpy.array访问图像数据
如果对生成图像有个较好的理解,接下来就可以执行基本的图像操作了。
众所周知,加载OpenCV图像最简单的方式是使用imread函数,该函数返回一个数组(二维或三维)。
从图像处理开始一步步学习:
1、将BGR图像在(0, 0)处的像素转化为白色
# coding=utf-8
import cv2img = cv2.imread('pic.jpg')
img[0, 0] = [255, 255, 255]
cv2.imwrite('pic1.png', img)
2、将坐标(150, 120)的当前蓝色值(127)变成255
# coding=utf-8
import cv2img = cv2.imread('pic.jpg')
print(img.item(150, 120, 0))
# 97
img.itemset((150, 120, 0), 255)
print(img.item(150, 120, 0))
# 255
注意:使用numpy.array的原因有两个,一是numpy.array处理这类问题是经过很好优化的,二是通过numpy优雅的方法能得到可读性更强的代码。
建议使用内置的滤波器和方法来处理整个图像,上述方法只适合处理特定的小区域。
3、操作通道:将指定通道(B、G或R)的所有值置为零
# coding=utf-8
import cv2# 下面代码将图像所有的G(绿色)值置为0
img = cv2.imread('pic.jpg')
img[:, :, 1] = 0
显示此图像,发现此图像没有绿色。
4、将一个图像的一部分拷贝到另一个图像上面(数组大小一样)
# coding=utf-8
import cv2img = cv2.imread('pic.jpg')
print(img.shape)
img2 = cv2.imread('pc.jpg')
print(img2.shape)
my_roi = img[150:350, 300:700]
img2[150:350, 300:700] = my_roi
cv2.imwrite('pc1.jpg', img2)
2.4 视频文件的读/写
OpenCV提供了VideoCapture类和VideoWriter类来支持各种格式的视频文件。
支持文件的格式类型会因系统的不同而变化,但都应该支持AVI格式。
在到达视频文件末尾之前,VideoCapture类可通过read函数来获取新的帧,每帧是一幅基于BGR格式的图像。
可将一幅图像传递给VideoWriter类的write函数,该函数会将这幅图像加到VideoWriter类所指向的文件中。
YUV是一种颜色空间,基于YUV的颜色编码是流媒体的常用编码方式。Y表示流明,U、V表示色度、浓度,这种表达方式起初是为了彩色电视与黑白电视之间的信号兼容。 对于图像每一点,Y确定其亮度,UV确认其彩度。
例子:读取AVI文件的帧,采用YUV颜色编码将其写入另一个帧中。
# coding=utf-8
import cv2videoCapture = cv2.VideoCapture('av.avi')
fps = videoCapture.get(cv2.CAP_PROP_FPS)
size = (int(videoCapture.get(cv2.CAP_PROP_FRAME_WIDTH)),int(videoCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
videoWriter = cv2.VideoWriter('av1.avi', cv2.VideoWriter_fourcc('I', '4', '2', '0'),fps, size)
success, frame = videoCapture.read()
while success:videoWriter.write(frame)success, frame = videoCapture.read()
2.5 捕获摄像头的帧
VideoCapture类可以获得摄像头的帧流,但对摄像头而言,通常不是用视频的文件名来构造VideoCapture类,而是需要传递摄像头的设备索引。
例子:捕获摄像头10秒的视频信息,写入一个AVI文件中。
# coding=utf-8
import cv2# 摄像头读取并写入文件
cameraCapture = cv2.VideoCapture(0) # 获取摄像头对象
fps = 30
size = (int(cameraCapture.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cameraCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
print(size)
videoWriter = cv2.VideoWriter('OutputVid.flv', cv2.VideoWriter_fourcc('F', 'L', 'V', '1'), fps, size) # 生成书写器对象
# 成功与否flag, 下一帧图像
success, frame = cameraCapture.read() # 摄像头对象读取数据
# 10s * 30fps
numFrameRamaining = 10 * fps - 1
print(success)
while success and numFrameRamaining > 0:videoWriter.write(frame) # 书写器对象记录帧success, frame = cameraCapture.read() # 摄像头对象numFrameRamaining -= 1 # 利用帧数计时:10s * 30fps
cameraCapture.release()
然而,VideoCapture类的get方法不能返回摄像头帧速率的准确值,它总是返回0.
为了针对摄像头创建合适的VideoWriter类,要么对帧速率作出假设,要么使用计时器来测量。
摄像头的数量和顺序由系统决定,但OpenCV没有提供任何查询摄像头数量和属性的方法。
如果使用无效索引构造了VideoCapture类,就不会得到帧,VideoCapture的read函数会返回(false, None)。
为了不让read函数从没有正确打开的VideoCapture类中获取数据,可在执行该函数后使用VideoCapture.isOpened方法做一个判断,该方法返回一个Boolean值。
当需要同步一组摄像头和一个多头摄像头时,read方法就不合适了,可用grab和retrive方法代替它。
对于一组摄像头:
success0 = cameraCapture0.grab()
success1 = cameraCapture1.grab()
if success0 and success1:frame0 = cameraCapture0.retrive()frame1 = cameraCapture1.retrive()
2.6 在窗口显示图像
imshow函数可以实现显示图像的操作。
有时图像会随即消失,我们采用下面代码保证显示视频时窗口上的帧可以一直更新。
# coding=utf-8
import cv2img = cv2.imread('pc.jpg')
cv2.imshow('pc.jpg', img)
cv2.waitKey()
cv2.destroyAllWindows()
imshow函数的两个参数:显示图像的帧名称,显示的图像本身。
destroyAllWindows函数可以释放由OpenCV创建的所有窗口。
2.7 在窗口显示摄像头帧
OpenCV的namedWindow、imshow和DestroyWindow函数允许指定窗口名来创建、显示和销毁窗口。
任意窗口下都可以通过waitKey函数来获取键盘输入,通过setMouseCallback函数来获取鼠标输入。
实时显示摄像头帧:
# coding=utf-8
import cv2clicked = False
def onMouse(event, x, y, flages, param):global clicked'''cv2_EVENT_LBUTTONUP 4 左键释放 cv2_EVENT_RBUTTONUP 5 右键释放 cv2_EVENT_MBUTTONUP 6 中间释放 '''if event == cv2.EVENT_LBUTTONUP:clicked = True#VideoCapture()中参数是0,表示打开笔记本的内置摄像头,参数是视频文件路径则打开视频
cameraCapture = cv2.VideoCapture(0)
cv2.namedWindow('MyWindow')
# 设置点击事件
cv2.setMouseCallback('MyWindow', onMouse)print('Showing camera feed. Click window or press any key to stop')
# 摄像头对象读取数据
success, frame = cameraCapture.read()
# waitKey的参数为等待键盘触发的时间,单位为毫秒,其返回值是-1(表示没有键盘按下)或ASCII码
#python提供了一个标准函数ord,该函数将字符转换为ASCII码
while success and cv2.waitKey(1) == -1 and not clicked:cv2.imshow('MyWindow', frame)success, frame = cameraCapture.read()cv2.destroyWindow('MyWindow')
cameraCapture.release()
OpenCV不提供任何处理窗口事件的方法。
当单击窗口的关闭按钮时,并不能关闭应用程序。
由于OpenCV有限的事件处理能力和GUI处理能力,许多开发人员更喜欢将OpenCV集成到其他应用程序框架中。
3 Cameo项目
OpenCV通常可以通过速成资料(cookbook)来学习。
下面首先创建一个抽象层,然后通过一个应用来构建学习OpenCV的框架,但每一步都会设计该应用的一个组件,以使应用具有扩展性和重用性。
为了开发人脸跟踪和图像处理的交互式应用,需要实时获取摄像头的输入。
该类应用覆盖了OpenCV大部分的功能,并且要求这些功能具有高效性、有效性。
具体而言,该应用将实时进行脸部合并,即对给定的两个摄像头的而输入流,应用要对两个输入流的人脸进行叠加。
为了使混合的场景看起来具有统一的感觉,可采用滤波器和扭曲技术。
用户会有一种融入现场表演的感觉,产生进入另一个场景并扮演人物角色的感觉。
在这样的应用中,如果有帧率低、不精确跟踪等缺陷,用户会立即感受到。
为了达到最好的效果,可以尝试采用传统成像和深度成像的方法。
这个应用程序名叫Cameo, Cameo像电影中的一个小配角。
4 Cameo-面向对象设计
通过前面介绍的OpenCV I/O功能可以知道,所有图像其实都是相似的,尽管图像的来源或去向不同。
无论是从哪里获得的图像流,或者要将其输出到哪里,都可以将相同逻辑应用到图像流的每个帧中。
在应用中,将I/O代码与应用程序代码分离会变得非常方便。例如Cameo会使用多个I/O流。
可创建CaptureManager类和WindowManager类作为高级的I/O流接口。
在应用程序中可以使用CaptureManager来读取新的帧,并能将帧分派到一个或多个输出中,这些输出包括静止的图像文件】视频文件以及窗口。
WindowManager类使应用程序代码能以面向对象的形式处理窗口和事件。
CaptureManager和WindowManager都具有可扩展性,因此,实现时可以不依赖OpenCV的I/O。
4.1 使用managers.CpatureManager提取视频流
无论该图像来自视频文件还是摄像头,OpenCV都可以获取、显示和记录图像流。
CaptureManager类对一些差异进行了抽象,并提供了更高级的接口从获取流中分配图像,再将图像分到一个或多个输出中。
在VideoCapture类中初始化CaptureManager类,在应用程序主循环的每一次迭代中
未完待续......