基于Joseph Howse&Joe Minichino著、刘冰&高博译《OpenCV 4计算机视觉 Python语言实现》撰写的学习记录。


基本I/O

大多数计算机视觉应用程序需要获取图像作为输入。大多数计算机视觉应用程序还会生成图像作为输出。交互式计算机视觉应用程序可能需要把摄像头作为输入源,还需要将窗口作为输出目标。但是其他可能的源和目标包括图像文件、视频文件以及原始字节。例如,如果把过程式图形合成到应用程序中,那么原始字节可能通过网络连接进行传输,也可能由算法生成。

读取/写入图像文件

快速参考:

# 读取图像
img = cv2.imread('MyPic.png')
img = cv2.imread('MyPic.png', cv2.IMREAD_COLOR)
    # 常用读取模式
    # cv2.IMREAD_COLOR 默认,提供3通道的BGR图像,每个通道一个8位值
    # cv2.IMREAD_GRAYSCALE 提供8位灰度图像
    # cv2.IMREAD_ANYCOLOR 提供每个通道8位的BGR图像或者8位灰度图像,具体取决于文件中的元数据
    # etc.

# 写入图像
cv2.imwrite('MyPic.png', img)

# 创建3*3全黑灰度图像
img = numpy.zeros((3, 3), dtype=numpy.uint8)

# 创建3*3全黑BGR图像
img = numpy.zeros((3, 3, 3), dtype=numpy.uint8)

# 转换图像格式
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

在OpenCV和NumPy中,一幅图像就是一个多维数组,有列像素和行像素,每个像素都有一个值。对于不同类型的图像数据,像素值可以使用不同的格式。例如,通过简单地创建一个二维NumPy数组,可以从头开始创建一幅3×3的黑色正方形图像:

image-20230731233829879

image-20230731233841237

每个像素都用一个8位整数表示,这意味着每个像素的值都在0~255的范围内,其中0表示黑色,255表示白色,中间的值表示灰色。这是一幅灰度图像。

使用cvtColor()函数把这幅图像转换成蓝–绿–红(Blue-Green-Red,BGR)格式:

image-20230731235225333

image-20230731235229947

BGR图像的每个像素都用一个三元数组表示,每个整数分别表示三个颜色通道(B、G和R)中的一个。HSV之类的其他常见颜色模型的表示方法也类似,只是取值范围不同。例如,HSV颜色模型的色调值的范围是0~180。

通过查看shape属性可以查看图像的结构,shape属性返回行、列和通道数(如果有多个通道的话)。

image-20230731235449946

上述代码将打印(5,3),表示我们有一幅5行3列的灰度图像。如果将该图像转换成BGR格式,shape将是(5,3,3),表示每个像素有3个通道。

图像可以从一种文件格式加载并保存为另一种格式,imwrite()的第一个参数为保存的文件名,其后缀指定了保存的格式。

举个例子,我们将一个PNG文件加载为灰度图像(在此过程中会丢失所有颜色信息),再将其保存为一个灰度PNG图像:

image-20230801000514967

imwrite()函数要求图像为BGR格式或者灰度格式,每个通道具有输出格式可以支持的特定位数。例如,BMP文件格式要求每个通道8位,而PNG允许每个通道8位或16位。

在图像和原始字节之间进行转换

快速参考:

# 读取灰度图像的像素值
print(img[0, 0])  # 255

# 读取BGR图像的像素值
print(img[0, 0])  # [255, 0, 0]

# 生成随机像素灰度图像
numpy.random.randint(0, 256, 120000).reshape(300, 400)

# 生成随机像素BGR图像
numpy.random.randint(0, 256, 120000).reshape(100, 400, 3)

从概念上讲,一个字节就是0~255范围内的一个整数。目前,在实时图形应用程序中,像素通常由每个通道一个字节来表示,但是也可以使用其他表示方式。

OpenCV图像是numpy.array类型的二维或者三维数组。8位灰度图像是包含字节值的一个二维数组。24位的BGR图像是一个三维数组,也包含字节值。我们可以通过使用类似于image[0,0]或者image[0,0,0]的表达式来访问这些值。第一个索引是像素的y坐标或者行,0表示顶部。第二个索引是像素的x坐标或者列,0表示最左边。第三个索引(如果有的话)表示一个颜色通道。

假设图像的每个通道有8位,我们可以将其强制转换为标准的Python bytearray对象(一维的):

image-20230801001157631

相反,假设bytearray以一种合适的顺序包含字节,我们对其进行强制转换后再将其变维,可以得到一幅numpy.array类型的图像:

image-20230801001239708

举个更完整的例子,我们将包含随机字节的bytearray转换为灰度图像和BGR图像:

image-20230801001255866

运行这个脚本之后,在脚本目录中应该有一对随机生成的图像。

此处,我们使用Python的标准os.urandom函数生成随机的原始字节,然后再将其转换成NumPy数组。

请注意,也可以使用像numpy.random.randint(0,256,120000).reshape(300,400)这样的语句直接(而且更有效)生成随机NumPy数组。我们使用os.urandom的唯一原因是:这有助于展示原始字节的转换。

基于numpy.array访问图像数据

快速参考:

# 修改BGR图像的像素值
img[0, 0] = [255, 255, 255]       # 将0,0坐标处的像素修改三个通道的值,变为白色

# 修改BGR图像的像素值(使用numpy.itemset函数运行效率更高)
img.itemset((150, 120, 0), 255)   # 将150,120坐标处、通道0(B)的值修改为255

# 将图像的所有G值设置为0
img[:, :, 1] = 0

numpy.array类对数组操作进行极大的优化,它允许某些类型的批量操作,而这些操作在普通Python列表中是不可用的。这些类型的numpy.array都是OpenCV中特定于数组类型的操作,对于图像操作来说很方便。

下面的例子中,我们将(150,120)处的蓝色通道值从其当前值更改为255:

image-20230801002603584

当需要操作整个图像或者感兴趣的大区域时,建议使用OpenCV的函数或者NumPy的数组切片。NumPy的数组切片允许指定索引的范围。

使用数组切片来操作颜色通道的一个例子,将一幅图像的所有G(绿色)值都设置为0:

image-20230801002739967

它指示程序从所有行和列中获取所有像素,并把绿色值(在三元BGR数组的一个索引处)设置为0。如果显示此图像,你会注意到绿色完全消失了。

通过使用NumPy的数组切片访问原始像素,我们可以做一些有趣的事情,其中之一是定义感兴趣区域(Region Of Interest,ROI)。一旦定义了感兴趣区域,就可以执行一系列的操作了。例如,可以把这个区域绑定到一个变量,定义第二个区域,将第一个区域的值赋给第二个区域(从而将图像的一部分复制到图像的另一个位置):

image-20230801003040787

需要确保确保两个区域在大小上一致。

我们可以访问numpy.array的属性,如下列代码所示:

image-20230801003228746

  • shape:描述数组形状的一个元组。对于图像,它(依次)包括高度、宽度、通道数(如果是彩色图像的话)。shape元组的长度是确定图像是灰度的还是彩色的一种有用方法。对于灰度图像,len(shape)==2,对于彩色图像,len(shape)==3
  • size:数组中的元素数。对于灰度图像,这和像素数是一样的。对于BGR图像,它是像素数的3倍,因为每个像素都由3个元素(B、G和R)表示。
  • dtype:数组元素的数据类型。对于每个通道8位的图像,数据类型是numpy.uint8

读取/写入视频文件

快速参考:

# 视频读取类
video_capture = cv2.VideoCapture('input.avi')

# 获取视频FPS
fps = video_capture.get(cv2.CAP_PROP_FPS)

# 获取视频尺寸
size = ((int)(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)), 
        (int)(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT)))

# 读取帧
success, frame = video_capture.read() # 返回读取成功标志和帧

# 视频写入类
video_writer = cv2.VideoWriter('output.avi', 
                               cv2.VideoWriter_fourcc('I', '4', '2', '0'), 
                               fps, 
                               size)
    # 常用编解码器
    # 0 表示未压缩的原始视频文件。文件扩展名应该是.avi
    # cv2.VideoWriter_fourcc('I','4','2','0') 表示未压缩的YUV编码,4:2:0色度抽样。这种编码是广泛兼容的,但是会产生大的文件。文件扩展名应该是.avi
    # cv2.VideoWriter_fourcc('X','V','I','D') 这个选项是一种相对较旧的MPEG-4编码。如果想限制生成的视频大小,这是一个不错的选项。文件扩展名应该是.avi
    # cv2.VideoWriter_fourcc('X','2','6','4') 这个选项是一种相对较新的MPEG-4编码。如果想限制生成的视频大小,这可能是最佳的选项。文件扩展名应该是.mp4
    # cv2.VideoWriter_fourcc('F','L','V','1') 这个选项表示Flash视频。文件扩展名应该是.flv

# 写入帧
video_writer.write(frame)

OpenCV提供了VideoCapture和VideoWriter类,支持各种视频文件格式。支持的格式取决于操作系统和OpenCV的构建配置,但是通常情况下,假设支持AVI格式是安全的。通过它的read方法,VideoCapture对象可以依次查询新的帧,直到到达视频文件的末尾。每一帧都是一幅BGR格式的图像。

相反,图像可以传递给VideoWriter类的write方法,该方法将图像添加到VideoWriter的文件中。我们来看一个例子,从一个AVI文件读取帧,再用YUV编码将其写入另一个文件:

image-20230801003532032

VideoWriter类的构造函数的参数值得特别注意,必须指定一个视频编解码器。可用的编解码器因系统而异。帧率和帧大小也必须指定。因为我们是从另一个视频复制的,所以这些属性可以从VideoCapture类的get方法读取。

捕捉摄像头帧

快速参考:

# 视频读取类配置为摄像头(其它参数同上节)
camera_capture = cv2.VideoCapture(0)  # 摄像头索引

摄像头帧流也可以用VideoCapture对象来表示。但是,对于摄像头,我们通过传递摄像头设备索引(而不是视频文件名称)来构造VideoCapture对象。

考虑下面这个例子,它从摄像头抓取10秒的视频,并将其写入AVI文件。代码更改的内容标记为粗体:

image-20230801005248970

对于某些系统上的一些摄像头,cameraCapture.get(cv2.CAP_PROP_FRAME_WIDTH)cameraCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)可能会返回不准确的结果。为了更加确定图像的实际大小,可以先抓取一帧,再用像h,w=frame.shape[:2]这样的代码来获得图像的高度和宽度。有时,你可能会遇到摄像头在开始产生大小稳定的好帧之前,产生一些大小不稳定的坏帧的情况。如果你关心的是如何防范这种情况,在开始捕捉会话时你可能想要读取并忽略一些帧。

在大多数情况下,VideoCapture的get方法不会返回摄像头帧率的准确值,通常会返回0。要为摄像头创建合适的VideoWriter类,我们必须对帧率做一个假设(就像前面代码中所做的那样),或者使用计时器测量帧率。

摄像头数量及其顺序取决于系统。可是,OpenCV不提供任何查询摄像头数量或者摄像头属性的方法。如果用无效的索引构造VideoCapture类,VideoCapture类将不会产生任何帧,它的read方法将返回(False,None)。要避免试图从未正确打开的VideoCapture对象检索帧,你可能想先调用VideoCapture.isOpened方法,返回一个布尔值。

当我们需要同步一组摄像头或者多摄像头相机(如立体摄像机)时,read方法是不合适的。我们可以改用grab和retrieve方法。对于一组(两台)摄像机,可以使用类似于下面的代码:

image-20230801010429892

在窗口中显示图像

快速参考:

# 显示图像
cv2.imshow('Title', img)        # 窗口名称及图像
cv2.waitKey()                   # 等待按键,参数为超时毫秒数,默认为0,表示无穷大,返回值为按键的ASCII码

# 注销所有窗口
cv2.destroyAllWindows()

OpenCV中一个最基本的操作是在窗口中显示图像。这可以通过imshow函数实现。如果你有任何其他GUI框架背景,那么可能认为调用imshow来显示图像就足够了。可是,在OpenCV中,只有当调用另一个函数waitKey时,才会绘制(或者重新绘制)窗口。后一个函数抽取窗口事件队列(允许处理各种事件,比如绘图),并且它返回用户在指定的超时时间内输入的任何键的键码。在某种程度上,这个基本设计简化了开发使用视频或网络摄像头输入的演示程序的任务,至少开发人员可以手动控制新帧的获取和显示。

下面是一个非常简单的示例脚本,用于从文件中读取图像,并对其进行显示:

image-20230801010756307

在窗口中显示摄像头帧

快速参考:

# 创建窗口(此函数不是必须调用的)
cv2.namedWindow('Title')

# 事件回调函数示例
clicked = False
def omMouse(event, x, y, flags, param):
    global clicked
    if event == cv2.EVENT_LBUTTONUP:  # 其它参数见下文
        clicked = True

# 设置鼠标事件回调函数
cv2.setMouseCallback('Title', onMouse)    # 窗口名,函数

# 释放视频(摄像头)占用
camera_capture.release()

OpenCV允许使用namedWindowimshowdestroyWindow函数来创建、重新绘制和注销指定的窗口。此外,任何窗口都可以通过waitKey函数捕获键盘输入,通过setMouseCallback函数捕获鼠标输入。我们来看一个例子,展示从实时摄像头获取的帧:

image-20230801011047690

waitKey的参数是等待键盘输入的毫秒数,默认情况下为0,这是一个特殊的值,表示无穷大。返回值可以是-1(表示未按下任何键),也可以是ASCII键码(如27表示Esc)。另外,请注意Python提供了一个标准函数ord,可以将字符转换成ASCII键码。例如,ord('a')返回97。

同样,请注意,OpenCV的窗口函数和waitKey是相互依赖的。OpenCV窗口只在调用waitKey时更新。相反,waitKey只在OpenCV窗口有焦点时才捕捉输入。

传递给setMouseCallback的鼠标回调应有5个参数,如代码示例所示。把回调的param参数设置为setMouseCallback的第3个可选参数,默认情况下为0。回调的事件参数是以下操作之一:

image-20230801012808136

image-20230801012823384

鼠标回调的flag参数可能是以下事件的一些按位组合:

image-20230801012847268

OpenCV不提供任何手动处理窗口事件的方法。例如,单击窗口关闭按钮不能停止应用程序。因为OpenCV的事件处理和GUI功能有限,许多开发人员更喜欢将其与其他应用程序框架集成。

项目:人脸追踪和图像处理

待更新。