1.2 图像的基本操作
1.2.1 数字图像的表示
我们在电子设备上看到的图像都可以称为数字图像,例如图1-2所示的Lena图像。
图1-2 Lena的数字图像
对计算机来说,这幅图像只是一些亮度不同的点。一幅尺寸为M×N的图像可以用M×N的矩阵(即M×N个点)表示,如图1-3所示。每个矩阵元素代表一个像素,元素的值表示这个位置图像的亮度,一般来说,值越大该点就越亮。放大图1-3(a)中白色方框区域可得到图1-3(b)所示效果,对应的像素的值为图1-3(c)中的值。通常,灰度图像用2维矩阵M×N表示,彩色(多通道)图像用3维矩阵M×N×3表示。对于图像显示来说,一般用无符号8位整数来表示像素亮度,取值范围为[0, 255]。
(a) (b) (c)
图1-3 数字图像的表示
图像数据按照自左向右、自上向下的顺序存储在计算机内存中,即以图像的左上角为原点(也有自左向右、自下向上的顺序,即以图像的左下角为原点)。图1-4表示的是单通道灰度图像数据在计算机中的存储顺序,Iij代表第i行第j列的像素值。图1-5表示的是3通道BGR彩色图像数据在计算机中的存储顺序,每个像素用3个值表示,即。需要说明一下,OpenCV中RGB彩色图像的通道顺序为BGR。
图1-4 单通道灰度图像数据在计算机中的存储顺序
图1-5 3通道BGR彩色图像数据在计算机中的存储顺序
1.2.2 图像文件的读写与显示
OpenCV提供了函数cv.imread()、cv.imshow()和cv.imwrite()来处理图像文件的读取、显示和写入。
1. 图像文件的读取
使用cv.imread()函数将图像文件读入内存:
retval = cv.imread(filename[, flags])
其中的主要参数介绍如下。
● filename:要读取的图像文件的文件名。
● flags:控制如何读入图像文件的标志。flags的取值和含义如表1-1所示。
● retval:读入的图像数据。
表1-1 参数flags的取值和含义
flags的默认值为cv.IMREAD_COLOR,即将读入的图像转换为3通道BGR图像数据。假如图像文件为单通道的灰度图像,读入后会被强制转换为3通道。cv.IMREAD_GRAYSCALE则返回单通道图像数据,假如图像文件为多通道图像,读入后会被强制转换为单通道图像。
cv.imread()支持多种格式图像文件的读取,OpenCV支持读取的图像文件格式如表1-2所示。
表1-2 OpenCV支持读取的图像文件格式
注意:想要OpenCV支持某种图像文件格式,需要有对应的文件格式库。只有在编译OpenCV时添加了相应的库,安装后OpenCV才能支持此格式。
2. 图像文件的显示
成功读取图像文件后,可以使用OpenCV提供的GUI(Graphical User Interface,图形用户界面)用cv.imshow()将图像在窗口中显示出来,如图1-6所示。
图1-6 OpenCV图像在窗口显示
cv.imshow(winname, mat)
其中的主要参数介绍如下。
● winname:图像显示窗口的名称。
● mat:要显示的图像数据。
前面提到对于图像显示来说,一般用无符号8位整数,取值范围为[0, 255]。根据mat的数据类型,cv.imshow()显示图像时会进行以下操作。
● 如果mat是8位无符号整数,则直接显示。
● 如果mat是16位无符号整数,则像素值域会做[0, 255*256]到[0, 255]的映射。
● 如果mat是32位或64位浮点数,则像素值域会做[0, 1]到[0, 255]的映射。
● 如果mat是32位整数,则需要用户根据应用上下文预先进行将像素值域映射到[0, 255]的处理。
通过函数cv.imshow()生成的窗口会根据显示的图像自动调整大小,用户不能手动改变窗口大小。如果想改变窗口大小,可以使用OpenCV提供的另一个函数cv.namedWindow()来生成窗口。
cv.namedWindow(winname[, flags])
其中的主要参数介绍如下。
● winname:窗口名称。
● flags:窗口的属性。flags值对应的窗口属性如表1-3所示。
表1-3 flags值对应的窗口属性
flags的默认值为 cv.WINDOW_AUTOSIZE|cv.WINDOW_KEEPRATIO|cv.WINDOW_GUI_EXPANDED。
调用函数cv.imshow()后还需要紧接着调用函数cv.waitKey()来执行GUI的housekeeping任务,这样才能实际显示图像和响应鼠标、键盘事件,否则不会显示图像且窗口可能被锁住。函数cv.waitKey()的功能是等待键盘按键按下。
retval = cv.waitKey([delay])
其中的主要参数介绍如下。
● delay:等待键盘事件的时间,单位为ms;如果值小于或等于0,则窗口会一直等待键盘按键按下。默认值为0。
● retval:如果指定的时间内没有按键按下,则返回-1,否则返回被按下按键的ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)。
函数cv.destroyWindow(winname)和cv.destroyAllWindows()用于销毁生成的窗口。
3. 图像文件的写入
将图像数据写入文件,可使用cv.imwrite()函数:
retval = cv.imwrite(filename, img[, params])
其中的主要参数介绍如下。
● filename:文件名。
● img:待写入的图像数据。
● params:指定文件格式。OpenCV可保存的文件格式如表1-4所示。
表1-4 OpenCV可保存的文件格式
存储的图像格式根据filename中的扩展名来决定,同时并不是所有的img都可以存为图像文件,目前只支持8位单通道和3通道(颜色顺序为BGR)矩阵。如果img为16位无符号整数类型,则需要存储为PNG、JPEG 2000或TIFF格式;若为32位浮点数类型,则需要存储为PFM、TIFF、OpenEXR或Radiance HDR格式。如果某格式的图像矩阵不支持保存为图像文件,可以先用cv.convertTo()函数或者cv.cvtColor()函数将矩阵转为可以保存的格式,再保存。另外需要注意的是,在保存文件时如果文件名已经存在,cv.imwrite()函数不会进行提醒,将直接覆盖以前的文件。
下面的例子展示了如何读入一幅彩色图像,读入的同时将原始图像转换为灰度图像,在窗口显示灰度图像,并将灰度图像保存到文件中。
import cv2 as cv def main(): # 读入图像, 同时转换为灰度图像 im_grey = cv.imread("lena.jpg", cv.IMREAD_GRAYSCALE) # 将灰度图像写入文件 cv.imwrite("lena_grey.jpg", im_grey) # 显示灰度图像 cv.imshow("Lena", im_grey) cv.waitKey() # 销毁窗口 cv.destroyAllWindows() if __name__ == '__main__': main()
将lena.jpg放在与例子相同的目录下,运行该例子的代码后,lena_grey.jpg将会出现在此目录。读入的原始图像如图1-2所示,转为灰度图像的显示窗口如图1-7所示。
图1-7 灰度图像显示窗口
1.2.3 视频文件的读写与显示
在介绍OpenCV如何读写与显示视频文件之前,先介绍一下编解码器(codec)。如果是图像文件,我们可以根据文件扩展名得知图像的格式,但是此经验并不能推广到视频文件中,因为视频文件的格式主要由压缩算法决定。压缩算法称为编码器(coder),解压算法称为解码器(decoder),编解码算法统称为编解码器(codec)。视频文件能否读写,关键看是否有相应的编解码器。编解码器的种类非常多,常用的有MJPG、XVID、DIVX等。视频文件的扩展名(如avi等)往往只能表示这是一个视频文件,我们并不能由其得知实际的编解码器。
OpenCV提供了两个类来处理视频文件的读写。读视频文件的类是VideoCapture,写视频文件的类是VideoWriter。
VideoCapture类既可以从视频文件读取图像,也可以从摄像头读取图像,可以使用该类的构造函数打开视频文件或者摄像头。如果VideoCapture类对象已经创建,可以使用cv.VideoCapture.open()函数打开,该函数会自动调用cv.VideoCapture.release()函数,先释放已经打开的视频文件,再打开新视频文件。如果要读取一帧图像,可以使用cv.Video Capture.read()函数。
打开摄像头:
cv.VideoCapture(index[, apiPreference])
其中的主要参数介绍如下。
● index:视频捕获设备的ID,0表示用默认后端打开默认摄像头。
● apiPreference:在有多个视频捕获后端时指定一个后端,如cv.CAP_DSHOW、cv.CAP_MSMF、cv.CAP_V4L等。默认值为cv.CAP_ANY。
打开视频文件:
cv.VideoCapture(filename[, apiPreference])
其中的主要参数介绍如下。
● filename:视频文件,它可以是以下类别。
○ 视频文件名,如video.avi。
○ 图像序列,如img_%02.jpg,会逐一读取图像文件img_00.jpg、img_01.jpg、img_ 02.jpg……
○ 视频流的URL(Uniform Resource Locator,统一资源定位符)。
○ gst-launch格式的GStreamer pipeline字符串。
● apiPreference:在有多个视频捕获后端时指定一个后端,如cv::CAP_FFMPEG、cv::CAP_IMAGES、cv::CAP_DSHOW。默认值为cv.CAP_ANY。
下面的例子演示了使用VideoCapture类读视频文件。
import sys import cv2 as cv def main(): # 打开第一个摄像头 #cap = cv.VideoCapture(0) # 打开视频文件 cap = cv.VideoCapture("slow_traffic_small.mp4") # 检查是否打开成功 if cap.isOpened() == False: print('Error opening the video source. ') sys.exit() while True: # 读取1帧视频,存放到im ret, im = cap.read() if not ret: print('No image read. ') break # 显示视频帧 cv.imshow('Live', im) # 等待30ms,如果有按键按下则退出循环 if cv.waitKey(30) >= 0: break # 销毁窗口 cv.destroyAllWindows() # 释放cap cap.release() if __name__ == '__main__': main()
图1-8为读取1帧视频后窗口显示的效果。
图1-8 读取1帧视频后窗口显示的效果
OpenCV提供了VideoWriter类来创建视频文件(写视频),在Linux系统中使用FFMPEG来写视频文件,Windows系统中使用FFMPEG、MSWF或者DSHOW,macOS系统中使用AVFoundation。与读视频文件不同的是,写视频文件需要在创建视频时设置一系列参数,包括文件名、编解码器、视频帧率、视频帧宽度和高度等。
首先创建VideoWriter类对象:
writer=cv.VideoWriter(filename, fourcc, fps, framesize[, iscolor])
其中的主要参数介绍如下。
● filename:创建的视频文件名。
● fourcc:使用4个字符表示的编解码器,可以是cv.VideoWriter_fourcc ('M', 'J', 'P','G')、cv.VideoWriter_fourcc('X','V',' I','D')、cv.VideoWriter_fourcc ('D',' I','V','X')等。编解码器列表可以在MSDN[1](微软的一个期刊产品)查询。如果使用某种编解码器无法创建视频文件,请尝试其他的编解码器。
[1] 网址为https://docs.microsoft.com/en-us/windows/win32/medfound/video-fourccs
● fps:视频帧率。
● framesize:视频帧宽度和高度。
● iscolor:如果值非0,编码器将按彩色帧进行编码;否则按灰度帧进行编码。
● writer:创建的VideoWriter对象。
然后使用函数cv.VideoWriter.writer()将视频帧写入文件:
cv.VideoWriter.write(image)
其中,image 表示待写入的视频帧数据,通常是BGR格式的彩色图像。需要注意,image的尺寸必须与前面的framesize一致。
下面的例子演示了如何写视频文件。本示例将生成一个视频文件,视频的第0帧是一个白色的“0”,第1帧是个白色的“1”,以此类推,共100帧。生成视频文件的播放效果如图1-9所示。
import sys import numpy as np import cv2 as cv def main(): # 设置视频帧的宽度和高度 frame_size = (320, 240) # 设置视频帧率 fps = 25 # 视频编解码格式 fourcc = cv.VideoWriter_fourcc('M', 'J', 'P', 'G') # 创建writer writer = cv.VideoWriter("myvideo.avi", fourcc, fps, frame_size) # 检查是否创建成功 if writer.isOpened() == False: print("Error creating video writer.") sys.exit() for i in range(0, 100): # 设置视频帧画面 im = np.zeros((frame_size[1], frame_size[0], 3), dtype=np.uint8) # 将数字绘制到画面上 cv.putText(im, str(i), (int(frame_size[0]/3), int(frame_size[1]*2/3)), cv.FONT_HERSHEY_SIMPLEX, 3.0, (255, 255, 255), 3) # 保存视频帧到文件myvideo.avi writer.write(im) # 释放writer writer.release() if __name__ == '__main__': main()
图1-9 生成视频文件的播放效果