六. 部署分类器-preprocess-speed-compare

前言

自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考

本次课程我们来学习课程第六章—部署分类器,一起来学习 CPU 端图像预处理方法以及速度对比

课程大纲可以看下面的思维导图

在这里插入图片描述

0. 简述

本小节目标:学习 CPU 端 bgr2rgb + normalization + hwc2chw 等图像预处理操作以及它们的性能比较

这节课程开始我们进入第六章节—部署分类器,这个章节偏实战,为大家准备了几个案例:

  • 6.0-preprocess-speed-compare
  • 6.1-deploy-classification
  • 6.2-deploy-classification-advanced
  • 6.3-int8-calibration
  • 6.4-trt-engine-inspector

第六章节准备的案例一共是五个,第一个是 preprocess-speed-compare,这个小节主要教大家如果用 CPU 做图像预处理都有哪些方法,哪种方法访问图像速度更快;6.1 小节主要给大家介绍初步的分类器部署该怎么做,这个小节为了方便大家理解整个代码写得比较简单,也没有涉及到任何 C++ 的设计模式,所以整个代码看起来有很多缺陷;6.2 小节是针对 6.1 小节的一个扩展,主要是把 6.1 小节中的很多问题给解决掉,另外我们自己在写推理框架的时候应该考虑哪些东西

6.3 小节给大家介绍 int8 calibration,我们在前面或多或少都有涉及到量化这个概念,其中校准是量化的一个重要环节,校准包括很多校准器比如 MinMaxCalibrator、EntropyCalibrator、LegacyCalibrator 等等,我们在部署时该如何使用这些校准器呢?选择哪个校准器呢?这都是我们在 6.3 小节需要讨论的问题;最后 6.4 小节主要给大家介绍 TensorRT 官方工具 trt-engine-explorer,这个工具主要是帮助大家观察经过 TensorRT 优化前后的推理引擎在架构上有什么不同,去理解 TensorRT 做了哪些优化,哪些层融合了,哪些节点添加了,哪些节点被删除了,这个方便我们更好的去理解 TensorRT 的优化,同时可以帮助我们分析 TensorRT 中哪些优化是可以进一步改善的

后续的一些案例主要是讲 Transformer 的一些部署以及面临的一些问题,比如 attention 计算瓶颈,LayerNormalization 这种节点和 CNN 中 Conv 这种节点相比推理性能差异又在哪里,还有纯 Transformer 模型和纯 CNN 模型以及 CNN+Transformer 模型相比它们的计算效率以及计算密度有什么不同(目前 2024/8/17 尚未更新

下面我们开始本次课程的学习🤗

1. 案例运行

在正式开始课程之前,博主先带大家跑通 6.0-preprocess-speed-compare 这个小节的案例🤗

源代码获取地址:https://github.com/kalfazed/tensorrt_starter

首先大家需要把 tensorrt_starter 这个项目给 clone 下来,指令如下:

git clone https://github.com/kalfazed/tensorrt_starter.git

也可手动点击下载,点击右上角的 Code 按键,将代码下载下来。至此整个项目就已经准备好了。也可以点击 here 下载博主准备好的源代码(注意代码下载于 2024/7/14 日,若有改动请参考最新

整个项目后续需要使用的软件主要有 CUDA、cuDNN、TensorRT、OpenCV,大家可以参考 Ubuntu20.04软件安装大全 进行相应软件的安装,博主这里不再赘述

假设你的项目、环境准备完成,下面我们一起来运行下 6.0-preprocess-speed-compare 小节案例代码

我们需要修改下整体的 Makefile.config,指定一些库的路径:

# tensorrt_starter/config/Makefile.config
# CUDA_VER                    :=  11
CUDA_VER                    :=  11.6
    
# opencv和TensorRT的安装目录
OPENCV_INSTALL_DIR          :=  /usr/local/include/opencv4
# TENSORRT_INSTALL_DIR        :=  /mnt/packages/TensorRT-8.4.1.5
TENSORRT_INSTALL_DIR        :=  /home/jarvis/lean/TensorRT-8.6.1.6

Note:大家查看自己的 CUDA 是多少版本,修改为对应版本即可,另外 OpenCV 和 TensorRT 修改为你自己安装的路径即可

接着我们就可以来执行编译,指令如下:

make -j64

输出如下:

在这里插入图片描述

接着执行:

./bin/trt-infer

输出如下:

在这里插入图片描述

我们这里对比了 CPU 端进行 bgr2rgb+norm+hwc2chw 预处理的五种不同方法执行的速度对比,在终端可以看到各个方法处理的时间,另外在 data 文件夹下保存着处理后的图片,如下所示:

在这里插入图片描述

Note:博主的 CPU 是 12th Gen Intel® Core™ i5-12400F,每次执行结果都不一样会有波动这个很正常,不同 CPU 测量出来的结果可能会有差异,另外图像分辨率不同时间也会有差异

如果大家能够看到上述输出结果,那就说明本小节案例已经跑通,下面我们就来看看具体的代码实现

2. 代码分析

2.1 main.cpp

我们先从 main.cpp 看起:

#include <iostream>
#include <memory>

#include "model.hpp"
#include "utils.hpp"
#include "timer.hpp"
#include "preprocess.hpp"

using namespace std;

void bgr2rgb_cpu_speed_test(){
    Timer   timer;
    string  imagePath = "data/fox.png";
    string  savePath  = "";
    cv::Mat src       = cv::imread(imagePath);
    cv::Mat tar(src.rows, src.cols, CV_8UC3);

    LOG("Starting cpu bgr2rgb speed test...");

    timer.start_cpu();
    preprocess_cv_cvtcolor(src, tar);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using cv::cvtcolor takes");
    savePath  = "data/fox-cvtcolor.png";
    cv::imwrite(savePath, tar);

    timer.start_cpu();
    preprocess_cv_mat_at(src, tar);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using cv::Mat::at takes");
    savePath  = "data/fox-mat-at.png";
    cv::imwrite(savePath, tar);

    timer.start_cpu();
    preprocess_cv_mat_iterator(src, tar);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using cv::MatIterator_ takes");
    savePath  = "data/fox-mat-iterator.png";
    cv::imwrite(savePath, tar);

    timer.start_cpu();
    preprocess_cv_mat_data(src, tar);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using cv::Mat data takes");
    savePath  = "data/fox-mat-data.png";
    cv::imwrite(savePath, tar);

    timer.start_cpu();
    preprocess_cv_pointer(src, tar);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using pointer takes");
    savePath  = "data/fox-pointer.png";
    cv::imwrite(savePath, tar);
}

void bgr2rgb_norm_hwc2chw_cpu_speed_test(){
    Timer   timer;
    string  imagePath = "data/fox.png";
    cv::Mat src       = cv::imread(imagePath);
    int     size      = src.cols * src.rows * src.channels();
    float*  tar       = (float*)malloc(size * sizeof(float));
    int     width     = 224;
    int     height    = 224;
    int     channel   = 3;
    int     classes   = 1000;
    float   mean[3]   = {0.406, 0.456, 0.485};
    float   std[3]    = {0.225, 0.224, 0.229};

    cv::resize(src, src, cv::Size(width, height));
    LOG("Starting cpu bgr2rgb + normalization + hwc2chw speed test...");

    timer.start_cpu();
    preprocess_cv_mat_at(src, tar, mean, std);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using cv::Mat::at takes");

    timer.start_cpu();
    preprocess_cv_mat_iterator(src, tar, mean, std);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using cv::MatIterator_ takes");

    timer.start_cpu();
    preprocess_cv_mat_data(src, tar, mean, std);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using cv::Mat data takes");

    timer.start_cpu();
    preprocess_cv_pointer(src, tar, mean, std);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using pointer takes");

    timer.start_cpu();
    preprocess_cv_array(src, tar, mean, std);
    timer.stop_cpu();
    timer.duration_cpu<Timer::ms>("Using array takes");    
}

int main(int argc, char const *argv[])
{
    // bgr2rgb_cpu_speed_test();
    bgr2rgb_norm_hwc2chw_cpu_speed_test();
}

上述代码主要用于测试不同方法在 OpenCV 中进行图像预处理操作时的速度,主要有 bgr2rgb_cpu_speed_test()bgr2rgb_norm_hwc2chw_cpu_speed_test() 两个函数,它们分别测量了不同方法在特定预处理操作中的耗时情况

bgr2rgb_cpu_speed_test() 函数

  • 该函数测试了使用不同方法将图像从 BGR 色彩空间转换为 RGB 色彩空间的速度
  • preprocess_cv_cvtcolor() 方法
    • 使用 cv::cvtColor 函数将图像从 BGR 转换为 RGB
  • preprocess_cv_mat_at() 方法
    • 使用 cv::Mat::at 方法逐元素访问并修改图像
  • preprocess_cv_mat_iterator() 方法
    • 使用 cv::MatIterator_ 遍历图像矩阵
  • preprocess_cv_mat_data() 方法
    • 直接通过指针访问图像数据
  • preprocess_cv_pointer() 方法
    • 手动通过指针运算来访问和修改元素

bgr2rgb_norm_hwc2chw_cpu_speed_test() 函数

  • 该函数测试了进行 BGR 到 RGB 转换、归一化以及 HWC 到 CHW 转换组合操作的速度
  • preprocess_cv_mat_at() 方法
    • 使用 cv::Mat::at 方式访问和处理像素
  • preprocess_cv_mat_iterator() 方法
    • 使用 cv::MatIterator_ 进行像素处理
  • preprocess_cv_mat_data() 方法
    • 通过指针直接访问图像数据进行处理
  • preprocess_cv_pointer() 方法
    • 使用手动指针运算

在 main.cpp 中我们实现了一个初步的在 CPU 端的前处理的性能比较,比较的是对于访问 CV::Mat 的数据,哪一种方式会比较快。因为有的时候我们可能会考虑在 CPU 上做前处理,比如说图像前处理放在 GPU 上时并不能充分把硬件资源吃满,导致硬件资源浪费,如果出现这种情况的话,我们可能会考虑把前处理放在 CPU 上,而把 DNN 的 forward 部分放在 GPU 上进行异步的推理

这里主要比较四种方法:

  • 使用 cv::Mat::at
  • 使用 cv::MatIterator_
  • 使用 cv::Mat.data
  • 使用 cv::Mat.ptr

同时我们在 main.cpp 中也比较了一下 CPU 端做 bgr2rgb 和 bgr2rgb + normalization + hwc2chw 的性能比较

2.2 preprocess.cpp

下面我们一起来看具体的函数实现,我们先从 bgr2rgb 函数中的方法实现看起

preprocess_cv_cvtcolor() 方法具体实现代码如下:

void preprocess_cv_cvtcolor(cv::Mat src, cv::Mat tar){
    cv::cvtColor(src, tar, cv::COLOR_RGB2BGR);
}

该方法非常的简单,主要是利用 OpenCV 提供的 cv::cvtColor 函数来完成从 RGB 到 BGR 色彩空间的转换

preprocess_cv_mat_at() 方法具体实现代码如下:

void preprocess_cv_mat_at(cv::Mat src, cv::Mat tar){
    for (int i = 0; i < src.rows; i++) {
        for (int j = 0; j < src.cols; j++) {
            tar.at<cv::Vec3b>(i, j)[2] = src.at<cv::Vec3b>(i, j)[0];
            tar.at<cv::Vec3b>(i, j)[1] = src.at<cv::Vec3b>(i, j)[1];
            tar.at<cv::Vec3b>(i, j)[0] = src.at<cv::Vec3b>(i, j)[2];
        }
    }
}

该方法通过双重循环遍历图像中的每一个像素,外层循环遍历图像每一行,内层循环遍历图像每一列,接着通过 .at<cv::Vec3b> 访问目标图像和源图像中的每一个像素并进行相应的通道转换,其中 cv::Vec3b 表示一个包含三个 uchar 值的向量即一个像素,它对应 BGR 三个通道的值

这种方法的优点在于直接访问和操作,可以允许你精确地控制每个像素地操作,在需要对像素级别进行复杂的自定义操作时非常有用,但是它的速度较慢,当图像较大时这种方法的效率会成为一个瓶颈

preprocess_cv_mat_iterator() 方法具体实现代码如下:

void preprocess_cv_mat_iterator(cv::Mat src, cv::Mat tar){
    cv::MatIterator_<cv::Vec3b> src_it = src.begin<cv::Vec3b>();
    cv::MatIterator_<cv::Vec3b> tar_it = tar.begin<cv::Vec3b>();
    cv::MatIterator_<cv::Vec3b> end    = src.end<cv::Vec3b>();
    for (; src_it != end; src_it++, tar_it++) {
        (*tar_it)[2] = (*src_it)[0];
        (*tar_it)[1] = (*src_it)[1];
        (*tar_it)[0] = (*src_it)[2];
    }
}

该方法利用 OpenCV 提供的 cv::MatIterator_ 迭代器来遍历和操作图像的每个像素,相较于 cv::Mat::at 方法,迭代器提供了一种更直接的方式来遍历矩阵,并且在某些情况下可以提高代码的执行效率。首先我们初始化三个迭代器,其中 src_it 指向源图像的起始位置,tar_it 指向目标图像的起始位置,end 指向源图像的结束位置,接着我们进行迭代循环和转换操作

该方法使用迭代器的方式使得代码更加简洁,并且迭代器的运算通常比 cv::Mat::at 更加高效,但是它的性能未必最佳,对于需要更高性能的场景直接操作指针或 cv::Mat::data 可能更合适

preprocess_cv_mat_data() 方法具体实现如下:

void preprocess_cv_mat_data(cv::Mat src, cv::Mat tar){
    int height   = src.rows;
    int width    = src.cols;
    int channels = src.channels();

    for (int i = 0; i < height; i ++) {
        for (int j = 0; j < width; j ++) {
            int index = i * width * channels + j * channels;
            tar.data[index + 2] = src.data[index + 0];
            tar.data[index + 1] = src.data[index + 1];
            tar.data[index + 0] = src.data[index + 2];
        }
    }
}

该方法直接使用指针访问图像数据,逐个像素进行 BGR 到 RGB 的转换,相比于之前的方法,这种方式更加接近底层操作,能够提高更高的性能,但代码的可读性相对较低

首先外层循环遍历图像的每一行,内层循环遍历图像的每一列,接着计算像素索引,通过像素索引去访问每一个像素从而实现颜色通道交换。其中 src.datatar.data 都是一个 uchar* 类型的指针,指向图像数据在内存中的起始位置,通过直接操作这个指针,可以访问和修改图像的原始数据

preprocess_cv_pointer() 方法具体实现如下:

void preprocess_cv_pointer(cv::Mat src, cv::Mat tar){
    for (int i = 0; i < src.rows; i ++) {
        cv::Vec3b* src_ptr = src.ptr<cv::Vec3b>(i);
        cv::Vec3b* tar_ptr = tar.ptr<cv::Vec3b>(i);
        for (int j = 0; j < src.cols; j ++) {
            tar_ptr[j][2] = src_ptr[j][0];
            tar_ptr[j][1] = src_ptr[j][1];
            tar_ptr[j][0] = src_ptr[j][2];
        }
    }
}

该方法通过使用指针来遍历和操作图像的每个像素进行 BGR 到 RGB 的颜色变换,相比直接访问图像数据,这种方法利用了指针的优势,同时保持了相对较好的可读性和操作的灵活性

首先我们逐行指针操作,外层循环遍历图像的每一行,接着进行行指针初始化,然后我们逐列操作,内存循环遍历图像的每一列,此时通过指针 src_ptr[i]tar_ptr[j] 可以直接访问和操作每个像素

我们执行下看下输出如下所示:

在这里插入图片描述

我们从图中可以看到速度最快的是最后两个使用指针操作的方法,相比之下直接使用 cv::cvtColor 方法耗时最严重

Note:博主每次执行的结果都不尽相同,但是总的来说使用指针操作的方法是速度最快的,大家以自己实际的 CPU 执行结果为准就行

下面我们来看 bgr2rgb_norm_hwc2chw 函数的实现,那大部分其实和 bgr2rgb 的方法差不多,只是添加了一个 normalization 以及 hwc2chw 操作,这里我们就简单过一下

preprocess_cv_mat_at() 方法具体实现如下:

void preprocess_cv_mat_at(cv::Mat src, float* tar, float* mean, float* std){
    float* ptar_ch0 = tar + src.rows * src.cols * 0;
    float* ptar_ch1 = tar + src.rows * src.cols * 1;
    float* ptar_ch2 = tar + src.rows * src.cols * 2;

    for (int i = 0; i < src.rows; i++) {
        for (int j = 0; j < src.cols; j++) {
            (*ptar_ch2++) = (src.at<cv::Vec3b>(i, j)[0] / 255.0f - mean[0]) / std[0];
            (*ptar_ch1++) = (src.at<cv::Vec3b>(i, j)[1] / 255.0f - mean[1]) / std[1];
            (*ptar_ch0++) = (src.at<cv::Vec3b>(i, j)[2] / 255.0f - mean[2]) / std[2];
        }
    }
}

该方法首先初始化目标指针,其中 ptar_ch0ptar_ch1ptar_ch2 分别指向目标数组 tar 中 R、G、B 三个通道的起始位置,这种布局方式确保图像数据在内存中按通道顺序存储,而不是按像素顺序存储,即 CHW 格式。接着双重循环遍历图像中的每个像素,外层循环遍历图像的每一行,内存循环遍历图像的每一列,最后进行颜色空间转换和归一化

preprocess_cv_mat_iterator() 方法具体实现如下:

void preprocess_cv_mat_iterator(cv::Mat src, float* tar, float* mean, float* std){
    float* ptar_ch0 = tar + src.rows * src.cols * 0;
    float* ptar_ch1 = tar + src.rows * src.cols * 1;
    float* ptar_ch2 = tar + src.rows * src.cols * 2;
    cv::MatIterator_<cv::Vec3b> it     = src.begin<cv::Vec3b>();
    cv::MatIterator_<cv::Vec3b> end    = src.end<cv::Vec3b>();

    for (; it != end; it++) {
        (*ptar_ch2++) = ((*it)[0] / 255.0f - mean[0]) / std[0];
        (*ptar_ch1++) = ((*it)[1] / 255.0f - mean[1]) / std[1];
        (*ptar_ch0++) = ((*it)[2] / 255.0f - mean[2]) / std[2];
    }
}

该方法使用迭代器 cv::MatIterator_ 来遍历图像数据,并在遍历过程中完成图像数据的 BGR 到 RGB 转换、归一化操作以及 HWC 到 CHW 格式的转换

preprocess_cv_mat_data() 方法具体实现如下:

void preprocess_cv_mat_data(cv::Mat src, float* tar, float* mean, float* std){
    float* ptar_ch0 = tar + src.rows * src.cols * 0;
    float* ptar_ch1 = tar + src.rows * src.cols * 1;
    float* ptar_ch2 = tar + src.rows * src.cols * 2;
    int height      = src.rows;
    int width       = src.cols;
    int channels    = src.channels();

    for (int i = 0; i < height; i ++) {
        for (int j = 0; j < width; j ++) {
            int index = i * width * channels + j * channels;
            (*ptar_ch2++) = (src.data[index + 0] / 255.0f - mean[0]) / std[0];
            (*ptar_ch1++) = (src.data[index + 1] / 255.0f - mean[1]) / std[1];
            (*ptar_ch0++) = (src.data[index + 2] / 255.0f - mean[2]) / std[2];
        }
    }
}

该方法直接操作图像的内存数据,通过计算每个像素在内存中的索引来进行 BGR 到 RGB 的转换、归一化处理以及从 HWC 到 CHW 格式的转换,相比于使用迭代器或 cv::Mat::at 方法,这种方法更加接近底层,通常在性能上更有优势

preprocess_cv_pointer() 方法具体实现如下:

void preprocess_cv_pointer(cv::Mat src, float* tar, float* mean, float* std){
    int area = src.rows * src.cols;
    int offset_ch0 = area * 0;
    int offset_ch1 = area * 1;
    int offset_ch2 = area * 2;

    for (int i = 0; i < src.rows; i ++) {
        cv::Vec3b* src_ptr = src.ptr<cv::Vec3b>(i);
        for (int j = 0; j < src.cols; j ++) {
            tar[offset_ch2++] = (src_ptr[j][0] / 255.0f - mean[0]) / std[0];
            tar[offset_ch1++] = (src_ptr[j][1] / 255.0f - mean[1]) / std[1];
            tar[offset_ch0++] = (src_ptr[j][2] / 255.0f - mean[2]) / std[2];
        }
    }
}

该方法同时使用指针来访问和处理图像数据,完成 BGR 到 RGB 的颜色空间转换、归一化处理以及从 HWC 到 CHW 格式的转换,这种方法结合了指针的高效性,通常在性能上有不错的表现

我们执行下看下输出如下所示:

在这里插入图片描述

我们从图中可以看到速度最快的依旧是指针操作的方法,速度最慢的是逐像素访问的方法

Note:博主每次执行的结果都不尽相同,但是总的来说使用指针操作的方法是速度最快的,大家以自己实际的 CPU 执行结果为准就行

3. 补充说明

值得注意的是这个小节的 Makefile 也有一些改动,主要体现在 bear 工具的使用,如下所示:

ifeq (, $(shell which bear))
BEARCMD       :=
else
ifeq (bear 3.0.18, $(shell bear --version))
BEARCMD       := bear --output config/compile_commands.json --
else
BEARCMD       := bear -o config/compile_commands.json
endif
endif

我们前面有提到过 bear 是一个用于生成 compile_commands.json 文件的工具,这个文件通常用于启用基于 clangd 或其他类似工具的代码分析和自动补全功能。bear 工具会拦截编译命令并记录它们,从而生成描述如何构建项目中每个源文件的 JSON 数据

另外在不同的 Ubuntu 系统中,bear 的指令可能有所不同,这个主要取决于安装的 bear 版本,在最新的 Ubuntu22.04 系统中推荐使用的 bear 版本是 3.0.18,比较老的 Ubuntu18.04 和 Ubuntu20.04 推荐使用的 bear 版本是 2.6 或者 2.7

关于 bear 的安装我们可以直接通过 apt-get 安装,指令如下:

sudo apt-get update
sudo apt-get install bear

当然我们也可以从源码编译安装,指令如下:

git clone https://github.com/rizsotto/Bear.git
cd Bear
git checkout tags/3.0.18
mkdir build && cd build
cmake .. && make
sudo make install

结语

本次课程我们学习了 CPU 端图像预处理的几种方法,包括逐像素方法,迭代器方法以及指针方法,其中指针方法耗时最慢,速度最快。当我们处理的图像较小在 GPU 端计算密度不高时我们可以采用这个小节所说的几种在 CPU 端处理的方法

OK,以上就是 6.0 小节案例的全部内容了,下节我们来学习 6.1 小节分类器的简单部署实现,敬请期待😄

下载链接

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱听歌的周童鞋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值