处理文件、摄像头和GUI
读取/写入图像文件
图像可以从一种文件格式加载并保存为另一种格式。例如,把一 幅图像从PNG转换为JPEG
import cv2
image = cv2.imread('xxx.png')
cv2/imwrite('xxx.jpg', image)
默认情况下,imread返回BGR格式的图像,即使该文件使用的是灰度格式。BGR表示与红–绿–蓝(Red-Green-Blue,RGB)相同的颜色模型,只是字节顺序相反。 我们还可以指定imread的模式,所支持的选项包括:
- cv2.IMREAD_COLOR:该模式是默认选项,提供3通道的BGR图像,每个通道一个8位值(0~255)(dtype=uint8)。
- cv2.IMREAD_GRAYSCALE:该模式提供8位灰度图像。
- cv2.IMREAD_ANYCOLOR:该模式提供每个通道8位的BGR图像或者8位灰度图像,具体取决于文件中的元数据。
- cv2.IMREAD_UNCHANGED:该模式读取所有的图像数据,包括作为第4通道的α或透明度通道(如果有的话)。
- cv2.IMREAD_ANYDEPTH:该模式加载原始位深度的灰度图像。例如,如果文件以这种格式表示一幅图像,那么它提供每个通道16位的一幅灰度图像。
- cv2.IMREAD_ANYDEPTH|cv2.IMREAD_COLOR:该组合模式加载原始位深度的BGR彩色图像。
- cv2.IMREAD_REDUCED_GRAYSCALE_2:该模式加载的灰度 图像的分辨率是原始分辨率的1/2。例如,如果文件包括一幅640×480 的图像,那么它加载的是一幅320×240的图像。
- cv2.IMREAD_REDUCED_COLOR_2:该模式加载每个通道8位 的BGR彩色图像,分辨率是原始图像的1/2。
- cv2.IMREAD_REDUCED_GRAYSCALE_4:该模式加载灰度图像,分辨率是原始图像的1/4。
- cv2.IMREAD_REDUCED_COLOR_4:该模式加载每个通道8位的彩色图像,分辨率是原始图像的1/4。
- cv2.IMREAD_REDUCED_GRAYSCALE_8:该模式加载灰度图像,分辨率是原始图像的1/8。
- ·cv2.IMREAD_REDUCED_COLOR_8:该模式加载每个通道8位的彩色图像,分辨率为原始图像的1/8。
举个例子,我们将一个PNG文件加载为灰度图像(在此过程中会丢 失所有颜色信息),再将其保存为一个灰度PNG图像:
import cv2
grayImage = cv2.imread('xxx.png', cv2.IMAGE_GRAYSCALE)
cv2.imwrite('xxx.png', grayImage)
imwrite()函数要求图像为BGR格式或者灰度格式,每个通道具有输出格式可以支持的特定位数。例如,BMP文件格式要求每个通道8位,而PNG允许每个通道8位或16位。
在图像和原始字节之间进行转换
从概念上讲,一个字节就是0~255范围内的一个整数。目前,在实时图形应用程序中,像素通常由每个通道一个字节来表示,但是也可以使用其他表示方式。
OpenCV图像是numpy.array类型的二维或者三维数组。8位灰度图像是包含字节值的一个二维数组。24位的BGR图像是一个三维数组,也包含字节值。我们可以通过使用类似于image[0,0]或者image[0,0,0] 的表达式来访问这些值。第一个索引是像素的y坐标或者行,0表示顶部。第二个索引是像素的x坐标或者列,0表示最左边。第三个索引(如果有的话)表示一个颜色通道。可以用下面的笛卡儿坐标系可视化数组的三维空间。
例如,在左上角为白色像素的8位灰度图像中,image[0,0]是255。在左上角为蓝色像素的24位(每个通道8位)BGR图像中,image[0,0]是[255,0,0]。
假设图像的每个通道有8位,我们可以将其强制转换为标准的 Python bytearray对象(一维的):
byteArray = bytearray(image)
相反,假设bytearray以一种合适的顺序包含字节,我们对其进行 强制转换后再将其变维,可以得到一幅numpy.array类型的图像:
grayImage = numpy.array(grayByteArray).reshape(height, weight)
bgrImage = numpy.array(bgrByteArray).reshape(height, weight, 3)
举个更完整的例子,我们将包含随机字节的bytearray转换为灰度 图像和BGR图像:
import cv2
import numpy
import os
# make an array of 120,000 random bytes.
randoByteArray = bytearray(os.urandom(120000))
flatNumpyArray = numpy.array(randomByteArray)
# convert the array to make a 400x300 grascale image
grayImage = flatNumpyArray.reshape(300,400)
cv2.imwrite('xxx.png', grayImage)
# convert the array to make a 400x100 color image
bgrImage = flatNumpyArray.reshape(100,400,3)
cv2.imwrite('xxx.png', bgrImage)
此处,我们使用Python的标准os.urandom函数生成随机的原始字节序列,bytearray()组成字节数组,然后再将其转换成NumPy数组。。请注意,也可以使用像 numpy.random.randint(0,256,120000).reshape(300,400)这样的语句直接(而且更有效)生成随机NumPy数组。我们使用os.urandom的唯一原因是: 这有助于展示原始字节的转换。
注目:
import os
random_bytes = os.urandom(12000)
print(random_bytes)
# the result of output is like
# b'\x9e\xfa\x85\x88\xab\x1e\x1a\xdb\xe4\x0c\x8f...'
对于 numpy.random.randint(0, 256, 120000)
这个代码,生成的数组维度是 (120000,)
,即一个一维数组,包含 120,000 个元素。
在 NumPy 中,生成的随机数组的维度由传递给 numpy.random.randint()
函数的参数决定。第一个参数是随机数的最小值(包含),第二个参数是随机数的最大值(不包含),第三个参数是生成的随机数的数量。
在这个例子中,我们传递了参数 (0, 256, 120000)
,因此生成了一个包含 120,000 个随机整数的一维数组。每个随机整数的取值范围是 0 到 255(包含 0 和 255)。
请注意,如果我们在 numpy.random.randint()
中传递了其他维度参数,例如 (0, 256, (100, 200))
,则会生成一个具有 (100, 200)
维度的二维数组,其中包含随机整数。根据需要,您可以调整维度参数来生成不同形状的随机数组。
基于numpy.array访问图像数据
我们已经知道在OpenCV中加载图像最简单(也是最常见)的方法是使用imread函数。我们还知道这将返回一幅图像,它实际上是一个数组(是二维还是三维取决于传递给imread的参数)。
numpy.array类对数组操作进行极大的优化,它允许某些类型的批量操作,而这些操作在普通Python列表中是不可用的。这些类型的numpy.array都是OpenCV中特定于数组类型的操作,对于图像操作来说很方便。但是,我们还是从一个基本的例子开始,逐步探讨图像操作。假设你想操作BGR图像的(0,0)坐标处的像素,并将其转换成白色像素:
import cv2
img = cv2.imread('xx.png')
img[0,0] = [255, 255, 255]
如果将修改后的图像保存到文件后再查看该图像,你会在图像的左上角看到一个白点。当然,这种修改并不是很有用,但是它显示了某种修改的可能性。现在,我们利用numpy.array的功能在数组上执行变换的速度比普通的Python列表要快得多。
假设你想更改某一特定像素的蓝色值,例如(150,120)坐标处的像素。numpy.array类型提供了一个方便的方法item,它有三个参数:x(或者left)位置、y(或者top)位置以及数组中(x,y)位置的索引(请记住,在BGR图像中,某个特定位置处的数据是一个三元数组, 包含按照B、G和R顺序排列的值),并返回索引位置的值。另一个方法 itemset可以将某一特定像素的特定通道的值设置为指定的值。 itemset有两个参数:三元组(x、y和索引)以及新值。
在下面的例子中,我们将(150,120)处的蓝色通道值从其当前值 更改为255:
import cv2
img = cv2.imread('xx.png')
img.itemset((150,120,0), 255) # sets the value of a pi
对于修改数组中的单个元素,itemset方法比我们在本节第一个例子中看到的索引语法要快一些。
同样,修改数组的一个元素本身并没有太大意义,但是它确实打开了一个充满可能性的世界。然而,就性能而言,这只适合于感兴趣的小区域。当需要操作整个图像或者感兴趣的大区域时,建议使用 OpenCV的函数或者NumPy的数组切片。NumPy的数组切片允许指定索引的范围。我们来考虑使用数组切片来操作颜色通道的一个例子。将一 幅图像的所有G(绿色)值都设置为0非常简单,如下面的代码所示:
import cv2
im = cv2.imread('xx.png')
img[:,:,1] = 0
这段代码执行了一个相当重要的操作,而且很容易理解。相关的代码行是最后一行,它指示程序从所有行和列中获取所有像素,并把绿色值(在三元BGR数组的一个索引处)设置为0。如果显示此图像, 你会注意到绿色完全消失了。
通过使用NumPy的数组切片访问原始像素,我们可以做一些有趣的事情,其中之一是定义感兴趣区域(Region Of Interest,ROI)。一 旦定义了感兴趣区域,就可以执行一系列的操作了。例如,可以把这个区域绑定到一个变量,定义第二个区域,将第一个区域的值(感兴趣的区域的值my_roi)赋给第二个区域(从而将图像的一部分复制到图像的另一个位置):
import cv2
img = cv2.imread('xx.png')
my_roi = img[0:100, 0:100] #定义感兴趣区域
img[300:400, 300:400] = my_roi #实现将图像一部分复制到图像另一个位置
确保两个区域在大小上一致很重要。如果大小不一致,NumPy会(立刻)控诉这两个形状不匹配。
最后,我们可以访问numpy.array的属性,如下列代码所示:
import cv2
img = cv2.imread('xx.png')
print(img.shape)
print(img.size)
print(img.dtype)
这三个属性的定义如下:
- shape:描述数组形状的一个元组。对于图像,它(依次)包括高度、宽度、通道数(如果是彩色图像的话)。shape元组的长度是确定图像是灰度的还是彩色的一种有用方法。对于灰度图像,
len(shape)==2
,对于彩色图像,len(shape)==3
。 - size:数组中的元素数。对于灰度图像,这和像素数是一样的。 对于BGR图像,它是像素数的3倍,因为每个像素都由3个元素(B、G 和R)表示。
- ·dtype:数组元素的数据类型。对于每个通道8位的图像,数据类型是numpy.uint8。
总之,强烈建议在使用OpenCV时,了解NumPy的一般情况以及 numpy.array的特殊情况。这个类是Python中使用OpenCV进行所有图像处理的基础。
读取/写入视频文件
OpenCV提供了VideoCapture和VideoWriter类,支持各种视频文件格式。支持的格式取决于操作系统和OpenCV的构建配置,但是通常情况下,假设支持AVI格式是安全的。通过它的read方法,VideoCapture对象可以依次查询新的帧,直到到达视频文件的末尾。每一帧都是一幅BGR格式的图像。
相反,图像可以传递给VideoWriter类的write方法,该方法将图像添加到VideoWriter的文件中。我们来看一个例子,从一个AVI文件读取帧,再用YUV编码将其写入另一个文件:
import cv2
videoCapture = cv2.VideoCapture('xx.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)))
videoWirter = cv2.VideoWriter('xx.avi', cv2.VideoWriter_fourcc('I', '4', '2', '0'),fps, size)
success, frame = videoCapture.read()
while success: # Loop until there are no more frames.
videoWriter.wirte(frame)
success, frame = videoCapture.read()
VideoWriter类的构造函数的参数值得特别注意。必须指定一个视频文件的名称。具有此名称的所有之前存在的文件都将被覆盖。还必须指定一个视频编解码器。可用的编解码器因系统而异。支持的选项可能包括以下内容:
- ·0:这个选项表示未压缩的原始视频文件。文件扩展名应该是.avi。
- cv2.VideoWriter_fourcc(‘I’,’4’,’2’,’0’):这个选项表示未压缩的YUV编 码,4:2:0色度抽样。这种编码是广泛兼容的,但是会产生大的文件。文件扩展名应该是.avi。
- cv2.VideoWriter_fourcc(‘P’,’I’,’M’,’1’):这个选项是MPEG-1。文件扩 展名应该是.avi。
- cv2.VideoWriter_fourcc(‘X’,’V’,’I’,’D’):这个选项是一种相对较旧的 MPEG-4编码。如果想限制生成的视频大小,这是一个不错的选项。文件扩展名应该是.avi。
- cv2.VideoWriter_fourcc(‘M’,’P’,’4’,’V’):这个选项是另一种相对较旧 的MPEG-4编码。如果想限制生成的视频大小,这是一个不错的选项。 文件扩展名应该是.mp4。
- cv2.VideoWriter_fourcc(‘X’,’2’,’6’,’4’):这个选项是一种相对较新的 MPEG-4编码。如果想限制生成的视频大小,这可能是最佳的选项。文件扩展名应该是.mp4。
- ·cv2.VideoWriter_fourcc(‘T’,’H’,’E’,’O’):这个选项是Ogg Vorbis。文 件扩展名应该是.ogv。
- ·cv2.VideoWriter_fourcc(‘F’,’L’,’V’,’1’):这个选项表示Flash视频。文 件扩展名应该是.flv。
帧率和帧大小也必须指定。因为我们是从另一个视频复制的,所以这些属性可以从VideoCapture类的get方法读取。
捕捉摄像头帧
摄像头帧流也可以用VideoCapture对象来表示。但是,对于摄像头,我们通过传递摄像头设备索引(而不是视频文件名称)来构造 VideoCapture对象。我们来考虑下面这个例子,它从摄像头抓取10秒的视频,并将其写入AVI文件。代码与上节的示例(从视频文件获取的,而不是从摄像头中获取的)类似:
import cv2
videoCapture = cv2.VideoCapture(0)
fps = videoCapture.get(cv2.CAP_PROP_FPS)
size = (int(videoCapture.get(cv2.CAP_PROP_FRAME_WIDTH)), int(videoCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
videoWirter = cv2.VideoWriter('xx.avi', cv2.VideoWriter_fourcc('I', '4', '2', '0'),fps, size)
success, frame = videoCapture.read()
numFrameRemaining = 10 * fps - 1 # 10 seconds of frames
while success and numFrameRemaining > 0:
videoWriter.wirte(frame)
success, frame = videoCapture.read()
numFrameRemaining -= 1
Tip:对于某些系统上的一些摄像头, cameraCapture.get(cv2.CAP_PROP_FRAME_WIDTH)和 cameraCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)可能会返回不准确的结果。为了更加确定图像的实际大小,可以先抓取一帧,再用像 h,w=frame.shape[:2]这样的代码来获得图像的高度和宽度。有时,你可 能会遇到摄像头在开始产生大小稳定的好帧之前,产生一些大小不稳 定的坏帧的情况。如果你关心的是如何防范这种情况,在开始捕捉会话时你可能想要读取并忽略一些帧。
可是,在大多数情况下,VideoCapture的get方法不会返回摄像头帧率的准确值,通常会返回0。 点击跳转上的官方文档警告如下:
当查询VideoCapture实例使用的后端不支持的属性时,返回值为0。 注意: 读/写属性涉及许多层。沿着这条链可能会发生一些意想不到的结 果[sic]。 VideoCapture->API Backend->Operating System->DeviceDriver- >Device Hardware 返回值可能与设备实际使用的值不同,也可能使用设备相关规则 (例如,步长或者百分比)对其进行编码。有效的行为取决于[sic]设备驱动程序和API后端。
要为摄像头创建合适的VideoWriter类,我们必须对帧率做一个假设(就像前面代码中所做的那样),或者使用计时器测量帧率。后一种方法更好,我们将在本章后面对其进行介绍。
当然,摄像头数量及其顺序取决于系统。可是,OpenCV不提供任何查询摄像头数量或者摄像头属性的方法。如果用无效的索引构造VideoCapture类,VideoCapture类将不会产生任何帧,它的read方法将返回(False,None)。要避免试图从未正确打开的VideoCapture对象检索帧,你可能想先调用VideoCapture.isOpened方法,返回一个布尔值。
当我们需要同步一组摄像头或者多摄像头相机(如立体摄像机)时,read方法是不合适的。我们可以改用grab和retrieve方法。对于一组(两台)摄像机,可以使用类似于下面的代码:
success0 = cameraCapture0.grab()
success1 = cameraCapture1.grab()
if success0 and success1:
frame0 = cameraCapture0.retrieve()
frame1 = cameraCapture1.retrieve()
首先,cameraCapture0.grab()
和cameraCapture1.grab()
用于从两个摄像机中抓取帧。这些函数返回一个布尔值,表示帧是否成功抓取。如果返回True
,则表示成功抓取帧;如果返回False
,则表示抓取失败或到达视频的末尾。
cameraCapture.retrieve()
用于检索已经抓取的帧。这个函数返回一个元组,包含两个元素。第一个元素frame0
是从cameraCapture0
抓取的帧,第二个元素frame1
是从cameraCapture1
抓取的帧。
在窗口中显示图像
OpenCV中一个最基本的操作是在窗口中显示图像。这可以通过 imshow函数实现。如果你有任何其他GUI框架背景,那么可能认为调用 imshow来显示图像就足够了。可是,在OpenCV中,只有当调用另一个 函数waitKey时,才会绘制(或者重新绘制)窗口。后一个函数抽取窗 口事件队列(允许处理各种事件,比如绘图),并且它返回用户在指 定的超时时间内输入的任何键的键码。在某种程度上,这个基本设计简化了开发使用视频或网络摄像头输入的演示程序的任务,至少开发人员可以手动控制新帧的获取和显示。
下面是一个非常简单的示例脚本,用于从文件中读取图像,并对 其进行显示:
import cv2
import numpy as np
img = cv2.imread('xx.png')
cv2.imshow('xx', img)
cv2.waitKey()
cv2.destroyAllWindows()
imshow函数有两个参数:显示图像的窗口名称以及图像自己的名称。将在下一节中对waitKey进行更详细的介绍。
恰如其名,destroyAllWindows函数会注销由OpenCV创建的所有窗 口。
在窗口中显示摄像头帧
OpenCV允许使用namedWindow、imshow和destroyWindow函数来创建、重新绘制和注销指定的窗口。此外,任何窗口都可以通过waitKey 函数捕获键盘输入,通过setMouseCallback函数捕获鼠标输入。我们来看一个例子,展示从实时摄像头获取的帧:
import cv2
clicked = False
def onMouse(event, x, y, flags, param):
global clicked
if event == cv2.EVENT_LBUTTONUP:
clicked = True
cameraCapture = cv2.VideoCapture(0)
cv2.nameWindow('MyWindow')
cv2.setMouseCallback('MyWindow', onMouse)
print('Showing camera feed. Click window or press any key to stop')
success, frame = cameraCapture.read()
while success and cv2.waitKey(1) == -1 and not clicked:
cv2.imshow('MyWindow', frame)
success, frame = cameraCapture.read()
cv2.destroyWindow('MyWindow')
waitKey的参数是等待键盘输入的毫秒数,默认情况下为0,这是 一个特殊的值,表示无穷大。返回值可以是-1(表示未按下任何 键),也可以是ASCII键码(如27表示Esc)。有关ASCII键码的列表, 请参阅点击跳转。另外,请注意Python提供了一 个标准函数ord,可以将字符转换成ASCII键码。例如,ord(‘a’)返回 97。
同样,请注意,OpenCV的窗口函数和waitKey是相互依赖的。 OpenCV窗口只在调用waitKey时更新。相反,waitKey只在OpenCV窗口 有焦点时才捕捉输入。
传递给setMouseCallback的鼠标回调应有5个参数,如代码示例所 示。把回调的param参数设置为setMouseCallback的第3个可选参数, 默认情况下为0。回调的事件参数是以下操作之一:
- cv2.EVENT_MOUSEMOVE:这个事件指的是鼠标移动。
- cv2.EVENT_LBUTTONDOWN:这个事件指的是按下左键时, 左键向下。
- cv2.EVENT_RBUTTONDOWN:这个事件指的是按下右键时, 右键向下。
- cv2.EVENT_MBUTTONDOWN:这个事件指的是按下中间键时,中间键向下。
- ·cv2.EVENT_LBUTTONUP:这个事件指的是释放左键时,左键回到原位。
- cv2.EVENT_RBUTTONUP:这个事件指的是释放右键时,右键回到原位。
- cv2.EVENT_MBUTTONUP:这个事件指的是释放中间键时,中间键回到原位。
- cv2.EVENT_LBUTTONDBLCLK:这个事件指的是双击左键。
- cv2.EVENT_RBUTTONDBLCLK:这个事件指的是双击右键。
- cv2.EVENT_MBUTTONDBLCLK:这个事件指的是双击中间键。
鼠标回调的flag参数可能是以下事件的一些按位组合:
- cv2.EVENT_FLAG_LBUTTON:这个事件指的是按下左键。
- cv2.EVENT_FLAG_RBUTTON:这个事件指的是按下右键。
- cv2.EVENT_FLAG_MBUTTON:这个事件指的是按下中间键。
- cv2.EVENT_FLAG_CTRLKEY:这个事件指的是按下Ctrl键。
- cv2.EVENT_FLAG_SHIFTKEY:这个事件指的是按下Shift键。
- cv2.EVENT_FLAG_ALTKEY:这个事件指的是按下Alt键。
可是,OpenCV不提供任何手动处理窗口事件的方法。例如,单窗口关闭按钮不能停止应用程序。因为OpenCV的事件处理和GUI功能有 限,许多开发人员更喜欢将其与其他应用程序框架集成。在下一节中,我们将设计一个抽象层来帮助OpenCV与应用程序框架集成。
项目Cameo(人脸跟踪和图像处理)面向对象的设计
可以用纯过程式风格编写Python应用程序。通常,这是通过小型应用程序(例如前面讨论过的基本I/O脚本)实现的。但是,从现在开始,我们将经常使用面向对象的风格,因为面向对象促进了模块化和可扩展性。
从对OpenCV的I/O功能的概述中,我们知道不管源图像或者目标图像是什么,所有图像都是相似的。不管获取的图像流是什么,或者将其作为输出发送到哪里,我们都可以对这个流的每一帧应用相同的特定于应用程序的逻辑。在使用多个I/O流的应用程序(例如Cameo)中,I/O代码和应用程序代码的分离变得特别方便。
我们将创建的类命名为CaptureManager和WindowManager,作为 I/O流的高级接口。应用程序代码可以使用CaptureManager读取新帧,也可以将每一帧分派给一个或多个输出,包括静态图像文件、视频文件和窗口(通过WindowManager类)。WindowManager类允许应用程序 代码以面向对象风格处理窗口和事件。 CaptureManager和WindowManager都是可扩展的。我们可以实现不依赖OpenCV的I/O。
基于managers.CaptureManager提取视频流
正如我们所看到的,OpenCV可以获取、显示和记录来自视频文件或来自摄像头的图像流,但是在每种情况下都会有一些特殊考虑的事项。CaptureManager类提取了一些差异并提供了一个更高级的接口, 将图像从获取流分发到一个或多个输出——静态图像文件、视频文件,或者窗口。
CaptureManager对象是由VideoCapture对象初始化的,并拥有 enterFrame和exitFrame方法,通常应该在应用程序主循环的每次迭代 中调用这两个方法。在调用enterFrame和exitFrame之间,应用程序可 以(任意次)设置一个channel属性并获得一个frame属性。channel属 性初始为0,只有多摄像头相机使用其他值。frame属性是在调用 enterFrame时,对应于当前通道状态的一幅图像。
CaptureManager类还拥有可以在任何时候调用的writeImage、 startWriting Video和stopWritingVideo方法。实际的文件写入被推迟到exitFrame。同样,在执行exitFrame方法期间,可以在窗口中显示frame,这取决于应用程序代码将WindowManager类作为CaptureManager构造函数的参数提供,还是通过设置 previewWindowManager属性提供。
如果应用程序代码操作frame,那么将在记录文件和窗口中体现这些操作。CaptureManager类有一个构造函数参数和一个名为 shouldMirrorPreview的属性,如果想要在窗口中镜像(水平翻转) frame,但不记录在文件中,那么此属性应该为True。通常,在面对摄像头时,用户更喜欢镜像实时摄像头回传信号。