大多数计算机视觉(Computer Vision,CV)应用程序需要获取图像作为输入。大多数计算机视觉应用程序还会生成图像作为输出。交互式计算机视觉应用程序可能需要把摄像头作为输入源,还需要将窗口作为输出目标。但是其他可能的源和目标包括图像文件、视频文件以及原始字节。例如,如果把过程式图形合成到应用程序中,那么原始字节可能通过网络连接进行传输,也可能由算法生成。我们来看看每一种可能性。
OpenCV提供了imread函数来从文件加载图像,也提供了imwrite函数来将图像写入文件。这些函数支持静态图像(非视频)的各种文件格式。支持的格式各不相同——在OpenCV的自定义构建中可以添加或删除某些格式——但是,通常BMP、PNG、JPEG和TIFF都是所支持的格式。
我们来研究一下在OpenCV和NumPy中图像表示的解剖结构。一幅图像就是一个多维数组,有列像素和行像素,每个像素都有一个值。对于不同类型的图像数据,像素值可以使用不同的格式。例如,通过简单地创建一个二维NumPy数组,可以从头开始创建一幅3×3的黑色正方形图像:
如果将这幅图像打印到控制台,获得的结果如下所示:
这里,每个像素都用一个8位整数表示,这意味着每个像素的值都在0~255的范围内,其中0表示黑色,255表示白色,中间的值表示灰色。这是一幅灰度图像。
现在,我们使用cv2.cvtColor函数把这幅图像转换成蓝–绿–红(Blue-Green-Red,BGR)格式:
我们来看图像是如何变化的:
如你所见,现在每个像素都用一个三元数组表示,每个整数分别表示三个颜色通道(B、G和R)中的一个。HSV之类的其他常见颜色模型的表示方法也类似,只是取值范围不同。例如,HSV颜色模型的色调值的范围是0~180。
有关颜色模型的更多内容,请参阅第3章,尤其是3.2节。
通过查看shape属性,你可以查看图像的结构,shape属性返回行、列和通道数(如果有多个通道的话)。
考虑如下示例:
上述代码将打印(5,3),表示我们有一幅5行3列的灰度图像。如果将该图像转换成BGR格式,shape将是(5,3,3),表示每个像素有3个通道。
图像可以从一种文件格式加载并保存为另一种格式。例如,把一幅图像从PNG转换为JPEG:
OpenCV的Python模块命名为cv2,尽管我们使用的是OpenCV 4.x而非OpenCV 2.x。以前,OpenCV有两个Python模块:cv2和cv。cv封装了用C实现的OpenCV的一个旧版本。目前,OpenCV只有cv2 Python模块,该模块封装了用C++实现的OpenCV当前版本。
默认情况下,imread返回BGR格式的图像,即使该文件使用的是灰度格式。BGR表示与红–绿–蓝(Red-Green-Blue,RGB)相同的颜色模型,只是字节顺序相反。
我们还可以指定imread的模式,所支持的选项包括:
·cv2.IMREAD_COLOR:该模式是默认选项,提供3通道的BGR图像,每个通道一个8位值(0~255)。
·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图像:
除非是绝对路径,否则图像的路径都是相对于工作目录(Python脚本的运行路径)的,因此在前面的例子中,MyPic.png必须在工作目录中,否则将找不到该图像。如果你希望避免对工作目录的假设,可以使用绝对路径,比如Windows上的C:\Users\Joe\Pictures\MyPic.png、Mac上的/Users/Joe/Pictures/MyPic.png,或者Linux上的/home/joe/pictures/MyPic.png。
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表示最左边。第三个索引(如果有的话)表示一个颜色通道。可以用下面的笛卡儿坐标系可视化数组的三维空间(见图2-1)。
图2-1 基于笛卡儿坐标系的数组三维空间
例如,在左上角为白色像素的8位灰度图像中,image[0,0]是255。在左上角为蓝色像素的24位(每个通道8位)BGR图像中,image[0,0]是[255,0,0]。
假设图像的每个通道有8位,我们可以将其强制转换为标准的Python bytearray对象(一维的):
相反,假设bytearray以一种合适的顺序包含字节,我们对其进行强制转换后再将其变维,可以得到一幅numpy.array类型的图像:
举个更完整的例子,我们将包含随机字节的bytearray转换为灰度图像和BGR图像:
此处,我们使用Python的标准os.urandom函数生成随机的原始字节,然后再将其转换成NumPy数组。请注意,也可以使用像numpy.random.randint(0,256,120000).reshape(300,400)这样的语句直接(而且更有效)生成随机NumPy数组。我们使用os.urandom的唯一原因是:这有助于展示原始字节的转换。
运行这个脚本之后,在脚本目录中应该有一对随机生成的图像:RandomGray.png和RandomColor.png。
图2-2是RandomGray.png的一个例子(你得到的结果很可能会有所不同,因为这是随机生成的)。
类似地,图2-3是RandomColor.png的一个例子。
既然我们已经对数据如何形成图像有了一个更好的理解,那么就可以开始对其执行基本操作了。
图2-2 随机生成的RandomGray.png图像
图2-3 随机生成的RandomColor.png图像
我们已经知道在OpenCV中加载图像最简单(也是最常见)的方法是使用imread函数。我们还知道这将返回一幅图像,它实际上是一个数组(是二维还是三维取决于传递给imread的参数)。
numpy.array类对数组操作进行极大的优化,它允许某些类型的批量操作,而这些操作在普通Python列表中是不可用的。这些类型的numpy.array都是OpenCV中特定于数组类型的操作,对于图像操作来说很方便。但是,我们还是从一个基本的例子开始,逐步探讨图像操作。假设你想操作BGR图像的(0,0)坐标处的像素,并将其转换成白色像素:
如果将修改后的图像保存到文件后再查看该图像,你会在图像的左上角看到一个白点。当然,这种修改并不是很有用,但是它显示了某种修改的可能性。现在,我们利用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:
对于修改数组中的单个元素,itemset方法比我们在本节第一个例子中看到的索引语法要快一些。
同样,修改数组的一个元素本身并没有太大意义,但是它确实打开了一个充满可能性的世界。然而,就性能而言,这只适合于感兴趣的小区域。当需要操作整个图像或者感兴趣的大区域时,建议使用OpenCV的函数或者NumPy的数组切片。NumPy的数组切片允许指定索引的范围。我们来考虑使用数组切片来操作颜色通道的一个例子。将一幅图像的所有G(绿色)值都设置为0非常简单,如下面的代码所示:
这段代码执行了一个相当重要的操作,而且很容易理解。相关的代码行是最后一行,它指示程序从所有行和列中获取所有像素,并把绿色值(在三元BGR数组的一个索引处)设置为0。如果显示此图像,你会注意到绿色完全消失了。
通过使用NumPy的数组切片访问原始像素,我们可以做一些有趣的事情,其中之一是定义感兴趣区域(Region Of Interest,ROI)。一旦定义了感兴趣区域,就可以执行一系列的操作了。例如,可以把这个区域绑定到一个变量,定义第二个区域,将第一个区域的值赋给第二个区域(从而将图像的一部分复制到图像的另一个位置):
确保两个区域在大小上一致很重要。如果大小不一致,NumPy会(立刻)控诉这两个形状不匹配。
最后,我们可以访问numpy.array的属性,如下列代码所示:
这三个属性的定义如下:
·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编码将其写入另一个文件:
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文件。代码与2.2.4节的示例(从视频文件获取的,而不是从摄像头中获取的)类似,更改的内容标记为粗体:
对于某些系统上的一些摄像头,cameraCapture.get(cv2.CAP_PROP_FRAME_WIDTH)和cameraCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)可能会返回不准确的结果。为了更加确定图像的实际大小,可以先抓取一帧,再用像h,w=frame.shape[:2]这样的代码来获得图像的高度和宽度。有时,你可能会遇到摄像头在开始产生大小稳定的好帧之前,产生一些大小不稳定的坏帧的情况。如果你关心的是如何防范这种情况,在开始捕捉会话时你可能想要读取并忽略一些帧。
可是,在大多数情况下,VideoCapture的get方法不会返回摄像头帧率的准确值,通常会返回0。http://docs.opencv.org/modules/highgui/doc/reading_and_writing_images_and_video.html上的官方文档警告如下:
当查询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方法。对于一组(两台)摄像机,可以使用类似于下面的代码:
OpenCV中一个最基本的操作是在窗口中显示图像。这可以通过imshow函数实现。如果你有任何其他GUI框架背景,那么可能认为调用imshow来显示图像就足够了。可是,在OpenCV中,只有当调用另一个函数waitKey时,才会绘制(或者重新绘制)窗口。后一个函数抽取窗口事件队列(允许处理各种事件,比如绘图),并且它返回用户在指定的超时时间内输入的任何键的键码。在某种程度上,这个基本设计简化了开发使用视频或网络摄像头输入的演示程序的任务,至少开发人员可以手动控制新帧的获取和显示。
下面是一个非常简单的示例脚本,用于从文件中读取图像,并对其进行显示:
imshow函数有两个参数:显示图像的窗口名称以及图像自己的名称。我们将在2.2.7节中对waitKey进行更详细的介绍。
恰如其名,destroyAllWindows函数会注销由OpenCV创建的所有窗口。
OpenCV允许使用namedWindow、imshow和destroyWindow函数来创建、重新绘制和注销指定的窗口。此外,任何窗口都可以通过waitKey函数捕获键盘输入,通过setMouseCallback函数捕获鼠标输入。我们来看一个例子,展示从实时摄像头获取的帧:
waitKey的参数是等待键盘输入的毫秒数,默认情况下为0,这是一个特殊的值,表示无穷大。返回值可以是-1(表示未按下任何键),也可以是ASCII键码(如27表示Esc)。有关ASCII键码的列表,请参阅http://www.asciitable.com/。另外,请注意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功能有限,许多开发人员更喜欢将其与其他应用程序框架集成。在本章的2.4节中,我们将设计一个抽象层来帮助OpenCV与应用程序框架集成。