Python-OpenCV3-计算机视觉秘籍-全-

Python OpenCV3 计算机视觉秘籍(全)

原文:OpenCV 3 Computer Vision with Python Cookbook

协议:CC BY-NC-SA 4.0

零、前言

计算机视觉是一个包含许多不同领域的广泛主题。 如果要开始在项目中使用计算机视觉算法,则入口点可能不明确。 即使您是经验丰富的计算机视觉工程师,毫无疑问,您还是需要深入探索或熟悉某些技术。 在这两种情况下,实用的方法都效果最佳。 仅通过将方法应用于实际问题,调整现有方法以满足您的要求以及使用示例,您才能完全理解任何计算机视觉算法的可能性和局限性。 这本书是专门为解决实际的计算机视觉任务而设计的。 本书中的秘籍使用 OpenCV(最流行,功能丰富且使用广泛的开源计算机视觉库)。 本书从最简单的示例发展到最复杂的示例,因此您将能够找到一些有用且易于理解的信息。

这本书是给谁的

本书适用于具有 Python 基本知识的开发人员。 如果您了解 OpenCV 的基础知识,并准备好构建比竞争对手更智能,更快,更复杂和更实用的计算机视觉系统,那么本书非常适合您。

本书涵盖的内容

第 1 章,“I/O 和 GUI”讲解了图像和视频的基本操作:加载,保存和显示。

第 2 章,“矩阵,颜色和过滤器”涵盖了使用矩阵进行操作的操作:访问图像,通道和像素的区域。 还介绍了各种颜色空间之间的转换以及过滤器的用法。

第 3 章,“等高线和分割”显示了如何创建图像遮罩,查找轮廓和分割图像。

第 4 章,“对象检测和机器学习”描述了检测和跟踪不同类型对象的方法,从特殊构造的(QR 码和 ArUCo 标记)场景到可以在自然场景中遇到的对象。

第 5 章,“深度学习”概述了与深度神经网络连接的 OpenCV 中的新功能。 它提供了加载深度学习模型并将其应用于计算机视觉任务的示例。

第 6 章,“线性代数”深入探讨了解决线性代数问题的有用数学方法,并提供了在计算机视觉中应用这些方法的示例。

第 7 章,“检测器和描述符”包含有关如何使用图像特征描述符的信息:如何使用不同的方法计算它们,如何显示它们以及如何将它们用于对象匹配、检测和跟踪目的。

第 8 章,“图像和视频处理”向读者展示如何处理图像序列并基于序列之间的相关性获得结果。

第 9 章,“多视图几何”描述了如何使用相机检索有关场景 3D 几何的信息。

充分利用这本书

秘籍中提到了开始使用各个秘籍的所有必需信息。

下载示例代码文件

您可以从 www.packtpub.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或登录 www.packtpub.com
  2. 选择支持选项卡。
  3. 单击代码下载和勘误。
  4. 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。

下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:

  • Windows 的 WinRAR/7-Zip
  • Mac 的 Zipeg/iZip/UnRarX
  • Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上。 如果代码有更新,它将在现有 GitHub 存储库上进行更新。

我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!

下载彩色图像

我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载

使用约定

本书中使用了许多文本约定。

CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“cv2.flip函数用于镜像图像。”

代码块设置如下:

import argparse
import cv2
parser = argparse.ArgumentParser()
parser.add_argument('--path', default='../data/Lena.png', help='Image path.')
params = parser.parse_args()
img = cv2.imread(params.path)

当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:

import argparse
import cv2
parser = argparse.ArgumentParser()
parser.add_argument('--path', default='../data/Lena.png', help='Image path.')
params = parser.parse_args()
img = cv2.imread(params.path)

任何命令行输入或输出的编写方式如下:

read ../data/Lena.png
shape: (512, 512, 3)
dtype: uint8

read ../data/Lena.png as grayscale
shape: (512, 512)
dtype: uint8

粗体:表示新术语,重要单词或您在屏幕上看到的单词。

警告或重要提示如下所示。

提示和技巧如下所示。

栏目

在本书中,您会发现几个经常出现的标题(准备工作如何执行...工作原理... ,[ ,另请参阅)。

要给出有关如何完成秘籍的明确说明,请按以下说明使用这些部分:

准备

本节告诉您在秘籍中会有什么期望,并介绍如何设置秘籍所需的任何软件或任何初步设置。

操作步骤

本节包含遵循秘籍所需的步骤。

工作原理

本节通常包括对上一节中发生的情况的详细说明。

更多

本节包含有关秘籍的其他信息,以使您对秘籍有更多的了解。

另见

本节提供了指向该秘籍其他有用信息的有用链接。

保持联系

始终欢迎读者的反馈。

一、I/O 和 GUI

在本章中,我们将介绍以下秘籍:

  • 从文件读取图像
  • 简单的图像转换 - 调整大小和翻转
  • 使用有损和无损压缩保存图像
  • 在 OpenCV 窗口中显示图像
  • 在 OpenCV 窗口中使用 UI 元素,例如按钮和轨迹栏
  • 绘制 2D 基本体-标记,直线,椭圆,矩形和文本
  • 处理来自键盘的用户输入
  • 通过处理鼠标的用户输入来使您的应用具有交互性
  • 从相机捕获并显示帧
  • 播放视频中的帧流
  • 获取帧流属性
  • 将帧流写入视频
  • 在视频文件的帧之间跳转

介绍

计算机视觉算法消耗并产生数据-它们通常将图像作为输入并生成输入的特征,例如轮廓,感兴趣的点或区域,对象的边界框或其他图像。 因此,处理图形信息的输入和输出是任何计算机视觉算法的重要组成部分。 这不仅意味着要读取和保存图像,还要显示有关其功能的其他信息。

在本章中,我们将介绍与 I/O 功能相关的基本 OpenCV 功能。 从秘籍中,您将学习如何从不同来源(文件系统或照相机)获取图像,显示它们以及保存图像和视频。 此外,本章还涉及使用 OpenCV UI 系统的主题。 例如,在创建窗口和跟踪栏时。

从文件读取图像

在本秘籍中,我们将学习如何从文件中读取图像。 OpenCV 支持读取不同格式的图像,例如 PNG,JPEG 和 TIFF。 让我们编写一个程序,该程序将图像的路径作为第一个参数,读取图像并打印其形状和大小。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

对于此秘籍,您需要执行以下步骤:

  1. 您可以使用cv2.imread函数轻松读取图像,该函数带有图像路径和可选标志:
import argparse
import cv2
parser = argparse.ArgumentParser()
parser.add_argument('--path', default='../data/Lena.png', help='Image path.')
params = parser.parse_args()
img = cv2.imread(params.path)
  1. 有时检查图像是否成功加载很有用:
assert img is not None  # check if the image was successfully loaded
print('read {}'.format(params.path))
print('shape:', img.shape)
print('dtype:', img.dtype)
  1. 加载图像并将其转换为灰度,即使它最初具有许多颜色通道也是如此:
img = cv2.imread(params.path, cv2.IMREAD_GRAYSCALE)
assert img is not None
print('read {} as grayscale'.format(params.path))
print('shape:', img.shape)
print('dtype:', img.dtype)

工作原理

加载的图像表示为 NumPy 数组。 在 OpenCV 中,矩阵使用相同的表示形式。 NumPy 数组具有诸如shape(它是图像的大小和颜色通道数)和dtype(它是基础数据类型(例如uint8float32))的属性。 请注意,OpenCV 以 BGR 而非 RGB 格式加载图像。

在这种情况下,shape元组应解释为:图像高度,图像宽度,颜色通道数。

cv.imread函数还支持可选标志,用户可以在其中指定是否应执行向uint8类型的转换以及图像是灰度还是彩色的。

使用默认参数运行代码后,您应该看到以下输出:

read ../data/Lena.png
shape: (512, 512, 3)
dtype: uint8

read ../data/Lena.png as grayscale
shape: (512, 512)
dtype: uint8

简单的图像转换 - 调整大小和翻转

现在我们可以加载图像了,该进行一些简单的图像处理了。 我们要检查的操作(调整大小和翻转)是基本操作,通常用作复杂的计算机视觉算法的预备步骤。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

对于此秘籍,我们需要执行以下步骤:

  1. 加载图像并打印其原始尺寸:
img = cv2.imread('../data/Lena.png')
print('original image shape:', img.shape)
  1. OpenCV 提供了几种使用cv2.resize函数的方法。 我们可以将以像素为单位的目标大小(widthheight)设置为第二个参数:
width, height = 128, 256
resized_img = cv2.resize(img, (width, height))
print('resized to 128x256 image shape:', resized_img.shape)
  1. 通过设置图像原始宽度和高度的倍数来调整大小:
w_mult, h_mult = 0.25, 0.5
resized_img = cv2.resize(img, (0, 0), resized_img, w_mult, h_mult)
print('image shape:', resized_img.shape)
  1. 使用最近邻插值而不是默认插值来调整大小:
w_mult, h_mult = 2, 4
resized_img = cv2.resize(img, (0, 0), resized_img, w_mult, h_mult, cv2.INTER_NEAREST)
print('half sized image shape:', resized_img.shape)
  1. 沿其水平x轴反射图像。 为此,我们应该将0作为cv2.flip函数的最后一个参数传递:
img_flip_along_x = cv2.flip(img, 0)
  1. 当然,可以沿垂直y轴翻转图像-只要传递大于0的任何值即可:
img_flip_along_y = cv2.flip(img, 1)
  1. 通过将任何负值传递给函数,我们可以同时翻转xy
img_flipped_xy = cv2.flip(img, -1)

工作原理

我们可以在cv2.resize中使用插值模式-它定义如何计算像素之间的值。 有很多类型的插值,每种插值都有不同的结果。 该参数可以作为最后一个参数传递,并且不影响结果的大小-仅影响输出的质量和平滑度。

默认情况下,使用双线性插值(cv2.INTER_LINEAR)。 但是在某些情况下,可能有必要应用其他更复杂的选项。

cv2.flip函数用于镜像图像。 它不会更改图像的大小,而是会交换像素。

使用有损和无损压缩保存图像

此秘籍将教您如何保存图像。 有时您想从计算机视觉算法中获取反馈。 一种方法是将结果存储在磁盘上。 反馈可能是最终图像,带有其他信息(例如轮廓,度量,值等)的图片,或者是复杂管道中各个步骤的结果。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

这是此秘籍的步骤:

  1. 首先,读取图片:
img = cv2.imread('../data/Lena.png')
  1. 将图像保存为 PNG 格式而不会降低质量,然后再次读取以检查在写入磁盘期间是否已保留所有信息:
# save image with lower compression—bigger file size but faster decoding
cv2.imwrite('../data/Lena_compressed.png', img, [cv2.IMWRITE_PNG_COMPRESSION, 0])

# check that image saved and loaded again image is the same as original one
saved_img = cv2.imread(params.out_png)
assert saved_img.all() == img.all()
  1. 将图像保存为 JPEG 格式:
# save image with lower quality—smaller file size
cv2.imwrite('../data/Lena_compressed.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 0])

工作原理

要保存图像,应使用cv2.imwrite函数。 文件的格式由此函数确定,可以在文件名中看到(支持 JPEG,PNG 等)。 保存图像有两个主要选项:保存时是否丢失某些信息。

cv2.imwrite函数采用三个参数:输出文件的路径,图像本身以及保存的参数。 将图像保存为 PNG 格式时,我们可以指定压缩级别。 IMWRITE_PNG_COMPRESSION的值必须在(0, 9)间隔中-数字越大,磁盘上的文件越小,但是解码过程越慢。

保存为 JPEG 格式时,我们可以通过设置IMWRITE_JPEG_QUALITY的值来管理压缩过程。 我们可以将其设置为 0 到 100 之间的任何值。但是,在这种情况下,越大越好。 较大的值导致较高的结果质量和较少的 JPEG 伪影。

在 OpenCV 窗口中显示图像

OpenCV 的众多出色功能之一是您可以非常轻松地可视化图像。 在这里,我们将学习有关在 OpenCV 中显示图像的所有信息。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

步骤如下:

  1. 加载图像以使用某些东西并获取其大小:
orig = cv2.imread('../data/Lena.png')
orig_size = orig.shape[0:2]
  1. 现在让我们显示图像。 为此,我们需要调用cv2.imshowcv2.waitKey函数:
cv2.imshow("Original image", orig)
cv2.waitKey(2000)

工作原理

现在,让我们了解一下这些函数。 需要cv2.imshow函数来显示图像-它的第一个参数是窗口的名称(请参见下面的屏幕快照中的窗口标题),第二个参数是我们要显示的图像。 cv2.waitKey函数对于控制窗口的显示时间是必需的。

请注意,必须明确控制显示时间,否则您将看不到任何窗口。 该函数以毫秒为单位显示窗口显示时间的持续时间。 但是,如果您按键盘上的任意键,则窗口将在指定时间之前消失。 我们将在以下秘籍之一中回顾此函数。

上面的代码导致以下结果:

在 OpenCV 窗口中使用 UI 元素,例如按钮和轨迹栏

在本秘籍中,我们将学习如何将 UI 元素(例如按钮和轨迹栏)添加到 OpenCV 窗口中以及如何使用它们。 跟踪栏是有用的 UI 元素,它们可以:

  • 显示整数变量的值(假设该值在预定义范围内)
  • 让我们通过更改轨迹栏位置以交互方式更改值

让我们创建一个程序,该程序允许用户通过交互更改每个红色绿色蓝色RGB)来指定图像的填充色 )通道值。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

要完成此秘籍,步骤如下:

  1. 首先创建一个名为window的 OpenCV 窗口:
import cv2, numpy as np

cv2.namedWindow('window')
  1. 创建一个变量,其中将包含图像的填充颜色值。 该变量是一个 NumPy 数组,具有三个值,这些值将被解释为[0, 255]范围内的蓝色,绿色和红色颜色分量(按此顺序):
fill_val = np.array([255, 255, 255], np.uint8)
  1. 添加一个辅助函数以从每个trackbar_callback函数中调用。 该函数将颜色成分索引和新值用作设置:
def trackbar_callback(idx, value):
    fill_val[idx] = value
  1. window中添加三个跟踪栏,然后使用 Python lambda函数将每个跟踪栏回调绑定到特定的颜色组件:
cv2.createTrackbar('R', 'window', 255, 255, lambda v: trackbar_callback(2, v))
cv2.createTrackbar('G', 'window', 255, 255, lambda v: trackbar_callback(1, v))
cv2.createTrackbar('B', 'window', 255, 255, lambda v: trackbar_callback(0, v))
  1. 在一个循环中,在具有三个轨迹栏的窗口中显示图像,并同时处理键盘输入:
while True:
    image = np.full((500, 500, 3), fill_val)
    cv2.imshow('window', image)
    key = cv2.waitKey(3)
    if key == 27: 
        break
cv2.destroyAllWindows()

工作原理

可能会显示如下所示的窗口,尽管它可能会有所不同,具体取决于 OpenCV 的版本及其构建方式:

绘制 2D 基本体 - 标记,直线,椭圆,矩形和文本

在实现第一个计算机视觉算法之后,您将希望看到其结果。 OpenCV 具有大量绘图函数,可让您突出显示图像中的任何特点。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

  1. 打开图像并获取其宽度和高度。 另外,定义一个简单的函数,该函数将在图像内返回一个随机点:
import cv2, random

image = cv2.imread('../data/Lena.png')
w, h = image.shape[1], image.shape[0]

def rand_pt(mult=1.):
    return (random.randrange(int(w*mult)),
            random.randrange(int(h*mult)))
  1. 让我们画点东西! 让我们画个圆圈:
cv2.circle(image, rand_pt(), 40, (255, 0, 0))
cv2.circle(image, rand_pt(), 5, (255, 0, 0), cv2.FILLED)
cv2.circle(image, rand_pt(), 40, (255, 85, 85), 2)
cv2.circle(image, rand_pt(), 40, (255, 170, 170), 2, cv2.LINE_AA)
  1. 现在让我们尝试画线:
cv2.line(image, rand_pt(), rand_pt(), (0, 255, 0))
cv2.line(image, rand_pt(), rand_pt(), (85, 255, 85), 3)
cv2.line(image, rand_pt(), rand_pt(), (170, 255, 170), 3, cv2.LINE_AA)
  1. 如果要绘制箭头,请使用arrowedLine()函数:
cv2.arrowedLine(image, rand_pt(), rand_pt(), (0, 0, 255), 3, cv2.LINE_AA)
  1. 要绘制矩形,OpenCV 具有rectangle()函数:
cv2.rectangle(image, rand_pt(), rand_pt(), (255, 255, 0), 3)
  1. 另外,OpenCV 包括绘制椭圆的函数。 让我们画一下:
cv2.ellipse(image, rand_pt(), rand_pt(0.3), random.randrange(360), 0, 360, (255, 255, 255), 3)
  1. 我们与绘图相关的最终函数是在图像上放置文本:
cv2.putText(image, 'OpenCV', rand_pt(), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 3)

工作原理

首先,cv2.circle给出最薄和最暗的蓝色图元。 第二次调用绘制了一个深蓝色的点。 第三次调用会产生带有尖锐边缘的淡蓝色圆圈。 最后一个调用cv2.circle揭示了最浅的蓝色圆圈,带有平滑的边框。

cv2.circle函数将图像作为第一个参数,并且以(xy)格式的中心位置,圆弧的半径和颜色作为强制参数。 您还可以指定线的粗细(FILLED的值提供一个实心圆)和线的类型(LINE_AA的提供无锯齿的边框)。

cv2.line函数可拍摄图像,起点和终点以及图像颜色(与第一次调用一样)。 (可选)您可以传递线的粗细和线的类型(同样,禁止混叠)。

我们将得到如下信息(位置可能因随机性而有所不同):

cv2.arrowedLine函数的参数与cv2.line的参数相同。

cv2.rectangle所采用的参数是要绘制的图像,左上角,右下角和颜色。 另外,可以指定厚度(或用FILLED值填充矩形)。

cv2.ellipse拍摄图像,中心位置为(xy)格式,半轴长度为(ab)格式,旋转角度,绘图的起始角度,绘图的终止角度以及线条的颜色和粗细(也可以绘制填充的椭圆)作为参数。

cv2.putText函数的参数包括图像,所放置的文本,文本左下角的位置,字体名称,符号比例以及颜色和粗细。

处理来自键盘的用户输入

OpenCV 具有简单明了的方式来处理键盘输入。 此功能内置在cv2.waitKey函数中。 让我们看看如何使用它。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

您将需要针对此秘籍执行以下步骤:

  1. 如前所述,打开图像并获取其宽度和高度。 另外,制作原始图像的副本,并定义一个简单函数,该函数将返回一个随机点,其图像内的坐标为:
import cv2, numpy as np, random

image = cv2.imread('../data/Lena.png')
w, h = image.shape[1], image.shape[0]
image_to_show = np.copy(image)

def rand_pt():
 return (random.randrange(w),
 random.randrange(h))
  1. 现在,当用户按下PLRET绘制点,线时, 矩形,椭圆形或文本。 另外,当用户按下C时,我们将清除图像,并在按下Esc键时关闭应用:
finish = False
while not finish:
    cv2.imshow("result", image_to_show)
    key = cv2.waitKey(0)
    if key == ord('p'):
        for pt in [rand_pt() for _ in range(10)]:
            cv2.circle(image_to_show, pt, 3, (255, 0, 0), -1)
    elif key == ord('l'):
        cv2.line(image_to_show, rand_pt(), rand_pt(), (0, 255, 0), 3)
    elif key == ord('r'):
        cv2.rectangle(image_to_show, rand_pt(), rand_pt(), (0, 0, 255), 3)
    elif key == ord('e'):
        cv2.ellipse(image_to_show, rand_pt(), rand_pt(), random.randrange(360), 0, 360, (255, 255, 0), 3)
    elif key == ord('t'):
        cv2.putText(image_to_show, 'OpenCV', rand_pt(), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 3)
    elif key == ord('c'):
        image_to_show = np.copy(image)
    elif key == 27:
        finish = True

工作原理

如您所见,我们只分析waitKey()返回值。 如果我们设置了持续时间并且没有按下任何键,则waitKey()将返回-1

启动代码并按下PLRET键后, 次,您将获得接近以下图像:

通过处理鼠标的用户输入来使您的应用具有交互性

在本秘籍中,我们将学习如何在 OpenCV 应用中启用鼠标输入的处理。 从鼠标获取事件的实例是窗口,因此我们需要使用cv2.imshow。 但是我们还需要添加鼠标事件的处理器。 让我们详细了解如何通过鼠标选择图像区域来实现裁剪功能。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

此秘籍的步骤如下:

  1. 首先,加载图像并进行复制:
import cv2, numpy as np

image = cv2.imread('../data/Lena.png')
image_to_show = np.copy(image)
  1. 现在,定义一些变量来存储鼠标状态:
mouse_pressed = False
s_x = s_y = e_x = e_y = -1
  1. 让我们实现鼠标事件的处理器。 这应该是一个带有四个参数的函数,如下所示:
def mouse_callback(event, x, y, flags, param):
    global image_to_show, s_x, s_y, e_x, e_y, mouse_pressed

    if event == cv2.EVENT_LBUTTONDOWN:
        mouse_pressed = True
        s_x, s_y = x, y
        image_to_show = np.copy(image)

    elif event == cv2.EVENT_MOUSEMOVE:
        if mouse_pressed:
            image_to_show = np.copy(image)
            cv2.rectangle(image_to_show, (s_x, s_y),
                          (x, y), (0, 255, 0), 1)

    elif event == cv2.EVENT_LBUTTONUP:
        mouse_pressed = False
        e_x, e_y = x, y
  1. 让我们创建一个窗口实例,该实例将捕获鼠标事件并将其转换为我们先前定义的处理器函数:
cv2.namedWindow('image')
cv2.setMouseCallback('image', mouse_callback)
  1. 现在,让我们实现应用的其余部分,它应该对按钮的按下做出反应并裁剪原始图像:
while True:
    cv2.imshow('image', image_to_show)
    k = cv2.waitKey(1)

    if k == ord('c'):
        if s_y > e_y:
            s_y, e_y = e_y, s_y
        if s_x > e_x:
            s_x, e_x = e_x, s_x

        if e_y - s_y > 1 and e_x - s_x > 0:
            image = image[s_y:e_y, s_x:e_x]
            image_to_show = np.copy(image)
    elif k == 27:
        break

cv2.destroyAllWindows()

工作原理

cv2.setMouseCallback中,我们将鼠标事件处理器mouse_callback分配给了名为image的窗口。

启动后,我们可以通过以下方式选择一个区域:在图像中的某个位置按鼠标左键,将鼠标拖动到终点,然后松开鼠标按钮以确认我们的选择已完成。 我们可以通过单击一个新的位置来重复该过程-先前的选择会消失:

通过点击键盘上的C按钮,我们可以在选定区域内切割一个区域,如下所示:

从相机捕获并显示帧

在本秘籍中,您将学习如何连接到 USB 摄像机并使用 OpenCV 实时捕获帧。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

对于此秘籍,步骤如下:

  1. 创建一个VideoCapture对象:
import cv2

capture = cv2.VideoCapture(0)
  1. 使用capture.read方法从相机读取帧,该方法返回一对:读取成功标志和frame本身:
while True:
    has_frame, frame = capture.read()
    if not has_frame:
        print('Can\'t get frame')
        break

    cv2.imshow('frame', frame)
    key = cv2.waitKey(3)
    if key == 27:
        print('Pressed Esc')
        break
  1. 通常建议您释放视频设备(在我们的情况下为摄像机)并销毁所有创建的窗口:
capture.release()
cv2.destroyAllWindows()

工作原理

通过cv2.VideoCapture 类在 OpenCV 中使用摄像机。 实际上,当同时使用摄像机和视频文件时,它提供了支持。 要实例化代表来自摄像机的帧流的对象,只需指定其编号(从零开始的设备索引)。 如果 OpenCV 不支持您的摄像机,您可以尝试重新编译 OpenCV,打开其他工业摄像机类型的可选支持。

播放视频中的帧流

在本秘籍中,您将学习如何使用 OpenCV 打开现有的视频文件。 您还将学习如何从打开的视频中重播帧。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

以下是此秘籍的步骤:

  1. 为视频文件创建一个VideoCapture对象:
import cv2

capture = cv2.VideoCapture('../data/drop.avi')
  1. 重播视频中的所有帧:
while True:
    has_frame, frame = capture.read()
    if not has_frame:
        print('Reached the end of the video')
        break

    cv2.imshow('frame', frame)
    key = cv2.waitKey(50)
    if key == 27:
        print('Pressed Esc')
        break

cv2.destroyAllWindows()

工作原理

使用视频文件实际上与使用相机相同,都是通过相同的cv2.VideoCapture类完成的。 但是,这一次,您应该指定要打开的视频文件的路径,而不是摄像机设备的索引。 根据可用的操作系统和视频编解码器,OpenCV 可能不支持某些视频格式。

在无限while循环中打开视频文件后,我们使用capture.read方法获取帧。 该函数返回一对:布尔帧读取成功标志,以及帧本身。 请注意,将以最大可能的速率读取帧,这意味着如果要以特定的 FPS 回放视频,则应自行实现。 在前面的代码中,调用cv2.imshow函数后,我们在cv2.waitKey函数中等待 50 毫秒。 假设在显示图像和解码视频上花费的时间可以忽略不计,则视频将以不超过 20 FPS 的速率重播。

可以看到以下框架:

获取帧流属性

在本秘籍中,您将学习如何获取VideoCapture属性,例如帧高和宽度,视频文件的帧数以及相机帧频。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

执行以下步骤:

  1. 让我们创建一个辅助函数,该函数将使用VideoCapture ID(摄像设备是视频设备还是视频路径),创建VideoCapture对象,并请求帧的高度和宽度,计数和速率:
import numpy
import cv2

def print_capture_properties(*args):
    capture = cv2.VideoCapture(*args)
    print('Created capture:', ' '.join(map(str, args)))
    print('Frame count:', int(capture.get(cv2.CAP_PROP_FRAME_COUNT)))
    print('Frame width:', int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)))
    print('Frame height:', int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    print('Frame rate:', capture.get(cv2.CAP_PROP_FPS))
  1. 让我们对视频文件调用这个函数:
print_capture_properties('../data/drop.avi')
  1. 现在,让我们请求相机捕获对象的属性:
print_capture_properties(0)

工作原理

与早期的秘籍一样,通过cv2.VideoCapture 类可以处理摄像机和视频帧流。 您可以使用capture.get函数获取属性,该函数获取属性 ID 并将其值作为浮点值返回。

请注意,根据所使用的操作系统和视频后端,并非可以访问所有请求的属性。

预期会有以下输出(可能会因 OS 和编译 OpenCV 的视频后端而异):

Created capture: ../data/drop.avi
Frame count: 182
Frame width: 256
Frame height: 240
Frame rate: 30.0

Created capture: 0 
Frame count: -1 
Frame width: 640 
Frame height: 480 
Frame rate: 30.0

将帧流写入视频

在本秘籍中,您将学习如何从 USB 摄像机实时捕获帧,以及如何使用指定的视频编解码器将帧同时写入视频文件。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

这是我们需要执行以完成此秘籍的步骤:

  1. 首先,像前面的秘籍一样,我们创建一个相机捕获对象,并获取框架的高度和宽度:
import cv2
capture = cv2.VideoCapture(0)
frame_width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
print('Frame width:', frame_width)
print('Frame height:', frame_height)
  1. 创建视频写入器:
video = cv2.VideoWriter('../data/captured_video.avi', cv2.VideoWriter_fourcc(*'X264'),
                        25, (frame_width, frame_height))
  1. 然后,在无限while循环中,捕获帧并使用video.write 方法将其写入:
while True:
    has_frame, frame = capture.read()
    if not has_frame:
        print('Can\'t get frame')
        break

    video.write(frame)

    cv2.imshow('frame', frame)
    key = cv2.waitKey(3)
    if key == 27:
        print('Pressed Esc')
        break
  1. 释放所有创建的VideoCaptureVideoWriter对象,并销毁窗口:
capture.release()
writer.release()
cv2.destroyAllWindows()

工作原理

使用cv2.VideoWriter类执行视频编写。 构造器采用输出视频路径四字符代码FOURCC),指定视频代码,所需的帧速率和帧大小。 编解码器代码的示例包括用于 MPEG-1 的PIM1; 用于 Motion-JPEG 的MJPG; 用于 XVID MPEG-4 的XVID; 和 H.264 的H264

在视频文件的帧之间跳转

在本秘籍中,您将学习如何将VideoCapture对象放置在不同的帧位置。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

此秘籍的步骤为:

  1. 首先,让我们创建一个VideoCapture对象并获取总帧数:
import cv2
capture = cv2.VideoCapture('../data/drop.avi')
frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
print('Frame count:', frame_count)
  1. 获取总帧数:
print('Position:', int(capture.get(cv2.CAP_PROP_POS_FRAMES)))
_, frame = capture.read()
cv2.imshow('frame0', frame)
  1. 请注意,capture.read方法会将当前视频位置向前移动一帧。 获取下一帧:
print('Position:', capture.get(cv2.CAP_PROP_POS_FRAMES))
_, frame = capture.read()
cv2.imshow('frame1', frame)
  1. 让我们跳到帧位置100
capture.set(cv2.CAP_PROP_POS_FRAMES, 100)
print('Position:', int(capture.get(cv2.CAP_PROP_POS_FRAMES)))
_, frame = capture.read()
cv2.imshow('frame100', frame)

cv2.waitKey()
cv2.destroyAllWindows()

工作原理

使用cv2.CAP_PROP_POS_FRAMES属性获取并设置视频位置。 根据视频的编码方式,设置属性可能不会导致设置请求的确切帧索引。 要设置的值必须在有效范围内。

运行该程序后,您应该看到以下输出:

Frame count: 182
Position: 0
Position: 1
Position: 100

应显示以下框架:

二、矩阵,颜色和过滤器

在本章中,我们将介绍以下秘籍:

  • 处理矩阵的创建,填充,访问元素和 ROI
  • 在不同的数据类型和缩放值之间转换
  • 使用 NumPy 的非图像数据持久化
  • 操作图像通道
  • 将图像从一种色彩空间转换为另一种色彩空间
  • 伽玛校正和逐元素算数
  • 均值/方差图像归一化
  • 计算图像直方图
  • 均衡图像直方图
  • 使用高斯,中值和双边过滤器消除噪声
  • 使用 Sobel 过滤器计算梯度图像
  • 创建和应用自己的过滤器
  • 使用实值 Gabor 过滤器处理图像
  • 使用离散傅里叶变换从空间域转到频域(并返回)
  • 为图像过滤在频域中操作图像
  • 使用不同阈值处理图像
  • 形态运算符
  • 二进制图像 - 图像遮罩和二进制操作

介绍

在本章中,我们将了解如何处理矩阵。 我们将学习如何在像素级别使用矩阵,以及可以应用于整个矩阵的操作和图像处理器。 您将了解如何访问任何像素,如何更改矩阵的数据类型和颜色空间,如何应用内置的 OpenCV 过滤器以及如何创建和使用自己的线性过滤器。

处理矩阵的创建,填充,访问元素和 ROI

本秘籍介绍了矩阵的创建和初始化,对元素的访问,像素的访问以及如何处理矩阵的一部分。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

要获得结果,必须执行几个步骤:

  1. 导入所有必要的模块:
import cv2, numpy as np
  1. 创建一个特定形状的矩阵,并将其填充为255作为值,该矩阵应显示以下内容:
image = np.full((480, 640, 3), 255, np.uint8)
cv2.imshow('white', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 创建一个矩阵并为每个像素的颜色设置单独的值,以将我们的矩阵变为红色:
image = np.full((480, 640, 3), (0, 0, 255), np.uint8)
cv2.imshow('red', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 用零填充矩阵以使其为黑色:
image.fill(0)
cv2.imshow('black', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 接下来,将某些单个像素的值设置为白色:
image[240, 160] = image[240, 320] = image[240, 480] = (255, 255, 255)
cv2.imshow('black with white pixels', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 现在,让我们将所有像素的第一个通道设置为255,以使黑色像素变为蓝色:
image[:, :, 0] = 255
cv2.imshow('blue with white pixels', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 现在,将图像中间垂直线上的像素设置为白色:
image[:, 320, :] = 255
cv2.imshow('blue with white line', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 最后,将特定区域内所有像素的第二个通道设置为255
image[100:600, 100:200, 2] = 255
cv2.imshow('image', image)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

OpenCV 的 Python 界面中的矩阵与 NumPy 数组一起显示。 NumPy 提供了强大而清晰的工具来处理多维矩阵(也称为张量)。 而且,当然,NumPy 支持纯二维矩阵。 这就是为什么我们需要导入其模块。 这就是为什么我们在此秘籍中使用大量np函数的原因。

在这里,有必要对矩阵的尺寸和类型说几句话。 矩阵具有两个独立的特征-形状类型和元素类型。 首先,让我们谈谈形状。 形状描述矩阵的所有尺寸。 矩阵通常具有三个空间维度:宽度(也称为列数),高度(也称为行数)和通道数。 通常以高度,宽度,通道格式进行订阅。 OpenCV 适用于全彩色或灰度矩阵。 这意味着 OpenCV 例程只能处理 3 通道或 1 通道。 可以将灰度矩阵想象成数字的平面表,其中每个元素(像素)仅存储一个值。 全彩色的可以视为表,其中每个元素连续存储三个值而不是一个。 全彩色矩阵的一个示例是分别具有红色,绿色和蓝色通道的矩阵-这意味着每个元素都存储红色,绿色和蓝色分量的值。 但是出于历史原因,OpenCV 会以 BGR 格式存储 RGB 表示的颜色值-请务必小心。

矩阵的另一个特征是其元素类型。 元素类型定义了用于表示元素值的数据类型。 例如,每个像素可以存储[0-255]范围内的值-在这种情况下为np.uint8。 或者,它可以存储floatnp.float32)或doublenp.float64)值。

np.full用于创建矩阵。 它采用以下参数:(高度,宽度,通道)格式的矩阵形状,每个像素(或像素的每个组成部分)的初始值以及像素值的类型。 可以将单个数字作为第二个参数传递-在这种情况下,所有像素值都使用该数字初始化。 同样,我们可以为每个像素元素传递初始编号。

np.fill可帮助您为所有像素分配相同的值-只需传递一个值即可分配为参数。 np.fillnp.full之间的区别在于,第一个不是创建矩阵,而是为现有元素分配值。

要访问单个像素,可以使用[]运算符并指定所需元素的索引; 例如,image[240, 160]使您可以访问高度240和宽度160的像素。 索引的顺序与矩阵形状中维的顺序相对应-第一个索引沿第一维,第二个索引沿第二维,依此类推。 如果只为某些尺寸指定索引,则将得到一个切片(具有较小尺寸编号的张量)。 可以通过使用冒号(:)而不是索引来处理沿维度的所有像素。 例如,image[:, 320, :]实际上意味着-提供沿高度的所有像素和沿宽度的具有索引 320 的尺寸和所有通道。

:符号还有助于指定矩阵内的某些区域-我们只需要在:之前添加索引,在:之后添加索引(范围的末尾不包含索引)。 例如,image[100:600, 100:200, 2]为我们提供了具有[100, 600]范围内的高度索引,[100, 200]范围内的宽度索引和通道索引2的所有像素。

在不同的数据类型和缩放值之间转换

此秘籍告诉您如何将矩阵元素的数据类型从uint8更改为float32并执行算术运算而无需担心钳位值(然后将所有内容转换回uint8)。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

此秘籍需要执行以下步骤:

  1. 导入所有必需的模块,打开图像,打印其形状和数据类型,然后在屏幕上显示:
import cv2, numpy as np
image = cv2.imread('../data/Lena.png')
print('Shape:', image.shape)
print('Data type:', image.dtype)
cv2.imshow('image', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 使用浮动数据类型元素将我们的图片转换为一张图片:
image = image.astype(np.float32) / 255
print('Shape:', image.shape)
print('Data type:', image.dtype)
  1. 通过2缩放图像元素,并裁剪值以将其保持在[0, 1]范围内:
cv2.imshow('image', np.clip(image*2, 0, 1))
cv2.waitKey()
cv2.destroyAllWindows()
  1. 将图像的元素缩放回[0, 255]范围,并将元素类型转换为 8 位无符号int
image = (image * 255).astype(np.uint8)
print('Shape:', image.shape)
print('Data type:', image.dtype)

cv2.imshow('image', image)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

要转换矩阵的数据类型,必须使用 NumPy 数组的astype函数。 该函数将所需的类型作为输入并返回转换后的数组。

要缩放矩阵的值,可以对矩阵本身使用代数运算:例如,只需将矩阵除以某个值(在前面的代码中为255),即可将矩阵的每个元素除以指定的值 。 缩放输入图像的值的结果应显示如下(左侧图像为原始图像,右侧图像为缩放版本):

使用 NumPy 的非图像数据持久化

以前,我们仅使用 OpenCV 的cv2.imwritecv2.imread函数分别保存和加载图像。 但是可以使用 NumPy 的数据持久性保存任何类型和形状的任何矩阵(不仅包含图像内容)。 在本秘籍中,我们将回顾如何做。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

执行以下步骤:

  1. 导入所有必要的模块:
import cv2, numpy as np
  1. 创建一个具有随机值初始化的矩阵,并打印其属性:
mat = np.random.rand(100, 100).astype(np.float32)
print('Shape:', mat.shape)
print('Data type:', mat.dtype)
  1. 使用np.savetxt函数将随机矩阵保存到文件中:
np.savetxt('mat.csv', mat)
  1. 现在,从我们刚刚编写的文件中加载它,并打印其形状和类型:
mat = np.loadtxt('mat.csv').astype(np.float32)
print('Shape:', mat.shape)
print('Data type:', mat.dtype)

工作原理

NumPy 的savetxtloadtxt函数使您可以存储和加载任何矩阵。 它们使用文本格式,因此您可以在文本编辑器中查看文件的内容。

操作图像通道

本秘籍是关于处理矩阵通道的。 这里介绍了如何访问各个通道,交换它们以及执行代数运算。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

执行以下步骤:

  1. 导入所有必需的模块,打开图像,然后输出其形状:
import cv2, numpy as np
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255
print('Shape:', image.shape)
  1. 交换红色和蓝色通道并显示结果:
image[:, :, [0, 2]] = image[:, :, [2, 0]]
cv2.imshow('blue_and_red_swapped', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 向后交换通道,并按不同比例缩放它们以更改图像的色彩:
image[:, :, [0, 2]] = image[:, :, [2, 0]]
image[:, :, 0] = (image[:, :, 0] * 0.9).clip(0, 1) 
image[:, :, 1] = (image[:, :, 1] * 1.1).clip(0, 1)
cv2.imshow('image', image)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

矩阵的最后一个维度负责通道。 这就是为什么我们在代码中进行操作。

要交换通道,我们应该可以访问矩阵的相应切片。 但是切片不是原始矩阵的副本,它们只是同一数据的不同视图。 这意味着我们不能像普通类型那样通过临时变量执行交换。 这里我们需要更复杂的东西,而 NumPy 不仅使我们可以获取一个切片,而且还可以获取一堆切片作为数据的新视图。 为此,我们应该以所需顺序枚举所有所需切片的索引,而不是单个索引。

当我们使用单个索引时,我们可以访问相应的通道,并且可以在切片上执行一些代数运算。

结果应如下所示:

将图像从一种色彩空间转换为另一种色彩空间

此秘籍告诉您有关色彩空间转换的信息。 默认情况下,OpenCV 中的全彩色图像以 RGB 颜色空间显示。 但是在某些情况下,有必要转向其他颜色表示形式。 例如,有一个单独的强度通道。 这里我们考虑改变图像色彩空间的方法。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

使用以下步骤:

  1. 导入所有必要的模块:
import cv2
import numpy as np
  1. 加载图像并打印其形状和类型:
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255
print('Shape:', image.shape)
print('Data type:', image.dtype)
  1. 将图像转换为灰度:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print('Converted to grayscale')
print('Shape:', gray.shape)
print('Data type:', gray.dtype)
cv2.imshow('gray', gray)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 将图像转换为 HSV 颜色空间:
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
print('Converted to HSV')
print('Shape:', hsv.shape)
print('Data type:', hsv.dtype)
cv2.imshow('hsv', hsv)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 通过将V通道乘以某个值来增加图像的亮度。 然后将图像转换为 RGB 颜色空间:
hsv[:, :, 2] *= 2
from_hsv = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
print('Converted back to BGR from HSV')
print('Shape:', from_hsv.shape)
print('Data type:', from_hsv.dtype)
cv2.imshow('from_hsv', from_hsv)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

要使用 OpenCV 更改图像的色彩空间,应使用cvtColor函数。 它获取源图像和特殊值,该特殊值对源进行编码并以色彩空间为目标。 该函数的返回值是转换后的图像。 OpenCV 支持 200 多种转换类型。 代码执行的结果应如下所示:

伽玛校正和逐元素算数

伽玛校正用于以非线性方式倾斜像素,值分布。 借助伽玛校正,可以调整图像的发光度,使其更容易看清。 在本秘籍中,您将学习如何将伽玛校正应用于图像。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

此秘籍的步骤如下:

  1. 将图像加载为灰度并将每个像素值转换为[0, 1]范围内的np.float32数据类型:
import cv2
import numpy as np

image = cv2.imread('../data/Lena.png', 0).astype(np.float32) / 255
  1. 使用指定的指数值gamma应用每个元素的指数:
gamma = 0.5
corrected_image = np.power(image, gamma)
  1. 显示源图像和结果图像:
cv2.imshow('image', image)
cv2.imshow('corrected_image', corrected_image)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

伽玛校正是调整图像像素强度的非线性操作。 通过输入和输出像素值V_out = V_in^γ之间的幂律关系来表示该操作。 指数系数大于 1 的值会使图像变暗,而小于 1 的值会使图像变亮。

预期上面的代码输出如下:

均值/方差图像归一化

有时有必要将某些值设置为像素值的统计矩。 当我们将0设置为平均值,将1设置为方差时,该操作称为标准化。 这在计算机视觉算法中用于处理具有特定范围和特定统计值的值可能很有用。 在这里,我们将检查图像归一化。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

执行以下步骤:

  1. 导入所有必要的模块:
import cv2
import numpy as np
  1. 加载图像并将其转换为包含[0,1]范围内的浮点元素的图像:
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255
  1. 从每个图像像素中减去平均值以获得零均值矩阵。 然后,将每个像素值除以其标准差即可得到单位方差矩阵:
image -= image.mean()
image /= image.std()

工作原理

矩阵带有 NumPy 数组类。 这些数组具有计算平均值和标准差的方法。 为了对矩阵进行归一化(即,获得零均值和单位方差的矩阵),我们需要减去平均值,这可以通过调用mean并将矩阵除以其标准差来获得。 您还可以使用cv2.meanStdDev函数,该函数同时计算平均值和标准差。

计算图像直方图

直方图显示一组值的水平分布; 例如,在图像中。 在本秘籍中,我们了解如何计算直方图。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

按着这些次序:

  1. 导入所有必要的模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载图像并显示它:
grey = cv2.imread('../data/Lena.png', 0)
cv2.imshow('original grey', grey)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 计算histogram函数:
hist, bins = np.histogram(grey, 256, [0, 255])
  1. 绘制histogram并显示:
plt.fill(hist)
plt.xlabel('pixel value')
plt.show()

工作原理

OpenCV 具有其自己的通用函数来计算直方图cv2.calcHist。 但是,在本秘籍中,我们将使用 NumPy,因为在这种特殊情况下,它使代码更简洁。 NumPy 具有特殊函数来计算直方图np.histogram。 例程的参数是输入图像,箱数和箱范围。 它返回一个带有直方图值和 bin 边值的数组。

要将直方图绘制为图形,我们需要使用 matplotlib 模块中的函数。 输出图应如下所示:

均衡图像直方图

图像直方图用于反映强度分布。 直方图的属性取决于图像属性。 例如,低对比度图像具有直方图,其中箱子聚集在某个值附近:大多数像素的值都在狭窄范围内。 低对比度的图像较难处理,因为小的细节表达不佳。 有一种技术可以解决此问题。 这称为直方图均衡。 本秘籍介绍了 OpenCV 中该方法的用法。 我们研究了如何对灰度图像和全彩色图像执行直方图均衡化。

准备

您需要安装带有 Python API 支持的 OpenCV3.x。

操作步骤

使用以下步骤:

  1. 导入所有必要的模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 将图像加载为灰度并显示:
grey = cv2.imread('../data/Lena.png', 0)
cv2.imshow('original grey', grey)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 均衡灰度图像的直方图:
grey_eq = cv2.equalizeHist(grey)
  1. 计算图像的均衡直方图并显示:
hist, bins = np.histogram(grey_eq, 256, [0, 255])
plt.fill_between(range(256), hist, 0)
plt.xlabel('pixel value')
plt.show()
  1. 显示均衡的图像:
cv2.imshow('equalized grey', grey_eq)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 将图像加载为 BGR 并将其转换为 HSV 颜色空间:
color = cv2.imread('../data/Lena.png')
hsv = cv2.cvtColor(color, cv2.COLOR_BGR2HSV)
  1. 均衡 HSV 图像的V通道,并将其转换回 RGB 颜色空间:
hsv[..., 2] = cv2.equalizeHist(hsv[..., 2])
color_eq = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
cv2.imshow('original color', color)
  1. 显示均衡的彩色图像:
cv2.imshow('equalized color', color_eq)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

为了均衡直方图,可以应用 OpenCV 的特殊函数。 它称为equalizeHist,它会拍摄需要增强其对比度的图像。 请注意,它仅拍摄单通道图像,因此我们只能将此函数直接用于灰度图像。 该例程的返回值是一个单通道均衡图像。

要将此函数应用于全彩色图像,我们需要对其进行转换,以便在一个通道中具有强度信息,而在其他通道中具有颜色信息。 HSV 色彩空间完全符合此要求,因为最后一个V通道编码亮度。 通过将输入图像转换为 HSV 颜色空间,将equalizeHist应用于V通道,并将结果转换回 RGB,我们可以均衡全色图像的直方图。

按照此秘籍中的步骤操作后,结果应如下所示:

使用高斯,中值和双边过滤器消除噪声

所有真实图像都嘈杂。 噪声不仅破坏了图像的外观,而且使算法很难将其作为输入来处理。 在本秘籍中,我们将考虑如何消除噪音或大幅降低噪音。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

执行以下步骤:

  1. 导入包:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载图像,将其转换为浮点,然后将其缩小到[0, 1]范围:
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255
  1. 通过向每个像素添加随机值来在图像中创建噪点并显示它:
noised = (image + 0.2 * np.random.rand(*image.shape).astype(np.float32))
noised = noised.clip(0, 1)
plt.imshow(noised[:,:,[2,1,0]])
plt.show()
  1. GaussianBlur应用于噪点图像并显示结果:
gauss_blur = cv2.GaussianBlur(noised, (7, 7), 0)
plt.imshow(gauss_blur[:, :, [2, 1, 0]])
plt.show()
  1. 应用median过滤:
median_blur = cv2.medianBlur((noised * 255).astype(np.uint8), 7)
plt.imshow(median_blur[:, :, [2, 1, 0]])
plt.show()
  1. 对我们的图像执行median过滤,并产生噪声:
bilat = cv2.bilateralFilter(noised, -1, 0.3, 10)
plt.imshow(bilat[:, :, [2, 1, 0]])
plt.show()

工作原理

cv2.GaussianBlur用于将Gaussian过滤器应用于图像。 此函数获取输入图像,核大小(核宽度,核高度)格式以及沿宽度和高度的标准差。 核大小应为正,奇数。

如果未指定沿高度的标准差或将其设置为零,则将X标准差的值用于两个方向。 如果我们将X标准差更改为零,也可以根据核大小计算标准差。

要应用median模糊,需要使用cv2.medianBlur函数。 它接受输入图像作为第一个参数,并接受核大小作为第二个参数。 核大小必须为正,奇数。

cv2.bilateralFilter函数提供了双边过滤。 它获取输入图像,窗口大小和颜色以及空间σ值。 如果窗口大小为负,则根据空间σ值计算得出。

前面代码的各种输出应显示如下:

使用 Sobel 算子计算梯度

在本秘籍中,您将学习如何使用Sobel过滤器来计算图像梯度的近似值。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

执行以下步骤:

  1. 导入包:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 以灰度读取图像:
image = cv2.imread('../data/Lena.png', 0)
  1. 使用Sobel运算符计算梯度近似值:
dx = cv2.Sobel(image, cv2.CV_32F, 1, 0)
dy = cv2.Sobel(image, cv2.CV_32F, 0, 1)
  1. 可视化结果:
plt.figure(figsize=(8,3))
plt.subplot(131)
plt.axis('off')
plt.title('image')
plt.imshow(image, cmap='gray')
plt.subplot(132)
plt.axis('off')
plt.imshow(dx, cmap='gray')
plt.title(r'$\frac{dI}{dx}$')
plt.subplot(133)
plt.axis('off')
plt.title(r'$\frac{dI}{dy}$')
plt.imshow(dy, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

OpenCV 的cv2.Sobel函数使用指定大小的线性过滤器来计算图像梯度近似值。 通过函数参数,您可以确切指定需要计算的导数,应使用的核以及输出图像的数据类型。

预期上面的代码输出如下:

创建和应用自己的过滤器

在本秘籍中,您将学习如何创建自己的线性过滤器并将其应用于图像。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

执行以下步骤:

  1. 导入包:
import math
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像:
image = cv2.imread('../data/Lena.png')
  1. 创建一个11x11锐化核:
KSIZE = 11
ALPHA = 2

kernel = cv2.getGaussianKernel(KSIZE, 0)
kernel = -ALPHA * kernel @ kernel.T
kernel[KSIZE//2, KSIZE//2] += 1 + ALPHA
  1. 使用我们刚创建的核过滤图像:
filtered = cv2.filter2D(image, -1, kernel)
  1. 可视化结果:
plt.figure(figsize=(8,4))
plt.subplot(121)
plt.axis('off')
plt.title('image')
plt.imshow(image[:, :, [2, 1, 0]])
plt.subplot(122)
plt.axis('off')
plt.title('filtered')
plt.imshow(filtered[:, :, [2, 1, 0]])
plt.tight_layout(True)
plt.show()

工作原理

OpenCV 的cv2.filter2d函数获取输入图像,输出结果数据类型,OpenCV ID(如果要保留输入图像数据类型,则为 -1)和过滤器核; 然后,对图像进行线性过滤。

在此秘籍中,我们构建了一个锐化的核,该核应强调源图像中的高频。 预期输出如下:

使用实值 Gabor 过滤器处理图像

在本秘籍中,您将学习如何构造Gabor过滤器核(用于检测图像中的边缘)并将其应用于图像。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

执行以下步骤:

  1. 导入包:
import math
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像为灰度并将其转换为np.float32
image = cv2.imread('../data/Lena.png', 0).astype(np.float32) / 255
  1. 构造实值Gabor过滤器核。 规范核,使其具有 L2 单位规范:
kernel = cv2.getGaborKernel((21, 21), 5, 1, 10, 1, 0, cv2.CV_32F)
kernel /= math.sqrt((kernel * kernel).sum())
  1. 过滤图像:
filtered = cv2.filter2D(image, -1, kernel)
  1. 可视化结果:
plt.figure(figsize=(8,3))
plt.subplot(131)
plt.axis('off')
plt.title('image')
plt.imshow(image, cmap='gray')
plt.subplot(132)
plt.title('kernel')
plt.imshow(kernel, cmap='gray')
plt.subplot(133)
plt.axis('off')
plt.title('filtered')
plt.imshow(filtered, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

Gabor过滤器是线性过滤器,其核是用余弦波调制的 2D 高斯调制。 可以使用cv2.getGaborKernel函数获得核,该函数采用诸如核大小,高斯标准差,波方向,波长,空间比和相位之类的参数。 Gabor过滤器有用的领域之一是检测已知方向的边缘。

预期输出如下:

使用离散傅里叶变换从空间域转到频域(以及返回)

在本秘籍中,您将学习如何使用离散傅立叶变换将灰度图像从空间表示转换为频率表示,然后再转换回去。

准备

安装 OpenCV 3.x Python 包和matplotlib包。

操作步骤

必须执行以下步骤:

  1. 导入所需的包:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 以灰度读取图像并将其转换为np.float32数据类型:
image = cv2.imread('../data/Lena.png', 0).astype(np.float32) / 255
  1. 应用离散傅立叶变换:
fft = cv2.dft(image, flags=cv2.DFT_COMPLEX_OUTPUT)
  1. 可视化频谱图:
shifted = np.fft.fftshift(fft, axes=[0, 1])
magnitude = cv2.magnitude(shifted[:, :, 0], shifted[:, :, 1])
magnitude = np.log(magnitude)

plt.axis('off')
plt.imshow(magnitude, cmap='gray')
plt.tight_layout()
plt.show()
  1. 将图像从频谱转换回空间表示形式:
restored = cv2.idft(fft, flags=cv2.DFT_SCALE | cv2.DFT_REAL_OUTPUT)

工作原理

OpenCV 使用快速傅立叶变换算法(由cv2.dft函数实现)来计算离散傅立叶变换,并将其用于其反向版本(cv2.idft函数)。 这些函数支持可选标志,这些标志指定输出是实数还是复数(分别为标志cv2.DFT_REAL_OUTPUTcv2.DFT_COMPLEX_OUTPUT),以及是否应缩放输出值(使用cv2.DFT_SCALE标志)。 np.fft.fftshift函数以这样的方式移动频谱,即对应于零频率的振幅位于数组的中心,并且更易于解释和进一步使用。

预期输出如下:

在频域中为图像过滤操作图像

在本秘籍中,您将学习如何在频域中操作图像。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

执行以下步骤:

  1. 导入包:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取图像为灰度并将其转换为np.float32数据类型:
image = cv2.imread('../data/Lena.png', 0).astype(np.float32) / 255
  1. 使用离散傅立叶变换将图像从空间域转换为频域:
fft = cv2.dft(image, flags=cv2.DFT_COMPLEX_OUTPUT)
  1. 移位 FFT 结果的方式应使低频位于数组的中心:
fft_shift = np.fft.fftshift(fft, axes=[0, 1])
  1. 将高频的振幅设置为零,而其他振幅保持不变:
sz = 25
mask = np.zeros(fft_shift.shape, np.uint8)
mask[mask.shape[0]//2-sz:mask.shape[0]//2+sz,
     mask.shape[1]//2-sz:mask.shape[1]//2+sz, :] = 1
fft_shift *= mask
  1. 将 DFT 结果移回:
fft = np.fft.ifftshift(fft_shift, axes=[0, 1])
  1. 使用逆离散傅里叶逆变换将滤波后的图像从频域转换回空间域:
filtered = cv2.idft(fft, flags=cv2.DFT_SCALE | cv2.DFT_REAL_OUTPUT)
  1. 可视化原始图像和过滤后的图像:
plt.figure()
plt.subplot(121)
plt.axis('off')
plt.title('original')
plt.imshow(image, cmap='gray')
plt.subplot(122)
plt.axis('off')
plt.title('no high frequencies')
plt.imshow(filtered, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

使用快速傅立叶变换,我们将图像从空间域转换到频域。 然后,我们创建一个遮罩,该遮罩的各处都为零,但中心处的矩形除外。 使用该遮罩,我们将高频的振幅设置为零,然后将图像转换回空间表示形式。

预期输出如下:

对于对频域滤波技术的更多应用感兴趣的读者,请参考第 6 章,“使用运动放大相机查看心跳”

使用不同阈值处理图像

在本秘籍中,您将学习如何使用不同的阈值方法将灰度图像转换为二进制图像。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

执行以下步骤:

  1. 导入包:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像:
image = cv2.imread('../data/Lena.png', 0)
  1. 应用一个简单的二进制阈值:
thr, mask = cv2.threshold(image, 200, 1, cv2.THRESH_BINARY)
print('Threshold used:', thr)
  1. 应用自适应阈值:
adapt_mask = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
                                   cv2.THRESH_BINARY_INV, 11, 10)
  1. 可视化结果:
plt.figure(figsize=(10,3))
plt.subplot(131)
plt.axis('off')
plt.title('original')
plt.imshow(image, cmap='gray')
plt.subplot(132)
plt.axis('off')
plt.title('binary threshold')
plt.imshow(mask, cmap='gray')
plt.subplot(133)
plt.axis('off')
plt.title('adaptive threshold')
plt.imshow(adapt_mask, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

OpenCV 具有许多不同类型的阈值和阈值化方法。 您可以将所有方法分为两类:全局(对所有像素使用相同的阈值)和自适应(对阈值依赖于像素的自适应)。

可以通过cv2.threshold函数使用第一组中的方法,该函数除其他参数外还采用阈值类型(例如cv2.THRESH_BINARYcv.THRESH_BINARY_INV)。

自适应阈值方法可通过cv2.adaptiveThreshold函数获得。 在自适应方法中,每个像素都有其自己的阈值,该阈值取决于周围的像素值。 在前面的代码中,我们使用cv2.ADAPTIVE_THRESH_MEAN_C方法进行阈值估计,该方法计算周围像素的平均值,并将该值减去用户指定的偏差(在我们的情况下为 10)用作逐像素阈值。

前面代码的各种输出应如下所示:

形态运算

在本秘籍中,您将学习如何将基本形态学运算应用于二进制图像。

准备

安装 OpenCV Python API 包和matplotlib包。

操作步骤

按着这些次序:

  1. 导入包:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像并使用大津的方法构建二进制图像:
image = cv2.imread('../data/Lena.png', 0)
_, binary = cv2.threshold(image, -1, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
  1. 使用3x3矩形遮罩施加腐蚀和膨胀 10 次:
eroded = cv2.morphologyEx(binary, cv2.MORPH_ERODE, (3, 3), iterations=10)
dilated = cv2.morphologyEx(binary, cv2.MORPH_DILATE, (3, 3), iterations=10)
  1. 使用类似椭圆的5x5结构元素进行 5 次形态学打开和关闭操作:
opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN,
                          cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)),
                          iterations=5)
closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE,
                          cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)),
                          iterations=5)
  1. 计算形态梯度:
grad = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT,
                          cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
  1. 可视化结果:
plt.figure(figsize=(10,10))
plt.subplot(231)
plt.axis('off')
plt.title('binary')
plt.imshow(binary, cmap='gray')
plt.subplot(232)
plt.axis('off')
plt.title('erode 10 times')
plt.imshow(eroded, cmap='gray')
plt.subplot(233)
plt.axis('off')
plt.title('dilate 10 times')
plt.imshow(dilated, cmap='gray')
plt.subplot(234)
plt.axis('off')
plt.title('open 5 times')
plt.imshow(opened, cmap='gray')
plt.subplot(235)
plt.axis('off')
plt.title('close 5 times')
plt.imshow(closed, cmap='gray')
plt.subplot(236)
plt.axis('off')
plt.title('gradient')
plt.imshow(grad, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

预期输出如下:

图像遮罩和二进制操作

在本秘籍中,您将学习如何使用二进制图像,包括如何应用二进制逐元素操作。

准备

您需要安装带有 Python API 支持的 OpenCV 3.x,以及 matplotlib 包。

操作步骤

此秘籍的步骤如下:

  1. 导入所有包:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 创建带有圆形遮罩的二进制图像:
circle_image = np.zeros((500, 500), np.uint8)
cv2.circle(circle_image, (250, 250), 100, 255, -1)
  1. 创建一个带有矩形遮罩的二进制图像:
rect_image = np.zeros((500, 500), np.uint8)
cv2.rectangle(rect_image, (100, 100), (400, 250), 255, -1)
  1. 使用按位 AND 运算符组合圆形和矩形遮罩:
circle_and_rect_image = circle_image & rect_image
  1. 使用按位或运算符组合圆形和矩形遮罩:
circle_or_rect_image = circle_image | rect_image
  1. 可视化结果:
plt.figure(figsize=(10,10))
plt.subplot(221)
plt.axis('off')
plt.title('circle')
plt.imshow(circle_image, cmap='gray')
plt.subplot(222)
plt.axis('off')
plt.title('rectangle')
plt.imshow(rect_image, cmap='gray')
plt.subplot(223)
plt.axis('off')
plt.title('circle & rectangle')
plt.imshow(circle_and_rect_image, cmap='gray')
plt.subplot(224)
plt.axis('off')
plt.title('circle | rectangle')
plt.imshow(circle_or_rect_image, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

使用np.uint8数组分别对应具有0255值来表示二进制图像(仅包含黑白像素的图像)很方便。 OpenCV 和 NumPy 都支持所有常用的二进制运算符:NOTANDORXOR。 它们可通过别名(例如~&|^)以及通过诸如cv2.bitwise_not/np.bitwise_notcv2.bitwise_and/np.bitwise_and之类的函数使用。

运行前面的代码后,预期输出如下:

三、轮廓和分割

在本章中,我们将介绍以下秘籍:

  • 使用大津算法将灰度图像二值化
  • 在二进制图像中查找外部和内部轮廓
  • 从二进制图像中提取连通组件
  • 将线和圆拟合为二维点集
  • 计算图像矩
  • 使用曲线 - 近似值,长度和面积
  • 检查点是否在轮廓内
  • 计算从每个像素到二维点集的距离
  • 使用 K 均值算法的图像分割
  • 使用分割种子的图像分割,分水岭算法

介绍

像素存储值。 值本身是图像的良好特征-它们可以告诉您有关图像统计信息的信息,但几乎没有其他内容。 值根据图像内容分组在一起-暗到浅的过渡形成边界,边界将场景划分为不同的对象。 边界连接在一起并显示轮廓。 轮廓在许多计算机视觉算法中起着重要作用。 它们帮助找到对象,将某事物的一个实例与另一个实例分开,最后帮助您了解整个场景。

本章阐明了与 OpenCV 中轮廓相关的所有内容。 我们将讨论查找,使用和显示它们的方法,并考虑基本的分割方法。

使用大津算法将灰度图像二值化

当输入图像中只有两个类并且想要在不进行任何手动阈值调整的情况下提取它们时,使用大津的方法将灰度图像转换为二进制图像非常有用。 在本秘籍中,您将学习如何做。

准备

在继续此秘籍之前,您将需要安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

要完成此秘籍,我们需要执行以下步骤:

  1. 导入模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像:
image = cv2.imread('../data/Lena.png', 0)
  1. 使用大津法估计阈值:
otsu_thr, otsu_mask = cv2.threshold(image, -1, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print('Estimated threshold (Otsu):', otsu_thr)
  1. 可视化结果:
plt.figure()
plt.subplot(121)
plt.axis('off')
plt.title('original')
plt.imshow(image, cmap='gray')
plt.subplot(122)
plt.axis('off')
plt.title('Otsu threshold')
plt.imshow(otsu_mask, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

大津的方法以这样一种方式估计灰度图像的阈值:在二值化并将原始图像转换为二进制遮罩之后,两类的总类内差异最小。大津的方法可以在cv2.threshold函数的帮助下使用,它已指定了标志cv2.THRESH_OTSU

预期从前面的代码输出以下内容:

Estimated threshold (Otsu): 116.0

在二进制图像中查找外部和内部轮廓

从二值图像中提取轮廓可以为您提供替代的图像表示,并允许您应用特定于轮廓的图像分析方法。 在本秘籍中,您将学习如何在二进制图像中找到轮廓。

准备

对于此秘籍,请确保已安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载测试二进制图像:
image = cv2.imread('../data/BnW.png', 0)
  1. 找到外部和内部轮廓。 将它们分为两级:
_, contours, hierarchy = cv2.findContours(image, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
  1. 准备外部轮廓二进制掩码:
image_external = np.zeros(image.shape, image.dtype)
for i in range(len(contours)):
    if hierarchy[0][i][3] == -1:
        cv2.drawContours(image_external, contours, i, 
        255, -1)
  1. 准备内部轮廓二进制掩码:
image_internal = np.zeros(image.shape, image.dtype)
for i in range(len(contours)):
    if hierarchy[0][i][3] != -1:
        cv2.drawContours(image_internal, contours, i, 
        255, -1)
  1. 可视化结果:
plt.figure(figsize=(10,3))
plt.subplot(131)
plt.axis('off')
plt.title('original')
plt.imshow(image, cmap='gray')
plt.subplot(132)
plt.axis('off')
plt.title('external')
plt.imshow(image_external, cmap='gray')
plt.subplot(133)
plt.axis('off')
plt.title('internal')
plt.imshow(image_internal, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

使用 OpenCV 函数cv2.findContours提取轮廓。 它支持不同的轮廓提取模式:

  • cv2.RETR_EXTERNAL:仅提取外部轮廓
  • cv2.RETR_CCOMP:用于提取内部和外部轮廓,并将它们组织为两级层次结构
  • cv2.RETR_TREE:用于提取内部和外部轮廓,并将它们组织成树状图
  • cv2.RETR_LIST:用于在不建立任何关系的情况下提取所有轮廓

此外,您可以指定是否需要轮廓压缩(使用cv2.CHAIN_APPROX_SIMPLE将轮廓的垂直和水平部分折叠到各自的端​​点中)(cv2.CHAIN_APPROX_NONE)。

该函数返回三个元素的元组,即修改后的图像,轮廓列表和轮廓层次结构属性列表。 层次结构属性描述了图像轮廓拓扑,每个列表元素是一个四元素元组,包含相同层次结构级别的下一个和上一个轮廓的从零开始的索引,然后分别是第一个子轮廓和第一个父轮廓。 如果没有轮廓,则对应的索引为-1

预期输出如下:

从二进制图像中提取连通组件

二进制图像中的已连接组件是非零值的区域。 每个连通组件的每个元素都被来自同一组件的至少一个其他元素包围。 而且不同的组件不会互相接触,每个组件周围都为零。

连接组件分析可能是图像处理的重要组成部分。 通常(在 OpenCV 中是事实),在图像中查找连通组件比查找所有轮廓要快得多。 因此,可以根据连通组件特征(例如区域,质心位置等)快速排除图像的所有不相关部分,以继续处理其余区域。

此秘籍向您展示如何使用 OpenCV 在二进制图像上查找连通组件。

准备

您需要安装具有 Python API 支持的 OpenCV3.x。

操作步骤

为了执行此秘籍,我们将执行以下步骤:

  1. 首先,我们导入所需的所有模块:
import cv2
import numpy as np
  1. 打开图像并在其中找到连通组件:
img = cv2.imread('../data/BnW.png', cv2.IMREAD_GRAYSCALE)

connectivity = 8
num_labels, labelmap = cv2.connectedComponents(img, connectivity, cv2.CV_32S)
  1. 显示原始图像和带有标签的缩放图像:
img = np.hstack((img, labelmap.astype(np.float32)/(num_labels - 1)))
cv2.imshow('Connected components', img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 打开另一个图像,找到其大津遮罩,并获得连通组件及其统计信息:
img = cv2.imread('../data/Lena.png', cv2.IMREAD_GRAYSCALE)
otsu_thr, otsu_mask = cv2.threshold(img, -1, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

output = cv2.connectedComponentsWithStats(otsu_mask, connectivity, cv2.CV_32S)
  1. 过滤出面积较小的组件并创建彩色图像,在该图像上绘制具有单独颜色的其余组件以及每个组件的中心。 然后,显示结果:
num_labels, labelmap, stats, centers = output

colored = np.full((img.shape[0], img.shape[1], 3), 0, np.uint8)

for l in range(1, num_labels):
    if stats[l][4] > 200:
        colored[labelmap == l] = (0, 255*l/num_labels, 255*num_labels/l)
        cv2.circle(colored, 
                   (int(centers[l][0]), int(centers[l][1])), 5, (255, 0, 0), cv2.FILLED)

img = cv2.cvtColor(otsu_mask*255, cv2.COLOR_GRAY2BGR)

cv2.imshow('Connected components', np.hstack((img, colored)))
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

OpenCV 中有两个函数可用于查找连通组件:cv2.connectedComponentscv2.connectedComponentsWithStats。 两者都采用相同的参数:要查找其组件的二进制图像,连接类型和输出图像的深度,以及组件的标签。 返回值会有所不同。

cv2.connectedComponents更简单,它返回一个组件编号的元组和一个带有组件标签的图像(labelmap)。 除了前一个函数的输出外,cv2.connectedComponentsWithStats还返回有关每个组件及其组件质心位置的统计信息。

标签图具有与输入图像相同的尺寸,并且其每个像素具有根据像素所属的成分在[0,组件编号]范围内的值。 统计量由形状的 Numpy 数组表示(组件编号 5)。 这五个元素对应于(x0y0,宽度,高度,面积)结构。 前四个元素是组件元素的边框的参数,最后一个参数是相应连通组件的面积。 重心的位置也是 Numpy 数组,但是具有形状(组件编号 2),其中每一行代表组件中心的(xy)坐标。

执行代码后,您将获得类似于以下内容的图像:

将直线和圆形拟合为二维点集

许多计算机视觉算法都处理点。 它们可能是轮廓点,关键点或其他东西。 而且,在某些情况下,我们知道所有这些点都应位于同一条曲线上,并具有已知的数学形状。 查找曲线参数的过程(在嘈杂数据的情况下)称为近似。 在这里,我们将回顾来自 OpenCV 的两个函数,它们可以找到一组点的椭圆和直线的近似值。

准备

您需要安装具有 Python API 支持的 OpenCV3.x。

操作步骤

  1. 首先,导入所有模块:
import cv2
import numpy as np
import random
  1. 在要绘制的图像上创建图像,并随机生成椭圆的参数,例如半轴长度和旋转角度:
img = np.full((512, 512, 3), 255, np.uint8)

axes = (int(256*random.uniform(0, 1)), int(256*random.uniform(0, 1)))
angle = int(180*random.uniform(0, 1))
center = (256, 256)
  1. 使用找到的参数为椭圆生成点,并向它们添加随机噪声:
pts = cv2.ellipse2Poly(center, axes, angle, 0, 360, 1)
pts += np.random.uniform(-10, 10, pts.shape).astype(np.int32)
  1. 在图像上绘制椭圆和生成的点,然后显示图像:
cv2.ellipse(img, center, axes, angle, 0, 360, (0, 255, 0), 3)

for pt in pts:
    cv2.circle(img, (int(pt[0]), int(pt[1])), 3, (0, 0, 255))

cv2.imshow('Fit ellipse', img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 找到最适合我们的噪点的椭圆参数,在图像上绘制结果椭圆,然后显示它:
ellipse = cv2.fitEllipse(pts)
cv2.ellipse(img, ellipse, (0, 0, 0), 3)

cv2.imshow('Fit ellipse', img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 创建清晰的图像,为y = x函数生成点,并向它们添加随机噪声:
img = np.full((512, 512, 3), 255, np.uint8)

pts = np.arange(512).reshape(-1, 1)
pts = np.hstack((pts, pts))
pts += np.random.uniform(-10, 10, pts.shape).astype(np.int32)
  1. 绘制y = x函数并生成点; 然后,显示图像:
cv2.line(img, (0,0), (512, 512), (0, 255, 0), 3)

for pt in pts:
    cv2.circle(img, (int(pt[0]), int(pt[1])), 3, (0, 0, 255))

cv2.imshow('Fit line', img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 找到噪声点的直线参数,绘制结果并显示图像:
vx,vy,x,y = cv2.fitLine(pts, cv2.DIST_L2, 0, 0.01, 0.01)
y0 = int(y - x*vy/vx)
y1 = int((512 - x)*vy/vx + y)
cv2.line(img, (0, y0), (512, y1), (0, 0, 0), 3)

cv2.imshow('Fit line', img)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

在 OpenCV 中,不同的函数旨在寻找不同类型曲线的近似值:cv2.fitEllipse表示椭圆,cv2.fitLine表示直线。 两者都执行类似的动作,最小化我们要拟合的曲线到所得曲线的点之间的距离,并且需要一些最小的点数才能拟合(cv2.fitEllipse为 5 点,cv2.fitLine为 2 点)。

cv2.fitEllipse仅接受一组二维点的参数,我们需要为其找到曲线参数,然后返回找到的参数,中心点,半轴长度和旋转角度。 当我们要显示结果时,这些参数可以直接传递到cv2.ellipse绘图函数。

另一个函数cv2.line具有更多参数。 如前所述,它将点设置为第一个参数,同时将最小化的距离函数的类型,控制距离函数的值以及(x0, y0)点和(vx, vy)线系数。 (x0, y0)确定线穿过的点。 该函数返回最适合设定点的线参数的(x0, y0, vx, vy)值。 值得一提的是,cv2.line不仅可以处理二维点,而且还可以处理三维点,并且算法本身对设置点的异常值具有鲁棒性,这是巨大的噪音或错误的结果 。 这两个事实使例程对于实际使用非常方便。 如果我们将三维点传递给cv2.line,那么我们当然会获得三维线的参数。

计算图像的矩

图像矩是根据图像计算出的统计值。 它们使我们能够分析整个图像。 请注意,通常首先要提取轮廓,然后才分别计算和处理每个分量矩,这通常很有用。 在本秘籍中,您将学习如何计算二进制/灰度图像的矩。

准备

您需要安装具有 Python API 支持的 OpenCV3.x。

操作步骤

  1. 导入模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 在黑色背景上绘制一个测试图像-以点(320240)为中心的白色椭圆:
image = np.zeros((480, 640), np.uint8)
cv2.ellipse(image, (320, 240), (200, 100), 0, 0, 360, 255, -1)
  1. 计算矩并打印其值:
m = cv2.moments(image)
for name, val in m.items():
    print(name, '\t', val)
  1. 执行一个简单的测试,以检查计算出的矩是否有意义,并使用图像的第一个矩来计算图像质心。 它必须靠近我们在上面指定的椭圆的中心:
print('Center X estimated:', m['m10'] / m['m00'])
print('Center Y estimated:', m['m01'] / m['m00'])

工作原理

对于二进制或灰度图像,使用 OpenCV 函数cv2.moments计算图像矩。 它返回计算出的矩的字典,并带有各自的名称。

目前,预期以下输出:

nu11     -2.809466679966455e-13
mu12     -422443285.20703125
mu21     -420182048.71875
m11      1237939564800.0
mu20     161575917357.31616
m10      5158101240.0
nu03     1.013174855849065e-10
nu12     -4.049505150683136e-10
nu21     -4.0278291313762605e-10
mu03     105694127.71875
nu30     1.618061841335058e-09
m30      683285449618080.0
nu02     0.00015660970937729079
m20      1812142855350.0
m00      16119315.0
mu02     40692263506.42969
nu20     0.0006218468887998859
m02      969157708320.0
m21      434912202354750.0
m01      3868620810.0
m03      252129278267070.0
mu11     -72.9990234375
mu30     1687957749.125
m12      310125260718570.0

估计的重心如下:

Center X estimated: 319.9950643063927
Center Y estimated: 239.999082467214

可以在这个页面上找到不同图像矩类型的定义。

使用曲线 - 近似值,长度和面积

本秘籍涵盖与曲线特征相关的 OpenCV 函数。 我们将回顾计算曲线长度和面积,获取凸包以及检查曲线是否凸的例程。 另外,我们将研究如何用较少的点数近似轮廓。 当您开发基于轮廓处理的算法时,所有这些事情都将很有用。 通过找到轮廓的不同特征,您可以构建启发式方法以滤除错误的轮廓。 因此,让我们开始吧。

准备

您需要安装具有 Python API 支持的 OpenCV3.x。

操作步骤

  1. 导入所有必需的模块,打开一个图像,然后在屏幕上显示它:
import cv2, random
import numpy as np
img = cv2.imread('bw.png', cv2.IMREAD_GRAYSCALE)
  1. 找到加载的图像的轮廓,绘制它们,然后显示结果:
im2, contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.drawContours(color, contours, -1, (0,255,0), 3)

cv2.imshow('contours', color)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 取第一个轮廓,在各种情况下找到其面积,并输出结果数字:
contour = contours[0]

print('Area of contour is %.2f' % cv2.contourArea(contour))
print('Signed area of contour is %.2f' % cv2.contourArea(contour, True))
print('Signed area of contour is %.2f' % cv2.contourArea(contour[::-1], True))
  1. 找到轮廓的长度,然后打印:
print('Length of closed contour is %.2f' % cv2.arcLength(contour, True))
print('Length of open contour is %.2f' % cv2.arcLength(contour, False))
  1. 找到轮廓的凸包,在图像上绘制并显示:
hull = cv2.convexHull(contour)
cv2.drawContours(color, [hull], -1, (0,0,255), 3)

cv2.imshow('contours', color)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 检查轮廓及其外壳的凸度:
print('Convex status of contour is %s' % cv2.isContourConvex(contour))
print('Convex status of its hull is %s' % cv2.isContourConvex(hull))
  1. 创建一个带有轨迹栏的窗口,以控制轮廓近似的质量,找到轮廓近似并显示结果:
cv2.namedWindow('contours')

img = np.copy(color)

def trackbar_callback(value):
    global img
    epsilon = value*cv2.arcLength(contour, True)*0.1/255
    approx = cv2.approxPolyDP(contour, epsilon, True)
    img = np.copy(color)
    cv2.drawContours(img, [approx], -1, (255,0,255), 3)

cv2.createTrackbar('Epsilon', 'contours', 1, 255, lambda v: trackbar_callback(v))
while True:
    cv2.imshow('contours', img)
    key = cv2.waitKey(3)
    if key == 27: 
        break

cv2.destroyAllWindows()

工作原理

cv2.contourArea计算轮廓的面积,顾名思义。 它以一个表示轮廓的点集作为其第一个参数,并使用一个布尔标志作为其第二个参数。 该例程返回轮廓的浮点面积。 该标志允许我们计算有符号(当True时)或无符号(当False时)区域,其中符号代表轮廓中点的顺时针或逆时针顺序。 关于cv2.contourArea的重要说明是,不能保证该区域对于具有自相交的轮廓是正确的。

获得曲线长度的函数是cv2.arcLength。 它接受两个参数,轮廓是第一个参数,标志是第二个参数。 该标志控制轮廓的闭合性,True意味着轮廓中的第一个点和最后一个点应被视为已连接,因此轮廓被闭合。 否则,第一个点和最后一个点之间的距离不会考虑所得的轮廓周长。

cv2.convexHull可帮助您找到轮廓的凸包。 它以轮廓为参数,并返回其凸包(也是轮廓)。 另外,您可以使用cv2.isContourConvex函数检查轮廓的凸度,只需将轮廓作为参数传递,当传递的轮廓为凸形时,返回值为True

要获得轮廓近似值,应使用cv2.approxPolyDP函数。 该函数实现了 Ramer–Douglas–Peucker 算法,该算法查找具有较少点和一定公差的轮廓。 它具有一个轮廓(应该近似),公差(它是原始轮廓与其近似之间的最大距离)和一个布尔标志(告诉函数是否将近似轮廓视为闭合)。 公差越大,近似值越粗糙,但是保留在结果轮廓中的点越少。 该函数返回指定参数的输入轮廓的近似值。

由于执行代码,您将看到一张紧随其后的图像:

检查点是否在轮廓内

在本秘籍中,我们将发现一种检查点是否在轮廓内或是否属于轮廓边界的方法。

准备

您需要安装具有 Python API 支持的 OpenCV3.x。

操作步骤

  1. 导入所有必需的模块,打开一个图像,然后在屏幕上显示它:
import cv2, random
import numpy as np
img = cv2.imread('bw.png', cv2.IMREAD_GRAYSCALE)
  1. 找到图像的轮廓并显示它们:
im2, contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.drawContours(color, contours, -1, (0,255,0), 3)

cv2.imshow('contours', color)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 定义一个回调函数来处理用户单击图像。 此函数在发生点击的地方绘制一个小圆圈,圆圈的颜色取决于点击是在轮廓内部还是外部:
contour = contours[0]
image_to_show = np.copy(color)
measure = True

def mouse_callback(event, x, y, flags, param): 
    global contour, image_to_show

    if event == cv2.EVENT_LBUTTONUP:
        distance = cv2.pointPolygonTest(contour, (x,y), measure)
        image_to_show = np.copy(color)
        if distance > 0:
            pt_color = (0, 255, 0)
        elif distance < 0:
            pt_color = (0, 0, 255) 
        else:
            pt_color = (128, 0, 128)
        cv2.circle(image_to_show, (x,y), 5, pt_color, -1)
        cv2.putText(image_to_show, '%.2f' % distance, (0, image_to_show.shape[1] - 5), 
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255))
  1. 使用我们的鼠标单击处理器显示图像。 另外,让我们跟踪M按钮的按下,以切换由于cv2.pointPolygonTest函数而得到的结果的模式:
cv2.namedWindow('contours')
cv2.setMouseCallback('contours', mouse_callback)

while(True):
 cv2.imshow('contours', image_to_show)
 k = cv2.waitKey(1)

 if k == ord('m'):
     measure = not measure
 elif k == 27:
     break

cv2.destroyAllWindows()

工作原理

OpenCV 中有一个特殊函数,可测量从点到轮廓的最小距离。 称为cv2.pointPolygonTest。 它接受三个参数,并返回测得的距离。 参数是轮廓,点和布尔标志,我们将在稍后讨论它们的目的。 结果距离可以为正,负或等于零,分别对应于轮廓内部,轮廓外部或轮廓点位置。 最后一个布尔参数确定我们的函数返回的是精确距离还是仅返回具有值的指示符(+1; 0; -1)。 指示器的符号与计算精确距离的模式具有相同的含义。

作为代码的结果,您将获得与此图像相似的内容:

计算图像的距离

在本秘籍中,您将学习如何计算距每个图像像素最接近的非零像素的距离。 此函数可用于以自适应方式执行图像处理,例如,根据到最近边缘的距离模糊具有不同强度的图像。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 在白色背景上绘制测试图像-黑色圆圈(无填充):
image = np.full((480, 640), 255, np.uint8)
cv2.circle(image, (320, 240), 100, 0)
  1. 计算从每个点到圆的距离:
distmap = cv2.distanceTransform(image, cv2.DIST_L2, cv2.DIST_MASK_PRECISE)
  1. 可视化结果:
plt.figure()
plt.imshow(distmap, cmap='gray')
plt.show()

工作原理

可以使用 OpenCV cv2.distanceTransform函数来计算距离图。 它计算到最接近的零像素的指定距离类型(cv2.DIST_L1cv2.DIST_L2cv2.DIST_C)。 您还可以更改用于计算近似距离的遮罩大小(可用选项为cv2.DIST_MASK_3cv2.DIST_MASK_5)。 您还可以使用cv2.DIST_MASK_PRECISE标志,这将导致计算的不是近似的,而是精确的距离。

预期输出如下:

使用 K 均值算法的图像分割

有时,图像中像素的颜色可以帮助确定语义上相邻区域的位置。 例如,在某些情况下,路面可能具有几乎相同的颜色。 通过颜色,我们可以找到所有道路像素。 但是,如果我们不知道道路的颜色怎么办? 在这里,K 均值聚类算法开始起作用。 该算法只需要知道一个图像中有多少个群集,或者换句话说,我们想要一个图像中有多少个群集。 有了这些信息,它可以自动找到最佳的群集。 在本秘籍中,我们将考虑如何使用 OpenCV 应用 K 均值图像分割。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入必要的模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 打开图像并将其转换为 Lab 颜色空间:
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255.
image_lab = cv2.cvtColor(image, cv2.COLOR_BGR2Lab)
  1. 将图像重塑为向量:
data = image_lab.reshape((-1, 3))
  1. 定义聚类数和完成分割过程的条件。 然后,执行 K 均值聚类:
num_classes = 4
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 50, 0.1)
_, labels, centers = cv2.kmeans(data, num_classes, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
  1. 将质心的颜色应用于与这些质心相关的所有像素。 然后,将分割后的图像重新成形为原始形状。 然后,将其转换为 RGB 颜色空间:
segmented_lab = centers[labels.flatten()].reshape(image.shape)
segmented = cv2.cvtColor(segmented_lab, cv2.COLOR_Lab2RGB)
  1. 一起显示原始图像和分段图像:
plt.subplot(121)
plt.axis('off')
plt.title('original')
plt.imshow(image[:, :, [2, 1, 0]])
plt.subplot(122)
plt.axis('off')
plt.title('segmented')
plt.imshow(segmented)
plt.show()

工作原理

要执行 K 均值聚类,我们应该使用cv2.kmeans函数。 它分别接受以下参数,输入数据,集群数量,带有标签的输入/输出数组(可以设置为None),停止过程标准,尝试次数以及用于控制集群过程的标志 。

让我们讨论每个论点。 输入数据必须是具有浮点值的点的向量,在本例中,我们具有三维点。 群集的数量决定了我们将在结果中得到多少群集,值越大,群集的数量越大,但是噪声的影响越大。 带有标签的输入/输出数组既可以用于确定聚类的初始位置,也可以用于获取结果聚类。 如果我们不想指定集群中心初始化,则应将此参数设置为None。 停止过程标准确定了尝试找到最佳聚类位置时分段过程的工作时间。 尝试次数定义了从不同的群集初始化启动群集过程的次数,以便以后选择最佳尝试。 这些标志确定集群初始化的类型; 可以使用cv2.KMEANS_RANDOM_CENTERS进行随机初始化,使用cv2.KMEANS_PP_CENTERS进行更复杂的初始化(kmeans ++算法),以及使用cv2.KMEANS_USE_INITIAL_LABELS传递用户指定的集群中心(在这种情况下,第三个参数不能为None)。

该函数为每个聚类返回紧凑性的双精度值,带有标签的向量以及每个标签的值。 群集的紧密度是每个群集点到相应中心的平方距离的总和。 带标签的向量的长度与输入数据向量的长度相同,并且其每个元素表示一个输出群集,该群集已设置为输入数据中的相应位置。 每个标签的值是聚类中心的值。

在此秘籍中,由于其将颜色信息和亮度信息分开的特性,因此使用了 Lab 颜色空间。 在 RGB 空间中,颜色和亮度在所有通道中混合在一起,但这会对分割过程产生负面影响。

请注意,在处理uint8图像时,OpenCV 将线性处理应用于 Lab 颜色空间值。 因此,在色彩空间之间进行转换时必须小心。 对于float32图像,像素值必须保持不变。 参见这里

启动代码后,您将获得类似于以下内容的图像:

使用分割种子的图像分割 - 分水岭算法

当我们有初始的分割点并想用相同的分割类自动填充周围区域时,将使用图像分割的分水岭算法。 这些初始的分割点称为种子,应该手动设置它们,但是在某些情况下,可以自动分配它们。 此秘籍展示了如何在 OpenCV 中实现分水岭分割算法。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入必要的模块和函数:
import cv2, random
import numpy as np
from random import randint
  1. 加载图像以进行分段并创建其副本,并创建其他图像以存储种子和分段结果:
img = cv2.imread('../data/Lena.png')
show_img = np.copy(img)

seeds = np.full(img.shape[0:2], 0, np.int32)
segmentation = np.full(img.shape, 0, np.uint8)
  1. 定义种子类型的数量,每种种子类型的颜色以及一些与鼠标事件一起使用的变量:
n_seeds = 9

colors = []
for m in range(n_seeds):
    colors.append((255 * m / n_seeds, randint(0, 255), randint(0, 255)))

mouse_pressed = False
current_seed = 1
seeds_updated = False
  1. 实现鼠标回调函数以处理鼠标事件; 让我们通过按下按钮拖动鼠标在图像上绘制种子:
def mouse_callback(event, x, y, flags, param):
    global mouse_pressed, seeds_updated

    if event == cv2.EVENT_LBUTTONDOWN:
        mouse_pressed = True
        cv2.circle(seeds, (x, y), 5, (current_seed), cv2.FILLED)
        cv2.circle(show_img, (x, y), 5, colors[current_seed - 1], 
        cv2.FILLED)
        seeds_updated = True

    elif event == cv2.EVENT_MOUSEMOVE:
        if mouse_pressed:
            cv2.circle(seeds, (x, y), 5, (current_seed), cv2.FILLED)
            cv2.circle(show_img, (x, y), 5, colors[current_seed - 
            1], cv2.FILLED)
            seeds_updated = True

    elif event == cv2.EVENT_LBUTTONUP:
        mouse_pressed = False
  1. 创建所有必要的窗口,设置回调,显示图像,并跟踪循环中按下的键盘按钮。 让我们通过按数字来更改当前种子以进行绘制。 并且,在完成种子更改过程后,使用分水岭算法对图像进行分割:
cv2.namedWindow('image')
cv2.setMouseCallback('image', mouse_callback)

while True:
    cv2.imshow('segmentation', segmentation)
    cv2.imshow('image', show_img)

    k = cv2.waitKey(1)

    if k == 27:
        break
    elif k == ord('c'):
        show_img = np.copy(img)
        seeds = np.full(img.shape[0:2], 0, np.int32)
        segmentation = np.full(img.shape, 0, np.uint8)
    elif k > 0 and chr(k).isdigit():
        n = int(chr(k))
        if 1 <= n <= n_seeds and not mouse_pressed:
            current_seed = n

    if seeds_updated and not mouse_pressed: 
        seeds_copy = np.copy(seeds)
        cv2.watershed(img, seeds_copy)
        segmentation = np.full(img.shape, 0, np.uint8)
        for m in range(n_seeds):
            segmentation[seeds_copy == (m + 1)] = colors[m]

        seeds_updated = False

cv2.destroyAllWindows()

工作原理

cv2.watershed函数实现该算法,并接受两个参数,即要分割的图像和初始种子。 分割的图像应为彩色和 8 位。 种子应以与分割后的图像相同的空间大小存储在图像中,但只有一个通道和不同的深度int32。 第二个参数中应使用不同的数字表示不同的种子,其他像素应设置为零。 该例程用相关的邻近种子填充种子图像中的零值。

从该秘籍启动代码后,您将看到类似于以下图像:

四、目标检测与机器学习

在本章中,我们将介绍以下秘籍:

  • 使用 GrabCut 算法获取对象遮罩
  • 使用 Canny 算法查找边缘
  • 使用霍夫变换检测直线和圆
  • 通过模板匹配查找对象
  • 实时中值流对象跟踪器
  • 通过跟踪 API 使用不同的算法跟踪对象
  • 计算两个帧之间的密集光流
  • 检测棋盘和圆形网格图案
  • 使用 SVM 模型的简单行人探测器
  • 使用不同的机器学习模型进行光学字符识别
  • 使用 Haar/LBP 级联检测人脸
  • 为 AR 应用检测 AruCo 模式
  • 在自然场景中检测文字
  • QR 码检测器和识别器

介绍

我们的世界包含许多物体。 每种类型的对象都有其自己的特征,这些特征使它与某些类型区别开来,同时又使其与其他类型相似。 通过其中的对象了解场景是计算机视觉的关键任务。 能够查找和跟踪各种对象,检测基本模式和复杂结构以及识别文本是具有挑战性和有用的技能,本章讨论有关如何通过 OpenCV 功能实现和使用它们的问题。

我们将回顾对几何图元(如直线,圆和棋盘)以及更复杂的对象(如行人,人脸,AruCo 和 QR 码图案)的检测。 我们还将执行对象跟踪任务。

使用 GrabCut 算法获取对象遮罩

在某些情况下,我们希望将对象与场景的其他部分分开; 换句话说,我们要为前景和背景创建遮罩。 这项工作由 GrabCut 算法解决。 它可以在半自动模式下构建对象遮罩。 它所需要的只是关于对象位置的初始假设。 基于这些假设,该算法执行多步迭代过程,以对前景像素和背景像素的统计分布进行建模,并根据这些分布找到最佳划分。 这听起来很复杂,但是用法非常简单。 让我们找出在 OpenCV 中应用这种复杂算法的难易程度。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

操作步骤

  1. 导入模块:
import cv2
import numpy as np
  1. 打开图像并定义鼠标回调函数以在图像上绘制一个矩形:
img = cv2.imread('../data/Lena.png', cv2.IMREAD_COLOR)
show_img = np.copy(img)

mouse_pressed = False
y = x = w = h = 0

def mouse_callback(event, _x, _y, flags, param):
    global show_img, x, y, w, h, mouse_pressed

    if event == cv2.EVENT_LBUTTONDOWN:
        mouse_pressed = True
        x, y = _x, _y
        show_img = np.copy(img)

    elif event == cv2.EVENT_MOUSEMOVE:
        if mouse_pressed:
            show_img = np.copy(img)
            cv2.rectangle(show_img, (x, y),
                          (_x, _y), (0, 255, 0), 3)

    elif event == cv2.EVENT_LBUTTONUP:
        mouse_pressed = False
        w, h = _x - x, _y - y
  1. 显示图像,并在完成矩形并按下键盘上的A按钮之后,使用以下代码关闭窗口:
cv2.namedWindow('image')
cv2.setMouseCallback('image', mouse_callback)

while True:
    cv2.imshow('image', show_img)
    k = cv2.waitKey(1)

    if k == ord('a') and not mouse_pressed:
        if w*h > 0:
            break

cv2.destroyAllWindows()
  1. 调用cv2.grabCut基于绘制的矩形创建对象遮罩。 然后,创建对象掩码并将其定义为:
labels = np.zeros(img.shape[:2],np.uint8)

labels, bgdModel, fgdModel = cv2.grabCut(img, labels, (x, y, w, h), None, None, 5, cv2.GC_INIT_WITH_RECT)

show_img = np.copy(img)
show_img[(labels == cv2.GC_PR_BGD)|(labels == cv2.GC_BGD)] //= 3

cv2.imshow('image', show_img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 定义鼠标回调以在图像上绘制遮罩。 有必要修复先前的cv2.grabCut调用中的错误:
label = cv2.GC_BGD
lbl_clrs = {cv2.GC_BGD: (0,0,0), cv2.GC_FGD: (255,255,255)}

def mouse_callback(event, x, y, flags, param):
    global mouse_pressed

    if event == cv2.EVENT_LBUTTONDOWN:
        mouse_pressed = True
        cv2.circle(labels, (x, y), 5, label, cv2.FILLED)
        cv2.circle(show_img, (x, y), 5, lbl_clrs[label], cv2.FILLED)

    elif event == cv2.EVENT_MOUSEMOVE:
        if mouse_pressed:
            cv2.circle(labels, (x, y), 5, label, cv2.FILLED)
            cv2.circle(show_img, (x, y), 5, lbl_clrs[label], cv2.FILLED)

    elif event == cv2.EVENT_LBUTTONUP:
        mouse_pressed = False
  1. 带遮罩显示图像; 使用白色绘制将对象像素标记为背景的位置,使用黑色绘制将背景区域标记为对象的位置。 然后,再次调用cv2.grabCut以获取固定的掩码。 最后,更新图像上的遮罩,并显示它:
cv2.namedWindow('image')
cv2.setMouseCallback('image', mouse_callback)

while True:
    cv2.imshow('image', show_img)
    k = cv2.waitKey(1)

    if k == ord('a') and not mouse_pressed:
        break
    elif k == ord('l'):
        label = cv2.GC_FGD - label

cv2.destroyAllWindows()

labels, bgdModel, fgdModel = cv2.grabCut(img, labels, None, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_MASK)

show_img = np.copy(img)
show_img[(labels == cv2.GC_PR_BGD)|(labels == cv2.GC_BGD)] //= 3

cv2.imshow('image', show_img)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

OpenCV 的cv2.grabCut实现了 GrabCut 算法。 此函数可以在多种模式下工作,并采用以下参数:输入 3 通道图像,带有像素初始标签的矩阵,(xywh)格式来定义标签初始化,两个用于存储进程状态的矩阵,多次迭代以及我们希望函数启动的模式。

该函数返回带有过程状态的标签矩阵和两个矩阵。 标签矩阵是单通道的,并且在每个像素中存储以下值之一:cv2.GC_BGD(这意味着像素绝对属于背景),cv2.GC_PR_BGD(这意味着像素可能位于背景中) ,cv2.GC_PR_FGD(对于可能是前景的像素),cv2.GC_FGD(对于肯定是前景的像素)。 如果我们要继续进行几次迭代,则需要两个状态矩阵。

该函数有三种可能的模式:cv2.GC_INIT_WITH_RECTcv2.GC_INIT_WITH_MASKcv2.GC_EVAL。 当我们想通过第三个参数中的矩形定义标签初始化时,使用第一个。 在这种情况下,将矩形外部的像素设置为cv2.GC_BGD值,将矩形内部的像素设置为cv2.GC_PR_FGD值。

当我们要使用第二个参数矩阵的值作为标签的初始化时,使用函数的第二种模式cv2.GC_INIT_WITH_MASK。 在这种情况下,这些值应设置为以下四个值之一:cv2.GC_BGDcv2.GC_PR_BGDcv2.GC_PR_FGDcv2.GC_FGD

第三种模式cv2.GC_EVAL用于以相同状态调用该函数进行另一次迭代。

在代码中,我们将背景变暗以可视化对象遮罩。 当我们要分割的对象具有与图像其他部分相似的亮度时,它可以很好地工作。 但是,在明亮的场景中有深色物体的情况下,它将不起作用。 因此,您可能需要在自己的项目中应用另一种可视化技术。

启动该代码的结果是,您将获得类似于以下内容的图片:

使用 Canny 算法查找边缘

边缘是一种有用的图像特征,可以在许多计算机视觉应用中使用。 在本秘籍中,您将学习如何使用 Canny 算法检测图像中的边缘。

准备

安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

这是完成此秘籍所需的步骤:

  1. 导入模块:
import cv2
import matplotlib.pyplot as plt
  1. 加载测试图像:
image = cv2.imread('../data/Lena.png')
  1. 使用 Canny 算法检测边缘:
edges = cv2.Canny(image, 200, 100)
  1. 可视化结果:
plt.figure(figsize=(8,5))
plt.subplot(121)
plt.axis('off')
plt.title('original')
plt.imshow(image[:,:,[2,1,0]])
plt.subplot(122)
plt.axis('off')
plt.title('edges')
plt.imshow(edges, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

Canny 边缘检测是计算机视觉中非常强大且流行的工具。 它以 John F. Canny 的名字命名,他于 1986 年提出了该算法。OpenCV 在函数cv2.Canny中实现了该算法。 您必须在此函数中为梯度幅度指定两个阈值:第一个阈值用于检测强边缘,第二个阈值用于滞后过程,在该过程中将生长强边缘。

预期输出如下:

使用霍夫变换检测直线和圆

在本秘籍中,您将学习如何应用霍夫变换来检测直线和圆。 当您需要执行基本图像分析并在图像中查找图元时,这是一种有用的技术。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入模块:
import cv2
import numpy as np
  1. 绘制测试图像:
img = np.zeros((500, 500), np.uint8)
cv2.circle(img, (200, 200), 50, 255, 3)
cv2.line(img, (100, 400), (400, 350), 255, 3)
  1. 使用概率霍夫变换检测线:
lines = cv2.HoughLinesP(img, 1, np.pi/180, 100, 100, 10)[0]
  1. 使用霍夫变换检测圆:
circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 15, param1=200, param2=30)[0]
  1. 绘制检测到的直线和圆:
dbg_img = np.zeros((img.shape[0], img.shape[1], 3), np.uint8) 
for x1, y1, x2, y2 in lines:
    print('Detected line: ({} {}) ({} {})'.format(x1, y1, x2, y2))
    cv2.line(dbg_img, (x1, y1), (x2, y2), (0, 255, 0), 2) 

for c in circles:
    print('Detected circle: center=({} {}), radius={}'.format(c[0], c[1], c[2]))
    cv2.circle(dbg_img, (c[0], c[1]), c[2], (0, 255, 0), 2)
  1. 可视化结果:
plt.figure(figsize=(8,4))
plt.subplot(121)
plt.title('original')
plt.axis('off')
plt.imshow(img, cmap='gray')
plt.subplot(122)
plt.title('detected primitives')
plt.axis('off')
plt.imshow(dbg_img)
plt.show()

工作原理

霍夫变换是一种用于检测参数化并以方便的数学形式表示的任何形状的技术。 基本上,对于源图像中的每个像素,霍夫变换都会找到一组满足观察结果的模型参数并将其存储在表中。 每个像素为可能模型的子集投票。 输出检测通过投票程序获得。

线的检测在函数cv2.HoughLineP中实现。 实际上,它没有实现原始的 Hough 变换,而是实现了优化的概率版本。 该函数采用诸如源图像,投票空间空间分辨率,投票空间角分辨率,最小投票阈值,最小行长和同一行上的点之间最大允许间隙之类的参数来链接它们,并返回检测到的行的列表,格式为start_pointend_point

圆的检测在函数cv2.HoughCircles中实现。 它采用输入源图像,检测方法(目前仅支持cv2.HOUGH_GRADIENT),逆投票空间分辨率,检测到的圆心之间的最小距离以及两个可选参数:第一个是 Canny 边缘检测程序的较高阈值,第二个是票数阈值。

预期上面的代码输出如下:

Detected line: (99 401) (372 353)
Detected circle: center=(201.5 198.5), radius=50.400001525878906

输出如下:

通过模板匹配查找对象

在图像中找到对象并不是一件容易的事,由于各种表示形式,同一实例看起来可能有很大的不同,乍一看,需要一些复杂的计算机视觉算法。 但是,如果我们限制此问题,则可以通过相对简单的方法成功解决该任务。 在本秘籍中,我们考虑在图像上查找与某些模板相对应的对象的方法。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

操作步骤

执行以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 加载图像并定义鼠标回调函数以选择图像 ROI。 所绘制矩形的内部将是我们用于匹配的模板:
img = cv2.imread('../data/Lena.png', cv2.IMREAD_COLOR)
show_img = np.copy(img)

mouse_pressed = False
y = x = w = h = 0

def mouse_callback(event, _x, _y, flags, param):
    global show_img, x, y, w, h, mouse_pressed

    if event == cv2.EVENT_LBUTTONDOWN:
        mouse_pressed = True
        x, y = _x, _y
        show_img = np.copy(img)

    elif event == cv2.EVENT_MOUSEMOVE:
        if mouse_pressed:
            show_img = np.copy(img)
            cv2.rectangle(show_img, (x, y),
                          (_x, _y), (0, 255, 0), 2)

    elif event == cv2.EVENT_LBUTTONUP:
        mouse_pressed = False
        w, h = _x - x, _y - y
  1. 显示图像,用鼠标选择要查找的对象,然后按A按钮完成该过程并获取模板:
cv2.namedWindow('image')
cv2.setMouseCallback('image', mouse_callback)

while True:
    cv2.imshow('image', show_img)
    k = cv2.waitKey(1)

    if k == ord('a') and not mouse_pressed:
        if w*h > 0:
            break

cv2.destroyAllWindows()

template = np.copy(img[y:y+h, x:x+w])
  1. 显示图像并处理按钮按下事件。 从 0 到 5 的数字决定了我们用来在图像上查找与模板相似的区域的方法。 匹配通过cv2.matchTemplate函数执行。 匹配完成后,我们将找到具有最高(或最低)相似性度量的点,并得出检测结果:
methods = ['cv2.TM_CCOEFF', 'cv2.TM_CCOEFF_NORMED', 'cv2.TM_CCORR',
            'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF', 'cv2.TM_SQDIFF_NORMED']

show_img = np.copy(img)

while True:
    cv2.imshow('image', show_img)
    k = cv2.waitKey()

    if k == 27:
        break
    elif k > 0 and chr(k).isdigit():
        index = int(chr(k))
        if 0 <= index < len(methods):
            method = methods[index]

            res = cv2.matchTemplate(img, template, eval(method))

            res = cv2.normalize(res, None, 0, 1, cv2.NORM_MINMAX)

            if index >= methods.index('cv2.TM_SQDIFF'):
                loc = np.where(res < 0.01)
            else:
                loc = np.where(res > 0.99)

            show_img = np.copy(img)
            for pt in zip(*loc[::-1]):
                cv2.rectangle(show_img, pt, (pt[0] + w, pt[1] + h), 
                              (0, 0, 255), 2)

            res = cv2.resize(res, show_img.shape[:2])*255
            res = cv2.cvtColor(res, cv2.COLOR_GRAY2BGR).astype(np.uint8)
            cv2.putText(res, method, (0, 30), cv2.FONT_HERSHEY_SIMPLEX, 
                        1, (0, 0, 255), 3)

            show_img = np.hstack((show_img, res))

cv2.destroyAllWindows()

工作原理

cv2.matchTemplate用于查找与模板相似的图像区域。 可以使用不同的方法(通过不同的数学运算来确定模板和图像上的色块之间的差异)来确定相似性。 但是,这些方法都无法找到具有不同比例或方向的模板。

此函数获取源图像,搜索模板以及补丁和模板比较的方法。 该方法由以下值确定:cv2.TM_CCOEFFcv2.TM_CCOEFF_NORMEDcv2.TM_CCORRcv2.TM_CCORR_NORMEDcv2.TM_SQDIFFcv2.TM_SQDIFF_NORMED。 名称中带有CCOEFF的方法使用相关系数计算,就像相似性度量一样-值越大,区域越相似。 使用CCORR的方法使用互相关计算来比较色块,使用SQDIFF的方法找到区域之间的平方差以进行比较。

该函数返回输入图像中所选相似性度量的分布。 返回的图像是一个单通道浮点,具有空间大小(W-w+1H-h+1),其中大写字母代表输入图像尺寸,小写字母代表模板尺寸。 返回图像的内容取决于我们使用的方法,对于具有相关性计算的方法,值越大表示匹配越好。 并且,顾名思义,具有平方差用法的方法具有最小的值作为完美匹配。

使用相关系数计算的方法给出的失配最少,但需要更多的计算。 平方差方法需要较少的计算,但结果却不太可靠。 如下图所示,这可能适用于小的和/或无特征的补丁。

执行代码后,您将获得类似于以下图像的图像(取决于模板和所选的方法):

Median Flow 跟踪器

在本秘籍中,我们将应用 Median Flow 对象跟踪器来跟踪视频中的对象。 该跟踪器可以实时工作(在现代硬件上甚至更快),并且可以准确,稳定地完成其工作。 另外,该跟踪器具有不错的功能,可以确定跟踪失败。 让我们看看如何在应用中使用它。

准备

在继续此秘籍之前,您需要安装带有 OpenCV Contrib 模块的 OpenCV 3.x Python API 包。

操作步骤

此秘籍的步骤为:

  1. 导入所有必要的模块:
import cv2
import numpy as np
  1. 打开视频文件,读取其框架,然后选择要跟踪的对象:
cap = cv2.VideoCapture("../data/traffic.mp4")

_, frame = cap.read()

bbox = cv2.selectROI(frame, False, True)

cv2.destroyAllWindows()
  1. 创建 Median Flow 跟踪器,并使用视频中的第一帧和我们选择的边界框对其进行初始化。 然后,一一读取剩余的帧,将它们输入到跟踪器中,并为每个帧获得一个新的边框。 显示边界框,以及“中值流”算法每秒能够处理的帧数:
tracker = cv2.TrackerMedianFlow_create()
status_tracker = tracker.init(frame, bbox)
fps = 0

while True:
    status_cap, frame = cap.read()
    if not status_cap:
        break

    if status_tracker:
        timer = cv2.getTickCount()
        status_tracker, bbox = tracker.update(frame)

    if status_tracker:
        x, y, w, h = [int(i) for i in bbox]
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 15)
        fps = cv2.getTickFrequency() / (cv2.getTickCount() - timer);
        cv2.putText(frame, "FPS: %.0f" % fps, (0, 80), cv2.FONT_HERSHEY_SIMPLEX, 3.5, (0, 0, 0), 8);
    else:
        cv2.putText(frame, "Tracking failure detected", (0, 80), cv2.FONT_HERSHEY_SIMPLEX, 3.5, (0,0,255), 8)

    cv2.imshow("MedianFlow tracker", frame)

    k = cv2.waitKey(1)

    if k == 27: 
        break

cv2.destroyAllWindows()

工作原理

要创建中位数流跟踪器,我们需要使用cv2.TrackerMedianFlow_create。 此函数返回跟踪器的实例。 接下来,应该使用要跟踪的对象来初始化跟踪器。 这可以通过init函数调用来完成。 对于跟踪器实例,应使用以下参数调用此函数:具有要跟踪的对象的框架和([xy,宽度,高度的对象的边界框 )格式。 如果初始化成功完成,则函数返回True

当我们有一个新框架时,我们想要为该对象获得一个新的边界框。 为此,我们需要使用新框架作为参数来调用跟踪器实例的update函数。 从该例程返回的值是跟踪器状态和新的边界框,格式仍为(xy,宽度,高度)。 跟踪器的状态是布尔变量,它显示跟踪器是否继续跟踪对象或跟踪过程是否失败。

值得一提的是cv2.selectROI函数。 它可以帮助您使用鼠标和键盘轻松选择图像上的区域。 它接受我们要选择 ROI 的图像,标志表示我们是否要网格化,标志指定 ROI 选择模式(从左上角或从中心)。 调用此函数后,图像将出现在屏幕上,您将能够单击并拖动鼠标以绘制一个矩形。 选择过程完成后,只需按键盘上的空格键,您将获得所选矩形的参数作为返回值。

启动前面的代码并选择一个对象后,您将看到如何在视频中跟踪该对象。 以下显示了几帧,并带有跟踪结果:

如您所见,此跟踪器成功处理了对象比例的更改,并在丢失跟踪对象时报告。

使用跟踪 API 和不同的算法跟踪对象

在本秘籍中,您将学习如何使用 OpenCV 跟踪贡献模块中实现的不同跟踪算法。 不同的跟踪算法在准确率,可靠性和速度方面具有不同的属性。 使用跟踪 API,您可以尝试找到最适合您的需求的 API。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包和 matplotlib 包。 OpenCV 必须使用 Contrib 模块构建,因为跟踪 API 并非主要 OpenCV 存储库的一部分。

操作步骤

要完成此秘籍,请执行以下步骤:

  1. 导入模块:
import cv2
  1. 创建主窗口并在不同的跟踪器上循环:
cv2.namedWindow('frame')

for name, tracker in (('KCF', cv2.TrackerKCF_create), 
                      ('MIL', cv2.TrackerMIL_create), 
                      ('TLD', cv2.TrackerTLD_create)):
    tracker = tracker()
    initialized = False
  1. 打开测试视频文件,然后选择一个对象:
video = cv2.VideoCapture('../data/traffic.mp4')
bbox = (878, 266, 1153-878, 475-266)
  1. 跟踪直到视频结束或按下Esc,并可视化当前跟踪的对象:
    while True:
        t0 = time.time()
        ok, frame = video.read()
        if not ok: 
            break

        if initialized:
            tracked, bbox = tracker.update(frame)
        else:
            cv2.imwrite('/tmp/frame.png', frame)
            tracked = tracker.init(frame, bbox)
            initialized = True

        fps = 1 / (time.time() - t0)
        cv2.putText(frame, 'tracker: {}, fps: {:.1f}'.format(name, fps),
                    (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2) 
        if tracked:
            bbox = tuple(map(int, bbox))
            cv2.rectangle(frame, (bbox[0], bbox[1]), 
                          (bbox[0]+bbox[2], bbox[1]+bbox[3]), 
                          (0, 255, 0), 3)
        cv2.imshow('frame', frame)
        if cv2.waitKey(3) == 27:
            break
  1. 关闭窗口:
cv2.destroyAllWindows()

工作原理

OpenCV 跟踪 API 提供对许多不同跟踪算法的访问,例如中位数流,核化相关过滤器KCF),跟踪学习检测TLD)等。 可以通过cv2.TrackerKCF_create方法实例化跟踪器(可以代替 KCF 来指定任何其他受支持的跟踪算法名称)。 必须为第一帧初始化跟踪模型,并使用方法tracker.init指定初始对象位置。 之后,必须使用tracker.update方法处理每个帧,该方法将返回跟踪状态和被跟踪对象的当前位置。

经过几个步骤,预计会得到以下输出(帧速率数字显然取决于硬件):

计算两个帧之间的密集光流

光流是一系列算法,用于解决在两个图像(通常是视频中的后续帧)之间寻找点的运动的问题。 密集光流算法可以找到一帧中所有像素的运动。 密集的光流可用于查找在一系列帧中移动的对象,或检测相机的移动。 在本秘籍中,我们将发现如何使用 OpenCV 函数以几种方式计算和显示密集的光流。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

操作步骤

您需要执行以下步骤:

  1. 导入我们将要使用的模块:
import cv2
import numpy as np
  1. 定义函数以显示光流:
def display_flow(img, flow, stride=40): 
    for index in np.ndindex(flow[::stride, ::stride].shape[:2]):
        pt1 = tuple(i*stride for i in index)
        delta = flow[pt1].astype(np.int32)[::-1]
        pt2 = tuple(pt1 + 10*delta)
        if 2 <= cv2.norm(delta) <= 10:
            cv2.arrowedLine(img, pt1[::-1], pt2[::-1], (0,0,255), 5, cv2.LINE_AA, 0, 0.4)

    norm_opt_flow = np.linalg.norm(flow, axis=2)
    norm_opt_flow = cv2.normalize(norm_opt_flow, None, 0, 1, cv2.NORM_MINMAX)

    cv2.imshow('optical flow', img)
    cv2.imshow('optical flow magnitude', norm_opt_flow)
    k = cv2.waitKey(1)

    if k == 27:
        return 1
    else:
        return 0
  1. 打开视频并获取其第一帧。 接下来,逐帧读取帧,并使用 Gunnar Farneback 的算法计算密集的光流。 然后,显示结果:
cap = cv2.VideoCapture("../data/traffic.mp4")
_, prev_frame = cap.read()

prev_frame = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
prev_frame = cv2.resize(prev_frame, (0,0), None, 0.5, 0.5)
init_flow = True

while True:
    status_cap, frame = cap.read()
    frame = cv2.resize(frame, (0,0), None, 0.5, 0.5)
    if not status_cap:
        break
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    if init_flow:
        opt_flow = cv2.calcOpticalFlowFarneback(prev_frame, gray, None, 
                                                0.5, 5, 13, 10, 5, 1.1, 
                                                cv2.OPTFLOW_FARNEBACK_GAUSSIAN)
        init_flow = False
    else:
        opt_flow = cv2.calcOpticalFlowFarneback(prev_frame, gray, opt_flow, 
                                                0.5, 5, 13, 10, 5, 1.1, 
                                                cv2.OPTFLOW_USE_INITIAL_FLOW)

    prev_frame = np.copy(gray)

    if display_flow(frame, opt_flow):
        break;

cv2.destroyAllWindows()
  1. 将视频捕获的位置设置为开始,然后读取第一帧。 创建一个可计算 Dual TV L1 光流的类的实例。 然后,一帧一帧地读取帧,并获取随后每对帧的光通量; 显示结果:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
_, prev_frame = cap.read()

prev_frame = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
prev_frame = cv2.resize(prev_frame, (0,0), None, 0.5, 0.5)

flow_DualTVL1 = cv2.createOptFlow_DualTVL1()

while True:
    status_cap, frame = cap.read()
    frame = cv2.resize(frame, (0,0), None, 0.5, 0.5)
    if not status_cap:
        break
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    if not flow_DualTVL1.getUseInitialFlow():
        opt_flow = flow_DualTVL1.calc(prev_frame, gray, None)
        flow_DualTVL1.setUseInitialFlow(True)
    else:
        opt_flow = flow_DualTVL1.calc(prev_frame, gray, opt_flow)

    prev_frame = np.copy(gray)

    if display_flow(frame, opt_flow):
        break;

cv2.destroyAllWindows()

工作原理

要计算光流,您需要两个图像(通常是视频中的连续帧)。 我们在代码中使用的两种方法都接受 8 位灰度图像作为帧。

首先,让我们讨论cv2.calcOpticalFlowFarneback函数的用法。 它使用以下参数,前一帧,当前帧,光流初始化,金字塔层之间的缩放比例,金字塔中的层数,平滑步骤的窗口大小,迭代次数,要查找的多项式的参数的邻域像素数,高斯的标准差(用于平滑多项式的导数),最后是标志。

最后一个参数管理光流过程,如果使用cv2.OPTFLOW_FARNEBACK_GAUSSIAN,则使用高斯过滤器对输入图像进行模糊处理,并且窗口的大小等于第六个参数的值; 如果使用cv2.OPTFLOW_USE_INITIAL_FLOW,则该算法将第三个参数视为光流的初始化-使用该参数,然后处理帧直到视频,并事先计算光流。 可以使用逻辑或运算来组合标志。

参考其余的参数,代码中使用的值被认为对算法有利,因此可以原样使用它们。 在大多数情况下,它们运行良好。

应用 Dual TV L1 光流算法是不同的。 我们需要通过调用cv2.createOptFlow_DualTVL1函数来创建cv2.DualTVL1OpticalFlow类的实例。 然后,我们可以通过调用创建实例的calc函数来获取光流。 该函数将前一帧,当前帧和光流初始化作为参数。

要获取或设置算法参数的值,您需要使用类函数。 如前所述,大多数参数都是使用在许多情况下都能正常工作的值初始化的。 您需要更改的是光流初始化的参数。 可以使用setUseInitialFlow函数来完成。

这两个函数作为计算结果都返回光流。 它表示为 2 通道的浮点值矩阵,并且具有与输入帧相同的空间大小。 第一个通道由每个帧像素的运动向量的X(水平)投影组成; 第二个通道用于运动向量的Y(垂直)投影。 因此,我们能够知道每个像素的运动方向以及幅度。

由于前面的代码,您将获得以下图像。 第一张图片用于 Farneback 的算法:

第二张图片用于 Dual TV L1:

如您所见,与 Farneback 的算法相比,Dual TV L1 提供了无孔的光流。 但是它要花费计算时间,Dual TV L1 算法要慢得多。

检测棋盘和圆形网格图案

在本秘籍中,您将学习如何检测棋盘和圆形网格图案。 这些模式在计算机视觉中非常有用,并且通常用于估计相机参数。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入模块:
import cv2
import matplotlib.pyplot as plt
  1. 用棋盘加载测试图像:
image_chess = cv2.imread('../data/chessboard.png')
  1. 检测棋盘图案:
found, corners = cv2.findChessboardCorners(image_chess, (6, 9))
assert found == True, "can't find chessboard pattern"
  1. 绘制检测到的图案:
dbg_image_chess = image_chess.copy()
cv2.drawChessboardCorners(dbg_image_chess, (6, 9), corners, found);
  1. 使用圆形网格图案加载测试图像:
image_circles = cv2.imread('../data/circlesgrid.png')
  1. 检测圆形网格图案:
found, corners = cv2.findCirclesGrid(image_circles, (6, 6), cv2.CALIB_CB_SYMMETRIC_GRID)
assert found == True, "can't find circles grid pattern"
  1. 绘制检测到的图案:
dbg_image_circles = image_circles.copy()
cv2.drawChessboardCorners(dbg_image_circles, (6, 6), corners, found);
  1. 可视化结果:
plt.figure(figsize=(8,8))
plt.subplot(221)
plt.title('original')
plt.axis('off')
plt.imshow(image_chess)
plt.subplot(222)
plt.title('detected pattern')
plt.axis('off')
plt.imshow(dbg_image_chess)
plt.show()
plt.subplot(223)
plt.title('original')
plt.axis('off')
plt.imshow(image_circles)
plt.subplot(224)
plt.title('detected pattern')
plt.axis('off')
plt.imshow(dbg_image_circles)
plt.tight_layout()
plt.show()

工作原理

校准模式检测通过两个 OpenCV 函数实现:cv2.findChessboardCornerscv2.findCirclesGrid。 这两个函数都返回布尔标志,指示是否找到了图案以及角点(如果找到)。

预期输出如下:

使用 SVM 模型的简单行人探测器

在本秘籍中,您将学习如何使用具有 HOG 特征的预训练 SVM 模型来检测行人。 行人检测是许多高级驾驶员辅助解决方案ADAS)的重要组成部分。 行人检测还用于视频监视系统和许多其他计算机视觉应用中。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入模块:
import cv2
import matplotlib.pyplot as plt
  1. 加载测试图像:
image = cv2.imread('../data/people.jpg')
  1. 创建 HOG 特征计算机和检测器:
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
  1. 检测图像中的人:
locations, weights = hog.detectMultiScale(image)
  1. 绘制检测到的人物边界框:
dbg_image = image.copy()
for loc in locations:
    cv2.rectangle(dbg_image, (loc[0], loc[1]), 
                  (loc[0]+loc[2], loc[1]+loc[3]), (0, 255, 0), 2)
  1. 可视化结果:
plt.figure(figsize=(12,6))
plt.subplot(121)
plt.title('original')
plt.axis('off')
plt.imshow(image[:,:,[2,1,0]])
plt.subplot(122)
plt.title('detections')
plt.axis('off')
plt.imshow(dbg_image[:,:,[2,1,0]])
plt.tight_layout()
plt.show()

工作原理

OpenCV 在类cv2.HOGDescriptor中实现定向直方图HOG)描述符计算功能。 可以使用线性 SVM 模型将同一类用于对象检测。 实际上,它已经具有带有权重的预训练行人检测器模型。 可以通过cv2.HOGDescriptor.getDefaultPeopleDetector方法获得模型。 使用hog.detectMultiScale方法,使用滑动窗口方法以多个比例检测对象。 该函数返回检测到的人的位置列表以及每个检测分数。 要了解更多信息,请访问这里

预期输出如下:

使用不同的机器学习模型的光学字符识别

在本秘籍中,您将学习如何训练基于 KNN 和 SVM 的数字识别模型。 这是一个简单的光学字符识别OCR)系统,也可以扩展为其他字符。 OCR 是一种功能强大的工具,可用于许多实际应用中,用于识别文本文档,自动阅读交通标志消息等。

准备

在继续此秘籍之前,您将需要安装 OpenCV 3.x Python API 包和matplotlib包。

操作步骤

  1. 导入模块:
import cv2
import numpy as np
  1. 指定一些常量:
CELL_SIZE = 20     # Digit image size. 
NCLASSES = 10      # Number of digits.
TRAIN_RATIO = 0.8  # Part of all samples used for training.
  1. 读取数字图像并准备标签:
digits_img = cv2.imread('../data/digits.png', 0)
digits = [np.hsplit(r, digits_img.shape[1] // CELL_SIZE) 
          for r in np.vsplit(digits_img, digits_img.shape[0] // CELL_SIZE)]
digits = np.array(digits).reshape(-1, CELL_SIZE, CELL_SIZE)
nsamples = digits.shape[0]
labels = np.repeat(np.arange(NCLASSES), nsamples // NCLASSES)
  1. 执行几何归一化,计算图像矩并对齐每个样本:
for i in range(nsamples):
    m = cv2.moments(digits[i])
    if m['mu02'] > 1e-3:
        s = m['mu11'] / m['mu02']
        M = np.float32([[1, -s, 0.5*CELL_SIZE*s], 
                        [0, 1, 0]])
        digits[i] = cv2.warpAffine(digits[i], M, (CELL_SIZE, CELL_SIZE))
  1. 随机排列样本:
perm = np.random.permutation(nsamples)
digits = digits[perm]
labels = labels[perm]
  1. 定义用于计算 HOG 描述符的函数:
def calc_hog(digits):
    win_size = (20, 20)
    block_size = (10, 10)
    block_stride = (10, 10)
    cell_size = (10, 10)
    nbins = 9
    hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, nbins)
    samples = []
    for d in digits: samples.append(hog.compute(d))
    return np.array(samples, np.float32)
  1. 准备训练和测试数据(特征和标签):
ntrain = int(TRAIN_RATIO * nsamples)
fea_hog_train = calc_hog(digits[:ntrain])
fea_hog_test = calc_hog(digits[ntrain:])
labels_train, labels_test = labels[:ntrain], labels[ntrain:]
  1. 创建一个 KNN 模型:
K = 3
knn_model = cv2.ml.KNearest_create()
knn_model.train(fea_hog_train, cv2.ml.ROW_SAMPLE, labels_train)
  1. 创建一个 SVM 模型:
svm_model = cv2.ml.SVM_create()
svm_model.setGamma(2)
svm_model.setC(1)
svm_model.setKernel(cv2.ml.SVM_RBF)
svm_model.setType(cv2.ml.SVM_C_SVC)
svm_model.train(fea_hog_train, cv2.ml.ROW_SAMPLE, labels_train)
  1. 定义评估模型的函数:
def eval_model(fea, labels, fpred):
    pred = fpred(fea).astype(np.int32)
    acc = (pred.T == labels).mean()*100

    conf_mat = np.zeros((NCLASSES, NCLASSES), np.int32)
    for c_gt, c_pred in zip(labels, pred):
        conf_mat[c_gt, c_pred] += 1

    return acc, conf_mat
  1. 评估 KNN 和 SVM 模型:
knn_acc, knn_conf_mat = eval_model(fea_hog_test, labels_test, lambda fea: knn_model.findNearest(fea, K)[1])
print('KNN accuracy (%):', knn_acc)
print('KNN confusion matrix:')
print(knn_conf_mat)

svm_acc, svm_conf_mat = eval_model(fea_hog_test, labels_test, lambda fea: svm_model.predict(fea)[1])
print('SVM accuracy (%):', svm_acc)
print('SVM confusion matrix:')
print(svm_conf_mat)

工作原理

在本秘籍中,我们应用了许多不同的 OpenCV 函数来构建用于识别数字的应用。 我们使用cv2.moment估计图像偏斜,然后使用cv2.warpAffine对其进行归一化。 使用cv2.ml.KNearest_createcv2.ml.SVM_create方法创建 KNN 和 SVM 模型。 我们随机地整理所有可用数据,然后将其分为训练/测试子集。 函数eval_model计算整体模型的准确率和混淆矩阵。 在结果中,我们可以看到,基于 SVM 的模型比 KNN 模型的结果要好一些。

预期输出如下:

KNN accuracy (%): 91.1
KNN confusion matrix:
[[101   0   0   0   0   0   1   0   0   2]
 [  0 112   3   0   0   0   0   0   0   0]
 [  0   1  93   1   0   0   0   0   2   0]
 [  1   0   3 100   0   3   0   0   1   1]
 [  1   0   2   8  78   3   4   0   1   5]
 [  0   0   0   5   0  82   1   0   4   1]
 [  0   0   0   0   1   0  92   0   0   0]
 [  0   0   3   6   2   1   0  76   1   2]
 [  0   0   0   1   0   2   0   1  80   2]
 [  2   1   1   1   0   0   0   4   4  97]]

SVM accuracy (%): 93.5
SVM confusion matrix:
[[100   0   1   0   0   0   1   0   0   2]
 [  0 112   2   0   0   0   0   1   0   0]
 [  0   0  93   0   1   0   0   1   2   0]
 [  1   0   2 100   0   2   0   1   2   1]
 [  1   0   1   2  93   2   0   1   0   2]
 [  0   0   0   3   1  85   1   1   2   0]
 [  0   0   0   0   1   0  92   0   0   0]
 [  0   0   1   3   3   2   0  82   0   0]
 [  2   0   0   1   0   2   0   0  79   2]
 [  1   1   1   1   1   1   0   4   1  99]]

混淆矩阵显示出一个模型产生了多少错误。 每行对应一个基本事实类别标签,每列对应一个预测的类别标签。 所有非对角线元素都是分类错误,而每个对角线元素都是适当分类的数量。

使用 Haar/LBP 级联检测人脸

当检测到照片上的面部时,您对手机或数码相机的印象如何? 毫无疑问,您想自己实现类似的功能,或者将人脸检测功能集成到算法中。 此秘籍展示了如何使用 OpenCV 轻松重复此操作。 让我们开始吧。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

操作步骤

此秘籍的步骤为:

  1. 导入我们需要的模块:
import cv2
import numpy as np
  1. 定义打开视频文件,调用检测器以查找图像中所有面部并显示结果的函数:
def detect_faces(video_file, detector, win_title):
    cap = cv2.VideoCapture(video_file)

    while True:
        status_cap, frame = cap.read()
        if not status_cap:
            break

        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        faces = detector.detectMultiScale(gray, 1.3, 5)

        for x, y, w, h in faces:
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 3)
            text_size, _ = cv2.getTextSize('Face', cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
            cv2.rectangle(frame, (x, y - text_size[1]), (x + text_size[0], y), (255, 255, 255), cv2.FILLED)
            cv2.putText(frame, 'Face', (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
        cv2.imshow(win_title, frame)

        if cv2.waitKey(1) == 27: 
            break

    cv2.destroyAllWindows()
  1. 从 OpenCV 加载经过预训练的 Haar 级联,然后调用我们的检测函数:
haar_face_cascade = cv2.CascadeClassifier('../data/haarcascade_frontalface_default.xml')

detect_faces('../data/faces.mp4', haar_face_cascade, 'Haar cascade face detector')
  1. 以略有不同的方式加载预训练的 LBP 级联,然后再次调用该函数以查找和显示面部:
lbp_face_cascade = cv2.CascadeClassifier()
lbp_face_cascade.load('../data/lbpcascade_frontalface.xml')

detect_faces(0, lbp_face_cascade, 'LBP cascade face detector')

工作原理

对象检测器是一种能够在图像中找到对象的算法,它可以计算其中存在对象的边界框的参数,还可以确定对象属于哪个类别(或类)。 在本秘籍中,我们仅使用一种类别的检测器,即正面正面。

检测器可以基于各种技术,并且通常涉及机器学习。 此秘籍告诉您如何使用基于级联的检测器。 这种检测器的主要优点之一是它的工作时间,它在现代硬件上以比实时更快的速度处理图像,这就是为什么它仍然很受欢迎。

OpenCV 包含许多用于不同目的的经过预先训练的检测器,您可以找到猫,眼睛,车牌,身体,当然还有人脸的边界框。 所有这些检测器都可以在 OpenCV 主存储库的/data子目录中找到。 所有检测器均以.xml文件表示,该文件包含检测器的所有参数。

要创建检测器,您需要使用cv2.CascadeClassifier类构造器。 您可以将带有级联参数的 XML 文件的路径传递给构造器-然后从文件中检测到它的加载。 另外,您可以稍后使用load函数加载参数,如前面的代码所示。

要使用加载的分类器,您需要调用其实例的detectMultiScale函数。 它接受以下参数:8 位灰度图像,您可以在其中找到对象,比例因子,邻居数,标志以及最小和最大对象大小。 比例因子决定了我们如何缩放图像以查找不同大小的对象。 值越大,计算速度越快,但拒绝中间尺寸的面的可能性也更高。 邻居号的调用增加了算法的健壮性,该号确定了当前对象应将其检测为真实对象的重叠检测次数。 标志用于先前创建的分类器,并且对于向后兼容是必需的。 最小和最大尺寸显然是我们要检测的对象尺寸的边界。 detectMultiScale返回输入图像中对象的边界框列表; 每个框的格式为(xy,宽度,高度)。

启动代码的结果是,您将看到如下图像:

如果您有兴趣训练自己的级联分类器,OpenCV 会提供有关此主题的出色教程。 可以在这里找到该教程。

为 AR 应用检测 AruCo 模式

了解相机在周围 3D 空间中的位置是一项非常具有挑战性且难以解决的任务。 专门设计的模式(称为 AruCo 标记)被调用以解决此问题。 每个标记都有足够的信息来确定相机的位置,并且还包含有关其自身的信息。 因此可以区分不同的标记,从而了解场景。 在本秘籍中,我们将介绍如何使用 OpenCV 创建和检测 AruCo 标记。

准备

在继续此秘籍之前,您需要安装带有 OpenCV Contrib 模块的 OpenCV 3.x Python API 包。

操作步骤

  1. 导入模块:
import cv2
import cv2.aruco as aruco
import numpy as np
  1. 使用不同的 AruCo 标记创建图像,对其进行模糊处理,然后显示它:
aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_6X6_250)

img = np.full((700, 700), 255, np.uint8)

img[100:300, 100:300] = aruco.drawMarker(aruco_dict, 2, 200)
img[100:300, 400:600] = aruco.drawMarker(aruco_dict, 76, 200)
img[400:600, 100:300] = aruco.drawMarker(aruco_dict, 42, 200)
img[400:600, 400:600] = aruco.drawMarker(aruco_dict, 123, 200)

img = cv2.GaussianBlur(img, (11, 11), 0)

cv2.imshow('Created AruCo markers', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
  1. 检测模糊图像上的标记。 绘制检测到的标记并显示结果:
aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_6X6_250)

corners, ids, _ = aruco.detectMarkers(img, aruco_dict)

img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
aruco.drawDetectedMarkers(img_color, corners, ids)

cv2.imshow('Detected AruCo markers', img_color)
cv2.waitKey(0)
cv2.destroyAllWindows()

工作原理

如前所述,AruCo 标记具有特殊的设计,并在内部的黑色和白色正方形中编码标识符。 因此,要创建适当的标记,必须遵循规则,并还要设置参数,例如标记大小和标识符。 所有这些都可以通过cv2.aruco.drawMarker函数来完成。 它接受标记的字典,标记的标识符和图像大小。 词典确定标记的外观和标记的 ID 之间的对应关系,并返回带有绘制标记的图像。 OpenCV 包含预定义的词典,可以使用cv2.aruco.getPredefinedDictionary函数(将字典名称作为参数)来检索。 在前面的代码中,使用cv2.aruco.DICT_6X6_250,并且该词典的名称意味着该词典由6x6标记(黑色和白色正方形内部网格的大小)组成,并且包含从 0 到 249 的标识符。

要检测图像中的 AruCo 标记,您需要使用cv2.aruco.detectMarkers例程。 此函数获取输入图像和需要从中查找标记的字典。 此函数的工作结果是,所有找到的标记均包含四个角的列表,标记 ID 的列表(顺序与角的列表相对应)和拒绝角的列表,这对于调试目的很有用。

为了方便快捷地得出检测结果,使用cv2.aruco.drawDetectedMarkers是合理的。 它接受图像以绘制检测到的角点列表和标识符列表。

代码启动的结果是,您将获得如下图像:

在自然场景中检测文字

在本秘籍中,您将学习如何使用预训练的卷积神经网络模型检测自然图像中的文本。 在自然环境中检测文本在读取交通标志消息,理解广告消息和阅读标语等应用中非常重要。

准备

在继续此秘籍之前,您将需要安装 OpenCV 3.x Python API 包和 matplotlib 包。 OpenCV 必须使用 Contrib 模块构建,因为高级文本识别功能不是 OpenCV 主存储库的一部分。

可以在opencv_contrib/modules/text/samples/textbox.prototxt找到修改后的.prototxt文件,其中包含此秘籍的模型描述。

模型权重可以从这里下载。

操作步骤

为了完成此秘籍,您需要执行以下步骤:

  1. 导入模块:
import cv2
  1. 加载文字图片:
img = cv2.imread('../data/scenetext01.jpg')
  1. 加载预训练的卷积神经网络并检测文本消息:
det = cv2.text.TextDetectorCNN_create(
       "../data/textbox.prototxt", "../data/TextBoxes_icdar13.caffemodel")
rects, probs = det.detect(img)
  1. 绘制置信度高于阈值的检测到的文本边界框:
THR = 0.3
for i, r in enumerate(rects):
    if probs[i] > THR:
        cv2.rectangle(img, (r[0], r[1]), (r[0]+r[2], r[1]+r[3]), (0, 255, 0), 2)
  1. 可视化结果:
plt.figure(figsize=(10,8))
plt.axis('off')
plt.imshow(img[:,:,[2,1,0]])
plt.tight_layout()
plt.show()

工作原理

OpenCV 中实现了许多不同的文本检测方法。 在本秘籍中,您学习了如何使用最新的深度学习方法来检测文本边界框。 OpenCV 类cv2.TextDetectorCNN_create创建一个 CNN(卷积神经网络)模型,并从指定的文件加载其预先训练的权重。 在那之后,您只需要调用det.detect方法,该方法将返回一个矩形列表以及包含文本的矩形的相关概率。

预期输出如下:

QR 码检测器

像 AruCo 标记一样,QR 码是另一种经过特殊设计的对象,用于存储信息和描述 3D 空间。 从食品包装到博物馆和机器人工厂,几乎到处都可以找到 QR 码。

在本秘籍中,我们将了解如何检测 QR 码并消除透视畸变以获得规范的代码视图。 这个任务听起来很容易完成,但是它需要很多 OpenCV 功能。 让我们找出如何做。

准备

在继续此秘籍之前,您将需要安装 OpenCV 3.x Python API 包。

操作步骤

  1. 导入我们需要的模块:
import cv2
import numpy as np
  1. 实现一个找到两条线的交点的函数:
def intersect(l1, l2):
    delta = np.array([l1[1] - l1[0], l2[1] - l2[0]]).astype(np.float32)

    delta = 1 / delta
    delta[:, 0] *= -1

    b = np.matmul(delta, np.array([l1[0], l2[0]]).transpose())
    b = np.diagonal(b).astype(np.float32)

    res = cv2.solve(delta, b)
    return res[0], tuple(res[1].astype(np.int32).reshape((2)))
  1. 定义一个函数,该函数通过计算四对变形点和非变形点之间的对应关系来消除透视变形:
def rectify(image, corners, out_size):
    rect = np.zeros((4, 2), dtype = "float32")
    rect[0] = corners[0]
    rect[1] = corners[1]
    rect[2] = corners[2]
    rect[3] = corners[3]

    dst = np.array([
        [0, 0],
        [out_size[1] - 1, 0],
        [out_size[1] - 1, out_size[0] - 1],
        [0, out_size[0] - 1]], dtype = "float32")

    M = cv2.getPerspectiveTransform(rect, dst)
    rectified = cv2.warpPerspective(image, M, out_size)
    return rectified
  1. 创建一个查找 QR 码外角的函数:
def qr_code_outer_corners(image):
    outer_corners_found = False
    outer_corners = []

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, th = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    _, contours, hierarchy = \
            cv2.findContours(th, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    cnts = []
    centers = []

    hierarchy = hierarchy.reshape((-1, 4))
    for i in range(hierarchy.shape[0]):
        i_next, i_prev, i_child, i_par = hierarchy[i]
        if all(v == -1 for v in hierarchy[i][:3]):
            if all(v == -1 for v in hierarchy[i_par][:2]):
                ids = [i, i_par, hierarchy[i_par][3]]
                corner_cnts = []
                for id_ in ids:
                    cnt = contours[id_]
                    apprx = \
                        cv2.approxPolyDP(cnt, cv2.arcLength(cnt, True) * 0.02, True)
                    if len(apprx) == 4:
                        corner_cnts.append(apprx.reshape((4, -1)))
                if len(corner_cnts) == 3:
                    cnts.append(corner_cnts)
                    all_pts = np.array(corner_cnts).reshape(-1, 2)

                    centers.append(np.mean(all_pts, 0))

    if len(centers) == 3: 
        distances_between_pts = np.linalg.norm(np.roll(centers, 1, 0) - centers, axis=1)
        max_dist_id = np.argmax(distances_between_pts)

        index_diag_pt_1 = max_dist_id
        index_diag_pt_2 = (max_dist_id - 1) % len(centers)
        index_corner_pt = (len(centers) - 1)*len(centers) // 2 - index_diag_pt_1 - index_diag_pt_2

        middle_pt = 0.5 * (centers[index_diag_pt_1] + centers[index_diag_pt_2])

        i_ul_pt = np.argmax(np.linalg.norm(cnts[index_corner_pt][-1] - middle_pt, axis=1))
        ul_pt = cnts[index_corner_pt][-1][i_ul_pt]

        for i in [index_diag_pt_1, index_diag_pt_2]:
            corner_cnts = cnts[i]
            outer_cnt = corner_cnts[-1]

            distances_to_mp = np.linalg.norm(outer_cnt - middle_pt, axis=1)
            max_dist_id = np.argmax(distances_to_mp) 

            vec_from_mid_to_diag = outer_cnt[max_dist_id] - middle_pt
            vec_from_mid_to_corner = ul_pt - middle_pt
            cross_prod = np.cross(vec_from_mid_to_corner, vec_from_mid_to_diag)

            diff_idx = 0

            if cross_prod > 0:
                ur_pt = outer_cnt[max_dist_id]
                ur_pt_2 = outer_cnt[(max_dist_id + 1) % len(outer_cnt)]
            else:
                bl_pt = outer_cnt[max_dist_id]
                bl_pt_2 = outer_cnt[(max_dist_id - 1) % len(outer_cnt)]

        ret, br_pt = intersect((bl_pt, bl_pt_2), (ur_pt, ur_pt_2))

        if ret == True:
            outer_corners_found = True
            outer_corners = [ul_pt, ur_pt, br_pt, bl_pt]

    return outer_corners_found, outer_corners
  1. 打开带有 QR 码的视频,在每个帧上找到 QR 码,如果成功,则显示代码角并取消扭曲代码以获取规范视图:
cap = cv2.VideoCapture('../data/qr.mp4')

while True:
    ret, frame = cap.read()
    if ret == False:
        break

    result, corners = qr_code_outer_corners(frame)

    qr_code_size = 300

    if result:
        if all((0, 0) < tuple(c) < (frame.shape[1], frame.shape[0]) for c in corners):
            rectified = rectify(frame, corners, (qr_code_size, qr_code_size))

            cv2.circle(frame, tuple(corners[0]), 15, (0, 255, 0), 2)
            cv2.circle(frame, tuple(corners[1]), 15, (0, 0, 255), 2)
            cv2.circle(frame, tuple(corners[2]), 15, (255, 0, 0), 2)
            cv2.circle(frame, tuple(corners[3]), 15, (255, 255, 0), 2)

            frame[0:qr_code_size, 0:qr_code_size] = rectified

    cv2.imshow('QR code detection', frame)

    k = cv2.waitKey(100)

    if k == 27:
        break

cap.release()
cv2.destroyAllWindows()

工作原理

如果您查看任何 QR 码,就会发现它的每个角点都有特殊标记。 这些标记只是彼此之间的白色和黑色方块。 因此,要检测和定位 QR 码,我们需要检测这三个特殊标记。 我们可以使用cv2.findContours来做到这一点。 我们需要利用中央黑色正方形内部的信息; 没有其他物体,因此也没有其他轮廓。 下一个白色正方形仅包含一个轮廓。 同样,下一个黑色正方形仅包含两个轮廓。 您可能还记得cv2.findContours可以返回图像上的轮廓层次结构。 我们只需要找到所描述的嵌套轮廓的结构。 此外,我们的标记具有正方形形状,我们可以使用此信息进一步排除误报。 使用cv2.approxPolyDP函数,我们可以用更少的点来近似轮廓。 我们的轮廓可以高精度地用四个顶点多边形近似。

找到标记及其轮廓后,我们应该确定它们的相互位置。 换句话说,我们应该找出是否有左下标记和右上标记,以及是否有左上标记。 左下和右上标记位于对角线上,因此它们之间的距离最大。 利用这一事实,我们可以选择对角标记和左上角的标记。 然后,我们需要找出我们的对角标记在左下角。 为此,我们找到 QR 码的中点,然后看看应该执行什么旋转(顺时针或逆时针)以匹配从中点到左上角的向量以及从中点到左上角的向量。 对角标记之一。 这可以通过找到向量叉积的Z投影的符号来完成。

现在我们知道了三个标记的角,并且我们需要找到 QR 码的最后一个角。 为此,我们找到了由对角线标记的外部正方形的边形成的线之间的交点。 这些事实为我们提供了两个带有两个变量的线性方程,交点的xy坐标。 cv2.solve可以解决这个问题,找到我们线性系统的解决方案。

至此,我们已经有了 QR 码的所有四个外角,我们需要消除透视变换并获得规范的代码视图。 这可以通过应用cv2.warpPerspective来完成。

启动代码后,您将获得类似于下图的内容:

五、深度学习

本章包含以下方面的秘籍:

  • 将图像表示为张量/BLOB
  • 从 Caffe,Torch 和 TensorFlow 格式加载深度学习模型
  • 获取所有层的输入和输出张量的形状
  • 卷积网络中的图像预处理和推理
  • 测量推理时间以及每个层对其的贡献
  • 使用 GoogleNet / Inception 和 ResNet 模型对图像进行分类
  • 使用单发检测(SSD)模型检测物体
  • 使用全卷积网络(FCN)模型分割场景
  • 使用单发检测(SSD)和 ResNet 模型进行人脸检测
  • 预测年龄和性别

介绍

深度学习让一切都变得更好。 还是? 似乎时间会证明一切。 但是毫无疑问的事实是,深度学习模型可以解决越来越多的问题。 深度学习现在在许多科学中扮演着重要的角色,计算机视觉也不例外。 OpenCV 最近从三种流行的框架CaffeTorchTensorflow中获得了加载和推断训练后的模型的能力。 本章告诉您如何使用 OpenCV 的此功能。 本章还包含分类,语义分割,对象检测和其他问题的不同现有模型的一些有用的实际应用。

将图像表示为张量/BLOB

用于计算机视觉的深度学习模型通常将图像作为输入。 但是,它们不使用图像,而是使用张量。 张量比图像更笼统。 它不受两空间和一通道尺寸的限制。 在本秘籍中,我们将学习如何将图像转换为多维张量。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 打开输入图像并打印其形状:
image_bgr = cv2.imread('../data/Lena.png', cv2.IMREAD_COLOR)
print(image_bgr.shape)
  1. 将图像转换为四维浮点张量:
image_bgr_float = image_bgr.astype(np.float32)
image_rgb = image_bgr_float[..., ::-1]
tensor_chw = np.transpose(image_rgb, (2, 0, 1))
tensor_nchw = tensor_chw[np.newaxis, ...]

print(tensor_nchw.shape)

工作原理

如您所知,OpenCV Python 包中的矩阵和图像与 NumPy 数组一起显示。 例如,先前代码中的cv2.imread给出了彩色图像,它是一个三维数组,其中所有三个维度分别对应于高度,宽度和通道。 可以将其想象为一个二维矩阵,其中的元素具有按高度乘以宽度的元素,并且每个元素为每个红色,绿色和蓝色通道存储三个值。 可以将此维度顺序编码为字母高度,宽度和通道HWC),并且沿着通道维度的数据以蓝色,绿色,红色的顺序存储。

张量是多维矩阵。 许多深度学习模型都接受用于高度,宽度和通道的三维浮点张量三个。 还有一个。 通常,模型不会一次处理一张图像,而是一次处理许多图像。 这堆图像称为批量,第四维处理该批量中的单个图像。

OpenCV 深度学习功能以 NCHW 维度顺序操作四维浮点张量:N表示批量中的图像数量,C 表示通道数,H 和 W 分别表示高度和宽度。

因此,要将图像转换为张量,我们需要执行以下步骤:

  1. 将图像转换为浮点数
  2. 如有必要,将通道的 BGR 顺序更改为 RGB
  3. 将 HWC 图像转换为 CHW 张量
  4. 在 CHW 张量中添加新尺寸以使其成为 NCHW

如您所见,这很容易。 但是每个步骤都非常重要,仅省略一个步骤可能会导致许多小时的调试,而这正是您试图了解和定位错误的原因。 例如,为什么以及何时需要重新排列 BGR 图像? 答案与模型训练中使用的通道顺序有关。 如果该模型用于处理 RGB 图像,则很有可能在 BGR 图像上表现不佳。 错过的这个小细节可能会花费您很多时间。

从 Caffe,Torch 和 TensorFlow 格式加载深度学习模型

OpenCV 的dnn模块的一大功能是能够从三个非常流行的框架中加载经过训练的模型:CaffeTorchTensorFlow。 它不仅使dnn模块非常有用,而且为将来自不同框架的模型组合到单个管道中提供了可能性。 在本秘籍中,我们将从这三个框架中学习如何使用网络。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.3.1(或更高版本)Python API 包。

操作步骤

请执行以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 加载Caffe模型:
net_caffe = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt', 
                                     '../data/bvlc_googlenet.caffemodel')
  1. Torch加载模型:
net_torch = cv2.dnn.readNetFromTorch('../data/torch_enet_model.net')
  1. 读取并解析经过训练的TensorFlow模型:
net_tensorflow = cv2.dnn.readNetFromTensorflow('../data/tensorflow_inception_graph.pb')

工作原理

要从框架中加载经过预训练的模型,您需要分别对CaffeTorchTensorFlow网络使用readNetFromCaffereadNetFromTorchreadNetFromTensorflow函数。 所有这些函数都返回cv2.dnn_Net对象,该对象是来自模型文件的图形的已解析版本。

值得一提的是,在加载具有复杂架构的模型或没有广泛分布的层的模型(例如,您最近添加或开发和实现的具有新型层的模型)时,可能会遇到问题。 OpenCV 的dnn模块仍在开发中,可能不包括深度学习框架的最新功能。 但是尽管如此,dnn模块还是有很多受支持的层类型来加载处理复杂任务的模型,这就是我们将在本章进一步介绍的内容。

在哪里可以找到预训练的深度学习模型? 在一些特殊的网页上,您可以找到预先训练的模型本身,以及有关训练过程的有用信息。 由于历史原因,这些模型列表称为模型动物园Caffe框架中创建的模型有这样一个列表可以在这里找到Tensorflow模型

获取所有层的输入和输出张量的形状

有时,有必要获取有关深度神经网络中的前向传递过程中数据形状发生了什么的信息。 例如,某些模型允许使用各种输入空间大小,在这种情况下,您可能想知道输出张量的形状。 OpenCV 可以选择不推论地获取所有张量(包括中间张量)的所有形状。 本秘籍回顾了使用此类功能以及与神经网络相关的其他有用例程的方式。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.3.1(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. Caffe加载模型并打印有关模型中使用的层类型的信息:
net = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt', 
                               '../data/bvlc_googlenet.caffemodel')

if not net.empty():
    print('Net loaded successfully\n')

print('Net contains:')
for t in net.getLayerTypes():
    print('\t%d layers of type %s' % (net.getLayersCount(t), t))
  1. 获取已加载模型的张量形状和指定的输入形状。 然后打印所有信息:
layers_ids, in_shapes, out_shapes = net.getLayersShapes([1, 3, 224, 224])

layers_names = net.getLayerNames()

print('Net layers shapes:')
for l in range(len(layers_names)):
    in_num, out_num = len(in_shapes[l]), len(out_shapes[l])
    print('Layer "%s" has %d input(s) and %d output(s)' 
          % (layers_names[l], in_num, out_num))
    for i in range(in_num):
        print('\tinput #%d has shape' % i, in_shapes[l][i].flatten())

    for i in range(out_num):
        print('\toutput #%d has shape' % i, out_shapes[l][i].flatten())

工作原理

cv2.dnn模块中Net类的getLayersShapes函数计算所有张量形状。 它接受形状作为输入,它是四个整数的列表。 列表中的元素是示例数,通道数,输入张量的宽度和高度。 该函数返回三个元素的元组:模型中的层标识符列表,每层的输入张量形状列表以及每层的输出张量形状列表。 当我们想获取有关层的其他信息时,层标识符列表是必需的,因为cv2.dnn_Net的某些函数会接受该列表中的标识符。 输入和输出形状的返回列表包含层所有输出的所有形状。 由于每一层可以具有多个输入和输出,所以这些返回的列表包含长度为4的 NumPy 整数数组的列表。

另外,我们在先前的代码中使用了其他一些函数。 让我们也讨论它们。 如果网络不包含任何层,则cv2.dnn_Netempty函数返回True。 它可用于检查是否已加载模型。

getLayerTypes函数返回模型中使用的所有层类型。 这些信息可以帮助您获得有关模型的基本概念。 getLayersCount函数获取层类型,并返回具有指定类型的多个层。 getLayerNames函数为您提供了模型中各层的所有名称。 基本上,神经网络模型包含这些层的名称,并且在加载和解析期间会保留它们。 这些名称由getLayerNames函数返回。

卷积网络中的图像预处理和推理

我们训练人工神经网络以用于我们的任务。 在这里,出现了一些条件。 首先,我们需要以网络可以处理的格式和范围准备输入数据。 其次,我们需要将数据正确地传递到网络。 OpenCV 帮助我们执行两个步骤,在本秘籍中,我们研究如何使用 OpenCV 的dnn模块轻松地将图像转换为张量并进行推理。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.3.1(或更高版本)Python API 包。

操作步骤

对于此秘籍,您需要完成以下步骤:

  1. 导入我们将要使用的模块:
import cv2
import numpy as np
  1. 打开输入图像,对其进行预处理,然后将其转换为张量:
image = cv2.imread('../data/Lena.png', cv2.IMREAD_COLOR)
tensor = cv2.dnn.blobFromImage(image, 1.0, (224, 224),
                               (104, 117, 123), False, False);
  1. 通过初步预处理将两个图像转换为张量:
tensor = cv2.dnn.blobFromImages([image, image], 1.0, (224, 224),
                                (104, 117, 123), False, True);
  1. 加载经过训练的神经网络模型:
net = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt', 
                               '../data/bvlc_googlenet.caffemodel')
  1. 设置加载模型的输入并执行推断:
net.setInput(tensor);
prob = net.forward();
  1. 重复设置输入并使用指定的层名称执行推理:
net.setInput(tensor, 'data');
prob = net.forward('prob');

工作原理

OpenCV dnn模块包含一个方便的函数,可通过预处理blobFromImage将图像转换为张量。 该函数的参数是输入图像(具有一个或三个通道),比例因子,以(宽度,高度)格式输出的空间大小,要减去的平均值,是否交换红色和蓝色通道的布尔标志,以及在调整大小之前是否从中心裁剪图像以保存对象在图像中的长宽比的布尔标记,还是只是在不保留对象比例的情况下调整大小。 blobFromImage函数在将图像转换为张量时经历以下步骤:

  1. 该函数调整图像大小。 如果裁切标记为True,则在保留宽高比的同时调整输入图像的大小。 图像的一个尺寸(宽度或高度)设置为所需的值,另一个尺寸设置为等于或大于size参数中的相应值。 然后,从中心得到的图像被裁剪为所需的尺寸。 如果裁剪标记为False,则该函数将调整为目标空间大小。
  2. 如有必要,该函数可将调整大小后的图像的值转换为浮点类型。
  3. 如果相应的参数为True,则该函数交换第一个和最后一个通道。 这是必要的,因为 OpenCV 加载后会以 BGR 通道顺序提供图像,但是某些深度学习模型可能会针对 RGB 通道顺序进行图像训练。
  4. 然后,该函数从图像的每个像素中减去平均值。 相应的参数可以是三值元组,也可以只是一值元组。 如果它是三值元组,则在交换通道后从相应的通道中减去每个值。 如果是单个值,则从每个通道中减去它。
  5. 将生成的图像乘以比例因子(第二个参数)。
  6. 将三维图像转换为具有 NCHW 尺寸顺序的三维张量。

blobFromImage函数返回执行了所有预处理的四维浮点张量。

重要的是,预处理必须与训练模型时的预处理相同。 否则,该模型可能无法正常工作甚至根本无法工作。 如果您自己训练了模型,则将了解所有参数。 但是,如果您已经在互联网上找到了模型,则需要检查模型的描述或训练脚本以获取必要的信息。

如果要从多个图像创建张量,则需要使用blobFromImages例程。 它具有与上一个函数相同的参数,但第一个参数除外,第一个参数应该是要从中创建张量的图像列表。 图像按照第一个参数中列出的顺序转换为张量。

要进行推断,您必须使用cv2.dnn_Net.setInput将张量设置为模型的输入,然后调用cv2.dnn_Net.forward以获取网络的输出。 setInput接受要设置的张量,还可以接受输入的名称。 当模型具有多个输入时,输入的名称将确定我们要设置的输入。

forward函数逐层执行从输入到输出的所有计算,并返回结果张量。 另外,您可以通过传递层名称作为参数来指定需要返回哪个层的输出。

出现一个问题,如何解释模型的输出? 解释取决于模型本身。 输入图像,分割图或某些更复杂的结构的类的可能性可能很大。 确切了解的唯一方法是检查有关模型的架构和训练过程的信息。

测量推理时间以及每个层对其的贡献

在本秘籍中,您将学习如何计算网络中以正向传播方式执行的浮点运算的总数,以及消耗的内存量。 当您想了解模型的局限性并揭示瓶颈的确切位置以进行优化时,这很有用。

准备

在继续此秘籍之前,您需要安装具有 Python API 支持的 OpenCV3.x。

操作步骤

您需要执行以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 导入Caffe模型:
model = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt',
                                 '../data/bvlc_googlenet.caffemodel')
  1. 计算在推理阶段执行的 FLOP 数量:
print('gflops:', model.getFLOPS((1,3,224,224))*1e-9)
  1. 报告存储权重和中间张量所消耗的内存量:
w,b = model.getMemoryConsumption((1,3,224,224))
print('weights (mb):', w*1e-6, ', blobs (mb):', b*1e-6)
  1. 对模拟输入执行正向传播:
blob = cv2.dnn.blobFromImage(np.zeros((224,224,3), np.uint8), 1, (224,224))
model.setInput(blob)
model.forward()
  1. 报告总时间:
total,timings = model.getPerfProfile()
tick2ms = 1e3/cv2.getTickFrequency()
print('inference (ms): {:2f}'.format(total*tick2ms))
  1. 报告每层推理时间:
layer_names = model.getLayerNames()
print('{: <30} {}'.format('LAYER', 'TIME (ms)'))
for (i,t) in enumerate(timings):
    print('{: <30} {:.2f}'.format(layer_names[i], t[0]*tick2ms))

工作原理

您可以使用model.getFLOPsmodel.getMemoryConsumption方法获得模型 FLOP 计数和消耗的内存量。 两种方法都将指定的 BLOB 形状作为输入。 每层推理时间统计信息在执行前向传递之后可用,并且可以通过model.getPerfProfile方法获得,该方法返回总推理时间和每层计时,所有信息均以滴答为单位。

预期输出如下:

gflops: 3.1904431360000003
weights (mb): 27.994208 , blobs (mb): 45.92096
inference (ms): 83.478832
LAYER TIME (ms)
conv1/7x7_s2 4.57
conv1/relu_7x7 0.00
pool1/3x3_s2 0.74
pool1/norm1 1.49
conv2/3x3_reduce 0.57
conv2/relu_3x3_reduce 0.00
conv2/3x3 11.53
conv2/relu_3x3 0.00
conv2/norm2 3.35
pool2/3x3_s2 0.90
inception_3a/1x1 0.55
...
inception_5b/relu_pool_proj 0.00
inception_5b/output 0.00
pool5/7x7_s1 0.07
pool5/drop_7x7_s1 0.00
loss3/classifier 0.30
prob 0.02

使用 GoogleNet/Inception 和 ResNet 模型的图像分类

在计算机视觉中,分类任务是对输入图像属于特定类别的概率的估计。 换句话说,算法必须确定图像的类别,主要目标是创建具有最少错误数量的分类器。 分类任务首先使深度学习算法比其他算法更具优势。 从那以后,深度学习引起了许多科学家和工程师的极大兴趣。 在本秘籍中,我们将将具有不同架构的三个模型应用于分类任务。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.3.1 Python API 包。

操作步骤

您需要按照以下步骤操作:

  1. 导入模块:
import cv2
import numpy as np
  1. 定义一个classify函数,该函数从视频中获取帧,将其转换为张量,将其馈送到神经网络,并选择概率最高的五个类别:
def classify(video_src, net, in_layer, out_layer, 
             mean_val, category_names, swap_channels=False):
    cap = cv2.VideoCapture(video_src)

    t = 0

    while True:
        status_cap, frame = cap.read()
        if not status_cap:
            break

        if isinstance(mean_val, np.ndarray):
            tensor = cv2.dnn.blobFromImage(frame, 1.0, (224, 224),
                       1.0, False);
            tensor -= mean_val
        else:
            tensor = cv2.dnn.blobFromImage(frame, 1.0, (224, 224),
                                   mean_val, swap_channels);
        net.setInput(tensor, in_layer);
        prob = net.forward(out_layer);

        prob = prob.flatten()

        r = 1
        for i in np.argsort(prob)[-5:]:
            txt = '"%s"; probability: %.2f' % (category_names[i], prob[i])
            cv2.putText(frame, txt, (0, frame.shape[0] - r*40), 
                        cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2);
            r += 1

        cv2.imshow('classification', frame)
        if cv2.waitKey(1) == 27:
            break

    cv2.destroyAllWindows()
    cap.release()
  1. 打开一个文件,其中包含以下类别的名称:
with open('../data/synset_words.txt') as f:
    class_names = [' '.join(l.split(' ')[1: ]).rstrip() for l in f.readlines()]
  1. Caffe加载GoogleNet模型并调用我们在“步骤 2”中定义的classify函数:
googlenet_caffe = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt', 
                                           '../data/bvlc_googlenet.caffemodel')

classify('../data/shuttle.mp4', googlenet_caffe, 'data', 'prob', (104, 117, 123), class_names)
  1. 再次从Caffe打开 ResNet-50 模型,加载带有平均值的张量,然后再次调用classify
resnet_caffe = cv2.dnn.readNetFromCaffe('../data/resnet_50.prototxt', 
                                           '../data/resnet_50.caffemodel')
mean = np.load('../data/resnet_50_mean.npy')

classify('../data/shuttle.mp4', resnet_caffe, 'data', 'prob', mean, class_names)
  1. 加载已经训练了TensorFlow中的GoogleNet模型的类别名称,从TensorFlow中加载该模型,并对视频中的帧进行分类:
with open('../data/imagenet_comp_graph_label_strings.txt') as f:
    class_names = [l.rstrip() for l in f.readlines()]

googlenet_tf = cv2.dnn.readNetFromTensorflow('../data/tensorflow_inception_graph.pb')

classify('../data/shuttle.mp4', googlenet_tf, 
         'input', 'softmax2', 117, class_names, True)

工作原理

用于分类的神经网络模型通常接受三通道图像,并产生具有跨类别概率的向量。 要使用经过训练的模型,您需要了解以下几点:

  • 在训练中使用了什么输入图像的预处理
  • 哪些层是输入,哪些层是输出
  • 输出张量中数据的组织方式
  • 输出张量中的值有什么含义

在我们的案例中,每个模型都需要自己的预处理。 此外,模型需要不同的通道顺序。 如果没有这两件事,模型将无法正常工作(有时会略微起作用,有时甚至会非常严重)。 此外,模型的输入和输出层名称不同。

分类中的输出向量包含所有类别的概率。 输出中最大值的索引是类别的索引。 要将此类索引转换为名称,您需要解析一个特殊文件,其中类别索引及其名称之间具有匹配项。 对于不同的模型,这些文件可能不同(在我们的情况下也不同)。

执行代码后,您将获得类似于以下内容的图像:

使用单发检测(SSD)模型检测对象

在本秘籍中,您将学习如何通过预训练的 MobileNet 网络使用单发检测SSD)方法来检测物体。 该模型支持 20 个类别,可用于需要在场景中查找对象的许多计算机视觉应用中,例如车辆碰撞警告。 要了解更多信息,请访问这里

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 导入Caffe模型:
model = cv2.dnn.readNetFromCaffe('../data/MobileNetSSD_deploy.prototxt',
                                 '../data/MobileNetSSD_deploy.caffemodel')
  1. 设置置信度阈值并指定模型支持的类:
CONF_THR = 0.3
LABELS = {1: 'aeroplane', 2: 'bicycle', 3: 'bird', 4: 'boat',
          5: 'bottle', 6: 'bus', 7: 'car', 8: 'cat', 9: 'chair',
          10: 'cow', 11: 'diningtable', 12: 'dog', 13: 'horse',
          14: 'motorbike', 15: 'person', 16: 'pottedplant',
          17: 'sheep', 18: 'sofa', 19: 'train', 20: 'tvmonitor'}
  1. 打开道路交通视频:
video = cv2.VideoCapture('../data/traffic.mp4')
while True:
    ret, frame = video.read()
    if not ret: break
  1. 检测对象:
    h, w = frame.shape[0:2]
    blob = cv2.dnn.blobFromImage(frame, 1/127.5, (300*w//h,300),
                                 (127.5,127.5,127.5), False)
    model.setInput(blob)
    output = model.forward()
  1. 绘制检测到的对象:
    for i in range(output.shape[2]):
        conf = output[0,0,i,2]
        if conf > CONF_THR:
            label = output[0,0,i,1]
            x0,y0,x1,y1 = (output[0,0,i,3:7] * [w,h,w,h]).astype(int)
            cv2.rectangle(frame, (x0,y0), (x1,y1), (0,255,0), 2)
            cv2.putText(frame, '{}: {:.2f}'.format(LABELS[label], conf), 
                        (x0,y0), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2)

    cv2.imshow('frame', frame)
    key = cv2.waitKey(3)
    if key == 27: break

cv2.destroyAllWindows() 

工作原理

在本秘籍中,我们使用 SSD 方法进行车辆检测,该方法使用 MobileNet 作为骨干网络。 该模型由 MS COCO 数据集进行了预训练,并支持许多通用类,例如人,汽车和鸟类。

在代码中,我们指定了检测成功所需的最低置信度(CONF_THR=0.3)。

预期输出如下:

使用全卷积网络(FCN)模型分割场景

在本秘籍中,您将学习如何将任意图像进行语义分割,分为 21 类,例如人,汽车和鸟。 当需要了解场景时,此功能非常有用。 例如,在增强现实应用中以及为驾驶员提供帮助。 要了解更多信息,请访问这里

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

这里下载模型权重并将文件保存到数据文件夹中。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 导入Caffe模型:
model = cv2.dnn.readNetFromCaffe('../data/fcn8s-heavy-pascal.prototxt',
                                 '../data/fcn8s-heavy-pascal.caffemodel')
  1. 加载图像并进行推断:
frame = cv2.imread('../data/scenetext01.jpg')
blob = cv2.dnn.blobFromImage(frame, 1, (frame.shape[1],frame.shape[0]))
model.setInput(blob)
output = model.forward()
  1. 使用每像素类标签计算图像:
labels = output[0].argmax(0)
  1. 可视化结果:
plt.figure(figsize=(14,10))
plt.subplot(121)
plt.axis('off')
plt.title('original')
plt.imshow(frame[:,:,[2,1,0]])
plt.subplot(122)
plt.axis('off')
plt.title('segmentation')
plt.imshow(labels)
plt.tight_layout()
plt.show()

工作原理

我们使用基于 VGG 的全卷积网络方法对每个像素进行场景分割。 该模型支持 21 个类。 该模型非常耗时,推理可能会占用大量 CPU 时间,因此请耐心等待。

预期结果如下:

使用单发检测(SSD)和 ResNet 模型的人脸检测

在本秘籍中,您将学习如何使用卷积神经网络模型检测面部。 在各种计算机视觉应用(例如面部增强)中都使用了在不同条件下准确检测面部的能力。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 加载模型并设置置信度阈值:
model = cv2.dnn.readNetFromCaffe('../data/face_detector/deploy.prototxt', 
                                 '../data/face_detector/res10_300x300_ssd_iter_140000.caffemodel')
CONF_THR = 0.5
  1. 打开视频:
video = cv2.VideoCapture('../data/faces.mp4')
while True:
    ret, frame = video.read()
    if not ret: break
  1. 检测当前帧中的人脸:
    h, w = frame.shape[0:2]
    blob = cv2.dnn.blobFromImage(frame, 1, (300*w//h,300), (104,177,123), False)
    model.setInput(blob)
    output = model.forward()
  1. 可视化结果:
    for i in range(output.shape[2]):
        conf = output[0,0,i,2]
        if conf > CONF_THR:
            label = output[0,0,i,1]
            x0,y0,x1,y1 = (output[0,0,i,3:7] * [w,h,w,h]).astype(int)
            cv2.rectangle(frame, (x0,y0), (x1,y1), (0,255,0), 2)
            cv2.putText(frame, 'conf: {:.2f}'.format(conf), (x0,y0),
                        cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2)

    cv2.imshow('frame', frame)
    key = cv2.waitKey(3)
    if key == 27: break

cv2.destroyAllWindows()

工作原理

我们对 ResNet-10 模型使用单发检测方法。 送入输入帧时,请注意指定平均颜色。

预期输出如下:

年龄和性别预测

在本秘籍中,您将学习如何通过图像预测一个人的年龄和性别。 一种可能的应用是例如收集有关人们在数字标牌显示器中查看内容的统计信息。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.x Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载模型:
age_model = cv2.dnn.readNetFromCaffe('../data/age_gender/age_net_deploy.prototxt',
                                     '../data/age_gender/age_net.caffemodel')
gender_model = cv2.dnn.readNetFromCaffe('../data/age_gender/gender_net_deploy.prototxt',
                                        '../data/age_gender/gender_net.caffemodel')
  1. 加载并裁剪源图像:
orig_frame = cv2.imread('../data/face.jpeg')
dx = (orig_frame.shape[1]-orig_frame.shape[0]) // 2
orig_frame = orig_frame[:,dx:dx+orig_frame.shape[0]]
  1. 可视化图像:
plt.figure(figsize=(6,6))
plt.title('original')
plt.axis('off')
plt.imshow(orig_frame[:,:,[2,1,0]])
plt.show()
  1. 用平均像素值加载图像,然后从源图像中减去它们:
mean_blob = np.load('../data/age_gender/mean.npy')
frame = cv2.resize(orig_frame, (256,256)).astype(np.float32)
frame -= np.transpose(mean_blob[0], (1,2,0))
  1. 设置年龄和性别列表:
AGE_LIST = ['(0, 2)','(4, 6)','(8, 12)','(15, 20)',
            '(25, 32)','(38, 43)','(48, 53)','(60, 100)']
GENDER_LIST = ['male','female']
  1. 分类性别:
blob = cv2.dnn.blobFromImage(frame, 1, (256,256))
gender_model.setInput(blob)
gender_prob = gender_model.forward()
gender_id = np.argmax(gender_prob)
print('Gender: {} with prob: {}'.format(GENDER_LIST[gender_id], gender_prob[0, gender_id]))
  1. 分类年龄段:
age_model.setInput(blob)
age_prob = age_model.forward()
age_id = np.argmax(age_prob)
print('Age group: {} with prob: {}'.format(AGE_LIST[age_id], age_prob[0, age_id]))

工作原理

在本秘籍中,我们使用了两种不同的模型:一种用于性别分类,另一种用于年龄组分类。 请注意,在此秘籍中,与其他秘籍相比,我们从源图像中减去每个像素的平均值,而不是每个通道的值。 您实际上可以将平均值可视化并看到平均的人脸。

这是输入图像:

预期输出如下:

Gender: female with prob: 0.9362890720367432
Age group: (25, 32) with prob: 0.9811384081840515

六、线性代数

本章包含以下方面的秘籍:

  • 正交 Procrustes 问题
  • 秩约束矩阵近似
  • 主成分分析
  • 线性方程组的求解系统(包括欠定和超定)
  • 求解多项式方程
  • 使用单纯形法进行线性规划

介绍

变量之间的线性相关性是所有可能选项中最简单的。 从近似和几何任务到数据压缩,相机校准和机器学习,它可以在许多应用中找到。 但是,尽管它很简单,但是当现实世界的影响发挥作用时,事情就会变得复杂。 从传感器收集的所有数据都包含一部分噪声,这可能导致线性方程组具有不稳定的解。 计算机视觉问题通常需要求解线性方程组。 即使在许多 OpenCV 函数中,这些线性方程也是隐藏的。 可以肯定的是,您将在计算机视觉应用中面对它们。 本章中的秘籍将使您熟悉线性代数的方法,这些方法可能有用并且实际上已在计算机视觉中使用。

正交 Procrustes 问题

最初,这个问题对寻找两个矩阵之间的正交变换的方式提出了质疑。 也许这与实际的计算机视觉应用无关,但是当您考虑到一组点确实是矩阵时,这种感觉可能会改变。 相机校准,刚体转换,摄影测量问题以及许多其他任务都需要解决正交 Procrustes 问题。 在本秘籍中,我们找到了估计点集旋转这一简单任务的解决方案,并研究了噪声输入数据如何影响我们的解决方案。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.0(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 生成初始点集。 然后通过将旋转矩阵应用于初始点来创建一组旋转点。 此外,向旋转点添加一部分噪声:
pts = np.random.multivariate_normal([150, 300], [[1024, 512], [512, 1024]], 50)

rmat = cv2.getRotationMatrix2D((0, 0), 30, 1)[:, :2]
rpts = np.matmul(pts, rmat.transpose())

rpts_noise = rpts + np.random.multivariate_normal([0, 0], [[200, 0], [0, 200]], len(pts))
  1. 使用奇异值分解SVD)解决正交 Procrustes 问题,并获得旋转矩阵的估计值:
M = np.matmul(pts.transpose(), rpts_noise)

sigma, u, v_t = cv2.SVDecomp(M)

rmat_est = np.matmul(v_t, u).transpose()
  1. 现在我们可以使用估计的旋转矩阵来找出我们的估计有多好。 为此,请计算反向旋转矩阵,然后将我们先前旋转的点乘以该矩阵。 然后,计算有噪声和无噪声的旋转点之间,旋转的反向点与初始反向点之间以及原始旋转矩阵及其估计值之间的欧几里得距离(L2):
res, rmat_inv = cv2.invert(rmat_est)
assert res != 0
pts_est = np.matmul(rpts, rmat_inv.transpose())

rpts_err = cv2.norm(rpts, rpts_noise, cv2.NORM_L2)
pts_err = cv2.norm(pts_est, pts, cv2.NORM_L2)
rmat_err = cv2.norm(rmat, rmat_est, cv2.NORM_L2)
  1. 显示我们的数据,将初始点显示为绿色圆圈,将旋转点显示为黄色圆圈,将反向点显示为白色细圆圈,将具有噪声的旋转点显示为红色细圆圈。 然后,打印有关点和矩阵之间的 L2 差的信息,并显示结果图像:
def draw_pts(image, points, color, thickness=cv2.FILLED):
    for pt in points:
        cv2.circle(img, tuple([int(x) for x in pt]), 10, color, thickness)

img = np.zeros([512, 512, 3])

draw_pts(img, pts, (0, 255, 0))
draw_pts(img, pts_est, (255, 255, 255), 2)
draw_pts(img, rpts, (0, 255, 255))
draw_pts(img, rpts_noise, (0, 0, 255), 2)

cv2.putText(img, 'R_points L2 diff: %.4f' % rpts_err, (5, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
cv2.putText(img, 'Points L2 diff: %.4f' % pts_err, (5, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
cv2.putText(img, 'R_matrices L2 diff: %.4f' % rmat_err, (5, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)

cv2.imshow('Points', img)
cv2.waitKey()

cv2.destroyAllWindows()

工作原理

为了找到正交 Procrustes 问题的解决方案,我们将 SVD 应用于两个矩阵的乘积:由初始点组成的矩阵和由旋转后的点组成的另一个矩阵。 每个矩阵中的行是对应点的(xy)坐标。 SVD 方法是众所周知的,可以使噪声结果稳定。 cv2.SVDecomp是在 OpenCV 中实现 SVD 的函数。 它接受一个矩阵(MxN)进行分解并返回三个矩阵。 返回的第一个矩阵是大小为MxN的矩形对角矩阵,对角线上的正数称为奇异值。 第二矩阵和第三矩阵分别是左奇异向量矩阵和右奇异向量矩阵的共轭转置。

SVD 是线性代数中非常方便的工具。 它能够产生可靠的解决方案,因此在许多不同的任务中经常使用。 我们不会深入研究 SVD 的理论,因为它是一个独立且确实是广泛的主题。 但是,我们将在本章后面的其他秘籍中了解此过程。

让我们还回顾一下前面代码中的 OpenCV 的另一个函数。 本书以前没有提到cv2.getRotationMatrix2D函数。 它为给定的旋转中心和角度以及比例尺计算仿射变换矩阵。 参数按以下顺序排列:旋转中心(格式为(xy),旋转角度(以度为单位),比例。 返回的值是2x3仿射变换矩阵。

cv2.invert找到给定一个矩阵的伪逆矩阵。 此函数接受要求逆的矩阵,并选择接受结果和求逆方法标志的矩阵。 默认情况下,该标志设置为cv2.DECOMP_LU,它将应用 LU 分解来查找结果。 另外,cv2.DECOMP_SVDcv2.DECOMP_CHOLESKY也可以作为选项使用; 第一个使用 SVD 查找伪逆矩阵(是,这是 SVD 的另一个应用),第二个使用 Cholesky 分解实现相同的目的。 该函数返回两个对象,一个float值和所得的倒置矩阵。 如果第一个返回值为 0,则输入矩阵为奇数。 在这种情况下,cv2.DECOMP_LUcv2.DECOMP_CHOLESKY无法产生结果,但是cv2.DECOMP_SVD计算伪逆矩阵。

从当前秘籍启动代码的结果是,您将获得与以下内容类似的结果:

如您所见,尽管添加噪声前后的点之间的差异相对较大,但初始点和估计点与旋转矩阵之间的差异很小。

如果您对 SVD 的理论感兴趣,那么此 Wikipedia 页面是一个不错的起点

秩约束矩阵近似

在本秘籍中,您将学习如何计算秩相关矩阵近似值。 该问题被表述为优化问题。 给定一个输入矩阵,目的是找到它的近似值,在该近似值下,使用 Frobenius 范数测量拟合,并且输出矩阵的秩不应大于给定值。 除其他领域外,此功能还用于数据压缩和机器学习。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.0(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 生成一个随机矩阵:
A = np.random.randn(10, 10)
  1. 计算 SVD:
w, u, v_t = cv2.SVDecomp(A)
  1. 计算秩约束矩阵近似值:
RANK = 5
w[RANK:,0] = 0
B = u @ np.diag(w[:,0]) @ v_t
  1. 检查结果:
print('Rank before:', np.linalg.matrix_rank(A))
print('Rank after:', np.linalg.matrix_rank(B))
print('Norm before:', cv2.norm(A))
print('Norm after:', cv2.norm(B))

工作原理

Eckart-Young-Mirsky 定理指出,可以通过计算 SVD(使用cv2.SVDecomp函数)并构造一个近似值(最小的奇异值设置为零)来解决问题,因此近似值等级不大于所需的值。 值。

输出如下所示:

Rank before: 10
Rank after: 5 
Norm before: 9.923378133354824
Norm after: 9.511025831320431

主成分分析

主成分分析PCA)旨在确定维度在数据中的重要性并建立新的基础。 在这个新的基础上,选择的方向要与其他方向具有最大的独立性。 由于具有最大的独立性,我们可以了解哪些数据维度承载更多信息,哪些数据维度承载较少。 PCA 用于许多应用,主要用于数据分析和数据压缩,但也可以用于计算机视觉。 例如,确定并跟踪物体的方向。 此秘籍将向您展示如何在 OpenCV 中进行操作。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.0(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入我们需要的模块:
import cv2
import numpy as np
  1. 定义将 PCA 应用于轮廓点并确定新基础的函数:
def contours_pca(contours):
    # join all contours points into the single matrix and remove unit dimensions 
    cnt_pts = np.vstack(contours).squeeze().astype(np.float32)

    mean, eigvec = cv2.PCACompute(cnt_pts, None)

    center = mean.squeeze().astype(np.int32)
    delta = (150*eigvec).astype(np.int32)
    return center, delta
  1. 定义一个函数,该函数显示将 PCA 应用于轮廓点的结果:
def draw_pca_results(image, contours, center, delta):
    cv2.drawContours(image, contours, -1, (255, 255, 0))

    cv2.line(image, tuple((center + delta[0])), 
                    tuple((center - delta[0])), 
                    (0, 255, 0), 2)

    cv2.line(image, tuple((center + delta[1])), 
                    tuple((center - delta[1])), 
                    (0, 0, 255), 2)

    cv2.circle(image, tuple(center), 20, (0, 255, 255), 2)
  1. 打开视频并逐帧分析。 对于每个框架,找到轮廓并将 PCA 应用于找到的轮廓。 然后,显示结果:
cap = cv2.VideoCapture("../data/opencv_logo.mp4")

while True:
    status_cap, frame = cap.read()
    if not status_cap:
        break

    frame = cv2.resize(frame, (0, 0), frame, 0.5, 0.5)
    edges = cv2.Canny(frame, 250, 150)

    _, contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    if len(contours):
        center, delta = contours_pca(contours)
        draw_pca_results(frame, contours, center, delta)

    cv2.imshow('PCA', frame)
    if cv2.waitKey(100) == 27:
        break

cv2.destroyAllWindows()

工作原理

使用 PCA 跟踪对象方向的主要思想是在旋转过程中对象不会发生变化。 因为它是同一对象,但方向不同,所以它具有自己的基础,并且该基础与对象一起旋转。 因此,我们需要在每时每刻确定这个基础,以找到物体的方向。 如果我们有正确的数据要分析,PCA 可以找到这样的基础。 让我们使用对象轮廓的点。 当然,它们在旋转过程中会更改其绝对位置,但会与对象一起旋转。 在每个方向上,轮廓的点都沿最大方向变化。 而且由于旋转不会使轮廓倾斜或扭曲,因此这些方向随对象旋转。

顾名思义,cv2.PCACompute实现了 PCA。 它找到数据协方差矩阵的特征向量和特征值。 此函数有两个重载。 我们在前面的代码中使用的第一个选项接受一个要分析的数据矩阵,一个预先计算的平均值,一个写计算出的特征向量的矩阵以及一些要返回的向量。 最后两个参数是可选的,可以省略(在这种情况下,将返回所有向量)。 同样,如果没有预先计算的平均值,则可以将第二个参数设置为“无”。 在这种情况下,该函数也会计算平均值。 数据矩阵通常是一组样本。 每个样本具有多个维度D,并且总体上有N个样本。 在这种情况下,数据矩阵必须为NxDN行也必须为NxD,并且每一行都是一个单独的样本。

如前所述,cv2.PCACompute存在第二个过载。 如前所述,它接受要分析的数据矩阵,并将预先计算的平均值作为前两个参数。 第三和第四参数是保留方差与存储计算向量的对象的比率。 该比率通过其方差确定要返回的向量数,比率越不平衡,保留的向量数就越大。 此参数允许您不固定向量的数量,而只保留方差最大的向量。

代码执行的结果是,您将获得类似于以下内容的图像:

线性方程组的求解系统(包括欠定和超定)

在本秘籍中,您将学习如何使用 OpenCV 求解线性方程组。 此功能是许多计算机视觉和机器学习算法的关键构建块。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 生成线性方程组:
N = 10
A = np.random.randn(N,N)
while np.linalg.matrix_rank(A) < N:
    A = np.random.randn(N,N)
x = np.random.randn(N,1)
b = A @ x
  1. 求解线性方程组:
ok, x_est = cv2.solve(A, b)

print('Solved:', ok)
if ok:
    print('Residual:', cv2.norm(b - A @ x_est))
    print('Relative error:', cv2.norm(x_est - x) / cv2.norm(x))
  1. 构造一个超定线性方程组:
N = 10
A = np.random.randn(N*2,N)
while np.linalg.matrix_rank(A) < N:
    A = np.random.randn(N*2,N)
x = np.random.randn(N,1)
b = A @ x
  1. 求解超定线性方程组:
ok, x_est = cv2.solve(A, b, flags=cv2.DECOMP_NORMAL)

print('\nSolved overdetermined system:', ok)
if ok:
    print('Residual:', cv2.norm(b - A @ x_est))
    print('Relative error:', cv2.norm(x_est - x) / cv2.norm(x))
  1. 构建一个不确定的线性方程组系统,该系统具有多个解决方案:
N = 10
A = np.random.randn(N,N*2)
x = np.random.randn(N*2,1)
b = A @ x
  1. 解决欠定线性方程组。 查找具有最小规范的解决方案:
w, u, v_t = cv2.SVDecomp(A, flags=cv2.SVD_FULL_UV)
mask = w > 1e-6
w[mask] = 1 / w[mask]
w_pinv = np.zeros((A.shape[1], A.shape[0]))
w_pinv[:N,:N] = np.diag(w[:,0])
A_pinv = v_t.T @ w_pinv @ u.T
x_est = A_pinv @ b

print('\nSolved underdetermined system')
print('Residual:', cv2.norm(b - A @ x_est))
print('Relative error:', cv2.norm(x_est - x) / cv2.norm(x))

工作原理

线性方程组可以使用 OpenCV 的cv2.solve函数求解。 它接受一个系数矩阵,系统的右侧和可选标志,然后返回一个解决方案(准确地说是成功指标和解决方案向量)。 如您在第一个示例中所看到的,它可用于解决具有独特解决方案的系统。

您可以指定cv2.DECOMP_NORMAL标志,在这种情况下,将构建内部标准化的线性方程组。 这可以用来解决带有一个或没有解的超定系统,在后一种情况下,返回最小二乘问题的解。

一个欠定的线性方程组没有或有多个解。 在前面的代码中,我们构建了具有多个解决方案的系统。 可以使用 Moore-Penrose 逆(代码中的A_pinv)找到具有最小范数的解。 由于存在多种解决方案,相对于我们用来生成系统右侧的解决方案,我们发现的解决方案可能会有更多错误。

这是预期输出的示例:

Solved: True
Residual: 2.7194799110210367e-15
Relative error: 1.1308382847616332e-15

Solved overdetermined system: True
Residual: 4.418021593470969e-15
Relative error: 5.810798787243048e-16

Solved underdetermined system
Residual: 9.296750665059115e-15
Relative error: 0.7288729621745673

求解多项式方程

在本秘籍中,您将学习如何使用 OpenCV 求解多项式方程。 这样的问题可能出现在诸如机器学习,计算代数和信号处理等领域。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 生成四次多项式方程:
N = 4
coeffs = np.random.randn(N+1,1)
  1. 找到复数域中的所有根:
retval, roots = cv2.solvePoly(coeffs)
  1. 检查根:
for i in range(N):
    print('Root', roots[i],'residual:', 
          np.abs(np.polyval(coeffs[::-1], roots[i][0][0]+1j*roots[i][0][1])))

工作原理

度为n的多项式方程在复数域中始终具有n根(但是其中一些可以重复)。 使用cv2.solvePoly函数可以找到所有解决方案。 它采用方程系数并返回所有根。

这是预期输出的示例:

Root [[ 0.0494519   1.12199842]] residual: [  1.50920942e-16]
Root [[-0.17045556  0\.        ]] residual: [ 0.]
Root [[ 0.0494519  -1.12199842]] residual: [  1.50920942e-16]
Root [[-8.1939051  0\.       ]] residual: [  1.80133686e-14]

使用单纯形法的线性规划

在本秘籍中,我们考虑优化问题的一种特殊情况,即线性约束问题。 这些任务意味着您需要考虑一组线性约束来优化(最大化或最小化)正变量的线性组合。 线性规划在计算机视觉中没有众所周知的直接应用,但是您可能会在以后遇到它。 因此,让我们看看如何使用 OpenCV 处理线性规划问题。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.0(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入模块:
import cv2
import numpy as np
  1. 为函数创建一个线性约束矩阵和权重,我们将对其进行优化:
m = 10
n = 10
constraints_mat = np.random.randn(m, n+1)
weights = np.random.randn(1, n)
  1. 通过调用cv2.solveLP将单纯形方法应用于任务。 然后,解析结果:
solution = np.array((n, 1), np.float32)
res = cv2.solveLP(weights, constrains_mat, solution)

if res == cv2.SOLVELP_SINGLE:
    print('The problem has the one solution')
elif res == cv2.SOLVELP_MULTI:
    print('The problem has the multiple solutions')
elif res == cv2.SOLVELP_UNBOUNDED:
    print('The solution is unbounded')
elif res == cv2.SOLVELP_UNFEASIBLE:
    print('The problem doesnt\'t have any solutions')

工作原理

cv2.solveLP接受三个参数:函数的权重,线性约束矩阵和用于保存结果的 NumPy 数组对象。 权重由浮点值向量(N, 1)(1, N)表示。 该向量的长度也意味着优化参数的数量。 线性约束矩阵是(M, N + 1)NumPy 数组,其中最后一列包含每个约束和每一行的常数项,最后一个元素除外,最后一个元素为相应的参数包含系数。 最后一个参数旨在存储解决方案(如果存在)。

通常,线性规划问题有四种可能的结果,它们可能只有一个解决方案,很多解决方案(在一定范围内),或者根本没有确定的解决方案。 在后一种情况下,问题可能是无限的或不可行的。 对于所有这四个结果,cv2.solveLP返回相应的值:cv2.SOLVELP_SINGLEcv2.SOLVELP_MULTIcv2.SOLVELP_UNBOUNDEDv2.SOLVELP_UNFEASIBLE

七、检测器和描述符

本章包含以下方面的秘籍:

  • 在图像中找到角点-Harris 和 FAST
  • 选择图像中的良好角点来跟踪
  • 绘制关键点,描述符和匹配项
  • 检测尺度不变关键点
  • 计算图像关键点的描述符-SURF,BRIEF 和 ORB
  • 查找描述符之间对应关系的匹配技术
  • 寻找可靠的匹配 - 交叉检查和比率测试
  • 基于模型的匹配过滤 - RANSAC
  • 用于构造全局图像描述符的 BoW 模型

介绍

可以根据比较图像中的区域来制定检测和跟踪任务。 如果我们能够在图像中找到特殊点并为这些点建立描述符,则可以比较描述符并得出有关图像中对象相似性的结论。 在计算机视觉中,这些特殊点称为关键点,但是围绕此概念出现了一些问题:如何在图像中找到真正的特殊位置? 您如何计算健壮且唯一的描述符? 您如何快速准确地比较这些描述符? 本章将解决所有这些查询,并引导您完成所有步骤,从找到关键点到使用 OpenCV 进行比较。

在图像中找到角点 - Harris 和 FAST

一个角可以认为是两个边的交集。 图像中角点的数学定义是不同的,但是反映了相同的想法。 角点是具有以下属性的点:沿任何方向移动该点都会导致该点的较小邻域发生变化。 例如,如果我们在图像的均匀区域上获取一个点,则移动该点不会改变附近的本地窗口中的任何内容。 边缘上的点不属于平原区域,并且又具有方向,其移动不影响该点的局部区域:这些是沿边缘的运动。 只有角对于所有方向都对移动敏感,因此,它们是跟踪或比较对象的良好候选者。 在本秘籍中,我们将学习如何使用 OpenCV 中的两种方法在图像上找到角点。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.0 版(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载图像并使用cv2.cornerHarris查找其角点:
img = cv2.imread('../data/scenetext01.jpg', cv2.IMREAD_COLOR)
corners = cv2.cornerHarris(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), 2, 3, 0.04)
  1. 处理并显示结果:
corners = cv2.dilate(corners, None)

show_img = np.copy(img)
show_img[corners>0.01*corners.max()]=[0,0,255]

corners = cv2.normalize(corners, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
show_img = np.hstack((show_img, cv2.cvtColor(corners, cv2.COLOR_GRAY2BGR)))

cv2.imshow('Harris corner detector', show_img)
if cv2.waitKey(0) == 27:
    cv2.destroyAllWindows()
  1. 创建一个FAST检测器并将其应用于图像:
fast = cv2.FastFeatureDetector_create(30, True, cv2.FAST_FEATURE_DETECTOR_TYPE_9_16)
kp = fast.detect(img)
  1. 绘制结果并显示图像:
show_img = np.copy(img)
for p in cv2.KeyPoint.convert(kp):
    cv2.circle(show_img, tuple(p), 2, (0, 255, 0), cv2.FILLED)

cv2.imshow('FAST corner detector', show_img)
if cv2.waitKey(0) == 27:
    cv2.destroyAllWindows()
  1. 禁用非最大抑制,获取角点并显示结果:
fast.setNonmaxSuppression(False)
kp = fast.detect(img)

for p in cv2.KeyPoint.convert(kp):
    cv2.circle(show_img, tuple(p), 2, (0, 255, 0), cv2.FILLED)

cv2.imshow('FAST corner detector', show_img)
if cv2.waitKey(0) == 27:
    cv2.destroyAllWindows()

工作原理

cv2.cornerHarris是 OpenCV 的函数,其名称如下实现了Harris角点检测器。 它包含六个参数:前四个参数是必需的,后两个参数具有默认值。 参数如下:

  • 单通道 8 位或浮点图像,在其上要检测角点
  • 邻域窗口的大小:应将其设置为大于 1 的较小值
  • 计算导数的窗口大小:应将其设置为奇数
  • 角点检测器的灵敏度系数:通常设置为 0.04
  • 您可以在其中存储结果的对象
  • 边界外推法

边界外推法确定图像扩展的方式。 可以将其设置为一堆值(cv2.BORDER_CONSTANTcv2.BORDER_REPLICATE等),默认情况下使用cv2.BORDER_REFLECT_101cv2.cornerHarris调用的结果是Harris量度的映射。 值较高的点更有可能成为好角。 启动与Harris角点检测器相关的代码的结果是,您将获得与以下图像类似的图像(图像的左侧是角点可视化,而右侧是Harris量度图):

我们在此秘籍中应用的另一种方法是来自加速段测试的特征(FAST)检测器。 它还以另一种方式在图像上找到角点。 它考虑每个点周围的一个圆并计算该圆的一些统计量。 让我们了解如何使用 FAST。

首先,我们需要使用cv2.FastFeatureDetector_create创建一个检测器。 该函数接受整数阈值,启用非最大抑制的标志以及确定相邻区域的大小和点数阈值的模式。 所有这些参数都可以稍后使用cv2.FastFeatureDetector类的相应方法(在先前代码中为setNonmaxSuppression)进行修改。

要在初始化后使用检测器,我们需要调用cv2.FastFeatureDetector.detect函数。 它拍摄一个单通道图像并返回cv2.KeyPoint对象的列表。 可以通过cv2.KeyPoint.convert将该列表转换为 numpy 数组。 结果数组中的每个元素都是角点。

执行与 FAST 检测器相关的代码将显示以下图像(启用了用于非最大抑制的左侧图像,禁用了用于非最大抑制的右侧图像):

选择图像中的良好角点来跟踪

在本秘籍中,您将学习如何检测图像中的关键点并应用简单的后处理试探法,以提高检测到的关键点的整体质量,例如摆脱关键点群集并删除相对较弱的关键点。 此功能在诸如对象跟踪和视频稳定之类的计算机视觉任务中很有用,因为提高检测到的关键点的质量会影响相应算法的最终质量。

准备

在继续此秘籍之前,您需要安装 OpenCV 3.0 版(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import matplotlib.pyplot as plt
  1. 加载测试图像:
img = cv2.imread('../data/Lena.png', cv2.IMREAD_GRAYSCALE)
  1. 找到良好的关键点:
corners = cv2.goodFeaturesToTrack(img, 100, 0.05, 10)
  1. 可视化结果:
for c in corners:
    x, y = c[0]
    cv2.circle(img, (x, y), 5, 255, -1)
plt.figure(figsize=(10, 10))
plt.imshow(img, cmap='gray')
plt.tight_layout()
plt.show()

工作原理

在此示例中,我们使用了 OpenCV 函数cv2.goodFeaturesToTrack。 此函数检测关键点并实现启发式列表,以通过选择良好的关键点的子集来提高诸如对象跟踪之类的计算机视觉任务的关键点的整体质量。 此函数确保关键点之间的距离不会太近,最小距离由minDistance参数调节。 qualityLevel参数调节相对于最强的关键点而言哪些关键点被认为是弱的,并将哪些关键点从最初检测到的关键点中删除。 该函数还具有参数maxCorners,这是检测到的关键点的最大数量。

预期输出如下:

绘制关键点,描述符和匹配项

找到关键点之后,您无疑想要查看这些关键点在原始图像中的位置。 OpenCV 是显示关键点和其他相关信息的便捷方法。 此外,您可以轻松地绘制来自不同图像的关键点之间的对应关系。 此秘籍告诉您如何可视化关键点以及匹配结果。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
import random
  1. 加载图像,在其中找到 FAST 关键点,并使用随机值填充每个关键点的大小和方向:
img = cv2.imread('../data/scenetext01.jpg', cv2.IMREAD_COLOR)

fast = cv2.FastFeatureDetector_create(160, True, cv2.FAST_FEATURE_DETECTOR_TYPE_9_16)
keyPoints = fast.detect(img)

for kp in keyPoints:
    kp.size = 100*random.random()
    kp.angle = 360*random.random()

matches = []
for i in range(len(keyPoints)):
    matches.append(cv2.DMatch(i, i, 1))
  1. 画出关键点:
show_img = cv2.drawKeypoints(img, keyPoints, None, (255, 0, 255))

cv2.imshow('Keypoints', show_img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 可视化有关关键点的大小和方向信息:
show_img = cv2.drawKeypoints(img, keyPoints, None, (0, 255, 0), 
                             cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('Keypoints', show_img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 显示关键点的匹配结果:
show_img = cv2.drawMatches(img, keyPoints, img, keyPoints, matches, None, 
                           flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('Matches', show_img)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

要显示关键点,您需要使用cv2.drawKeypoints。 此函数将源图像,关键点列表,目标图像,颜色和标志作为参数。 在最简单的情况下,您只需要通过前三个即可。 源图像用作背景,但此函数不会更改它,结果将被放置在目标图像中。 关键点列表是一个对象,由关键点检测器返回,因此您可以将该列表直接传递给cv2.drawKeypoints函数,而无需进行任何处理。 颜色只是绘图颜色。 最后一个参数flags允许您控制绘图模式-默认情况下,它具有cv2.DRAW_MATCHES_FLAGS_DEFAULT值,在这种情况下,关键点显示为相同直径的普通圆。 此标志的第二个选项是cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS。 在这种情况下,这些点将被绘制为具有不同直径的圆,并且方向也将显示为从圆心开始的一条线。 绘制的关键点直径显示了邻域,该邻域用于计算关键点; 方向会显示关键点的特定方向(如果关键点有此方向)。 cv2.drawKeypoints返回带有绘制关键点的结果图像。

cv2.drawMatches可帮助您显示关键点匹配过程之后各点之间的对应关系。 该函数的参数为​​:第一幅图像及其关键点列表,第二幅图像及其关键点,这些关键点的匹配结果列表,目标图像,用于绘制对应关系的颜色,用于绘制没有关键点的颜色匹配项,用于绘制匹配项的遮罩和一个标志。 通常,在关键点检测和匹配之后,您具有前五个参数的值。 默认情况下,匹配点和不匹配点(单个)的颜色是随机生成的,但是您可以使用任何值进行设置。 匹配的掩码是值的列表,其中非零值表示应显示对应的匹配(具有相同的索引)。 默认情况下,遮罩为空,并绘制所有匹配项。 最后一个参数控制要显示的关键点的模式。 可以将其设置为cv2.DRAW_MATCHES_FLAGS_DEFAULTcv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS,并可选地与cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS值共轭。

前两个值与cv2.drawKeypoints函数具有相同的含义。 最终值cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS允许您不显示不匹配的关键点。

通过执行代码,您将获得与以下图像类似的图像:

检测尺度不变关键点

现实世界中的物体在移动,这使得将它们与以前的外观进行精确比较变得更加困难。 当它们接近相机时,物体会变大。 为了应对这种情况,我们应该能够检测对对象的大小差异不敏感的关键点。 尺度不变特征变换SIFT)描述符专门设计用于处理不同的对象尺度,并为对象找到相同的特征,无论它们的大小如何。 此秘籍向您展示如何使用 OpenCV 中的 SIFT 实现。

准备

在继续此秘籍之前,您需要安装带有 Contrib 模块的 OpenCV 3.0 版(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块并加载图像:
import cv2
import numpy as np

img0 = cv2.imread('../data/Lena.png', cv2.IMREAD_COLOR)
img1 = cv2.imread('../data/Lena_rotated.png', cv2.IMREAD_COLOR)
img1 = cv2.resize(img1, None, fx=0.75, fy=0.75)
img1 = np.pad(img1, ((64,)*2, (64,)*2, (0,)*2), 'constant', constant_values=0)
imgs_list = [img0, img1]
  1. 创建一个 SIFT 关键点检测器:
detector = cv2.xfeatures2d.SIFT_create(50)
  1. 检测每个图像中的关键点,可视化这些关键点并显示结果:
for i in range(len(imgs_list)):
    keypoints, descriptors = detector.detectAndCompute(imgs_list[i], None)

    imgs_list[i] = cv2.drawKeypoints(imgs_list[i], keypoints, None, (0, 255, 0),
                                     flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('SIFT keypoints', np.hstack(imgs_list))
cv2.waitKey()

cv2.destroyAllWindows()

工作原理

要创建 SIFT 关键点检测器的实例,您需要使用cv2.xfeatures2d.SIFT_create函数。 它的所有参数都有默认值,参数本身是:查找和返回的关键点数量,要使用的比例金字塔中的级别数量,用于调整算法灵敏度的两个阈值以及用于平滑图片的σ方差 。 所有参数都很重要,但首先可能需要调整的是关键点的数量和σ。 最后一个控制您不关心的对象的最大尺寸,这对于消除图像中的噪点和细小的细节很有用。

通过完成秘籍中的代码后,您将获得类似于以下内容的图像:

如您所见,尽管右侧图像稍微倾斜并且尺寸小于右侧图像,但在图像中仍可以找到相同的关键点配置。 这是 SIFT 描述符的关键功能。

计算图像关键点的描述符 - SURF,BRIEF 和 ORB

在先前的秘籍中,我们研究了几种在图像中找到关键点的方法。 基本上,关键点只是特殊区域的位置。 但是,我们如何区分这些位置呢? 当我们要跟踪一系列帧中的对象时,在很多情况下都会出现此问题,尤其是在视频处理中。 该秘籍涵盖了表征关键点邻域的一些有效方法,换句话说,就是计算关键点描述符。

准备

在继续此秘籍之前,您需要安装带有 Contrib 模块的 OpenCV 3.0 版(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入我们需要的模块并加载图像:
import cv2
import numpy as np

img = cv2.imread('../data/scenetext01.jpg', cv2.IMREAD_COLOR)
  1. 创建一个 SURF 特征检测器并调整其一些参数。 然后,将其应用于加载的图像并显示结果:
surf = cv2.xfeatures2d.SURF_create(10000)
surf.setExtended(True)
surf.setNOctaves(3)
surf.setNOctaveLayers(10)
surf.setUpright(False)

keyPoints, descriptors = surf.detectAndCompute(img, None)

show_img = cv2.drawKeypoints(img, keyPoints, None, (255, 0, 0), 
                             cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('SURF descriptors', show_img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 创建一个 BRIEF 关键点描述符并将其应用于 SURF 关键点。 之后,显示结果的关键点:
brief = cv2.xfeatures2d.BriefDescriptorExtractor_create(32, True)

keyPoints, descriptors = brief.compute(img, keyPoints)

show_img = cv2.drawKeypoints(img, keyPoints, None, (0, 255, 0), 
                             cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('BRIEF descriptors', show_img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 初始化 ORB 特征检测器。 此后,检测关键点并计算图像的描述符。 然后,在图像中绘制关键点:
orb = cv2.ORB_create()

orb.setMaxFeatures(200)

keyPoints = orb.detect(img, None)
keyPoints, descriptors = orb.compute(img, keyPoints)

show_img = cv2.drawKeypoints(img, keyPoints, None, (0, 0, 255), 
                             cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('ORB descriptors', show_img)
cv2.waitKey()
cv2.destroyAllWindows()

工作原理

先前使用的所有关键点描述符均实现cv2.Feature2D接口,并具有相同的使用方式。 所有这些都需要首先创建描述符对象。 然后,就有可能设置或调整所创建描述符的某些参数。 值得一提的是,描述符具有算法参数的默认值,并且这些选择的默认值在许多情况下都能很好地工作。 准备使用描述符时,应使用detectcomputedetectAndCompute方法来检索指定图像的关键点和/或描述符。

要创建 SURF 描述符,您需要调用cv2.xfeatures2d.SURF_create函数。 它需要大量的参数,但是幸运的是所有参数都有默认值。 此函数返回初始化的 SURF 描述符对象。 要将其应用于图像,可以通过调用detectAndCompute函数找到关键点及其描述符。 您需要将输入图像传递给此函数,输入图像遮罩(如果没有提供遮罩,则可以设置为None),用于存储计算的描述符的对象以及用于标识是否应使用预先计算的关键点的标志。 该函数为每个返回的关键点返回关键点列表和描述符列表。

要创建一个简短的描述符,您需要使用cv2.BriefDescriptorExtractor_create函数。 该函数将算法的参数作为参数,并返回一个初始化的描述符对象。 BRIEF 描述符无法检测关键点,因此仅实现compute方法,该方法返回输入图像和先前检测到的关键点的描述符。

可以使用cv2.ORB_create函数创建 ORB 关键点检测器。 同样,此函数对该算法的参数采用了一系列具体细节,并返回了一个已构造且可立即使用的对象。

秘籍中的代码产生以下图像:

查找描述符之间对应关系的匹配技术

我们想在检测和跟踪任务中找到关键点之间的对应关系,但是我们无法比较这些点本身。 相反,我们应该处理关键点描述符。 关键点描述符是专门开发的,以便可以对其进行比较。 此秘籍向您展示 OpenCV 的方法,用于比较描述符并使用各种匹配技术建立描述符之间的对应关系。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 定义一个处理视频文件的函数。 此函数获取每一帧,并与该帧的关键点和之前的 40 帧匹配:
def video_keypoints(matcher, cap=cv2.VideoCapture("../data/traffic.mp4"), 
                    detector=cv2.ORB_create(40)):
    cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
    while True:
        status_cap, frame = cap.read()
        frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5)
        if not status_cap:
            break
        if (cap.get(cv2.CAP_PROP_POS_FRAMES) - 1) % 40 == 0:
            key_frame = np.copy(frame)
            key_points_1, descriptors_1 = detector.detectAndCompute(frame, None)
        else:
            key_points_2, descriptors_2 = detector.detectAndCompute(frame, None)
            matches = matcher.match(descriptors_2, descriptors_1)
            frame = cv2.drawMatches(frame, key_points_2, key_frame, key_points_1, 
                                    matches, None, 
                                    flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS | 
                                    cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
        cv2.imshow('Keypoints matching', frame)
        if cv2.waitKey(300) == 27:
            break

    cv2.destroyAllWindows()
  1. 将帧与暴力匹配进行比较:
bf_matcher = cv2.BFMatcher_create(cv2.NORM_HAMMING2, True)
video_keypoints(bf_matcher)
  1. 将 KD 树索引应用于 SURF 描述符:
flann_kd_matcher = cv2.FlannBasedMatcher()
video_keypoints(flann_kd_matcher, detector=cv2.xfeatures2d.SURF_create(20000))
  1. 对二进制 ORB 特征使用局部敏感哈希LSH):
FLANN_INDEX_LSH = 6
index_params = dict(algorithm=FLANN_INDEX_LSH, table_number=20, key_size=15, multi_probe_level=2)
search_params = dict(checks=10)

flann_kd_matcher = cv2.FlannBasedMatcher(index_params, search_params)
video_keypoints(flann_kd_matcher)
  1. 使用复合 KD 树和 K 均值索引算法重新运行该过程:
FLANN_INDEX_COMPOSITE = 3
index_params = dict(algorithm=FLANN_INDEX_COMPOSITE, trees=16)
search_params = dict(checks=10)

flann_kd_matcher = cv2.FlannBasedMatcher(index_params, search_params)
video_keypoints(flann_kd_matcher, detector=cv2.xfeatures2d.SURF_create(20000))

工作原理

OpenCV 支持许多不同的匹配类型。 所有这些都使用cv2.DescriptorMatcher接口实现,因此任何类型的匹配器都支持相同的方法和相同的使用场景。 匹配器用法有两种类型:检测模式和跟踪模式。 从技术上讲,这两种模式之间没有太大区别,因为在两种情况下,我们都需要有两组描述符来匹配它们。 问题是我们是否将第一个集合上载一次,然后与另一个集合进行比较,还是每次将两个描述符集传递给匹配函数。 要上传描述符集,您需要使用cv2.DescriptorMatcher.add函数,该函数仅接受您的描述符列表。 在完成描述符的添加后,在某些情况下,您需要调用cv2.DescriptorMatcher.train方法来告知匹配程序有关描述符的句柄,并为匹配过程做准备。

cv2.DescriptorMatcher有几种执行匹配的方法,并且所有这些方法都有检测和跟踪模式的重载。 cv2.DescriptorMatcher.match用于获取描述符之间的单个最佳对应关系。 cv2.DescriptorMatcher.knnMatchcv2.DescriptorMatcher.radiusMatch返回多个描述符之间的最佳对应关系。

查找最佳描述符匹配的最简单,最明显的方法是只比较所有可能的对,然后选择最佳的。 不用说,这种方法非常慢。 但是,如果您决定使用它(例如,作为参考),则需要调用cv2.BFMatcher_create函数。 它采用一种距离度量进行描述符比较,并启用交叉检查标志。

要创建更智能,更快的匹配器,您需要调用cv2.FlannBasedMatcher。 默认情况下,它将使用默认参数创建 KD 树索引。 要创建其他类型的匹配器并设置其参数,您需要为cv2.FlannBasedMatcher函数传递两个字典。 首先,字典描述了索引描述符及其参数的算法。 第二个参数描述了寻找最佳匹配的过程。

启动代码后,您将获得类似于以下内容的图像:

寻找可靠的匹配 - 交叉检查和比率测试

在本秘籍中,您将学习如何使用交叉检查和比率测试来匹配过滤器关键点。 这些技术可用于过滤不良匹配并改善已建立通信的整体质量。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import matplotlib.pyplot as plt
  1. 加载测试图像:
img0 = cv2.imread('../data/Lena.png', cv2.IMREAD_GRAYSCALE)
img1 = cv2.imread('../data/Lena_rotated.png', cv2.IMREAD_GRAYSCALE)
  1. 创建检测器,检测关键点和计算机描述符:
detector = cv2.ORB_create(100)
kps0, fea0 = detector.detectAndCompute(img0, None)
kps1, fea1 = detector.detectAndCompute(img1, None)
  1. 使用k = 2创建 K 最近邻描述符匹配器,然后从左到右查找匹配项,反之亦然:
matcher = cv2.BFMatcher_create(cv2.NORM_HAMMING, False)
matches01 = matcher.knnMatch(fea0, fea1, k=2)
matches10 = matcher.knnMatch(fea1, fea0, k=2)
  1. 使用比率测试创建用于过滤器匹配的函数,并过滤所有匹配项:
 def ratio_test(matches, ratio_thr):
    good_matches = []
    for m in matches:
        ratio = m[0].distance / m[1].distance
        if ratio < ratio_thr:
            good_matches.append(m[0])
    return good_matches

RATIO_THR = 0.7 # Lower values mean more aggressive filtering.
good_matches01 = ratio_test(matches01, RATIO_THR)
good_matches10 = ratio_test(matches10, RATIO_THR)
  1. 进行交叉检查匹配测试-只保留从左到右和从右到左列表中都存在的那些:
good_matches10_ = {(m.trainIdx, m.queryIdx) for m in good_matches10}
final_matches = [m for m in good_matches01 if (m.queryIdx, m.trainIdx) 
                 in good_matches10_]
  1. 可视化结果:
dbg_img = cv2.drawMatches(img0, kps0, img1, kps1, final_matches, None)
plt.figure()
plt.imshow(dbg_img[:,:,[2,1,0]])
plt.tight_layout()
plt.show()

工作原理

在此秘籍中,我们实现了两种启发式方法来过滤不良匹配。 第一个是比率测试。 它检查最佳匹配项是否明显好于次优匹配项。 通过比较匹配分数来执行检查。 使用cv2.BFMatcher类的knnMatch方法,找到每个关键点的两个最佳匹配。

第二种启发式方法是交叉检查测试。 对于AB这两个图像,它检查A中关键点在B中找到的匹配项是否相同。 在A中找到了B中的关键点。 保留在两个方向上找到的对应关系,并删除其他对应关系。

以下是预期的输出:

基于模型的匹配过滤 - RANSAC

在本秘籍中,您将学习如何使用随机样本共识RANSAC)算法在两个图像之间进行单应性转换的情况下,稳健地过滤两个图像中的关键点之间的匹配 。 此技术有助于过滤出不正确的匹配项,而仅在两个图像之间保留满足运动模型的匹配项。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载测试图像:
img0 = cv2.imread('../data/Lena.png', cv2.IMREAD_GRAYSCALE)
img1 = cv2.imread('../data/Lena_rotated.png', cv2.IMREAD_GRAYSCALE)
  1. 检测关键点和计算机描述符:
detector = cv2.ORB_create(100)
kps0, fea0 = detector.detectAndCompute(img0, None)
kps1, fea1 = detector.detectAndCompute(img1, None)
matcher = cv2.BFMatcher_create(cv2.NORM_HAMMING, False)
matches = matcher.match(fea0, fea1)
  1. 将单应性模型牢固地拟合到找到的关键点对应关系中,并获得内部匹配的掩码:
pts0 = np.float32([kps0[m.queryIdx].pt for m in matches]).reshape(-1,2)
pts1 = np.float32([kps1[m.trainIdx].pt for m in matches]).reshape(-1,2)
H, mask = cv2.findHomography(pts0, pts1, cv2.RANSAC, 3.0)
  1. 可视化结果:
plt.figure()
plt.subplot(211)
plt.axis('off')
plt.title('all matches')
dbg_img = cv2.drawMatches(img0, kps0, img1, kps1, matches, None)
plt.imshow(dbg_img[:,:,[2,1,0]])
plt.subplot(212)
plt.axis('off')
plt.title('filtered matches')
dbg_img = cv2.drawMatches(img0, kps0, img1, kps1, [m for i,m in enumerate(matches) if mask[i]], None)
plt.imshow(dbg_img[:,:,[2,1,0]])
plt.tight_layout()
plt.show()

工作原理

在此秘籍中,我们使用鲁棒的 RANSAC 算法估计两个图像之间的单应性模型参数。 通过带有cv2.RANSAC参数的cv2.findHomography函数来完成。 该函数返回通过点对应关系以及 Inliers 遮罩估计的单应变换。 内线遮罩处理满足估计运动模型且具有足够低误差的对应关系。 在我们的情况下,误差被计算为匹配点和根据运动模型转换的相应点之间的欧几里得距离。

以下是预期的输出:

用于构造全局图像描述符的 BoW 模型

在本秘籍中,您将学习如何应用词袋BoW)模型来计算全局图像描述符。 该技术可用于构建机器学习模型以解决图像分类问题。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

操作步骤

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载两个训练图像:
img0 = cv2.imread('../data/people.jpg', cv2.IMREAD_GRAYSCALE)
img1 = cv2.imread('../data/face.jpeg', cv2.IMREAD_GRAYSCALE)
  1. 检测每个训练图像的关键点和计算机描述符:
detector = cv2.ORB_create(500)
_, fea0 = detector.detectAndCompute(img0, None)
_, fea1 = detector.detectAndCompute(img1, None)
descr_type = fea0.dtype
  1. 构造 BoW 词汇表:
bow_trainer = cv2.BOWKMeansTrainer(50)
bow_trainer.add(np.float32(fea0))
bow_trainer.add(np.float32(fea1))
vocab = bow_trainer.cluster().astype(descr_type))
  1. 创建一个用于计算全局图像 BoW 描述符的对象:
bow_descr = cv2.BOWImgDescriptorExtractor(detector, cv2.BFMatcher(cv2.NORM_HAMMING))
bow_descr.setVocabulary(vocab)
  1. 加载测试图像,找到关键点,然后计算全局图像描述符:
img = cv2.imread('../data/Lena.png', cv2.IMREAD_GRAYSCALE)
kps = detector.detect(img, None)
descr = bow_descr.compute(img, kps)
  1. 可视化描述符:
plt.figure(figsize=(10,8))
plt.title('image BoW descriptor')
plt.bar(np.arange(len(descr[0])), descr[0])
plt.xlabel('vocabulary element')
plt.ylabel('frequency')
plt.tight_layout()
plt.show()

工作原理

词袋模型分为两个阶段。 在训练阶段,我们会收集训练图像的本地图像描述符(在我们的示例中为img0img1)并将它们聚集成词汇。 在第二阶段,将在输入图像中找到的本地描述符与词汇表所有单词进行比较,并列出每个单词出现的频率列表(例如,选择为最接近的单词) ),例如,频率向量,它形成了全局图像描述符。

以下是预期的输出:

八、图像和视频处理

本章包含以下方面的秘籍:

  • 使用仿射和透视变换使图像变形
  • 使用任意变换重新映射图像
  • 使用 Lucas-Kanade 算法跟踪帧之间的关键点
  • 背景减法
  • 将许多图像拼接成全景图
  • 使用非本地均值算法对照片降噪
  • 构造 HDR 图像
  • 通过图像修复消除照片中的缺陷

介绍

通过将一组图像作为一个整体而不是一堆单独的独立图像进行处理,计算机视觉算法可以实现更为出色的结果。 如果已知图像之间的相关性(可能是从不同角度拍摄的某些对象的视频文件中的帧序列),则可以利用它们。 本章使用的算法考虑了帧之间的关系。 这些算法包括背景减法,图像拼接,视频稳定,超重投影和构建 HDR 图像。

使用仿射和透视变换使图像变形

在本秘籍中,我们将介绍两种用于几何变换图像的主要方法:仿射和透视变形。 第一个用于删除简单的几何变换,例如旋转,缩放,平移及其组合,但是它不能将会聚的线变成平行的线。 在这里,透视变换开始起作用。 其目的是消除两条平行线在透视图中汇合时的透视变形。 让我们找出如何在 OpenCV 中使用所有这些转换。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块,打开输入图像,然后复制它:
import cv2
import numpy as np

img = cv2.imread('../data/circlesgrid.png', cv2.IMREAD_COLOR)
show_img = np.copy(img)
  1. 定义两个函数来实现点选择过程:
selected_pts = []

def mouse_callback(event, x, y, flags, param):
    global selected_pts, show_img

    if event == cv2.EVENT_LBUTTONUP:
        selected_pts.append([x, y])
        cv2.circle(show_img, (x, y), 10, (0, 255, 0), 3)

def select_points(image, points_num):
    global selected_pts
    selected_pts = []

    cv2.namedWindow('image')
    cv2.setMouseCallback('image', mouse_callback)

    while True:
        cv2.imshow('image', image)

        k = cv2.waitKey(1)

        if k == 27 or len(selected_pts) == points_num:
            break

    cv2.destroyAllWindows()

    return np.array(selected_pts, dtype=np.float32)
  1. 选择图像中的三个点,使用cv2.getAffineTransform计算仿射变换,然后使用cv2.warpAffine应用仿射变换。 然后,显示结果图像:
show_img = np.copy(img)
src_pts = select_points(show_img, 3)
dst_pts = np.array([[0, 240], [0, 0], [240, 0]], dtype=np.float32)

affine_m = cv2.getAffineTransform(src_pts, dst_pts)

unwarped_img = cv2.warpAffine(img, affine_m, (240, 240))

cv2.imshow('result', np.hstack((show_img, unwarped_img)))
k = cv2.waitKey()

cv2.destroyAllWindows()
  1. 找到一个仿射逆变换,将其应用并显示结果:
inv_affine = cv2.invertAffineTransform(affine_m)
warped_img = cv2.warpAffine(unwarped_img, inv_affine, (320, 240))

cv2.imshow('result', np.hstack((show_img, unwarped_img, warped_img)))
k = cv2.waitKey()

cv2.destroyAllWindows()
  1. 使用cv2.getRotationMatrix2D创建一个旋转比例的仿射扭曲,并将其应用于图像:
rotation_mat = cv2.getRotationMatrix2D(tuple(src_pts[0]), 6, 1)

rotated_img = cv2.warpAffine(img, rotation_mat, (240, 240))

cv2.imshow('result', np.hstack((show_img, rotated_img)))
k = cv2.waitKey()

cv2.destroyAllWindows()
  1. 在图像中选择四个点,使用cv2.getPerspectiveTransform创建透视变形矩阵,然后将其应用于图像并显示结果:
show_img = np.copy(img)
src_pts = select_points(show_img, 4)
dst_pts = np.array([[0, 240], [0, 0], [240, 0], [240, 240]], dtype=np.float32)

perspective_m = cv2.getPerspectiveTransform(src_pts, dst_pts)

unwarped_img = cv2.warpPerspective(img, perspective_m, (240, 240))

cv2.imshow('result', np.hstack((show_img, unwarped_img)))
k = cv2.waitKey()

cv2.destroyAllWindows()

这个怎么运作

仿射变换和透视图变换本质上都是矩阵乘法运算,其中元素的位置被某些扭曲矩阵重新映射。 因此,要应用变换,我们需要计算这样的翘曲矩阵。 对于仿射变换,可以使用cv2.getAffineTransform函数完成。 它以两组点作为参数:第一个点包含变换之前的三个点,第二个点包含变形后的三个对应点。 集合中点的顺序确实很重要,两个数组的点顺序应该相同。 要在透视扭曲的情况下创建变换矩阵,可以应用cv2.getPerspectiveTransform

同样,它在扭曲前后接受两组点,但是点集的长度应为4。 这两个函数都返回转换矩阵,但是它们的形状不同:cv2.getAffineTransform计算2x3矩阵,cv2.getPerspectiveTransform计算3x3矩阵。

要应用计算的转换,我们需要调用相应的 OpenCV 函数。 为了进行仿射变形,使用了cv2.warpAffine。 它获取输入图像,2x3转换矩阵,输出图像大小,像素插值模式,边界外推模式和边界外推值。 cv2.warpPerspective用于应用透视变换。 其参数与cv2.warpAffine的含义相同。 唯一的区别是转换矩阵(第二个参数)必须为3x3。 这两个函数都返回变形的图像。

有两个与仿射变换相关的有用函数:cv2.invertAffineTransformcv2.getRotationMatrix2D。 第一种是在您进行仿射变换并且需要获得逆仿射(也就是仿射)时使用。 它采用此现有的仿射变换并返回反变换。 cv2.getRotationMatrix2D不太通用,但经常用于仿射变换-缩放旋转。 此函数采用以下参数:(xy)格式的旋转中心点,旋转角度和比例因子,并返回2x3仿射变换矩阵。 该矩阵可用作cv2.warpAffine中的相应参数。

启动代码后,您将获得类似于以下内容的图像:

图中的第一行是具有三个选定点及其对应的仿射变换的输入图像。 第二行是逆变换和带比例变换的旋转的结果; 第三行包含具有四个选定点的输入图像,是透视变换的结果。

使用任意变换重新映射图像

在本秘籍中,您将学习如何使用每像素映射来变换图像。 这是一项非常通用的功能,已在许多计算机视觉应用中使用,例如图像拼接,相机帧不失真以及许多其他功能。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import math
import cv2
import numpy as np
  1. 加载测试图像:
img = cv2.imread('../data/Lena.png')
  1. 准备每个像素的变换图:
xmap = np.zeros((img.shape[1], img.shape[0]), np.float32)
ymap = np.zeros((img.shape[1], img.shape[0]), np.float32)
for y in range(img.shape[0]):
    for x in range(img.shape[1]):
        xmap[y,x] = x + 30 * math.cos(20 * x / img.shape[0])
        ymap[y,x] = y + 30 * math.sin(20 * y / img.shape[1])
  1. 重新映射源图像:
remapped_img = cv2.remap(img, xmap, ymap, cv2.INTER_LINEAR, None, cv2.BORDER_REPLICATE)
  1. 可视化结果:
plt.figure(0)
plt.axis('off')
plt.imshow(remapped_img[:,:,[2,1,0]])
plt.show()

这个怎么运作

通用的逐像素转换是通过cv2.remap函数实现的。 它接受一个源图像和两个映射(可以作为具有两个通道的一个映射来传递),并返回转换后的图像。 该函数还接受指定必须执行像素值内插和外推的参数。 在我们的情况下,我们指定双线性插值,超出范围的值将替换为最近的(空间上)范围内的像素值。 该函数非常通用,通常用作许多计算机视觉应用的构建块。

以下是预期的结果:

使用 Lucas-Kanade 算法跟踪帧之间的关键点

在本秘籍中,您将学习如何使用稀疏的 Lucas-Kanade 光流算法来跟踪视频中帧之间的关键点。 此功能在许多计算机视觉应用中很有用,例如对象跟踪和视频稳定化。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
  1. 打开测试视频并初始化辅助变量:
video = cv2.VideoCapture('../data/traffic.mp4')
prev_pts = None
prev_gray_frame = None
tracks = None
  1. 开始阅读视频中的帧,将每个图像转换为灰度:
while True:
    retval, frame = video.read()
    if not retval: break
    gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
  1. 使用稀疏的 Lucas-Kanade 光流算法跟踪上一帧的关键点,或者,如果您刚刚启动或按下C,请检测关键点,以便在下一帧中可以跟踪一些内容:
    if prev_pts is not None:
        pts, status, errors = cv2.calcOpticalFlowPyrLK(
            prev_gray_frame, gray_frame, prev_pts, None, winSize=(15,15), maxLevel=5,
            criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
        good_pts = pts[status == 1]
        if tracks is None: tracks = good_pts
        else: tracks = np.vstack((tracks, good_pts))
        for p in tracks:
            cv2.circle(frame, (p[0], p[1]), 3, (0, 255, 0), -1)
    else:
        pts = cv2.goodFeaturesToTrack(gray_frame, 500, 0.05, 10)
        pts = pts.reshape(-1, 1, 2)
  1. 记住当前点和当前框架。 现在可视化结果并处理键盘输入:
    prev_pts = pts
    prev_gray_frame = gray_frame

    cv2.imshow('frame', frame)
    key = cv2.waitKey() & 0xff
    if key == 27: break
    if key == ord('c'): 
        tracks = None
        prev_pts = None
  1. 关闭所有窗口:
cv2.destroyAllWindows()

这个怎么运作

在本秘籍中,我们打开一个视频,使用我们先前使用的cv2.goodFeaturesToTrack函数检测初始关键点,并使用稀疏的 Lucas-Kanade 光流算法开始跟踪点,该算法已在 OpenCV 中通过cv2.calcOpticalFlowPyrLK函数实现 。 OpenCV 实现了该算法的金字塔形式,这意味着首先在较小尺寸的图像中计算光流,然后在较大的图像中进行精修。 金字塔大小由maxLevel参数控制。 该函数还采用 Lucas-Kanade 算法的参数,例如窗口大小(winSize)和终止条件。 其他参数是前一帧和当前帧,以及来自前一帧的关键点。 这些函数返回当前帧中的跟踪点,成功标志数组和跟踪错误。

下图是积分跟踪结果的示例:

背景扣除

如果您有一个稳定场景的视频,其中有一些物体在四处移动,则可以将静止的背景与变化的前景分开。 在这里,我们将向您展示如何在 OpenCV 中进行操作。

准备

在继续此秘籍之前,您需要安装带有 Contrib 模块的 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 定义一个打开视频文件的函数,并对每个帧应用一些背景减影算法:
def split_image_fgbg(subtractor, open_sz=(0,0), close_sz=(0,0), show_bg=False, show_shdw=False):
    kernel_open = kernel_close = None

    if all(i > 0 for i in open_sz):
        kernel_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, open_sz)

    if all(i > 0 for i in close_sz):
        kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, close_sz)

    cap = cv2.VideoCapture('../data/traffic.mp4')
    while True:
        status_cap, frame = cap.read()
        if not status_cap:
            break

        frame = cv2.resize(frame, None, fx=0.5, fy=0.5)

        fgmask = subtractor.apply(frame)

        objects_mask = (fgmask == 255).astype(np.uint8)
        shadows_mask = (fgmask == 127).astype(np.uint8)

        if kernel_open is not None:
            objects_mask = cv2.morphologyEx(objects_mask, cv2.MORPH_OPEN, kernel_open)

        if kernel_close is not None:
            objects_mask = cv2.morphologyEx(objects_mask, cv2.MORPH_CLOSE, kernel_close)
            if kernel_open is not None:
                shadows_mask = cv2.morphologyEx(shadows_mask, cv2.MORPH_CLOSE, kernel_open)

        foreground = frame
        foreground[objects_mask == 0] = 0

        if show_shdw:
            foreground[shadows_mask > 0] = (0, 255, 0)

        cv2.imshow('foreground', foreground)

        if show_bg:
            background = fgbg.getBackgroundImage()
            if background is not None:
                cv2.imshow('background', background) 

        if cv2.waitKey(30) == 27:
            break

    cap.release()
    cv2.destroyAllWindows()
  1. 将由 KadewTraKuPong 和 Bowden 创建的基于高斯混合的背景/前景分割算法应用于视频:
fgbg = cv2.bgsegm.createBackgroundSubtractorMOG()

split_image_fgbg(fgbg, (2, 2), (40, 40))
  1. 创建由 Zoran Zivkovic 开发的高斯混合分割算法的改进版本的实例:
fgbg = cv2.createBackgroundSubtractorMOG2()

split_image_fgbg(fgbg, (3, 3), (30, 30), True)
  1. 使用 Godbehere,Matsukawa 和 Goldberg 的背景减除算法创建背景遮罩:
fgbg = cv2.bgsegm.createBackgroundSubtractorGMG()

split_image_fgbg(fgbg, (5, 5), (25, 25))
  1. 根据 Sagi Zeevi 的建议,应用基于计数的背景减法算法:
fgbg = cv2.bgsegm.createBackgroundSubtractorCNT()

split_image_fgbg(fgbg, (5, 5), (15, 15), True)
  1. 使用基于最近邻方法的背景分割技术:
fgbg = cv2.createBackgroundSubtractorKNN()

split_image_fgbg(fgbg, (5, 5), (25, 25), True)

这个怎么运作

所有背景减法器都实现cv2.BackgroundSubtractor接口,因此它们都有一定的方法集:

  • cv2.BackgroundSubtractor.apply:获取分割遮罩
  • cv2.BackgroundSubtractor.getBackgroundImage:检索背景图像

apply方法接受彩色图像作为参数并返回背景遮罩。 此遮罩通常包含三个值:0用于背景像素,255用于前景像素和127用于阴影像素。 阴影像素是背景中强度较低的像素。 值得一提的是,并非所有减法器都支持阴影像素分析。

getBackgroundImage返回背景图像,如果没有移动的物体,则应返回背景图像。 同样,只有少数减法器能够计算这样的图像。

毫不奇怪,所有减法算法都有内部参数。 幸运的是,这些参数中的许多都可以与默认值一起很好地工作。 历史参数是可以首先调整的参数之一。 基本上,这是减法器开始生成分段掩码之前需要分析的帧数。 因此,通常您会获得第一帧的完整背景遮罩。

您已经注意到,我们将形态学操作应用于运动对象遮罩。 由于几个原因,我们需要此步骤。 首先,运动物体的某些部分可能质地较差。 由于所有相邻像素都非常相似,因此很难检测运动。 第二个原因是我们的背景分割检测器不够理想。 错误地将移动物体的一部分标记为背景会导致错误。 应用形态学可以帮助我们使用先验信息,这些信息不能仍然是运动对象中的一部分。

上面的代码生成的图像类似于下图:

将许多图像拼接成全景图

OpenCV 有很多计算机视觉算法。 其中一些是低级的,而另一些则在特殊情况下使用。 但是有功能,可以使用日常应用将许多算法结合在一起。 这些管道之一是全景拼接。 这个相当复杂的过程可以在 OpenCV 中轻松完成,并得到不错的结果。 此秘籍向您展示如何使用 OpenCV 工具创建自己的全景图。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载我们将要合并为全景图的图像:
images = []
images.append(cv2.imread('../data/panorama/0.jpg', cv2.IMREAD_COLOR))
images.append(cv2.imread('../data/panorama/1.jpg', cv2.IMREAD_COLOR))
  1. 创建一个全景拼接器,将图像传递给它,然后解析结果:
stitcher = cv2.createStitcher()
ret, pano = stitcher.stitch(images)

if ret == cv2.STITCHER_OK:
    cv2.imshow('panorama', pano)
    cv2.waitKey()

    cv2.destroyAllWindows()
else:
    print('Error during stiching')

这个怎么运作

cv2.createStitcher建立了全景拼接算法的实例。 要将其应用于全景图创建,您需要调用其stitch方法。 此方法接受要组合的图像数组,并返回拼接结果状态以及全景图像。 状态可能具有以下值之一:

  • cv2.STITCHER_OK
  • cv2.STITCHER_ERR_NEED_MORE_IMGS
  • cv2.STITCHER_ERR_HOMOGRAPHY_EST_FAIL
  • cv2.STITCHER_ERR_CAMERA_PARAMS_ADJUST_FAIL

第一个值表示成功创建了全景图。 其他值告诉您全景图尚未合成,并向您提示了可能的原因。

拼接成功与否取决于输入图像。 它们应该具有重叠的区域。 重叠的区域越多,算法就越容易匹配框架并将其正确映射到最终全景图。 另外,最好从旋转的相机拍摄照片。 相机的微小移动是可以的,但是是不希望的。

执行代码后,您将看到类似于下图的图像:

正如您在图中看到的,反射并没有破坏最终结果:该算法成功处理了这种情况。 由于图像具有大的重叠区域和许多具有丰富纹理的区域,因此可以实现此结果。 对于没有纹理的物体,反射可能会受到阻碍。

使用非本地均值算法的照片降噪

在本秘籍中,您将学习如何使用非局部均值算法消除图像中的噪点。 当照片受到过多噪点的影响时,此功能很有用,因此有必要将其删除以获得更好的图像。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载测试图像:
img = cv2.imread('../data/Lena.png')
  1. 产生随机的高斯噪声:
noise = 30 * np.random.randn(*img.shape)
img = np.uint8(np.clip(img + noise, 0, 255))
  1. 使用非本地均值算法执行去噪:
denoised_nlm = cv2.fastNlMeansDenoisingColored(img, None, 10)
  1. 可视化结果:
plt.figure(0, figsize=(10,6))
plt.subplot(121)
plt.axis('off')
plt.title('original')
plt.imshow(img[:,:,[2,1,0]])
plt.subplot(122)
plt.axis('off')
plt.title('denoised')
plt.imshow(denoised_nlm[:,:,[2,1,0]])
plt.show()

这个怎么运作

非本地均值算法是通过 OpenCV 中的一系列函数实现的:cv2.fastNlMeansDenoisingcv2.fastNlMeansDenoisingColoredcv2.fastNlMeansMulticv2.fastNlMeansDenoisingColoredMulti。 这些函数可以拍摄一张图像或多张图像(灰度或彩色)。 在此秘籍中,我们使用了cv2.fastNlMeansDenoisingColored函数,该函数会拍摄一张 BGR 图像并返回去噪的图像。 该函数采用一些参数,其中参数h代表降噪强度; 较高的值可减少噪点,但图像更平滑。 其他参数指定非本地均值算法参数,例如模板模式大小和搜索窗口空间(相应命名)。

下图显示了预期的结果:

构造 HDR 图像

几乎所有现代相机甚至手机都具有神奇的 HDR 模式,它产生了真正的奇迹效果-照片中没有曝光不足或曝光过度的区域。 HDR高动态范围),您可以在 OpenCV 中重现这样的结果! 本秘籍告诉您有关 HDR 成像功能以及如何正确使用它们的信息。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载图像和曝光时间:
imgs_names = ['33', '100', '179', '892', '1560', '2933']

exp_times = []
images = []

for name in imgs_names:
    exp_times.append(1/float(name))
    images.append(cv2.imread('../data/hdr/%s.jpg' % name, cv2.IMREAD_COLOR))

exp_times = np.array(exp_times).astype(np.float32)
  1. 恢复 CRF:
calibrate = cv2.createCalibrateDebevec()
response = calibrate.process(images, exp_times)
  1. 计算 HDR 图像:
merge_debevec = cv2.createMergeDebevec()
hdr = merge_debevec.process(images, exp_times, response)
  1. 将 HDR 图像转换为低动态范围LDR)图像以能够显示它:
tonemap = cv2.createTonemapDurand(2.4)
ldr = tonemap.process(hdr)

ldr = cv2.normalize(ldr, None, 0, 1, cv2.NORM_MINMAX)

cv2.imshow('ldr', ldr)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 应用此技术合并具有各种曝光度的图像:
merge_mertens = cv2.createMergeMertens()
fusion = merge_mertens.process(images)

fusion = cv2.normalize(fusion, None, 0, 1, cv2.NORM_MINMAX)

cv2.imshow('fusion', fusion)
cv2.waitKey()
cv2.destroyAllWindows()

这个怎么运作

首先,您需要具有一组已知曝光时间不同的图像。 现代相机将大量信息(包括曝光时间)存储在图像文件中,因此值得检查图像的属性。

计算 HDR 图像时,首先需要恢复 CRF相机响应函数),这是每种颜色的实际强度与像素强度(在[0, 255]范围内)之间的映射。 通道。 通常它是非线性的,因此不可能简单地将不同曝光的图像组合在一起。 可以通过使用cv2.createCalibrateDebevec创建校准算法的实例来完成。 创建校准实例后,您需要调用其process方法并传递图像数组和曝光时间数组。 process方法返回摄像机的 CRF。

下一步是创建 HDR 图像。 为此,我们应该通过调用cv2.createMergeDebevec获得照片合并算法的实例。 构造对象时,我们需要调用其process方法并传递图像,曝光时间和 CRF 作为参数。 结果,我们获得了 HDR 图像,该图像无法用imshow显示,但是可以用imwrite.hdr格式保存并在专用工具中查看。

现在我们需要显示我们的 HDR 图像。 为此,我们需要将其动态范围正确地压缩到 8 位。 此过程称为音调映射。 要执行此过程,您需要使用cv2.createTonemapDurand构建一个音调映射对象并调用其process函数。 此函数接受 HDR 图像并返回浮点图像。

还有另一种方法来合并具有不同曝光度的照片。 您需要使用cv2.createMergeMertens函数创建另一个算法实例。 生成的对象具有process方法,该方法合并了我们的图像-只需将它们作为参数传递即可。 函数工作的结果是合并图像。

从该秘籍启动代码后,您将看到类似于下图所示的图像:

图的第一行是两张具有不同曝光量的原始图像:左一张的曝光时间长,而右一张的曝光时间短。 结果,我们可以看到灯泡旁边的台灯标签和 QR 码。 最下面的行包含秘籍代码中两种方法的结果-在两种情况下,我们都可以看到所有详细信息。

通过图像修复消除照片中的缺陷

有时,照片图像有缺陷。 对于已扫描的旧照片尤其如此:它们可能有划痕,斑点和污点。 所有这些缺陷都会阻碍照片的欣赏。 根据其周围环境重建图像各部分的过程称为“修复”,而 OpenCV 具有此算法的实现。 在这里,我们将介绍利用此 OpenCV 功能的方法。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 定义一个封装掩码创建的类:
class MaskCreator:
    def __init__(self, image, mask):
        self.prev_pt = None
        self.image = image
        self.mask = mask
        self.dirty = False
        self.show()
        cv2.setMouseCallback('mask', self.mouse_callback)

    def show(self):
        cv2.imshow('mask', self.image)

    def mouse_callback(self, event, x, y, flags, param):
        pt = (x, y)
        if event == cv2.EVENT_LBUTTONDOWN:
            self.prev_pt = pt
        elif event == cv2.EVENT_LBUTTONUP:
            self.prev_pt = None

        if self.prev_pt and flags & cv2.EVENT_FLAG_LBUTTON:
            cv2.line(self.image, self.prev_pt, pt, (127,)*3, 5)
            cv2.line(self.mask, self.prev_pt, pt, 255, 5)

            self.dirty = True
            self.prev_pt = pt
            self.show()
  1. 加载图像,创建其缺陷版本和遮罩,应用 Inpaint 算法,然后显示结果:
img = cv2.imread('../data/Lena.png')

defect_img = img.copy()
mask = np.zeros(img.shape[:2], np.uint8)
m_creator = MaskCreator(defect_img, mask)

while True:
    k = cv2.waitKey()
    if k == 27:
        break
    if k == ord('a'):
        res_telea = cv2.inpaint(defect_img, mask, 3, cv2.INPAINT_TELEA)
        res_ns = cv2.inpaint(defect_img, mask, 3, cv2.INPAINT_NS)
        cv2.imshow('TELEA vs NS', np.hstack((res_telea, res_ns)))
    if k == ord('c'):
        defect_img[:] = img
        mask[:] = 0
        m_creator.show()
cv2.destroyAllWindows()

这个怎么运作

要在 OpenCV 中修复图像,需要使用cv2.inpaint函数。 它接受四个参数:

  • 有缺陷的图像:它必须是 8 位彩色或灰度级图像
  • 缺陷遮罩:它必须是 8 位单通道,并且必须与第一个参数中的图像大小相同
  • 邻域半径损坏的像素周围区域的大小,应在计算其颜色时使用
  • 修复模式:修复算法的类型

缺陷遮罩应包含原始图像上像素的非零值,需要恢复该值。 邻域半径是在修复过程中考虑的算法周围的像素范围; 它应具有较小的值,以防止剧烈的模糊效果。 修复模式必须为以下值之一:cv2.INPAINT_TELEAcv2.INPAINT_NS。 根据具体情况,一种算法可能会比另一种算法更好,反之亦然,因此最好比较两种算法的结果并选择最佳算法。 cv2.inpaint返回生成的修复图像。

启动代码后,您将看到类似的图像:

如上图所示,最容易修复的缺陷是很小或几乎没有纹理的区域,这不足为奇。 修复算法没有实现任何魔术,因此在图像的复杂部分中存在可见但有色的瑕疵。

九、多视图几何

本章涵盖以下秘籍:

  • 针孔相机模型校准
  • 鱼眼镜头模型校准
  • 立体相机校准 - 外在性估计
  • 失真点和不失真点
  • 消除图像中的镜头失真效果
  • 通过三角测量从两个观测值还原 3D 点
  • 通过 PnP 算法找到相对的相机对象姿态
  • 通过立体校正对齐两个视图
  • 对极几何 - 计算基本和本质矩阵
  • 将基本矩阵分解为旋转和平移
  • 估计立体图像的视差图
  • 特例 2 - 视图几何 - 估计单应变换
  • 平面场景 - 将单应性分解为旋转和平移
  • 旋转相机 CAS - 从单应性估计相机旋转

介绍

将 3D 场景投影到 2D 图像上(换句话说,使用照相机)可以消除有关场景对象离摄影师的距离的信息。 但是在某些情况下,可以还原 3D 信息。 这不仅需要了解有关对象或摄像机配置的信息,还需要具有摄像机的固有参数。 本章介绍了从相机校准到 3D 对象位置重建和深度图检索的所有 2D 图像获取 3D 信息的必要步骤。

针孔相机模型校准

针孔相机模型以及其他模型都是最简单的数学模型,但它可以应用于许多实际的摄影设备。 此秘籍告诉您如何校准相机,例如,找到其固有参数和失真系数。

准备

在继续此秘籍之前,您需要安装 OpenCV(3.3 版或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 从相机捕获帧,检测每个帧上的棋盘图案,并累积帧和角,直到我们有足够多的样本:
cap = cv2.VideoCapture(0)

pattern_size = (10, 7)

samples = []

while True:
    ret, frame = cap.read()
    if not ret:
        break

    res, corners = cv2.findChessboardCorners(frame, pattern_size)

    img_show = np.copy(frame)
    cv2.drawChessboardCorners(img_show, pattern_size, corners, res)
    cv2.putText(img_show, 'Samples captured: %d' % len(samples), (0, 
    40), 
                cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2)
    cv2.imshow('chessboard', img_show)

    wait_time = 0 if res else 30
    k = cv2.waitKey(wait_time)

    if k == ord('s') and res:
        samples.append((cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), 
        corners))
    elif k == 27:
        break

cap.release()
cv2.destroyAllWindows()
  1. 使用cv2.cornerSubPix细化所有检测到的角点:
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)

for i in range(len(samples)):
    img, corners = samples[i]
    corners = cv2.cornerSubPix(img, corners, (10, 10), (-1,-1), criteria)
  1. 通过将所有精确的角点传递到cv2.calibrateCamera来找到相机的固有参数:
pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32)
pattern_points[:, :2] = np.indices(pattern_size).T.reshape(-1, 2)

images, corners = zip(*samples)

pattern_points = [pattern_points]*len(corners)

rms, camera_matrix, dist_coefs, rvecs, tvecs = \
    cv2.calibrateCamera(pattern_points, corners, images[0].shape, 
    None, None)

np.save('camera_mat.npy', camera_matrix)
np.save('dist_coefs.npy', dist_coefs)

这个怎么运作

相机校准旨在找到两组固有参数:相机矩阵和失真系数。 相机矩阵确定 3D 点的坐标如何映射到图像中的无量纲像素坐标,但是实际的图像镜头也会使图像变形,因此直线会转变为曲线。 失真系数使您可以消除这种扭曲。

整个相机校准过程可以分为三个阶段:

  • 收集大量数据,例如图像和检测到的棋盘图案
  • 提炼棋盘边角坐标
  • 优化相机参数以使其与观察到的变形和投影相匹配

为了收集用于相机校准的数据,您需要检测特定大小的棋盘图案,并累积成对的图像和找到的角点的坐标。 从第 4 章,“对象检测和机器学习”的“检测棋盘和圆形网格图案”,cv2.findChessboardCorners中可以知道,实现了棋盘角检测。 有关更多信息,请参见第 4 章“对象检测和机器学习”。 值得一提的是,棋盘上的角是由两个黑色正方形形成的角,在cv2.findChessboardCorners中传递的图案大小应与真实棋盘图案中的图案大小相同。 样本的数量及其沿视场的分布也非常重要。 在实际情况下,50 到 100 个样本就足够了。*

下一步是细化角的坐标。 由于cv2.findChessboardCorners不能给出非常准确的结果,因此需要此阶段,因此我们需要找到实际的角位置。 cv2.cornerSubPix以子像素精度为角坐标提供精度。 它接受以下参数:

  • 灰度图像
  • 检测到的角点的大致坐标
  • 细化区域的大小以找到更准确的角点位置
  • 细化区域中心的区域大小,可以忽略
  • 停止提炼过程的标准

角的粗坐标是cv2.findChessboardCorners返回的坐标。 细化区域应较小,但应包括角点的实际位置; 否则,返回粗角。 要忽略的区域的大小应小于细化区域,并且可以通过传递(-1, -1)作为其值来禁用。 停止条件可以是以下类型之一cv2.TERM_CRITERIA_EPScv2.TERM_CRITERIA_MAX_ITER或两者的组合。 cv2.TERM_CRITERIA_EPS确定前一个和下一个角点位置的差异。 如果实际差异小于定义的差异,则将停止该过程。 cv2.TERM_CRITERIA_MAX_ITER确定最大迭代次数。 cv2.cornerSubPix返回相同数量的带有精确坐标的角。

一旦我们确定了角点的位置,就可以找到相机的参数了。 cv2.calibrateCamera解决了这个问题。 您需要向此函数传递一些参数,这些参数列出如下:

  • 所有样本的对象点坐标
  • 所有样本的角点坐标
  • (宽度,高度)格式的图像形状
  • 两个数组保存平移和旋转向量(可以设置为 None)
  • 标志和停止条件(均具有默认值)

对象点是棋盘坐标系中棋盘角的 3D 坐标。 因为我们对每个帧使用相同的模式,所以角的 3D 坐标是相同的,并且因为我们使用等距分布的角,所以 3D 坐标也等距分布在平面上(所有点的z = 0)。 角点之间的实际距离无关紧要,因为摄影机会消除 z 坐标(物体距摄影机的距离),因此它可以是较小但更近的图案,也可以是较大但更远的图案-图像相同。 cv2.calibrateCamera返回五个值:所有样本的平均重投影误差,相机矩阵,失真系数,旋转和所有样本的平移向量。 重投影误差是图像中某个角与该角的 3D 点的投影之间的差。 理想情况下,角点的投影及其在图像中的原始位置应相同,但由于噪声而存在差异。 该差异以像素为单位。 该差异越小,校准效果越好。 相机矩阵的形状为3x3。 失真系数的数量取决于标记,默认情况下等于 5。

执行此代码后,您将看到以下图片:

鱼眼镜头模型校准

如果您的相机具有宽广的视角,并因此导致强烈的变形,则需要使用鱼眼镜头模型。 OpenCV 提供了与鱼眼镜头模型一起使用的功能。 让我们回顾一下如何在 OpenCV 中校准此类摄像机。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 从相机捕获帧,检测每个帧上的棋盘图案,并累积帧和角,直到我们有足够多的样本:
cap = cv2.VideoCapture(0)

pattern_size = (10, 7)

samples = []

while True:
    ret, frame = cap.read()
    if not ret:
        break

    res, corners = cv2.findChessboardCorners(frame, pattern_size)

    img_show = np.copy(frame)
    cv2.drawChessboardCorners(img_show, pattern_size, corners, res)
    cv2.putText(img_show, 'Samples captured: %d' % len(samples), (0, 40), 
                cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2)
    cv2.imshow('chessboard', img_show)

    wait_time = 0 if res else 30
    k = cv2.waitKey(wait_time)

    if k == ord('s') and res:
        samples.append((cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), corners))
    elif k == 27:
        break

cap.release()
cv2.destroyAllWindows()
  1. 使用cv2.cornerSubPix细化所有检测到的角点:
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)

for i in range(len(samples)):
    img, corners = samples[i]
    corners = cv2.cornerSubPix(img, corners, (10, 10), (-1,-1), criteria)
  1. 导入必要的模块,打开输入图像,然后复制它:
pattern_points = np.zeros((1, np.prod(pattern_size), 3), np.float32)
pattern_points[0, :, :2] = np.indices(pattern_size).T.reshape(-1, 2)

images, corners = zip(*samples)

pattern_points = [pattern_points]*len(corners)

print(len(pattern_points), pattern_points[0].shape, pattern_points[0].dtype)
print(len(corners), corners[0].shape, corners[0].dtype)

rms, camera_matrix, dis t_coefs, rvecs, tvecs = \
    cv2.fisheye.calibrate(pattern_points, corners, images[0].shape, None, None)

np.save('camera_mat.npy', camera_matrix)
np.save('dist_coefs.npy', dist_coefs)

这个怎么运作

鱼眼相机和针孔相机的相机校准程序基本相同,因此强烈建议使用“针孔相机模型校准”秘籍,因为针孔相机盒的所有主要步骤和建议都适用于鱼眼镜头

让我们回顾一下主要区别。 要校准鱼眼模型相机,您需要使用cv2.fisheye.calibrate函数。 它接受与cv2.calibrateCamera相同的参数,但是此函数仅支持其自己的标志值。 幸运的是,此参数具有默认值。

执行此代码的结果是,您将看到类似于以下图像:

立体相机校准 - 外在性估计

在本秘籍中,您将学习如何校准立体对,即使用校准图案的照片估计两个摄像机之间的相对旋转和平移。 在处理立体相机时,将使用此功能-您需要知道装备参数才能重建有关场景的 3D 信息。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import glob
import numpy as np
  1. 设置图案大小并准备带有图像的列表:
PATTERN_SIZE = (9, 6)
left_imgs = list(sorted(glob.glob('../data/stereo/case1/left*.png')))
right_imgs = list(sorted(glob.glob('../data/stereo/case1/right*.png')))
assert len(left_imgs) == len(right_imgs)
  1. 找到棋盘点:
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)
left_pts, right_pts = [], []
img_size = None

for left_img_path, right_img_path in zip(left_imgs, right_imgs):
    left_img = cv2.imread(left_img_path, cv2.IMREAD_GRAYSCALE)
    right_img = cv2.imread(right_img_path, cv2.IMREAD_GRAYSCALE)
    if img_size is None:
        img_size = (left_img.shape[1], left_img.shape[0])

    res_left, corners_left = cv2.findChessboardCorners(left_img, PATTERN_SIZE)
    res_right, corners_right = cv2.findChessboardCorners(right_img, PATTERN_SIZE)

    corners_left = cv2.cornerSubPix(left_img, corners_left, (10, 10), (-1,-1),
                                    criteria)
    corners_right = cv2.cornerSubPix(right_img, corners_right, (10, 10), (-1,-1), 
                                     criteria)

    left_pts.append(corners_left)
    right_pts.append(corners_right)
  1. 准备校准图案点:
pattern_points = np.zeros((np.prod(PATTERN_SIZE), 3), np.float32)
pattern_points[:, :2] = np.indices(PATTERN_SIZE).T.reshape(-1, 2)
pattern_points = [pattern_points] * len(left_imgs)
  1. 估计立体对参数:
err, Kl, Dl, Kr, Dr, R, T, E, F = cv2.stereoCalibrate(
    pattern_points, left_pts, right_pts, None, None, None, None, img_size, flags=0)
  1. 报告校准结果:
print('Left camera:')
print(Kl)
print('Left camera distortion:')
print(Dl)
print('Right camera:')
print(Kr)
print('Right camera distortion:')
print(Dr)
print('Rotation matrix:')
print(R)
print('Translation:')
print(T)

这个怎么运作

要使用 OpenCV 校准立体对,必须同时从两台摄像机捕获校准模式的几张照片。 在我们的案例中,我们使用了9x6的棋盘。 我们使用cv2.findChessboardCorners函数找到板的角,将用于相机参数估计。 我们还需要在其本地坐标系中的校准图案点。 由于我们知道模式的大小及其形状,因此可以显式构造点列表pattern_points。 请注意,此处使用的单位将用于两个摄像机之间的转换向量。

校准本身在cv2.stereoCalibrate函数中执行。 作为输入,它需要一个图像点列表和一个图案点列表。 您还可以为校准参数指定初始猜测,并指定要优化的参数以及要保持不变的参数。 该函数以像素,第一相机参数,第一相机失真系数,第二相机参数,第二相机失真系数,相机之间的旋转和平移以及基本矩阵和基本矩阵的形式返回校准误差。

以下是预期的输出:

Left camera:
[[ 534.36681752    0\.          341.45684657]
 [   0\.          534.29616718  235.72519106]
 [   0\.            0\.            1\.        ]]
Left camera distortion:
[[ -2.79470900e-01   4.71876981e-02   1.39511507e-03  -1.64158448e-04
    7.01729203e-02]]
Right camera:
[[ 537.88729748    0\.          327.29925115]
 [   0\.          537.43063947  250.10021993]
 [   0\.            0\.            1\.        ]]
Right camera distortion:
[[-0.28990693  0.12537789 -0.00040656  0.00053461 -0.03844589]]
Rotation matrix:
[[ 0.99998995  0.00355598  0.00273003]
 [-0.00354058  0.99997791 -0.00562461]
 [-0.00274997  0.00561489  0.99998046]]
Translation:
[[-3.33161159]
 [ 0.03706722]
 [-0.00420814]]

失真点和非失真点

相机镜头会产生图像失真。 校准过程旨在查找这些变形的参数,以及将 3D 点投影到图像平面上的参数。 此秘籍告诉您如何应用相机矩阵和失真系数以获取未失真的图像点并将其失真。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载相机的相机矩阵和失真系数:
camera_matrix = np.load('../data/pinhole_calib/camera_mat.npy')
dist_coefs = np.load('../data/pinhole_calib/dist_coefs.npy')
  1. 打开相机拍摄的国际象棋棋盘的照片,然后找到并优化角点:
img = cv2.imread('../data/pinhole_calib/img_00.png')
pattern_size = (10, 7)
res, corners = cv2.findChessboardCorners(img, pattern_size)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)
corners = cv2.cornerSubPix(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), 
                           corners, (10, 10), (-1,-1), criteria)
  1. 取消扭曲角的坐标并将其转换为 3D 点:
h_corners = cv2.undistortPoints(corners, camera_matrix, dist_coefs)
h_corners = np.c_[h_corners.squeeze(), np.ones(len(h_corners))]
  1. 将角点的 3D 坐标投影到图像上而不应用失真:
img_pts, _ = cv2.projectPoints(h_corners, (0, 0, 0), (0, 0, 0), camera_matrix, None)

for c in corners:
    cv2.circle(img, tuple(c[0]), 10, (0, 255, 0), 2)

for c in img_pts.squeeze().astype(np.float32):
    cv2.circle(img, tuple(c), 5, (0, 0, 255), 2)

cv2.imshow('undistorted corners', img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 将角点的 3D 坐标投影到图像上并应用镜头变形:
img_pts, _ = cv2.projectPoints(h_corners, (0, 0, 0), (0, 0, 0), camera_matrix, dist_coefs)

for c in img_pts.squeeze().astype(np.float32):
    cv2.circle(img, tuple(c), 2, (255, 255, 0), 2)

cv2.imshow('reprojected corners', img)
cv2.waitKey()
cv2.destroyAllWindows()

这个怎么运作

cv2.undistortPoints查找图像中各点的同类坐标。 此函数消除了镜头变形并投影了点,使其处于无量纲坐标。 该函数接受以下参数:图像中的 2D 点数组,3x3相机矩阵,一组失真系数,用于存储结果的对象以及在立体视觉中使用的校正和投影矩阵,并且现在无所谓。 最后三个参数是可选的。 cv2.undistortPoints返回未失真和未投影点的集合。

cv2.undistortPoints返回的点是理想的-它们的坐标是无量纲的,并且不会因镜头而失真。 如果需要将它们投影回去,则需要将它们转换为 3D 点。 为此,我们只需要向每个点添加第三个Z坐标即可。 由于这些点的坐标是同质的,因此Z等于 1。

当我们拥有 3D 点并将其投影到图像上时,cv2.projectPoints开始起作用。 在一般情况下,此函数在某个坐标系中获取点的 3D 坐标,对其进行旋转和平移以获取相机坐标系中的坐标,然后应用相机矩阵和变形系数以找到这些点在图像平面上的投影 。

cv2.>projectPoints的参数包括:某些局部坐标系中的 3D 点数组,从局部坐标系到相机坐标系的转换的旋转和平移向量,3x3相机矩阵,失真系数数组,用于存储结果点的对象,用于存储 Jacobian 值的对象以及宽高比的值。 同样,最后三个参数是可选的,可以省略。 此函数返回 3D 点和 Jacobian 值的投影坐标和变形坐标。 如果要获取没有透镜变形的点的位置,则可以将None传递为变形系数数组的值。

执行此代码的结果是,您将看到类似于以下图像:

图中的绿色圆圈是棋盘角的原始位置。 红色的是角的投影坐标,但没有镜头失真。 浅蓝色点是变形后的投影坐标-它们正好在绿色圆圈的中心。 另外,您可能会注意到,绿色和浅蓝色的圆圈不是在直线上,而是红色的圆圈。 这是镜头变形的影响。 您也许还可以注意到,对于远离图像中心的角,红色和浅蓝色圆圈坐标之间的差异非常明显,尽管靠近图像中心的圆圈几乎相同。 这是由于镜头变形的程度而发生的,这取决于该点距镜头中心的距离。

消除图像中的镜头失真效果

如果需要从整个图像中消除镜头畸变的影响,则需要使用密集的重映射。 本质上,不失真算法以补偿镜头效果的方式扭曲和压缩图像,但是压缩会导致出现空白区域。 此秘籍告诉您如何使图像不失真并从未失真的图像中删除空白区域。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载同一相机拍摄的相机矩阵和畸变系数以及照片:
camera_matrix = np.load('../data/pinhole_calib/camera_mat.npy')
dist_coefs = np.load('../data/pinhole_calib/dist_coefs.npy')
img = cv2.imread('../data/pinhole_calib/img_00.png')
  1. 使用cv2.undistort取消扭曲图像-图像中将出现空白区域:
ud_img = cv2.undistort(img, camera_matrix, dist_coefs)

cv2.imshow('undistorted image', ud_img)
cv2.waitKey(0)

cv2.destroyAllWindows()
  1. 通过计算最佳相机矩阵并将其应用以获得没有黑色区域的未经失真的图像来消除空白区域:
opt_cam_mat, valid_roi = cv2.getOptimalNewCameraMatrix(camera_matrix, dist_coefs, img.shape[:2][::-1], 0)

ud_img = cv2.undistort(img, camera_matrix, dist_coefs, None, opt_cam_mat)

cv2.imshow('undistorted image', ud_img)
cv2.waitKey(0)

cv2.destroyAllWindows()

这个怎么运作

cv2.undistort消除图像中的镜头失真。 它采用以下参数:要失真的图像,相机矩阵,失真系数数组,存储未失真图像的对象以及最佳相机矩阵。 最后两个参数是可选的。 该函数返回未失真的图像。 如果错过了cv2.undistort的最后一个参数,则生成的图像将包含空白区域(黑色)。 最佳摄影机矩阵参数可让您获得没有这些伪影的图像,但是我们需要一种计算此最佳摄影机矩阵的方法,OpenCV 会为其提供服务。

cv2.getOptimalNewCameraMatrix创建最佳相机矩阵,以消除未失真图像上的黑色区域。 它需要相机矩阵,失真系数,(宽度,高度)格式的原始图像大小,alpha 因子,所得图像大小(同样的(宽度,高度)格式),以及设置摄像机的主要相机点在输出图像的中心的布尔标志。 最后两个参数是可选的。 alpha 因子是[0. 1]范围内的两倍,它表示删除空白区域的程度:0 表示完全删除,因此损失了一部分图像像素,而 1 表示保留了所有图像像素。 以及空白区域。 如果未设置输出图像尺寸,则将其设置为与输入图像的尺寸相同。

从秘籍启动代码后,您将看到类似于以下内容的图像:

如您所见,上面的图像在边框附近有黑色区域,下面的图像则没有。

通过三角测量从两个观测值来还原 3D 点

在本秘籍中,您将学习如何在两个视图中给定观察值来重建 3D 点坐标。 这是许多更高级别的 3D 重建算法和 SLAM 系统的基础。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块。
import cv2
import numpy as np
  1. 生成测试相机的投影矩阵:
P1 = np.eye(3, 4, dtype=np.float32)
P2 = np.eye(3, 4, dtype=np.float32)
P2[0, 3] = -1
  1. 生成测试点:
N = 5
points3d = np.empty((4, N), np.float32)
points3d[:3, :] = np.random.randn(3, N)
points3d[3, :] = 1
  1. 将 3D 点投影到两个视图中并添加噪点:
points1 = P1 @ points3d
points1 = points1[:2, :] / points1[2, :]
points1[:2, :] += np.random.randn(2, N) * 1e-2

points2 = P2 @ points3d
points2 = points2[:2, :] / points2[2, :]
points2[:2, :] += np.random.randn(2, N) * 1e-2
  1. 从嘈杂的观察中重建点:
points3d_reconstr = cv2.triangulatePoints(P1, P2, points1, points2)
points3d_reconstr /= points3d_reconstr[3, :]
  1. 打印结果:
print('Original points')
print(points3d[:3].T)
print('Reconstructed points')
print(points3d_reconstr[:3].T)

这个怎么运作

我们在 3D 空间中生成随机点,并将其投影到两个测试视图中。 然后,我们向这些观测值添加噪声,并使用 OpenCV 函数cv2.triangulatePoints重建 3D 点。 作为输入,该函数从两个摄像机和每个视图的摄像机投影矩阵(从世界坐标系到视图坐标系的投影映射)获取观测值。 它返回世界坐标系中的重建点。

以下是可能的结果:

Original points
[[ 0.48245686 -2.05779004  1.3458606 ]
 [-0.18333936 -1.00662899 -0.46047512]
 [-0.51193094 -0.54561883  0.20674749]
 [ 1.05258393 -1.55241323  0.60368073]
 [ 1.80103588 -0.83367926 -0.59293056]]
Reconstructed points
[[ 0.47777811 -2.05873108  1.3407315 ]
 [-0.17389734 -0.99433696 -0.45361272]
 [-0.51100874 -0.54552656  0.20692034]
 [ 1.05780101 -1.54776227  0.60341281]
 [ 1.81407869 -0.83914387 -0.59897166]]

通过 PnP 算法找到相对的相机对象姿态

相机会删除与要拍摄的物体有多远的信息。 它可能是一个很小但很近的物体,也可能是一个很大而又很远的物体(图像可能是相同的),但是通过知道物体的几何尺寸,我们可以计算出物体到相机的距离。 通常,我们对对象几何形状的了解是对象局部坐标系中某些 3D 点集的位置。 通常,我们不仅要知道相机和物体的局部坐标系之间的距离,而且要知道物体的方位。 使用 OpenCV 可以成功完成此任务。 如果我们知道对象的 3D 点及其在图像上的相应 2D 投影的配置,那么本秘籍将向您展示如何找到对象的 6 自由度(自由度)位置。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载相机矩阵,失真系数和相机拍摄的对象的照片:
camera_matrix = np.load('../data/pinhole_calib/camera_mat.npy')
dist_coefs = np.load('../data/pinhole_calib/dist_coefs.npy')
img = cv2.imread('../data/pinhole_calib/img_00.png')
  1. 检测图像中的对象点,在本例中为棋盘角:
pattern_size = (10, 7)
res, corners = cv2.findChessboardCorners(img, pattern_size)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)
corners = cv2.cornerSubPix(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), 
                           corners, (10, 10), (-1,-1), criteria)
  1. 创建 3D 对象点的配置:
pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32)
pattern_points[:, :2] = np.indices(pattern_size).T.reshape(-1, 2)
  1. 使用cv2.solvePnP查找对象的位置和方向:
ret, rvec, tvec = cv2.solvePnP(pattern_points, corners, camera_matrix, dist_coefs, 
                               None, None, False, cv2.SOLVEPNP_ITERATIVE)
  1. 通过应用找到的旋转和平移,将对象的点投影回图像。 绘制投影点:
img_points, _ = cv2.projectPoints(pattern_points, rvec, tvec, camera_matrix, dist_coefs)

for c in img_points.squeeze():
    cv2.circle(img, tuple(c), 10, (0, 255, 0), 2)

cv2.imshow('points', img)
cv2.waitKey()

cv2.destroyAllWindows()

这个怎么运作

cv2.solvePnP能够通过对象在本地坐标系中的 3D 点及其在图像上的 2D 投影找到对象的平移和旋转。 它接受一组 3D 点,一组 2D 点,一个3x3相机矩阵,畸变系数,初始旋转和平移向量(可选),是否使用初始位置和方向的标记以及问题求解器的类型 。 前两个参数应包含相同数量的点。 求解器的类型可以是许多类型之一:cv2.SOLVEPNP_ITERATIVEcv2.SOLVEPNP_EPNPcv2.SOLVEPNP_DLS等。

默认情况下,使用cv2.SOLVEPNP_ITERATIVE,在很多情况下它都能获得不错的结果。 cv2.solvePnP返回三个值:成功标志,旋转向量和平移向量。 成功标志表示问题已正确解决。 平移向量的单位与对象的 3D 局部点相同。 旋转向量以 Rodrigues 形式返回:向量的方向表示对象绕其旋转的轴,向量的范数表示旋转角度。

从秘籍启动代码后,它将显示类似于以下内容的图像:

通过立体校正对齐两个视图

在本秘籍中,您将学习如何校正具有已知参数的使用立体摄像机拍摄的两个图像,使得对于(x[l], y[l]),右图中相应的对极线是y[r] = y[l],反之亦然。 这极大地简化了特征匹配和密集的立体估计算法。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载立体相机校准参数:
data = np.load('../data/stereo/case1/stereo.npy').item()
Kl, Dl, Kr, Dr, R, T, img_size = data['Kl'], data['Dl'], data['Kr'], data['Dr'], \
                                 data['R'], data['T'], data['img_size']
  1. 加载左右测试图像:
left_img = cv2.imread('../data/stereo/case1/left14.png')
right_img = cv2.imread('../data/stereo/case1/right14.png')
  1. 估计立体整流参数:
R1, R2, P1, P2, Q, validRoi1, validRoi2 = cv2.stereoRectify(Kl, Dl, Kr, Dr, 
                                                            img_size, R, T)
  1. 准备立体整流变换图:
xmap1, ymap1 = cv2.initUndistortRectifyMap(Kl, Dl, R1, Kl, img_size, cv2.CV_32FC1)
xmap2, ymap2 = cv2.initUndistortRectifyMap(Kr, Dr, R2, Kr, img_size, cv2.CV_32FC1)
  1. 纠正图像:
left_img_rectified = cv2.remap(left_img, xmap1, ymap1, cv2.INTER_LINEAR)
right_img_rectified = cv2.remap(right_img, xmap2, ymap2, cv2.INTER_LINEAR)
  1. 可视化结果:
plt.figure(0, figsize=(12,10))
plt.subplot(221)
plt.title('left original')
plt.imshow(left_img, cmap='gray')
plt.subplot(222)
plt.title('right original')
plt.imshow(right_img, cmap='gray')
plt.subplot(223)
plt.title('left rectified')
plt.imshow(left_img_rectified, cmap='gray')
plt.subplot(224)
plt.title('right rectified')
plt.imshow(right_img_rectified, cmap='gray')
plt.tight_layout()
plt.show()

这个怎么运作

我们加载先前从文件中估计的立体装备参数。 校正过程本身估计这种相机变换,以使两个单独的图像平面之后变为同一平面。 这极大地简化了极线几何约束,并使所有其他与立体相关的算法的工作变得更加容易。

使用cv2.stereoRectify函数估计校正变换参数-它获取立体装备参数并返回校正参数:第一摄像机旋转,第二摄像机旋转,第一摄像机投影矩阵,第二摄像机投影矩阵,视差-深度映射矩阵,所有像素均有效的第一相机 ROI 和所有像素均有效的第二相机 ROI。

我们只使用前两个参数。 第一和第二摄像机旋转用于使用cv2.initUndistortRectifyMap函数构建每像素图的校正变换。 一次计算映射后,即可将其用于使用立体装备捕获的任何图像。

预期结果如下所示:

对极几何 - 计算基本和本质矩阵

在本秘籍中,您将学习如何计算基本矩阵和基本矩阵,即其中包含对极几何约束的矩阵。 这些矩阵可用于重建立体装备的外部参数以及其他两视图视觉算法。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载左/右图像点对应关系和各个相机校准参数:
data = np.load('../data/stereo/case1/stereo.npy').item()
Kl, Kr, Dl, Dr, left_pts, right_pts, E_from_stereo, F_from_stereo = \
    data['Kl'], data['Kr'], data['Dl'], data['Dr'], \
    data['left_pts'], data['right_pts'], data['E'], data['F']
  1. 将左右点列表堆叠到数组中:
left_pts = np.vstack(left_pts)
right_pts = np.vstack(right_pts)
  1. 消除镜头变形:
left_pts = cv2.undistortPoints(left_pts, Kl, Dl, P=Kl)
right_pts = cv2.undistortPoints(right_pts, Kr, Dr, P=Kr)
  1. 估计基本矩阵:
F, mask = cv2.findFundamentalMat(left_pts, right_pts, cv2.FM_LMEDS)
  1. 估计本质矩阵:
E = Kr.T @ F @ Kl
  1. 打印结果:
print('Fundamental matrix:')
print(F)
print('Essential matrix:')
print(E)

这个怎么运作

我们使用cv2.findFundamentalMat函数从左右图像点对应关系估计基本矩阵。 此函数支持几种不同的基本矩阵参数估计算法,例如cv2.FM_7POINT(7 点算法),cv2.FM_8POINT(8 点算法),cv2.FM_LMEDS(最低中值方法)和cv2.FM_RANSAC( 基于 RANSAC 的方法)。 两个可选参数指定基于 RANSAC 的估计算法的错误阈值,以及用于中位数最小和基于 RANSAC 的方法的置信度。

以下是预期结果:

Fundamental matrix:
[[  1.60938825e-08  -2.23906409e-06  -2.53850603e-04]
 [  2.97226703e-06  -2.38236386e-07  -7.70276666e-02]
 [ -2.55190056e-04   7.69760820e-02   1.00000000e+00]]
Essential matrix:
[[  4.62585055e-03  -6.43487140e-01  -4.17486092e-01]
 [  8.53590806e-01  -6.84088948e-02  -4.08817705e+01]
 [  2.63679084e-01   4.07046349e+01  -2.20825664e-01]]

将基本矩阵分解为旋转和平移

在本秘籍中,您将学习如何将基本矩阵分解为两个假设,这些假设关于立体装备中两个摄像机之间的相对旋转和平移向量。 估计立体装备参数时使用此功能。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载预先计算的基本矩阵:
data = np.load('../data/stereo/case1/stereo.npy').item()
E = data['E']
  1. 将基本矩阵分解为两个可能的旋转和平移:
R1, R2, T = cv2.decomposeEssentialMat(E)
  1. 打印结果:
print('Rotation 1:')
print(R1)
print('Rotation 2:')
print(R2)
print('Translation:')
print(T)

这个怎么运作

我们使用 OpenCV cv2.decomposeEssentialMat函数,该函数将基本矩阵作为输入,并返回两个候选摄像机之间的旋转和一个转换向量候选。 请注意,由于翻译向量只能恢复到一定规模,因此以标准化形式(单位长度)返回。

以下是预期结果:

Rotation 1:
[[ 0.99981105 -0.01867927  0.00538031]
 [-0.01870903 -0.99980965  0.00553437]
 [ 0.00527591 -0.00563399 -0.99997021]]
Rotation 2:
[[ 0.99998995  0.00355598  0.00273003]
 [-0.00354058  0.99997791 -0.00562461]
 [-0.00274997  0.00561489  0.99998046]]
Translation:
[[ 0.99993732]
 [-0.01112522]
 [ 0.00126302]]

估计立体图像的视差图

在本秘籍中,您将学习如何从两个校正后的图像中计算视差图。 此功能在许多需要恢复场景深度信息的计算机视觉应用中很有用,例如,高级驾驶员辅助应用中的避免碰撞。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载左右校正图像:
left_img = cv2.imread('../data/stereo/left.png')
right_img = cv2.imread('../data/stereo/right.png')
  1. 使用立体块匹配算法计​​算视差图:
stereo_bm = cv2.StereoBM_create(32)
dispmap_bm = stereo_bm.compute(cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY), 
                               cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY))
  1. 使用立体半全局块匹配算法计​​算视差图:
stereo_sgbm = cv2.StereoSGBM_create(0, 32)
dispmap_sgbm = stereo_sgbm.compute(left_img, right_img)
  1. 可视化结果:
plt.figure(figsize=(12,10))
plt.subplot(221)
plt.title('left')
plt.imshow(left_img[:,:,[2,1,0]])
plt.subplot(222)
plt.title('right')
plt.imshow(right_img[:,:,[2,1,0]])
plt.subplot(223)
plt.title('BM')
plt.imshow(dispmap_bm, cmap='gray')
plt.subplot(224)
plt.title('SGBM')
plt.imshow(dispmap_sgbm, cmap='gray')
plt.show()

这个怎么运作

我们使用两种不同的算法进行视差图计算-块匹配和半全局块匹配。 在使用cv2.StereoBM_createcv2.StereoSGBM_create(在其中指定最大可能视差)实例化映射估计对象之后,我们调用compute方法,该方法将获取两张图像并返回视差图。

请注意,有必要将经过校正的图像作为compute方法的输入。 返回的视差图将包含每个像素的视差值,例如,对应于场景中同一点的左右图像点之间的像素水平偏移。 然后可以使用该偏移量还原 3D 中的实际点。

创建视差估计器时,您可以指定一些特定于所使用算法的参数。 有关更详细的描述,您可以参考 OpenCV 的文档

OpenCV 中还有一个名为cudastereo的模块,该模块是通过 CUDA 支持构建的,该模块提供了更优化的立体算法。 您还可以在 OpenCV Contrib 存储库中检出stereo模块,该模块还包含一些其他算法。

预期结果如下所示:

特例 2 - 视图几何 - 估计单应变换

如果需要将点从一个平面投影到另一个平面,可以通过应用单应性矩阵来实现。 如果知道平面的对应变换,则可以使用此矩阵将点从一个平面投影到另一平面。 OpenCV 具有查找单应性矩阵的功能,此秘籍向您展示如何使用和应用它。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载相机矩阵,失真系数和相机拍摄的两帧:
camera_matrix = np.load('../data/pinhole_calib/camera_mat.npy')
dist_coefs = np.load('../data/pinhole_calib/dist_coefs.npy')
img_0 = cv2.imread('../data/pinhole_calib/img_00.png')
img_1 = cv2.imread('../data/pinhole_calib/img_10.png')
  1. 取消扭曲帧:
img_0 = cv2.undistort(img_0, camera_matrix, dist_coefs)
img_1 = cv2.undistort(img_1, camera_matrix, dist_coefs)
  1. 在两个图像上找到棋盘角:
pattern_size = (10, 7)
res_0, corners_0 = cv2.findChessboardCorners(img_0, pattern_size)
res_1, corners_1 = cv2.findChessboardCorners(img_1, pattern_size)

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)
corners_0 = cv2.cornerSubPix(cv2.cvtColor(img_0, cv2.COLOR_BGR2GRAY), 
                           corners_0, (10, 10), (-1,-1), criteria)
corners_1 = cv2.cornerSubPix(cv2.cvtColor(img_1, cv2.COLOR_BGR2GRAY), 
                           corners_1, (10, 10), (-1,-1), criteria)
  1. 在两个图像的点之间找到单应性:
H, mask = cv2.findHomography(corners_0, corners_1)
  1. 应用找到的单应性矩阵将点从第一张图像投影到第二张图像:
center_0 = np.mean(corners_0.squeeze(), 0)
center_0 = np.r_[center_0, 1]
center_1 = H @ center_0
center_1 = (center_1 / center_1[2]).astype(np.float32)

img_0 = cv2.circle(img_0, tuple(center_0[:2]), 10, (0, 255, 0), 3)
img_1 = cv2.circle(img_1, tuple(center_1[:2]), 10, (0, 0, 255), 3)
  1. 使用找到的单应性矩阵变换第一张图像并显示结果:
img_0_warped = cv2.warpPerspective(img_0, H, img_0.shape[:2][::-1])

cv2.imshow('homography', np.hstack((img_0, img_1, img_0_warped)))
cv2.waitKey()
cv2.destroyAllWindows()

这个怎么运作

为了能够将一个点从一个平面投影到另一个平面,首先需要计算单应矩阵。 可以使用cv2.findHomography执行。 此函数接受以下参数:

  • 来自源(第一)平面的一组点
  • 来自目标(第二个)平面的一组点
  • 查找单应性的方法
  • 过滤异常值的阈值
  • 离群值的输出遮罩
  • 最大迭代次数
  • 置信度

除前两个参数外,所有参数均使用默认值。 方法参数描述应使用哪种算法计算单应性。 默认情况下,所有点都被使用,但是如果您的数据倾向于包含相当数量的离群值(噪声或误选点很大的点),则最好使用以下方法之一:cv2.RANSACcv2.LMEDS, 或cv2.RHO。 这些方法可以正确滤除异常值。 过滤离群值的阈值是以像素为单位的距离,该距离确定点的类型:离群值或离群值。 遮罩是一个对象,用于存储每个点的内部/外部类的值。 最大迭代次数和置信度确定了解决方案的正确性。 cv2.findHomography返回找到的单应性矩阵和点的掩码值。 还值得一提的是,您需要检查结果矩阵是否不是空对象,因为无法找到所有点集的解决方案。

找到单应性矩阵后,可以将其传递给cv2.warpPerspective并将其应用于图像投影。 也可以通过用单应性矩阵乘以点来投影点(请参见代码)。

最后,执行代码后,您将看到类似于以下图像:

平面场景 - 将单应性分解为旋转和平移

单应性矩阵可以分解为两个平面对象视图之间的相对平移和旋转向量。 此秘籍向您展示如何在 OpenCV 中进行操作。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载相机矩阵,畸变系数和同一平面对象(棋盘图案)的两张照片。 然后,取消扭曲照片:
camera_matrix = np.load('../data/pinhole_calib/camera_mat.npy')
dist_coefs = np.load('../data/pinhole_calib/dist_coefs.npy')
img_0 = cv2.imread('../data/pinhole_calib/img_00.png')
img_0 = cv2.undistort(img_0, camera_matrix, dist_coefs)
img_1 = cv2.imread('../data/pinhole_calib/img_10.png')
img_1 = cv2.undistort(img_1, camera_matrix, dist_coefs)
  1. 在两个图像中找到图案的角点:
pattern_size = (10, 7)
res_0, corners_0 = cv2.findChessboardCorners(img_0, pattern_size)
res_1, corners_1 = cv2.findChessboardCorners(img_1, pattern_size)

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3)
corners_0 = cv2.cornerSubPix(cv2.cvtColor(img_0, cv2.COLOR_BGR2GRAY), 
                           corners_0, (10, 10), (-1,-1), criteria)
corners_1 = cv2.cornerSubPix(cv2.cvtColor(img_1, cv2.COLOR_BGR2GRAY), 
                           corners_1, (10, 10), (-1,-1), criteria)
  1. 找到从第一帧到第二帧的单应变换矩阵:
H, mask = cv2.findHomography(corners_0, corners_1)
  1. 找到我们估计的单应矩阵的可能平移和旋转:
ret, rmats, tvecs, normals = cv2.decomposeHomographyMat(H, camera_matrix)

这个怎么运作

cv2.decomposeHomographyMat将单应性矩阵分解为旋转和平移。 由于解决方案不是唯一的,因此该函数最多返回四组可能的平移,旋转和法向向量集。 cv2.decomposeHomographyMat接受3x3单应矩阵和 3x3 摄影机矩阵作为参数。 返回值为:找到的解的数量,3x3旋转矩阵的列表,平移向量的列表和法线向量的列表。 每个返回的列表包含与找到的解决方案数量一样多的元素。

旋转相机案例 - 从单应性估计相机旋转

在本秘籍中,您将学习如何从仅相对于其光学中心进行旋转运动的摄像机捕获的两个视图之间的单应变换中提取旋转。 例如,如果您需要估计两个视图之间的旋转,这非常有用,并且假设与场景点的距离相比平移可以忽略不计。 在风景照片拼接中通常就是这种情况。

准备

在继续此秘籍之前,您需要安装 OpenCV 版本 3.3(或更高版本)Python API 包。

怎么做

您需要完成以下步骤:

  1. 导入必要的模块:
import cv2
import numpy as np
  1. 加载预先计算的单应性和相机参数:
data = np.load('../data/rotational_homography.npy').item()
H, K = data['H'], data['K']
  1. 从单应性变换中考虑摄像机参数:
H_ = np.linalg.inv(K) @ H @ K
  1. 计算近似旋转矩阵:
w, u, vt = cv2.SVDecomp(H_)
R = u @ vt
if cv2.determinant(R) < 0:
    R *= 1
  1. 将旋转矩阵转换为旋转向量:
rvec = cv2.Rodrigues(R)[0]
  1. 打印结果:
print('Rotation vector:')
print(rvec)

这个怎么运作

如果相机仅绕其光学中心旋转,则单应性变换的形式非常简单-它基本上是一个旋转矩阵,但由于单应性在图像像素空间中起作用,因此乘以了相机矩阵参数。 第一步,我们从单应性矩阵中剔除相机参数。 此后,它必须是旋转矩阵(按比例缩放)。 由于单应性参数中可能存在噪声,因此所得矩阵可能不是适当的旋转矩阵,例如行列式等于 1 的正交矩阵。 这就是为什么我们使用奇异值分解构造最接近(在 Frobenius 范数中)旋转矩阵的原因。

下面显示了预期的结果:

Rotation vector:
[[ 0.12439561]
 [ 0.22688715]
 [ 0.32641321]]
posted @ 2025-09-21 12:12  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报