OpenCV-Python实战------分析与加速OpenCV应用程序
-
- [0. 前言](#0. 前言)
- [1. 用 Numba 加速](#1. 用 Numba 加速)
- [2. 使用 CPU 加速](#2. 使用 CPU 加速)
- [3. Numba,CUDA 和 GPU 加速](#3. Numba,CUDA 和 GPU 加速)
- 相关链接
0. 前言
当应用程序运行缓慢时,首先需要找出代码中哪些具体部分花费了大量处理时间。找到这些代码部分(也称为瓶颈)的一个好方法是分析应用程序。一个好的分析器可以在不对应用程序进行修改的情况下对其进行分析,其中一个称为 pyinstrument。下面,我们使用 pyinstrument 对《检测与跟踪物体》一节中的应用程序进行分析:
shell
$ pyinstrument -o profile.html -r html main.py
我们通过 -o 选项指定了要保存性能分析报告信息的输出 .html 文件。
我们还通过 -r 选项指定了报告的呈现方式,表明希望生成 HTML 输出。应用终止后,性能分析报告就会生成,并可以在浏览器中查看。
我们也可以省略这两个选项。
在后一种情况下,报告将显示在控制台中。一旦我们终止应用程序,可以在浏览器中打开生成的 .html 文件,它将显示类似于下图的报告:

首先,我们可以注意到脚本本身花费了相当多的时间。这应该是意料之中的,因为目标检测模型在每一帧上都要进行推理,而且这是一项相当繁重的操作。我们还可以注意到跟踪也花费了相当多的时间,尤其是在 iou 函数中。
通常,根据应用程序的具体用途,为了加速跟踪,用一个更高效的函数替换 iou 函数就足够了。在这个应用程序中,iou 函数用于计算 iou_matrix,该矩阵存储了每一对可能的检测框和跟踪框的交并比 (Intersection Over Union, IoU) 度量。当我们致力于加速代码时,为了节省时间,用一个加速版本替换原有代码并再次进行分析以检查是否满足需求,可能是个好主意。
接下来,让我们从应用程序中提取出相关的代码,并使用 Numba 分析加速的可能性。
1. 用 Numba 加速
Numba 是一个编译器,它使用低级虚拟机 (Low-Level Virtual Machine, LLVM) 编译器基础设施来优化纯 Python 编写的代码。它高效地编译数学密集型的 Python 代码,以达到类似于 C、C++ 和 Fortran 的性能。它能理解一系列 numpy 函数、Python 构造库和运算符,以及标准库中的一系列数学函数,并通过简单的注解为图形处理单元 (Graphical Processing Unit, GPU) 和中央处理单元 (Central Processing Unit, CPU) 生成相应的本地代码。
在本节中,我们将使用 IPython 交互式解释器来处理代码。它是一个增强型的交互式 Python shell,支持所谓的"魔法命令",在本节中,我们将使用它对函数执行进行计时。可以直接在控制台中使用解释器,也使用 Jupyter Notebook 或 JupyterLab。
导入 NumPy 和 Numba:
python
import numpy as np
import numba
在这里,我们将介绍 Numba 的一些重要可能性,并通过一个具体的例子说明结果,以便了解 Numba 如何帮助加速应用程序代码。
现在,让我们提取出想要加速的代码,步骤如下:
(1) 首先,这是计算两个边界框的 IoU 的函数:
python
def iou(a: np.ndarray, b: np.ndarray) -> float:
a_tl, a_br = a[:4].reshape((2, 2))
b_tl, b_br = b[:4].reshape((2, 2))
int_tl = np.maximum(a_tl, b_tl)
int_br = np.minimum(a_br, b_br)
int_area = np.prod(np.maximum(0., int_br - int_tl))
a_area = np.prod(a_br - a_tl)
b_area = np.prod(b_br - b_tl)
return int_area / (a_area + b_area - int_area)
保持它与《检测与跟踪物体》一节中的一致。
(2) 接下来是使用上述函数计算 iou_matrix 的代码部分,如下所示:
python
def calc_iou_matrix(detections,trackers):
iou_matrix = np.zeros((len(detections), len(trackers)), dtype=np.float32)
for d, det in enumerate(detections):
for t, trk in enumerate(trackers):
iou_matrix[d, t] = iou(det, trk)
return iou_matrix
我们将相应的循环和矩阵定义封装在一个新的函数中。
(3) 为了测试性能,让我们定义两组随机的边界框:
python
A = np.random.rand(100,4)
B = np.random.rand(100,4)
我们定义了两组各包含 100 个边界框的集合。
(4) 现在,我们可以通过运行以下代码来估算计算这些边界框的 iou_matrix 需要多少时间:
python
%timeit calc_iou_matrix(A,B)
%timeit 魔法命令会多次执行该函数,计算平均执行时间以及与平均值的偏差,并输出结果:
shell
130 ms ± 717 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
我们可以注意到,计算该矩阵大约需要 1 秒的三分之一。因此,如果场景中有 100 个物体,并且我们希望在 1 秒内处理多个帧,那么应用程序将出现巨大的瓶颈。现在,让我们在 CPU 上加速这段代码。
2. 使用 CPU 加速
Numba 有几个代码生成工具,可以从 Python 代码中生成机器码。其核心功能之一是 @numba.jit 装饰器。这个装饰器允许你标记一个函数,以便由 Numba 编译器进行优化。例如,下面的函数计算数组中所有元素的乘积:
python
@numba.jit(nopython=True)
def product(a):
result = 1
for i in range(len(a)):
result*=a[i]
return result
它可以被视为 np.product 的自定义实现。该装饰器告诉 Numba 将该函数编译成机器码,与 Python 版本相比,这带来了更快的执行时间。Numba 总是尝试编译指定的函数。如果函数中的某些操作无法完全编译,Numba 会回退到所谓的"对象模式",该模式使用 Python/C API 并将所有值作为 Python 对象来处理,以便对它们执行操作。
后者比前者慢得多。当我们传递 nopython=True 时,我们明确告诉它:如果函数无法编译成完整的机器码,则引发异常。
我们可以将同一个装饰器用于 iou 函数,如下所示:
python
@numba.jit(nopython=True)
def iou(a: np.ndarray, b: np.ndarray) -> float:
a_tl, a_br = a[0:2],a[2:4]
b_tl, b_br = b[0:2],b[2:4]
int_tl = np.maximum(a_tl, b_tl)
int_br = np.minimum(a_br, b_br)
int_area = product(np.maximum(0., int_br - int_tl))
a_area = product(a_br - a_tl)
b_area = product(b_br - b_tl)
return int_area / (a_area + b_area - int_area)
我们可以注意到,该函数与原始的 Python 函数略有不同。
现在,我们可以像以前一样为函数计时,如下所示:
python
%timeit calc_iou_matrix(A,B)
后者产生以下输出:
shell
5.72 ms ± 136 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)
我们可以注意到,我们已经有了巨大的加速(大约 20 倍),但让我们继续吧。我们可以注意到 calc_iou_matrix 仍在纯 Python 中,并且具有嵌套循环,这可能会花费很多时间。让我们为其创建一个编译版本,如下所示:
python
@numba.jit(nopython=True)
def calc_iou_matrix(detections,trackers):
iou_matrix = np.zeros((len(detections), len(trackers)), dtype=np.float32)
for d in range(len(detections)):
det = detections[d]
for t in range(len(trackers)):
trk = trackers[t]
iou_matrix[d, t] = iou(det, trk)
%timeit calc_iou_matrix(A,B)
同样,此功能与原始功能不同,因为 Numba 无法解释枚举。计时此实现将产生类似于以下内容的输出:
shell
1.77 ms ± 10.5 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)
我们再次实现了加速。这个版本比前一个版本快了一倍。让我们继续加速,尽可能让它更快,但在此之前,让我们先熟悉一下 vectorize 装饰器。
vectorize 装饰器允许从作用于标量参数的函数创建可以用作 NumPy ufunc 类的函数,如下面的函数所示:
python
@numba.vectorize
def custom_operation(a,b):
if b == 0:
return 0
return a*b if a>b else a/b
当给定一对标量时,该函数执行某个特定操作,而 vectorize 装饰器使得在 NumPy 数组上执行相同的操作成为可能,例如:
python
custom_operation(A,B)
NumPy 的强制转换规则同样适用------例如,你可以将其中一个数组替换为标量或形状为 (1,4) 的数组,如下所示:
python
custom_operation(A,np.ones((1,4)))
我们将用来加速 iou_matrix 计算的另一个装饰器是 guvectorize。这个装饰器将 vectorize 的概念更进一步。它允许编写返回不同维度数组的 ufunc。我们可以注意到,在计算 IoU 矩阵时,输出数组的形状由每个传入数组中的边界框数量组成。我们如下使用该装饰器来计算矩阵:
python
@numba.guvectorize(['(f8[:, :], f8[:, :], f8[:, :])'], '(m,k),(n,k1)->(m, n)')
def calc_iou_matrix(x, y, z):
for i in range(x.shape[0]):
for j in range(y.shape[1]):
z[i, j] = iou(x[i],y[i])
%timeit calc_iou_matrix(A,B)
第一个参数告诉 Numba 编译一个处理 8 字节浮点数 (float64) 的函数。它还使用分号指定了输入和输出数组的维度。第二个参数是签名,它指定了输入和输出数组的维度如何相互匹配。当我们使用输入执行函数时,输出 z 已经以正确的形状存在等待,只需要在函数中填充即可。
如果我们像之前一样对这个实现进行计时,将得到类似于以下的结果:
shell
196 µs ± 2.46 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
再次,我们比上一个案例快了大约 30 倍。与最初的纯 Python 实现相比,我们快了大约 1000 倍,这相当令人印象深刻。
3. Numba,CUDA 和 GPU 加速
我们已经看到使用 Numba 创建 CPU 加速代码是多么简单。Numba 还提供了类似的接口,可以使用统一计算设备架构 (Compute Unified Device Architecture, CUDA) 在 GPU 上进行计算。让我们将 IoU 矩阵计算函数移植到 GPU 上,使用 Numba 进行计算。
我们可以通过稍微修改装饰器参数来指示 Numba 在 GPU 上进行计算:
python
from numba import cuda
@numba.guvectorize(['(f8[:, :], f8[:, :], f8[:, :])'], '(m,k),(n,k1)->(m, n)',target="cuda")
def calc_iou_matrix(x, y, z):
for i in range(x.shape[0]):
for j in range(y.shape[1]):
z[i, j] = iou(x[i],y[j])
这里,我们通过传递 target="cuda" 指示 Numba 在 GPU 上进行计算。我们在 iou 函数上也有工作要做,新的函数如下所示:
python
@cuda.jit(device=True)
def iou(a: np.ndarray, b: np.ndarray) -> float:
xx1 = max(a[0], b[0])
yy1 = max(a[1], b[1])
xx2 = min(a[2], b[2])
yy2 = min(a[3], b[3])
w = max(0., xx2 - xx1)
h = max(0., yy2 - yy1)
wh = w * h
result = wh / ((a[2]-a[0])*(a[3]-a[1])
+ (b[2]-b[0])*(b[3]-b[1]) - wh)
return result
首先,我们更改了装饰器,现在使用 numba.cuda.jit 而不是 numba.jit。后者指示 Numba 创建一个在 GPU 上执行的函数。该函数本身是从一个在 GPU 设备上运行的函数中调用的。为此,我们传递了 device=True,这明确表明此函数旨在用于在 GPU 上计算的函数。
还可以注意到,我们做了相当多的修改,以便消除所有 NumPy 函数调用。与 CPU 加速一样,这是因为 numba.cuda 目前无法执行原函数中的所有操作,我们将其替换为 numba.cuda 支持的操作。
通常,在计算机视觉中,只有当应用程序使用深度神经网络 (Deep Neural Network, DNN) 时,才需要 GPU 加速。大多数现代深度学习框架,如 TensorFlow、PyTorch 和 MXNet,都开箱即用地支持 GPU 加速,使我们可以远离底层 GPU 编程,而专注于自己的模型。在分析了这些框架之后,如果发现自己有一个特定的算法,认为必须直接用 CUDA 实现,则可能需要分析 numba.cuda API,该 API 支持大多数 CUDA 功能。
相关链接
OpenCV-Python实战(1)------OpenCV简介与图像处理基础
OpenCV-Python实战(2)------图像与视频文件的处理
OpenCV-Python实战(3)------OpenCV中绘制图形与文本
OpenCV-Python实战(4)------OpenCV常见图像处理技术
OpenCV-Python实战(5)------OpenCV图像运算
OpenCV-Python实战(6)------OpenCV中的色彩空间和色彩映射
OpenCV-Python实战(8)------直方图均衡化
OpenCV-Python实战(9)------OpenCV用于图像分割的阈值技术
OpenCV-Python实战(10)------OpenCV轮廓检测
OpenCV-Python实战(11)------OpenCV轮廓检测相关应用
OpenCV-Python实战(12)------一文详解AR增强现实
OpenCV-Python实战(13)------OpenCV与机器学习的碰撞
OpenCV-Python实战(14)------人脸检测详解
OpenCV-Python实战(15)------面部特征点检测详解
OpenCV-Python实战(16)------人脸追踪详解
OpenCV-Python实战(17)------人脸识别详解
OpenCV-Python实战(18)------深度学习简介与入门示例
OpenCV-Python实战(19)------OpenCV与深度学习的碰撞
OpenCV-Python实战(20)------OpenCV计算机视觉项目在Web端的部署
OpenCV-Python实战(21)------OpenCV人脸检测项目在Web端的部署
OpenCV-Python实战(22)------使用Keras和Flask在Web端部署图像识别应用
OpenCV-Python实战(23)------将OpenCV计算机视觉项目部署到云端
OpenCV-Python实战(24)------打造实时图像滤镜系统
OpenCV-Python实战(25)------基于深度传感器与凸性分析打造实时手势识别系统
OpenCV-Python实战(26)------复杂场景下的实时物体检测与跟踪
OpenCV-Python实战(27)------基于对极几何的3D场景重建
OpenCV-Python实战(28)------OpenCV计算摄影从HDR图像融合到全景拼接
OpenCV-Python实战(29)------基于视觉显著性的自动多目标跟踪系统
OpenCV-Python实战(30)------基于支持向量机的交通标志识别模型
OpenCV-Python实战(31)------实时面部情绪检测与识别系统