OpenCV-Python实战——分析与加速OpenCV应用程序
- 0. 前言
- 1. 用 Numba 加速
- 2. 使用 CPU 加速
- 3. Numba,CUDA 和 GPU 加速
- 相关链接
0. 前言
当应用程序运行缓慢时,首先需要找出代码中哪些具体部分花费了大量处理时间。找到这些代码部分(也称为瓶颈)的一个好方法是分析应用程序。一个好的分析器可以在不对应用程序进行修改的情况下对其进行分析,其中一个称为 pyinstrument。下面,我们使用pyinstrument对《检测与跟踪物体》一节中的应用程序进行分析:
$ pyinstrument-oprofile.html-rhtml 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:
importnumpyasnpimportnumba在这里,我们将介绍Numba的一些重要可能性,并通过一个具体的例子说明结果,以便了解Numba如何帮助加速应用程序代码。
现在,让我们提取出想要加速的代码,步骤如下:
(1)首先,这是计算两个边界框的IoU的函数:
defiou(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)returnint_area/(a_area+b_area-int_area)保持它与《检测与跟踪物体》一节中的一致。
(2)接下来是使用上述函数计算iou_matrix的代码部分,如下所示:
defcalc_iou_matrix(detections,trackers):iou_matrix=np.zeros((len(detections),len(trackers)),dtype=np.float32)ford,detinenumerate(detections):fort,trkinenumerate(trackers):iou_matrix[d,t]=iou(det,trk)returniou_matrix我们将相应的循环和矩阵定义封装在一个新的函数中。
(3)为了测试性能,让我们定义两组随机的边界框:
A=np.random.rand(100,4)B=np.random.rand(100,4)我们定义了两组各包含100个边界框的集合。
(4)现在,我们可以通过运行以下代码来估算计算这些边界框的iou_matrix需要多少时间:
%timeit calc_iou_matrix(A,B)%timeit魔法命令会多次执行该函数,计算平均执行时间以及与平均值的偏差,并输出结果:
130ms ±717μs per loop(mean ± std. dev. of7runs,10loops each)我们可以注意到,计算该矩阵大约需要1秒的三分之一。因此,如果场景中有100个物体,并且我们希望在1秒内处理多个帧,那么应用程序将出现巨大的瓶颈。现在,让我们在CPU上加速这段代码。
2. 使用 CPU 加速
Numba有几个代码生成工具,可以从Python代码中生成机器码。其核心功能之一是@numba.jit装饰器。这个装饰器允许你标记一个函数,以便由Numba编译器进行优化。例如,下面的函数计算数组中所有元素的乘积:
@numba.jit(nopython=True)defproduct(a):result=1foriinrange(len(a)):result*=a[i]returnresult它可以被视为np.product的自定义实现。该装饰器告诉Numba将该函数编译成机器码,与Python版本相比,这带来了更快的执行时间。Numba总是尝试编译指定的函数。如果函数中的某些操作无法完全编译,Numba会回退到所谓的“对象模式”,该模式使用Python/C API并将所有值作为Python对象来处理,以便对它们执行操作。
后者比前者慢得多。当我们传递nopython=True时,我们明确告诉它:如果函数无法编译成完整的机器码,则引发异常。
我们可以将同一个装饰器用于iou函数,如下所示:
@numba.jit(nopython=True)defiou(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)returnint_area/(a_area+b_area-int_area)我们可以注意到,该函数与原始的Python函数略有不同。
现在,我们可以像以前一样为函数计时,如下所示:
%timeit calc_iou_matrix(A,B)后者产生以下输出:
5.72ms ±136μs per loop(mean ± std. dev. of7runs,1loop each)我们可以注意到,我们已经有了巨大的加速(大约20倍),但让我们继续吧。我们可以注意到calc_iou_matrix仍在纯Python中,并且具有嵌套循环,这可能会花费很多时间。让我们为其创建一个编译版本,如下所示:
@numba.jit(nopython=True)defcalc_iou_matrix(detections,trackers):iou_matrix=np.zeros((len(detections),len(trackers)),dtype=np.float32)fordinrange(len(detections)):det=detections[d]fortinrange(len(trackers)):trk=trackers[t]iou_matrix[d,t]=iou(det,trk)%timeit calc_iou_matrix(A,B)同样,此功能与原始功能不同,因为Numba无法解释枚举。计时此实现将产生类似于以下内容的输出:
1.77ms ±10.5μs per loop(mean ± std. dev. of7runs,1loop each)我们再次实现了加速。这个版本比前一个版本快了一倍。让我们继续加速,尽可能让它更快,但在此之前,让我们先熟悉一下vectorize装饰器。vectorize装饰器允许从作用于标量参数的函数创建可以用作NumPy ufunc类的函数,如下面的函数所示:
@numba.vectorizedefcustom_operation(a,b):ifb==0:return0returna*bifa>belsea/b当给定一对标量时,该函数执行某个特定操作,而vectorize装饰器使得在NumPy数组上执行相同的操作成为可能,例如:
custom_operation(A,B)NumPy的强制转换规则同样适用——例如,你可以将其中一个数组替换为标量或形状为(1,4)的数组,如下所示:
custom_operation(A,np.ones((1,4)))我们将用来加速iou_matrix计算的另一个装饰器是guvectorize。这个装饰器将vectorize的概念更进一步。它允许编写返回不同维度数组的ufunc。我们可以注意到,在计算IoU矩阵时,输出数组的形状由每个传入数组中的边界框数量组成。我们如下使用该装饰器来计算矩阵:
@numba.guvectorize(['(f8[:, :], f8[:, :], f8[:, :])'],'(m,k),(n,k1)->(m, n)')defcalc_iou_matrix(x,y,z):foriinrange(x.shape[0]):forjinrange(y.shape[1]):z[i,j]=iou(x[i],y[i])%timeit calc_iou_matrix(A,B)第一个参数告诉Numba编译一个处理8字节浮点数 (float64) 的函数。它还使用分号指定了输入和输出数组的维度。第二个参数是签名,它指定了输入和输出数组的维度如何相互匹配。当我们使用输入执行函数时,输出z已经以正确的形状存在等待,只需要在函数中填充即可。
如果我们像之前一样对这个实现进行计时,将得到类似于以下的结果:
196µs ±2.46µs per loop(mean ± std. dev. of7runs,10000loops each)再次,我们比上一个案例快了大约30倍。与最初的纯Python实现相比,我们快了大约1000倍,这相当令人印象深刻。
3. Numba,CUDA 和 GPU 加速
我们已经看到使用Numba创建CPU加速代码是多么简单。Numba 还提供了类似的接口,可以使用统一计算设备架构 (Compute Unified Device Architecture,CUDA) 在GPU上进行计算。让我们将IoU矩阵计算函数移植到GPU上,使用Numba进行计算。
我们可以通过稍微修改装饰器参数来指示Numba在GPU上进行计算:
fromnumbaimportcuda@numba.guvectorize(['(f8[:, :], f8[:, :], f8[:, :])'],'(m,k),(n,k1)->(m, n)',target="cuda")defcalc_iou_matrix(x,y,z):foriinrange(x.shape[0]):forjinrange(y.shape[1]):z[i,j]=iou(x[i],y[j])这里,我们通过传递target="cuda"指示Numba在GPU上进行计算。我们在iou函数上也有工作要做,新的函数如下所示:
@cuda.jit(device=True)defiou(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)returnresult首先,我们更改了装饰器,现在使用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实战(7)——直方图详解
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)——实时面部情绪检测与识别系统
OpenCV-Python实战(32)——基于深度学习的物体分类与定位
OpenCV-Python实战(33)——构建实时目标检测与跟踪系统