Python OpenCV3 计算机视觉秘籍:1~5

2023-04-27 15:46:34 浏览数 (2)

一、I/O 和 GUI

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

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

介绍

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

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

从文件读取图像

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

准备

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

操作步骤

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

  1. 您可以使用cv2.imread函数轻松读取图像,该函数带有图像路径和可选标志:
代码语言:javascript复制
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. 有时检查图像是否成功加载很有用:
代码语言:javascript复制
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. 加载图像并将其转换为灰度,即使它最初具有许多颜色通道也是如此:
代码语言:javascript复制
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类型的转换以及图像是灰度还是彩色的。

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

代码语言:javascript复制
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. 加载图像并打印其原始尺寸:
代码语言:javascript复制
img = cv2.imread('../data/Lena.png')
print('original image shape:', img.shape)
  1. OpenCV 提供了几种使用cv2.resize函数的方法。 我们可以将以像素为单位的目标大小(widthheight)设置为第二个参数:
代码语言:javascript复制
width, height = 128, 256
resized_img = cv2.resize(img, (width, height))
print('resized to 128x256 image shape:', resized_img.shape)
  1. 通过设置图像原始宽度和高度的倍数来调整大小:
代码语言:javascript复制
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. 使用最近邻插值而不是默认插值来调整大小:
代码语言:javascript复制
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函数的最后一个参数传递:
代码语言:javascript复制
img_flip_along_x = cv2.flip(img, 0)
  1. 当然,可以沿垂直y轴翻转图像-只要传递大于0的任何值即可:
代码语言:javascript复制
img_flip_along_y = cv2.flip(img, 1)
  1. 通过将任何负值传递给函数,我们可以同时翻转xy
代码语言:javascript复制
img_flipped_xy = cv2.flip(img, -1)

工作原理

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

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

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

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

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

准备

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

操作步骤

这是此秘籍的步骤:

  1. 首先,读取图片:
代码语言:javascript复制
img = cv2.imread('../data/Lena.png')
  1. 将图像保存为 PNG 格式而不会降低质量,然后再次读取以检查在写入磁盘期间是否已保留所有信息:
代码语言:javascript复制
# 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 格式:
代码语言:javascript复制
# 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. 加载图像以使用某些东西并获取其大小:
代码语言:javascript复制
orig = cv2.imread('../data/Lena.png')
orig_size = orig.shape[0:2]
  1. 现在让我们显示图像。 为此,我们需要调用cv2.imshowcv2.waitKey函数:
代码语言:javascript复制
cv2.imshow("Original image", orig)
cv2.waitKey(2000)

工作原理

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YUeJUC9h-1681870701134)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/86d2652c-2b32-4b05-a28d-6da534e63291.png)]

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

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

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

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

准备

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

操作步骤

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

  1. 首先创建一个名为window的 OpenCV 窗口:
代码语言:javascript复制
import cv2, numpy as np

cv2.namedWindow('window')
  1. 创建一个变量,其中将包含图像的填充颜色值。 该变量是一个 NumPy 数组,具有三个值,这些值将被解释为[0, 255]范围内的蓝色,绿色和红色颜色分量(按此顺序):
代码语言:javascript复制
fill_val = np.array([255, 255, 255], np.uint8)
  1. 添加一个辅助函数以从每个trackbar_callback函数中调用。 该函数将颜色成分索引和新值用作设置:
代码语言:javascript复制
def trackbar_callback(idx, value):
    fill_val[idx] = value
  1. window中添加三个跟踪栏,然后使用 Python lambda函数将每个跟踪栏回调绑定到特定的颜色组件:
代码语言:javascript复制
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. 在一个循环中,在具有三个轨迹栏的窗口中显示图像,并同时处理键盘输入:
代码语言:javascript复制
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 的版本及其构建方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VYG7HMPN-1681870701135)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/eb1906a9-8862-4ed8-85dc-e0edc70901c0.png)]

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

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

准备

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

操作步骤

  1. 打开图像并获取其宽度和高度。 另外,定义一个简单的函数,该函数将在图像内返回一个随机点:
代码语言:javascript复制
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. 让我们画点东西! 让我们画个圆圈:
代码语言:javascript复制
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. 现在让我们尝试画线:
代码语言:javascript复制
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()函数:
代码语言:javascript复制
cv2.arrowedLine(image, rand_pt(), rand_pt(), (0, 0, 255), 3, cv2.LINE_AA)
  1. 要绘制矩形,OpenCV 具有rectangle()函数:
代码语言:javascript复制
cv2.rectangle(image, rand_pt(), rand_pt(), (255, 255, 0), 3)
  1. 另外,OpenCV 包括绘制椭圆的函数。 让我们画一下:
代码语言:javascript复制
cv2.ellipse(image, rand_pt(), rand_pt(0.3), random.randrange(360), 0, 360, (255, 255, 255), 3)
  1. 我们与绘图相关的最终函数是在图像上放置文本:
代码语言:javascript复制
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函数可拍摄图像,起点和终点以及图像颜色(与第一次调用一样)。 (可选)您可以传递线的粗细和线的类型(同样,禁止混叠)。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SkZghRSs-1681870701135)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/8a77ab2a-11f9-4035-8dc6-074387159590.png)]

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

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

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

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

处理来自键盘的用户输入

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

准备

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

操作步骤

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

  1. 如前所述,打开图像并获取其宽度和高度。 另外,制作原始图像的副本,并定义一个简单函数,该函数将返回一个随机点,其图像内的坐标为:
代码语言:javascript复制
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键时关闭应用:
代码语言:javascript复制
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键后, 次,您将获得接近以下图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pCvH5wd-1681870701136)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/49721400-40ee-456e-a0ee-0cdf6a8f1d53.png)]

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

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

准备

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

操作步骤

此秘籍的步骤如下:

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

image = cv2.imread('../data/Lena.png')
image_to_show = np.copy(image)
  1. 现在,定义一些变量来存储鼠标状态:
代码语言:javascript复制
mouse_pressed = False
s_x = s_y = e_x = e_y = -1
  1. 让我们实现鼠标事件的处理器。 这应该是一个带有四个参数的函数,如下所示:
代码语言:javascript复制
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. 让我们创建一个窗口实例,该实例将捕获鼠标事件并将其转换为我们先前定义的处理器函数:
代码语言:javascript复制
cv2.namedWindow('image')
cv2.setMouseCallback('image', mouse_callback)
  1. 现在,让我们实现应用的其余部分,它应该对按钮的按下做出反应并裁剪原始图像:
代码语言:javascript复制
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的窗口。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XEaireUY-1681870701136)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/40b77dd2-209c-4db3-9e4a-14f52d3dfca5.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E8SPAvF5-1681870701136)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/ced3b5c9-3777-482a-b581-2f285bac9045.png)]

从相机捕获并显示帧

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

准备

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

操作步骤

对于此秘籍,步骤如下:

  1. 创建一个VideoCapture对象:
代码语言:javascript复制
import cv2

capture = cv2.VideoCapture(0)
  1. 使用capture.read方法从相机读取帧,该方法返回一对:读取成功标志和frame本身:
代码语言:javascript复制
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. 通常建议您释放视频设备(在我们的情况下为摄像机)并销毁所有创建的窗口:
代码语言:javascript复制
capture.release()
cv2.destroyAllWindows()

工作原理

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

播放视频中的帧流

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

准备

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

操作步骤

以下是此秘籍的步骤:

  1. 为视频文件创建一个VideoCapture对象:
代码语言:javascript复制
import cv2

capture = cv2.VideoCapture('../data/drop.avi')
  1. 重播视频中的所有帧:
代码语言:javascript复制
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 的速率重播。

可以看到以下框架:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I2oDOIVr-1681870701136)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/d9d0ccdc-edd8-4bab-a35e-5f4ac6436c20.png)]

获取帧流属性

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

准备

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

操作步骤

执行以下步骤:

  1. 让我们创建一个辅助函数,该函数将使用VideoCapture ID(摄像设备是视频设备还是视频路径),创建VideoCapture对象,并请求帧的高度和宽度,计数和速率:
代码语言:javascript复制
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. 让我们对视频文件调用这个函数:
代码语言:javascript复制
print_capture_properties('../data/drop.avi')
  1. 现在,让我们请求相机捕获对象的属性:
代码语言:javascript复制
print_capture_properties(0)

工作原理

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

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

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

代码语言:javascript复制
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. 首先,像前面的秘籍一样,我们创建一个相机捕获对象,并获取框架的高度和宽度:
代码语言:javascript复制
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. 创建视频写入器:
代码语言:javascript复制
video = cv2.VideoWriter('../data/captured_video.avi', cv2.VideoWriter_fourcc(*'X264'),
                        25, (frame_width, frame_height))
  1. 然后,在无限while循环中,捕获帧并使用video.write 方法将其写入:
代码语言:javascript复制
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对象,并销毁窗口:
代码语言:javascript复制
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对象并获取总帧数:
代码语言:javascript复制
import cv2
capture = cv2.VideoCapture('../data/drop.avi')
frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
print('Frame count:', frame_count)
  1. 获取总帧数:
代码语言:javascript复制
print('Position:', int(capture.get(cv2.CAP_PROP_POS_FRAMES)))
_, frame = capture.read()
cv2.imshow('frame0', frame)
  1. 请注意,capture.read方法会将当前视频位置向前移动一帧。 获取下一帧:
代码语言:javascript复制
print('Position:', capture.get(cv2.CAP_PROP_POS_FRAMES))
_, frame = capture.read()
cv2.imshow('frame1', frame)
  1. 让我们跳到帧位置100
代码语言:javascript复制
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属性获取并设置视频位置。 根据视频的编码方式,设置属性可能不会导致设置请求的确切帧索引。 要设置的值必须在有效范围内。

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

代码语言:javascript复制
Frame count: 182
Position: 0
Position: 1
Position: 100

应显示以下框架:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TlLyILQ9-1681870701137)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/8d41e6d5-e1bc-4080-8ddc-99c0b3484fa6.png)]

二、矩阵,颜色和过滤器

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

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

介绍

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

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

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

准备

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

操作步骤

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

  1. 导入所有必要的模块:
代码语言:javascript复制
import cv2, numpy as np
  1. 创建一个特定形状的矩阵,并将其填充为255作为值,该矩阵应显示以下内容:
代码语言:javascript复制
image = np.full((480, 640, 3), 255, np.uint8)
cv2.imshow('white', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 创建一个矩阵并为每个像素的颜色设置单独的值,以将我们的矩阵变为红色:
代码语言:javascript复制
image = np.full((480, 640, 3), (0, 0, 255), np.uint8)
cv2.imshow('red', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 用零填充矩阵以使其为黑色:
代码语言:javascript复制
image.fill(0)
cv2.imshow('black', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 接下来,将某些单个像素的值设置为白色:
代码语言:javascript复制
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,以使黑色像素变为蓝色:
代码语言:javascript复制
image[:, :, 0] = 255
cv2.imshow('blue with white pixels', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 现在,将图像中间垂直线上的像素设置为白色:
代码语言:javascript复制
image[:, 320, :] = 255
cv2.imshow('blue with white line', image)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 最后,将特定区域内所有像素的第二个通道设置为255
代码语言:javascript复制
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. 导入所有必需的模块,打开图像,打印其形状和数据类型,然后在屏幕上显示:
代码语言:javascript复制
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. 使用浮动数据类型元素将我们的图片转换为一张图片:
代码语言:javascript复制
image = image.astype(np.float32) / 255
print('Shape:', image.shape)
print('Data type:', image.dtype)
  1. 通过2缩放图像元素,并裁剪值以将其保持在[0, 1]范围内:
代码语言:javascript复制
cv2.imshow('image', np.clip(image*2, 0, 1))
cv2.waitKey()
cv2.destroyAllWindows()
  1. 将图像的元素缩放回[0, 255]范围,并将元素类型转换为 8 位无符号int
代码语言:javascript复制
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),即可将矩阵的每个元素除以指定的值 。 缩放输入图像的值的结果应显示如下(左侧图像为原始图像,右侧图像为缩放版本):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pffMyh4Y-1681870701137)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/8bfcb7b2-164b-49ae-87ce-55b20a4208c8.png)]

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

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

准备

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

操作步骤

执行以下步骤:

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

工作原理

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

操作图像通道

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

准备

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

操作步骤

执行以下步骤:

  1. 导入所有必需的模块,打开图像,然后输出其形状:
代码语言:javascript复制
import cv2, numpy as np
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255
print('Shape:', image.shape)
  1. 交换红色和蓝色通道并显示结果:
代码语言:javascript复制
image[:, :, [0, 2]] = image[:, :, [2, 0]]
cv2.imshow('blue_and_red_swapped', image)
代码语言:javascript复制
cv2.waitKey()
cv2.destroyAllWindows()
  1. 向后交换通道,并按不同比例缩放它们以更改图像的色彩:
代码语言:javascript复制
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 不仅使我们可以获取一个切片,而且还可以获取一堆切片作为数据的新视图。 为此,我们应该以所需顺序枚举所有所需切片的索引,而不是单个索引。

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

结果应如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kU5MiJC5-1681870701137)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/08fa400a-8c1d-419a-9b5d-6496d9613b3b.png)]

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

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

准备

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

操作步骤

使用以下步骤:

  1. 导入所有必要的模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 加载图像并打印其形状和类型:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255
print('Shape:', image.shape)
print('Data type:', image.dtype)
  1. 将图像转换为灰度:
代码语言:javascript复制
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 颜色空间:
代码语言:javascript复制
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 颜色空间:
代码语言:javascript复制
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 多种转换类型。 代码执行的结果应如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lxUeyQvR-1681870701138)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/0d0da3e5-59db-48c0-a728-cc7e3dfdf83f.png)]

伽玛校正和逐元素算数

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

准备

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

操作步骤

此秘籍的步骤如下:

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

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

工作原理

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xityVTgA-1681870701138)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/1fa7a7cb-5aa1-4357-a27e-8c6e81f1177d.png)]

均值/方差图像归一化

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

准备

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

操作步骤

执行以下步骤:

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

工作原理

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

计算图像直方图

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

准备

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

操作步骤

按着这些次序:

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

工作原理

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rl39128t-1681870701138)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/060235d0-9bb5-4294-bd69-48b57e9cf21c.png)]

均衡图像直方图

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

准备

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

操作步骤

使用以下步骤:

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

工作原理

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EabfEC0J-1681870701138)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/b7beddec-8b6d-4af6-a5f4-3cae2e38fa9a.png)]

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

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

准备

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

操作步骤

执行以下步骤:

  1. 导入包:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载图像,将其转换为浮点,然后将其缩小到[0, 1]范围:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255
  1. 通过向每个像素添加随机值来在图像中创建噪点并显示它:
代码语言:javascript复制
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应用于噪点图像并显示结果:
代码语言:javascript复制
gauss_blur = cv2.GaussianBlur(noised, (7, 7), 0)
plt.imshow(gauss_blur[:, :, [2, 1, 0]])
plt.show()
  1. 应用median过滤:
代码语言:javascript复制
median_blur = cv2.medianBlur((noised * 255).astype(np.uint8), 7)
plt.imshow(median_blur[:, :, [2, 1, 0]])
plt.show()
  1. 对我们的图像执行median过滤,并产生噪声:
代码语言:javascript复制
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函数提供了双边过滤。 它获取输入图像,窗口大小和颜色以及空间σ值。 如果窗口大小为负,则根据空间σ值计算得出。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3zq5lAum-1681870701138)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/16dfa6cb-bc2e-49c6-a999-7ea6eebd8542.png)]

使用 Sobel 算子计算梯度

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

准备

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

操作步骤

执行以下步骤:

  1. 导入包:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 以灰度读取图像:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', 0)
  1. 使用Sobel运算符计算梯度近似值:
代码语言:javascript复制
dx = cv2.Sobel(image, cv2.CV_32F, 1, 0)
dy = cv2.Sobel(image, cv2.CV_32F, 0, 1)
  1. 可视化结果:
代码语言:javascript复制
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函数使用指定大小的线性过滤器来计算图像梯度近似值。 通过函数参数,您可以确切指定需要计算的导数,应使用的核以及输出图像的数据类型。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-094s7m5C-1681870701139)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/abd778e8-0ac4-4516-a498-56d2e8bbd295.png)]

创建和应用自己的过滤器

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

准备

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

操作步骤

执行以下步骤:

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

kernel = cv2.getGaussianKernel(KSIZE, 0)
kernel = -ALPHA * kernel @ kernel.T
kernel[KSIZE//2, KSIZE//2]  = 1   ALPHA
  1. 使用我们刚创建的核过滤图像:
代码语言:javascript复制
filtered = cv2.filter2D(image, -1, kernel)
  1. 可视化结果:
代码语言:javascript复制
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)和过滤器核; 然后,对图像进行线性过滤。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f3HTcenU-1681870701139)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/5e0a059c-aeaf-404a-9091-7106c78f1cc7.png)]

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

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

准备

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

操作步骤

执行以下步骤:

  1. 导入包:
代码语言:javascript复制
import math
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像为灰度并将其转换为np.float32
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', 0).astype(np.float32) / 255
  1. 构造实值Gabor过滤器核。 规范核,使其具有 L2 单位规范:
代码语言:javascript复制
kernel = cv2.getGaborKernel((21, 21), 5, 1, 10, 1, 0, cv2.CV_32F)
kernel /= math.sqrt((kernel * kernel).sum())
  1. 过滤图像:
代码语言:javascript复制
filtered = cv2.filter2D(image, -1, kernel)
  1. 可视化结果:
代码语言:javascript复制
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过滤器有用的领域之一是检测已知方向的边缘。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BUy2XX35-1681870701139)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/916f6c64-1493-42b0-9ffd-1a0669359a09.png)]

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

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

准备

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

操作步骤

必须执行以下步骤:

  1. 导入所需的包:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 以灰度读取图像并将其转换为np.float32数据类型:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', 0).astype(np.float32) / 255
  1. 应用离散傅立叶变换:
代码语言:javascript复制
fft = cv2.dft(image, flags=cv2.DFT_COMPLEX_OUTPUT)
  1. 可视化频谱图:
代码语言:javascript复制
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. 将图像从频谱转换回空间表示形式:
代码语言:javascript复制
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函数以这样的方式移动频谱,即对应于零频率的振幅位于数组的中心,并且更易于解释和进一步使用。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xTVoCCpD-1681870701139)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/d6aa3f3b-4712-4865-be44-4b1204430744.png)]

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

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

准备

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

操作步骤

执行以下步骤:

  1. 导入包:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取图像为灰度并将其转换为np.float32数据类型:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', 0).astype(np.float32) / 255
  1. 使用离散傅立叶变换将图像从空间域转换为频域:
代码语言:javascript复制
fft = cv2.dft(image, flags=cv2.DFT_COMPLEX_OUTPUT)
  1. 移位 FFT 结果的方式应使低频位于数组的中心:
代码语言:javascript复制
fft_shift = np.fft.fftshift(fft, axes=[0, 1])
  1. 将高频的振幅设置为零,而其他振幅保持不变:
代码语言:javascript复制
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 结果移回:
代码语言:javascript复制
fft = np.fft.ifftshift(fft_shift, axes=[0, 1])
  1. 使用逆离散傅里叶逆变换将滤波后的图像从频域转换回空间域:
代码语言:javascript复制
filtered = cv2.idft(fft, flags=cv2.DFT_SCALE | cv2.DFT_REAL_OUTPUT)
  1. 可视化原始图像和过滤后的图像:
代码语言:javascript复制
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()

工作原理

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

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VV74SrIF-1681870701140)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/1725dda4-df17-488b-8537-9abc8c7fa96a.png)]

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

使用不同阈值处理图像

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

准备

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

操作步骤

执行以下步骤:

  1. 导入包:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', 0)
  1. 应用一个简单的二进制阈值:
代码语言:javascript复制
thr, mask = cv2.threshold(image, 200, 1, cv2.THRESH_BINARY)
print('Threshold used:', thr)
  1. 应用自适应阈值:
代码语言:javascript复制
adapt_mask = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
                                   cv2.THRESH_BINARY_INV, 11, 10)
  1. 可视化结果:
代码语言:javascript复制
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)用作逐像素阈值。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zk0GTcQf-1681870701140)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/9ad11854-de41-4391-830d-276b7c8f5d02.png)]

形态运算

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

准备

安装 OpenCV Python API 包和matplotlib包。

操作步骤

按着这些次序:

  1. 导入包:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像并使用大津的方法构建二进制图像:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', 0)
_, binary = cv2.threshold(image, -1, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
  1. 使用3x3矩形遮罩施加腐蚀和膨胀 10 次:
代码语言:javascript复制
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 次形态学打开和关闭操作:
代码语言:javascript复制
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. 计算形态梯度:
代码语言:javascript复制
grad = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT,
                          cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
  1. 可视化结果:
代码语言:javascript复制
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()

工作原理

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vMKCutp7-1681870701140)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/1b3f9e4b-a1cc-4667-b1de-e1f97eefb75c.png)]

图像遮罩和二进制操作

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

准备

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

操作步骤

此秘籍的步骤如下:

  1. 导入所有包:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 创建带有圆形遮罩的二进制图像:
代码语言:javascript复制
circle_image = np.zeros((500, 500), np.uint8)
cv2.circle(circle_image, (250, 250), 100, 255, -1)
  1. 创建一个带有矩形遮罩的二进制图像:
代码语言:javascript复制
rect_image = np.zeros((500, 500), np.uint8)
cv2.rectangle(rect_image, (100, 100), (400, 250), 255, -1)
  1. 使用按位 AND 运算符组合圆形和矩形遮罩:
代码语言:javascript复制
circle_and_rect_image = circle_image & rect_image
  1. 使用按位或运算符组合圆形和矩形遮罩:
代码语言:javascript复制
circle_or_rect_image = circle_image | rect_image
  1. 可视化结果:
代码语言:javascript复制
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之类的函数使用。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JocJrXJZ-1681870701140)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/f1cc2d73-d8f8-4d79-bfc7-6ae2febfb7cb.png)]

三、轮廓和分割

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

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

介绍

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

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

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

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

准备

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

操作步骤

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

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 读取测试图像:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', 0)
  1. 使用大津法估计阈值:
代码语言:javascript复制
otsu_thr, otsu_mask = cv2.threshold(image, -1, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print('Estimated threshold (Otsu):', otsu_thr)
  1. 可视化结果:
代码语言:javascript复制
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

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

代码语言:javascript复制
Estimated threshold (Otsu): 116.0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y9uhZWTA-1681870701141)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/8667eecb-207d-49d2-bb0c-dbeb49e244e8.png)]

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

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载测试二进制图像:
代码语言:javascript复制
image = cv2.imread('../data/BnW.png', 0)
  1. 找到外部和内部轮廓。 将它们分为两级:
代码语言:javascript复制
_, contours, hierarchy = cv2.findContours(image, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
  1. 准备外部轮廓二进制掩码:
代码语言:javascript复制
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. 准备内部轮廓二进制掩码:
代码语言:javascript复制
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. 可视化结果:
代码语言:javascript复制
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

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ijn9ETa3-1681870701141)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/98a2e38c-2135-4ee3-ba71-d9762b390700.png)]

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

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

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

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

准备

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

操作步骤

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

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

connectivity = 8
num_labels, labelmap = cv2.connectedComponents(img, connectivity, cv2.CV_32S)
  1. 显示原始图像和带有标签的缩放图像:
代码语言:javascript复制
img = np.hstack((img, labelmap.astype(np.float32)/(num_labels - 1)))
cv2.imshow('Connected components', img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 打开另一个图像,找到其大津遮罩,并获得连通组件及其统计信息:
代码语言:javascript复制
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. 过滤出面积较小的组件并创建彩色图像,在该图像上绘制具有单独颜色的其余组件以及每个组件的中心。 然后,显示结果:
代码语言:javascript复制
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)坐标。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6FIxe2V2-1681870701141)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/b36d256f-4cb4-43b5-a59f-2a54c345c6c7.png)]

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

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

准备

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

操作步骤

  1. 首先,导入所有模块:
代码语言:javascript复制
import cv2
import numpy as np
import random
  1. 在要绘制的图像上创建图像,并随机生成椭圆的参数,例如半轴长度和旋转角度:
代码语言:javascript复制
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. 使用找到的参数为椭圆生成点,并向它们添加随机噪声:
代码语言:javascript复制
pts = cv2.ellipse2Poly(center, axes, angle, 0, 360, 1)
pts  = np.random.uniform(-10, 10, pts.shape).astype(np.int32)
  1. 在图像上绘制椭圆和生成的点,然后显示图像:
代码语言:javascript复制
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. 找到最适合我们的噪点的椭圆参数,在图像上绘制结果椭圆,然后显示它:
代码语言:javascript复制
ellipse = cv2.fitEllipse(pts)
cv2.ellipse(img, ellipse, (0, 0, 0), 3)

cv2.imshow('Fit ellipse', img)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 创建清晰的图像,为y = x函数生成点,并向它们添加随机噪声:
代码语言:javascript复制
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函数并生成点; 然后,显示图像:
代码语言:javascript复制
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. 找到噪声点的直线参数,绘制结果并显示图像:
代码语言:javascript复制
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. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 在黑色背景上绘制一个测试图像-以点(320240)为中心的白色椭圆:
代码语言:javascript复制
image = np.zeros((480, 640), np.uint8)
cv2.ellipse(image, (320, 240), (200, 100), 0, 0, 360, 255, -1)
  1. 计算矩并打印其值:
代码语言:javascript复制
m = cv2.moments(image)
for name, val in m.items():
    print(name, 't', val)
  1. 执行一个简单的测试,以检查计算出的矩是否有意义,并使用图像的第一个矩来计算图像质心。 它必须靠近我们在上面指定的椭圆的中心:
代码语言:javascript复制
print('Center X estimated:', m['m10'] / m['m00'])
print('Center Y estimated:', m['m01'] / m['m00'])

工作原理

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

目前,预期以下输出:

代码语言:javascript复制
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

估计的重心如下:

代码语言:javascript复制
Center X estimated: 319.9950643063927
Center Y estimated: 239.999082467214

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

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

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

准备

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

操作步骤

  1. 导入所有必需的模块,打开一个图像,然后在屏幕上显示它:
代码语言:javascript复制
import cv2, random
import numpy as np
img = cv2.imread('bw.png', cv2.IMREAD_GRAYSCALE)
  1. 找到加载的图像的轮廓,绘制它们,然后显示结果:
代码语言:javascript复制
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. 取第一个轮廓,在各种情况下找到其面积,并输出结果数字:
代码语言:javascript复制
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. 找到轮廓的长度,然后打印:
代码语言:javascript复制
print('Length of closed contour is %.2f' % cv2.arcLength(contour, True))
print('Length of open contour is %.2f' % cv2.arcLength(contour, False))
  1. 找到轮廓的凸包,在图像上绘制并显示:
代码语言:javascript复制
hull = cv2.convexHull(contour)
cv2.drawContours(color, [hull], -1, (0,0,255), 3)

cv2.imshow('contours', color)
cv2.waitKey()
cv2.destroyAllWindows()
  1. 检查轮廓及其外壳的凸度:
代码语言:javascript复制
print('Convex status of contour is %s' % cv2.isContourConvex(contour))
print('Convex status of its hull is %s' % cv2.isContourConvex(hull))
  1. 创建一个带有轨迹栏的窗口,以控制轮廓近似的质量,找到轮廓近似并显示结果:
代码语言:javascript复制
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 算法,该算法查找具有较少点和一定公差的轮廓。 它具有一个轮廓(应该近似),公差(它是原始轮廓与其近似之间的最大距离)和一个布尔标志(告诉函数是否将近似轮廓视为闭合)。 公差越大,近似值越粗糙,但是保留在结果轮廓中的点越少。 该函数返回指定参数的输入轮廓的近似值。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xC5gVJ4-1681870701141)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/e5770cc8-c43a-44fb-a164-c841365b2363.png)]

检查点是否在轮廓内

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

准备

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

操作步骤

  1. 导入所有必需的模块,打开一个图像,然后在屏幕上显示它:
代码语言:javascript复制
import cv2, random
import numpy as np
img = cv2.imread('bw.png', cv2.IMREAD_GRAYSCALE)
  1. 找到图像的轮廓并显示它们:
代码语言:javascript复制
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. 定义一个回调函数来处理用户单击图像。 此函数在发生点击的地方绘制一个小圆圈,圆圈的颜色取决于点击是在轮廓内部还是外部:
代码语言:javascript复制
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函数而得到的结果的模式:
代码语言:javascript复制
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)。 指示器的符号与计算精确距离的模式具有相同的含义。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2P4n3Pw-1681870701142)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/a79ffc8a-b1c5-47a0-95a0-d5d53d44d60e.png)]

计算图像的距离

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 在白色背景上绘制测试图像-黑色圆圈(无填充):
代码语言:javascript复制
image = np.full((480, 640), 255, np.uint8)
cv2.circle(image, (320, 240), 100, 0)
  1. 计算从每个点到圆的距离:
代码语言:javascript复制
distmap = cv2.distanceTransform(image, cv2.DIST_L2, cv2.DIST_MASK_PRECISE)
  1. 可视化结果:
代码语言:javascript复制
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标志,这将导致计算的不是近似的,而是精确的距离。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CMLAP0pH-1681870701142)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/922a3f79-fbb4-44d2-b7b1-b2daa1d96350.png)]

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

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

准备

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

操作步骤

  1. 导入必要的模块:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 打开图像并将其转换为 Lab 颜色空间:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png').astype(np.float32) / 255.
image_lab = cv2.cvtColor(image, cv2.COLOR_BGR2Lab)
  1. 将图像重塑为向量:
代码语言:javascript复制
data = image_lab.reshape((-1, 3))
  1. 定义聚类数和完成分割过程的条件。 然后,执行 K 均值聚类:
代码语言:javascript复制
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 颜色空间:
代码语言:javascript复制
segmented_lab = centers[labels.flatten()].reshape(image.shape)
segmented = cv2.cvtColor(segmented_lab, cv2.COLOR_Lab2RGB)
  1. 一起显示原始图像和分段图像:
代码语言:javascript复制
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图像,像素值必须保持不变。 参见这里。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-geMjzMDH-1681870701142)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/393c9a0b-1b9e-40f8-b31b-22f85bdf8ad7.png)]

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

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

准备

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

操作步骤

  1. 导入必要的模块和函数:
代码语言:javascript复制
import cv2, random
import numpy as np
from random import randint
  1. 加载图像以进行分段并创建其副本,并创建其他图像以存储种子和分段结果:
代码语言:javascript复制
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. 定义种子类型的数量,每种种子类型的颜色以及一些与鼠标事件一起使用的变量:
代码语言:javascript复制
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. 实现鼠标回调函数以处理鼠标事件; 让我们通过按下按钮拖动鼠标在图像上绘制种子:
代码语言:javascript复制
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. 创建所有必要的窗口,设置回调,显示图像,并跟踪循环中按下的键盘按钮。 让我们通过按数字来更改当前种子以进行绘制。 并且,在完成种子更改过程后,使用分水岭算法对图像进行分割:
代码语言:javascript复制
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。 第二个参数中应使用不同的数字表示不同的种子,其他像素应设置为零。 该例程用相关的邻近种子填充种子图像中的零值。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tqzm7Zbp-1681870701142)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/84fcc195-759b-4f96-befe-6785b77ecf49.png)]

四、目标检测与机器学习

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

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

介绍

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

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

使用 GrabCut 算法获取对象遮罩

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 打开图像并定义鼠标回调函数以在图像上绘制一个矩形:
代码语言:javascript复制
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按钮之后,使用以下代码关闭窗口:
代码语言:javascript复制
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基于绘制的矩形创建对象遮罩。 然后,创建对象掩码并将其定义为:
代码语言:javascript复制
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调用中的错误:
代码语言:javascript复制
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以获取固定的掩码。 最后,更新图像上的遮罩,并显示它:
代码语言:javascript复制
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用于以相同状态调用该函数进行另一次迭代。

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pNH9NvBZ-1681870701142)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/2c29800d-cfb5-48c2-b3ba-bf639db9760d.png)]

使用 Canny 算法查找边缘

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

准备

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

操作步骤

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

  1. 导入模块:
代码语言:javascript复制
import cv2
import matplotlib.pyplot as plt
  1. 加载测试图像:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png')
  1. 使用 Canny 算法检测边缘:
代码语言:javascript复制
edges = cv2.Canny(image, 200, 100)
  1. 可视化结果:
代码语言:javascript复制
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中实现了该算法。 您必须在此函数中为梯度幅度指定两个阈值:第一个阈值用于检测强边缘,第二个阈值用于滞后过程,在该过程中将生长强边缘。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZGsRu1M0-1681870701143)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/ed3ea57c-41fc-4ee1-a988-e71dfdaea667.png)]

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

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 绘制测试图像:
代码语言:javascript复制
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. 使用概率霍夫变换检测线:
代码语言:javascript复制
lines = cv2.HoughLinesP(img, 1, np.pi/180, 100, 100, 10)[0]
  1. 使用霍夫变换检测圆:
代码语言:javascript复制
circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 15, param1=200, param2=30)[0]
  1. 绘制检测到的直线和圆:
代码语言:javascript复制
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. 可视化结果:
代码语言:javascript复制
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 边缘检测程序的较高阈值,第二个是票数阈值。

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

代码语言:javascript复制
Detected line: (99 401) (372 353)
Detected circle: center=(201.5 198.5), radius=50.400001525878906

输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MB7qtU6r-1681870701143)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/7569abc6-2992-4f1e-81c8-b5cb8b50ec91.png)]

通过模板匹配查找对象

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

准备

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

操作步骤

执行以下步骤:

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 加载图像并定义鼠标回调函数以选择图像 ROI。 所绘制矩形的内部将是我们用于匹配的模板:
代码语言:javascript复制
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按钮完成该过程并获取模板:
代码语言:javascript复制
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函数执行。 匹配完成后,我们将找到具有最高(或最低)相似性度量的点,并得出检测结果:
代码语言:javascript复制
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),其中大写字母代表输入图像尺寸,小写字母代表模板尺寸。 返回图像的内容取决于我们使用的方法,对于具有相关性计算的方法,值越大表示匹配越好。 并且,顾名思义,具有平方差用法的方法具有最小的值作为完美匹配。

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4qrX7DHd-1681870701143)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/17192fb6-1559-4e88-944f-7f2d65d7857a.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZKHOTlYz-1681870701143)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/075836f8-7b2c-4a7b-86c9-8ac4cca884e2.png)]

Median Flow 跟踪器

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

准备

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

操作步骤

此秘籍的步骤为:

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

_, frame = cap.read()

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

cv2.destroyAllWindows()
  1. 创建 Median Flow 跟踪器,并使用视频中的第一帧和我们选择的边界框对其进行初始化。 然后,一一读取剩余的帧,将它们输入到跟踪器中,并为每个帧获得一个新的边框。 显示边界框,以及“中值流”算法每秒能够处理的帧数:
代码语言:javascript复制
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 选择模式(从左上角或从中心)。 调用此函数后,图像将出现在屏幕上,您将能够单击并拖动鼠标以绘制一个矩形。 选择过程完成后,只需按键盘上的空格键,您将获得所选矩形的参数作为返回值。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZqyQo0l-1681870701144)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/a1a970c8-1cb5-45bd-89c1-1860042d76b2.png)]

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

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

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

准备

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

操作步骤

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

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

for name, tracker in (('KCF', cv2.TrackerKCF_create), 
                      ('MIL', cv2.TrackerMIL_create), 
                      ('TLD', cv2.TrackerTLD_create)):
    tracker = tracker()
    initialized = False
  1. 打开测试视频文件,然后选择一个对象:
代码语言:javascript复制
video = cv2.VideoCapture('../data/traffic.mp4')
bbox = (878, 266, 1153-878, 475-266)
  1. 跟踪直到视频结束或按下Esc,并可视化当前跟踪的对象:
代码语言:javascript复制
    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. 关闭窗口:
代码语言:javascript复制
cv2.destroyAllWindows()

工作原理

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qdM7YlNo-1681870701144)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/1335ea75-b632-4986-ab27-db742fded009.png)]

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

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

准备

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

操作步骤

您需要执行以下步骤:

  1. 导入我们将要使用的模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 定义函数以显示光流:
代码语言:javascript复制
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 的算法计算密集的光流。 然后,显示结果:
代码语言:javascript复制
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 光流的类的实例。 然后,一帧一帧地读取帧,并获取随后每对帧的光通量; 显示结果:
代码语言:javascript复制
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 的算法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SGjLuRjp-1681870701144)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/608a6d12-519b-4d89-92e5-fcf7981d0c74.png)]

第二张图片用于 Dual TV L1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e8RbtEo8-1681870701144)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/baf17b6f-6121-47ad-97d4-529e08345a2e.png)]

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

检测棋盘和圆形网格图案

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import matplotlib.pyplot as plt
  1. 用棋盘加载测试图像:
代码语言:javascript复制
image_chess = cv2.imread('../data/chessboard.png')
  1. 检测棋盘图案:
代码语言:javascript复制
found, corners = cv2.findChessboardCorners(image_chess, (6, 9))
assert found == True, "can't find chessboard pattern"
  1. 绘制检测到的图案:
代码语言:javascript复制
dbg_image_chess = image_chess.copy()
cv2.drawChessboardCorners(dbg_image_chess, (6, 9), corners, found);
  1. 使用圆形网格图案加载测试图像:
代码语言:javascript复制
image_circles = cv2.imread('../data/circlesgrid.png')
  1. 检测圆形网格图案:
代码语言:javascript复制
found, corners = cv2.findCirclesGrid(image_circles, (6, 6), cv2.CALIB_CB_SYMMETRIC_GRID)
assert found == True, "can't find circles grid pattern"
  1. 绘制检测到的图案:
代码语言:javascript复制
dbg_image_circles = image_circles.copy()
cv2.drawChessboardCorners(dbg_image_circles, (6, 6), corners, found);
  1. 可视化结果:
代码语言:javascript复制
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。 这两个函数都返回布尔标志,指示是否找到了图案以及角点(如果找到)。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1wRsU6zX-1681870701144)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/2d2e9a16-b81d-42eb-b3bd-ba99a76e5035.png)]

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

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import matplotlib.pyplot as plt
  1. 加载测试图像:
代码语言:javascript复制
image = cv2.imread('../data/people.jpg')
  1. 创建 HOG 特征计算机和检测器:
代码语言:javascript复制
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
  1. 检测图像中的人:
代码语言:javascript复制
locations, weights = hog.detectMultiScale(image)
  1. 绘制检测到的人物边界框:
代码语言:javascript复制
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. 可视化结果:
代码语言:javascript复制
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方法,使用滑动窗口方法以多个比例检测对象。 该函数返回检测到的人的位置列表以及每个检测分数。 要了解更多信息,请访问这里。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gx5XeWq2-1681870701145)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/cdc8edee-1f56-4011-85b9-dc96da81f1de.png)]

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

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 指定一些常量:
代码语言:javascript复制
CELL_SIZE = 20     # Digit image size. 
NCLASSES = 10      # Number of digits.
TRAIN_RATIO = 0.8  # Part of all samples used for training.
  1. 读取数字图像并准备标签:
代码语言:javascript复制
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. 执行几何归一化,计算图像矩并对齐每个样本:
代码语言:javascript复制
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. 随机排列样本:
代码语言:javascript复制
perm = np.random.permutation(nsamples)
digits = digits[perm]
labels = labels[perm]
  1. 定义用于计算 HOG 描述符的函数:
代码语言:javascript复制
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. 准备训练和测试数据(特征和标签):
代码语言:javascript复制
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 模型:
代码语言:javascript复制
K = 3
knn_model = cv2.ml.KNearest_create()
knn_model.train(fea_hog_train, cv2.ml.ROW_SAMPLE, labels_train)
  1. 创建一个 SVM 模型:
代码语言:javascript复制
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. 定义评估模型的函数:
代码语言:javascript复制
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 模型:
代码语言:javascript复制
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 模型的结果要好一些。

预期输出如下:

代码语言:javascript复制
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. 导入我们需要的模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 定义打开视频文件,调用检测器以查找图像中所有面部并显示结果的函数:
代码语言:javascript复制
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 级联,然后调用我们的检测函数:
代码语言:javascript复制
haar_face_cascade = cv2.CascadeClassifier('../data/haarcascade_frontalface_default.xml')

detect_faces('../data/faces.mp4', haar_face_cascade, 'Haar cascade face detector')
  1. 以略有不同的方式加载预训练的 LBP 级联,然后再次调用该函数以查找和显示面部:
代码语言:javascript复制
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,宽度,高度)。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EhfVlwiG-1681870701145)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/bdc380c0-169b-42a5-918e-38c4cfd1b862.png)]

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

为 AR 应用检测 AruCo 模式

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

准备

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

操作步骤

  1. 导入模块:
代码语言:javascript复制
import cv2
import cv2.aruco as aruco
import numpy as np
  1. 使用不同的 AruCo 标记创建图像,对其进行模糊处理,然后显示它:
代码语言:javascript复制
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. 检测模糊图像上的标记。 绘制检测到的标记并显示结果:
代码语言:javascript复制
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是合理的。 它接受图像以绘制检测到的角点列表和标识符列表。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iJjB2hPb-1681870701145)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/6e21d6fe-95ff-4d56-9d06-c27680d57439.png)]

在自然场景中检测文字

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

准备

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

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

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

操作步骤

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

  1. 导入模块:
代码语言:javascript复制
import cv2
  1. 加载文字图片:
代码语言:javascript复制
img = cv2.imread('../data/scenetext01.jpg')
  1. 加载预训练的卷积神经网络并检测文本消息:
代码语言:javascript复制
det = cv2.text.TextDetectorCNN_create(
       "../data/textbox.prototxt", "../data/TextBoxes_icdar13.caffemodel")
rects, probs = det.detect(img)
  1. 绘制置信度高于阈值的检测到的文本边界框:
代码语言:javascript复制
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. 可视化结果:
代码语言:javascript复制
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方法,该方法将返回一个矩形列表以及包含文本的矩形的相关概率。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqzdGcEa-1681870701145)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/541548ca-780e-4237-8838-ac9db631f101.png)]

QR 码检测器

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

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

准备

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

操作步骤

  1. 导入我们需要的模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 实现一个找到两条线的交点的函数:
代码语言:javascript复制
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. 定义一个函数,该函数通过计算四对变形点和非变形点之间的对应关系来消除透视变形:
代码语言:javascript复制
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 码外角的函数:
代码语言:javascript复制
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 码,如果成功,则显示代码角并取消扭曲代码以获取规范视图:
代码语言:javascript复制
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来完成。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-57WgpoOF-1681870701146)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/b520db6a-6a90-4106-bf0b-9acec9ac91bb.png)]

五、深度学习

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

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

介绍

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

将图像表示为张量/BLOB

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

准备

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

操作步骤

您需要完成以下步骤:

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 打开输入图像并打印其形状:
代码语言:javascript复制
image_bgr = cv2.imread('../data/Lena.png', cv2.IMREAD_COLOR)
print(image_bgr.shape)
  1. 将图像转换为四维浮点张量:
代码语言:javascript复制
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. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 加载Caffe模型:
代码语言:javascript复制
net_caffe = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt', 
                                     '../data/bvlc_googlenet.caffemodel')
  1. Torch加载模型:
代码语言:javascript复制
net_torch = cv2.dnn.readNetFromTorch('../data/torch_enet_model.net')
  1. 读取并解析经过训练的TensorFlow模型:
代码语言:javascript复制
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. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. Caffe加载模型并打印有关模型中使用的层类型的信息:
代码语言:javascript复制
net = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt', 
                               '../data/bvlc_googlenet.caffemodel')

if not net.empty():
    print('Net loaded successfullyn')

print('Net contains:')
for t in net.getLayerTypes():
    print('t%d layers of type %s' % (net.getLayersCount(t), t))
  1. 获取已加载模型的张量形状和指定的输入形状。 然后打印所有信息:
代码语言:javascript复制
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. 导入我们将要使用的模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 打开输入图像,对其进行预处理,然后将其转换为张量:
代码语言:javascript复制
image = cv2.imread('../data/Lena.png', cv2.IMREAD_COLOR)
tensor = cv2.dnn.blobFromImage(image, 1.0, (224, 224),
                               (104, 117, 123), False, False);
  1. 通过初步预处理将两个图像转换为张量:
代码语言:javascript复制
tensor = cv2.dnn.blobFromImages([image, image], 1.0, (224, 224),
                                (104, 117, 123), False, True);
  1. 加载经过训练的神经网络模型:
代码语言:javascript复制
net = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt', 
                               '../data/bvlc_googlenet.caffemodel')
  1. 设置加载模型的输入并执行推断:
代码语言:javascript复制
net.setInput(tensor);
prob = net.forward();
  1. 重复设置输入并使用指定的层名称执行推理:
代码语言:javascript复制
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. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 导入Caffe模型:
代码语言:javascript复制
model = cv2.dnn.readNetFromCaffe('../data/bvlc_googlenet.prototxt',
                                 '../data/bvlc_googlenet.caffemodel')
  1. 计算在推理阶段执行的 FLOP 数量:
代码语言:javascript复制
print('gflops:', model.getFLOPS((1,3,224,224))*1e-9)
  1. 报告存储权重和中间张量所消耗的内存量:
代码语言:javascript复制
w,b = model.getMemoryConsumption((1,3,224,224))
print('weights (mb):', w*1e-6, ', blobs (mb):', b*1e-6)
  1. 对模拟输入执行正向传播:
代码语言:javascript复制
blob = cv2.dnn.blobFromImage(np.zeros((224,224,3), np.uint8), 1, (224,224))
model.setInput(blob)
model.forward()
  1. 报告总时间:
代码语言:javascript复制
total,timings = model.getPerfProfile()
tick2ms = 1e3/cv2.getTickFrequency()
print('inference (ms): {:2f}'.format(total*tick2ms))
  1. 报告每层推理时间:
代码语言:javascript复制
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方法获得,该方法返回总推理时间和每层计时,所有信息均以滴答为单位。

预期输出如下:

代码语言:javascript复制
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. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 定义一个classify函数,该函数从视频中获取帧,将其转换为张量,将其馈送到神经网络,并选择概率最高的五个类别:
代码语言:javascript复制
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. 打开一个文件,其中包含以下类别的名称:
代码语言:javascript复制
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函数:
代码语言:javascript复制
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
代码语言:javascript复制
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中加载该模型,并对视频中的帧进行分类:
代码语言:javascript复制
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)

工作原理

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3YjL3Qz9-1681870701146)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/c20b89be-a263-4674-88db-d3719d0b7ae2.png)]

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

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

准备

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

操作步骤

您需要完成以下步骤:

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 导入Caffe模型:
代码语言:javascript复制
model = cv2.dnn.readNetFromCaffe('../data/MobileNetSSD_deploy.prototxt',
                                 '../data/MobileNetSSD_deploy.caffemodel')
  1. 设置置信度阈值并指定模型支持的类:
代码语言:javascript复制
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. 打开道路交通视频:
代码语言:javascript复制
video = cv2.VideoCapture('../data/traffic.mp4')
while True:
    ret, frame = video.read()
    if not ret: break
  1. 检测对象:
代码语言:javascript复制
    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. 绘制检测到的对象:
代码语言:javascript复制
    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)。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A9qfikVP-1681870701146)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/0a7c9283-3af2-432f-8896-da1e81a25153.png)]

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

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

准备

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

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

操作步骤

您需要完成以下步骤:

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 导入Caffe模型:
代码语言:javascript复制
model = cv2.dnn.readNetFromCaffe('../data/fcn8s-heavy-pascal.prototxt',
                                 '../data/fcn8s-heavy-pascal.caffemodel')
  1. 加载图像并进行推断:
代码语言:javascript复制
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. 使用每像素类标签计算图像:
代码语言:javascript复制
labels = output[0].argmax(0)
  1. 可视化结果:
代码语言:javascript复制
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 时间,因此请耐心等待。

预期结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JwmywnSd-1681870701146)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/8e48a66e-0e38-4352-8072-bf58dc13bfc1.png)]

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

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

准备

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

操作步骤

您需要完成以下步骤:

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
  1. 加载模型并设置置信度阈值:
代码语言:javascript复制
model = cv2.dnn.readNetFromCaffe('../data/face_detector/deploy.prototxt', 
                                 '../data/face_detector/res10_300x300_ssd_iter_140000.caffemodel')
CONF_THR = 0.5
  1. 打开视频:
代码语言:javascript复制
video = cv2.VideoCapture('../data/faces.mp4')
while True:
    ret, frame = video.read()
    if not ret: break
  1. 检测当前帧中的人脸:
代码语言:javascript复制
    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. 可视化结果:
代码语言:javascript复制
    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 模型使用单发检测方法。 送入输入帧时,请注意指定平均颜色。

预期输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7yUoFMio-1681870701147)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/ec41a9cf-da5a-4fdb-9dc2-7afb24b68ff2.png)]

年龄和性别预测

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

准备

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

操作步骤

您需要完成以下步骤:

  1. 导入模块:
代码语言:javascript复制
import cv2
import numpy as np
import matplotlib.pyplot as plt
  1. 加载模型:
代码语言:javascript复制
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. 加载并裁剪源图像:
代码语言:javascript复制
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. 可视化图像:
代码语言:javascript复制
plt.figure(figsize=(6,6))
plt.title('original')
plt.axis('off')
plt.imshow(orig_frame[:,:,[2,1,0]])
plt.show()
  1. 用平均像素值加载图像,然后从源图像中减去它们:
代码语言:javascript复制
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. 设置年龄和性别列表:
代码语言:javascript复制
AGE_LIST = ['(0, 2)','(4, 6)','(8, 12)','(15, 20)',
            '(25, 32)','(38, 43)','(48, 53)','(60, 100)']
GENDER_LIST = ['male','female']
  1. 分类性别:
代码语言:javascript复制
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. 分类年龄段:
代码语言:javascript复制
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]))

工作原理

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

这是输入图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ex6dWKkS-1681870701147)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv3-cv-py-cb/img/601e8fd5-56a5-4bc4-aea8-28e630140a52.png)]

预期输出如下:

代码语言:javascript复制
Gender: female with prob: 0.9362890720367432
Age group: (25, 32) with prob: 0.9811384081840515

0 人点赞