七. 部署YOLOv8检测器-affine-transformation

目录

    • 前言
    • 0. 简述
    • 1. 案例运行
    • 2. 补充说明
    • 3. 代码分析
      • 3.1 main.cpp
      • 3.2 preprocess.cu
    • 结语
    • 下载链接
    • 参考

前言

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

本次课程我们来学习课程第七章—部署YOLOv8检测器,一起来学习仿射变换

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

在这里插入图片描述

0. 简述

本小节目标:学习 affine transformation 仿射变换

这节我们主要学习仿射变换 warpAffine,理解仿射变换矩阵及其逆矩阵

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

1. 案例运行

在正式开始课程之前,博主先带大家跑通 7.2-affine-transformation 这个小节的案例🤗

源代码获取地址: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软件安装大全 进行相应软件的安装,博主这里不再赘述

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

开始之前我们需要在 tensorrt_starter/chapter7-deploy-yolo-detection/7.2-affine-transformation 小节中创建一个 results 文件夹用于保存仿射变换后的图片

创建完后 7.2 小节整个目录结构如下:

在这里插入图片描述

接着我们需要运行代码来执行仿射变换,在此之前我们需要修改下整体的 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-cuda

输出如下:

在这里插入图片描述

在 results 文件夹下我们可以看到经过各个预处理方式后的图片,大家可以查看,如下图所示:

在这里插入图片描述

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

2. 补充说明

在分析代码之前我们先来看下韩君老师在这小节中写的 README 文档

这个小节是接着第二章节的 2.10-bilinear-interpolation 的扩展,大家感兴趣的话可以看看:二. CUDA编程入门-双线性插值计算

之前在 classification model 推理的时候,我们只需要对图像做一次 resize 就好了,但是在做 detection model 例如 yolo 推理的时候,我们首先需要将图片 resize 到 yolo model 可以识别的大小(例如 640x640),之后我们得到在这个尺寸下的 bbox,但是我们在绘图的时候还需要将 bbox 还原成原图的大小,所以需要某一种形式进行 resize 的正向/反向的变换,这个可以通过 warpAffine 仿射变换来实现

其实我们前面 bilinear-interpolation 案例的实现已经非常贴近 warpAffine 了,部分代码如下所示:

// bilinear interpolation -- 计算x,y映射到原图时最近的4个坐标
int src_y1 = floor((y + 0.5) * scaled_h - 0.5);
int src_x1 = floor((x + 0.5) * scaled_w - 0.5);
int src_y2 = src_y1 + 1;
int src_x2 = src_x1 + 1;

...

// bilinear interpolation -- 计算原图在目标图中的x, y方向上的偏移量
y = y - int(srcH / (scaled_h * 2)) + int(tarH / 2);
x = x - int(srcW / (scaled_w * 2)) + int(tarW / 2);

warpAffine 的基本公式如下:

// forward
forward_scale = min(tar_w / src_w, tar_h / src_h);
tar_x         = src_x * forward_scale + forward_shift_x;
tar_y         = src_y * forward_scale + forward_shift_y;

// reverse
reverse_scale   = 1 / forward_scale;
reverse_shift_x = -forward_shift / forward_scale_x;
reverse_shift_y = -forward_shift / forward_scale_y;
src_x           = tar_x * reverse_scale + reverse_shift_x;
src_y           = tar_y * reverse_scale + reverse_shift_y;

规范之后可以写成下面这种形式:

// forward
tar_x  =  src_x * forward_scale + src_y * 0             + forward_shift_x;
tar_y  =  src_x * 0             + src_y * forward_scale + forward_shift_y;

// reverse
src_x  =  tar_x * reverse_scale + tar_y * 0             + reverse_shift_x;
src_y  =  tar_x * 0             + tar_y * reverse_scale + reverse_shift_y;

我们可以通过 Matrix 的形式保存这些 scale 和 shift 等需要的时候直接使用,在 yolo 的 preprocess 中由于我们需要把图片 resize 成 letterbox 所以可以把 scale 和 shift 写成下面的形式:

// 存储forward时需要的scale和shift
void calc_forward_matrix(TransInfo trans){
    forward[0] = forward_scale;
    forward[1] = 0;
    forward[2] = - forward_scale * trans.src_w * 0.5 + trans.tar_w * 0.5;
    forward[3] = 0;
    forward[4] = forward_scale;
    forward[5] = - forward_scale * trans.src_h * 0.5 + trans.tar_h * 0.5;
};

// 存储reverse时需要的scale和shift
void calc_reverse_matrix(TransInfo trans){
    reverse[0] = reverse_scale;
    reverse[1] = 0;
    reverse[2] = - reverse_scale * trans.tar_w * 0.5 + trans.src_w * 0.5;
    reverse[3] = 0;
    reverse[4] = reverse_scale;
    reverse[5] = - reverse_scale * trans.tar_h * 0.5 + trans.src_h * 0.5;
};

// 仿射变换的计算公式
__device__ void affine_transformation(
    float trans_matrix[6], 
    int src_x, int src_y, 
    float* tar_x, float* tar_y)
{
    *tar_x = trans_matrix[0] * src_x + trans_matrix[1] * src_y + trans_matrix[2];
    *tar_y = trans_matrix[3] * src_x + trans_matrix[4] * src_y + trans_matrix[5];
}

Note:关于仿射变换的更多细节大家感兴趣的可以看看杜老师的讲解视频以及博主之前写的文章:

  • 手写AI/预处理高性能
  • YOLOv5推理详解及预处理高性能实现

大家可能看上面的 warpAffine 的代码有些困惑,那其实对照着公式来看还是比较清晰的,博主这边还是快速过一遍吧

在目标检测任务中,我们的预处理操作通常是先对图像进行等比缩放,然后居中,多余部分填充,类似于下图所展示的,这个过程也被叫做 letterbox 添加灰条:

在这里插入图片描述

整个过程可以拆分为以下三个步骤

1. 等比缩放,矩阵 S S S 实现(缩放倍数 scale 为目标图像与源图像宽比值和高比值的最小值)

在这里插入图片描述

2. 将图片中心平移到左上角坐标原点,矩阵 O O O 实现

在这里插入图片描述

3. 将图片平移到目标位置的中心,矩阵 T T T 实现

在这里插入图片描述

预处理的过程可以通过三个矩阵完成,分别为缩放矩阵 S S S,平移矩阵 O O O,平移矩阵 T T T,将这三个矩阵可以进行合并成一个矩阵 M ( M = T O S ) M(M=TOS) M(M=TOS),其中 M M M 矩阵被称为仿射变换矩阵,可以帮助我们完成图像预处理工作

我们可以直接写出仿射变换矩阵 M M M 的计算公式,如下式 (4) 所示,其中 O r i g i n Origin Origin 代表源图像, D s t Dst Dst 代表目标图像, ( x , y ) (x,y) (x,y) 为源图像上任意像素点坐标, ( x ′ , y ′ ) (x',y') (x,y) 为目标图像上任意像素点坐标

在这里插入图片描述

通过上述变换可以得到源图像任意像素值的坐标对应在目标图像的位置,那么整个预处理过程也就可以通过仿射变换矩阵 M M M 来完成。除了将源图像通过 M M M 变换到目标图像,我们也要考虑将目标图像映射回源图像,即求 M M M 矩阵的逆矩阵 M − 1 M^{-1} M1

逆变换矩阵 M − 1 M^{-1} M1 计算公式如下:

在这里插入图片描述

则有

在这里插入图片描述

OK,以上就是 warpAffine Matrix 的推导公式,理解了之后大家再来看之前的代码会发现还是比较容易简单的:

// forward
tar_x  =  src_x * forward_scale + src_y * 0             + forward_shift_x;
tar_y  =  src_x * 0             + src_y * forward_scale + forward_shift_y;

// reverse
src_x  =  tar_x * reverse_scale + tar_y * 0             + reverse_shift_x;
src_y  =  tar_x * 0             + tar_y * reverse_scale + reverse_shift_y;

理解了上面的内容我们下面来看代码就轻松很多了

3. 代码分析

3.1 main.cpp

我们先从 main.cpp 看起:

#include <stdio.h>
#include <cuda_runtime.h>
#include <iostream>

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

using namespace std;

int main(){
    Timer timer;

    string file_path     = "data/deer.png";
    string output_prefix = "results/";
    string output_path   = "";

    cv::Mat input = cv::imread(file_path);
    int tar_h = 500;
    int tar_w = 550;
    int tactis;

    cv::Mat resizedInput_cpu;
    cv::Mat resizedInput_gpu;
    
    /* 
     * bilinear interpolation resize的CPU/GPU速度比较
     * 由于CPU端做完预处理之后,进入如果有DNN也需要将数据传送到device上,
     * 所以这里为了让测速公平,仅对下面的部分进行测速:
     *
     * - host端
     *     cv::resize的bilinear interpolation
     *     normalization进行归一化处理
     *     BGR2RGB来实现通道调换
     *
     * - device端
     *     bilinear interpolation + normalization + BGR2RGB的自定义核函数
     *
     * 由于这个章节仅是初步CUDA学习,大家在自己构建推理模型的时候可以将这些地方进行封装来写的好看点,
     * 在这里代码我们更关注实现的逻辑部分
     *
     * tatics 列表
     * 0: 最近邻差值缩放 + 全图填充
     * 1: 双线性差值缩放 + 全图填充
     * 2: 双线性差值缩放 + 填充(letter box)
     * 3: 双线性差值缩放 + 填充(letter box) + 平移居中
     * 4: 仿射变换(letter box)
     * */
    
    resizedInput_cpu = preprocess_cpu(input, tar_h, tar_w, timer, tactis);
    output_path = output_prefix + getPrefix(file_path) + "_resized_bilinear_cpu.png";
    cv::cvtColor(resizedInput_cpu, resizedInput_cpu, cv::COLOR_RGB2BGR);
    cv::imwrite(output_path, resizedInput_cpu);

    tactis = 0;
    resizedInput_gpu = preprocess_gpu(input, tar_h, tar_w, timer, tactis);
    output_path = output_prefix + getPrefix(file_path) + "_resized_nearest_gpu.png";
    cv::imwrite(output_path, resizedInput_gpu);

    tactis = 1;
    resizedInput_gpu = preprocess_gpu(input, tar_h, tar_w, timer, tactis);
    output_path = output_prefix + getPrefix(file_path) + "_resized_bilinear_gpu.png";
    cv::imwrite(output_path, resizedInput_gpu);

    tactis = 2;
    resizedInput_gpu = preprocess_gpu(input, tar_h, tar_w, timer, tactis);
    output_path = output_prefix + getPrefix(file_path) + "_resized_bilinear_letterbox_gpu.png";
    cv::imwrite(output_path, resizedInput_gpu);

    tactis = 3;
    resizedInput_gpu = preprocess_gpu(input, tar_h, tar_w, timer, tactis);
    output_path = output_prefix + getPrefix(file_path) + "_resized_bilinear_letterbox_center_gpu.png";
    cv::imwrite(output_path, resizedInput_gpu);

    tactis = 4;
    resizedInput_gpu = preprocess_gpu(input, tar_h, tar_w, timer, tactis);
    output_path = output_prefix + getPrefix(file_path) + "_resized_warpaffine_letterbox_center_gpu.png";
    cv::imwrite(output_path, resizedInput_gpu);

    return 0;
}

main 函数中我们对输入图像进行了各种预处理操作,并比较了 CPU 和 GPU 端的图像缩放、归一化、通道交换等操作的性能差异,最后将每个策略的处理结果保存为不同的文件

3.2 preprocess.cu

main 函数中的 preprocess_gpu 会通过不同的 tactics 调用不同的 Kernel 核函数完成预处理操作,主要包括以下四种:

void resize_bilinear_gpu(
    uint8_t* d_tar, uint8_t* d_src, 
    int tarW, int tarH, 
    int srcW, int srcH, 
    int tactis) 
{
    dim3 dimBlock(16, 16, 1);
    dim3 dimGrid(tarW / 16 + 1, tarH / 16 + 1, 1);
    
    //scaled resize
    float scaled_h = (float)srcH / tarH;
    float scaled_w = (float)srcW / tarW;
    float scale = (scaled_h > scaled_w ? scaled_h : scaled_w);

    if (tactis > 1) {
        scaled_h = scale;
        scaled_w = scale;
    }

    // for affine transformation
    TransInfo    trans(srcW, srcH, tarW, tarH);
    AffineMatrix affine;
    affine.init(trans);
    
    switch (tactis) {
    case 0:
        resize_nearest_BGR2RGB_kernel <<<dimGrid, dimBlock>>> (d_tar, d_src, tarW, tarH, srcW, srcH, scaled_w, scaled_h);
        break;
    case 1:
        resize_bilinear_BGR2RGB_kernel <<<dimGrid, dimBlock>>> (d_tar, d_src, tarW, tarH, srcW, srcH, scaled_w, scaled_h);
        break;
    case 2:
        resize_bilinear_BGR2RGB_kernel <<<dimGrid, dimBlock>>> (d_tar, d_src, tarW, tarH, srcW, srcH, scaled_w, scaled_h);
        break;
    case 3:
        resize_bilinear_BGR2RGB_shift_kernel <<<dimGrid, dimBlock>>> (d_tar, d_src, tarW, tarH, srcW, srcH, scaled_w, scaled_h);
        break;
    case 4:
        resize_warpaffine_BGR2RGB_kernel <<<dimGrid, dimBlock>>> (d_tar, d_src, trans, affine);
        break;
    default:
        break;
    }
}

前面三种策略我们在 2.10-bilinear-interpolation 小节案例中已经详细分析过了,这边博主就不再赘述了,我们重点来看第四种策略,也就是 warpAffine 仿射变换的 Kernel 核函数实现,完整的代码如下所示:

struct TransInfo{
    int src_w;
    int src_h;
    int tar_w;
    int tar_h;
    TransInfo(int srcW, int srcH, int tarW, int tarH):
        src_w(srcW), src_h(srcH), tar_w(tarW), tar_h(tarH){}
};

struct AffineMatrix{
    float forward[6];
    float reverse[6];
    float forward_scale;
    float reverse_scale;

    void calc_forward_matrix(TransInfo trans){
        forward[0] = forward_scale;
        forward[1] = 0;
        forward[2] = - forward_scale * trans.src_w * 0.5 + trans.tar_w * 0.5;
        forward[3] = 0;
        forward[4] = forward_scale;
        forward[5] = - forward_scale * trans.src_h * 0.5 + trans.tar_h * 0.5;
    };

    void calc_reverse_matrix(TransInfo trans){
        reverse[0] = reverse_scale;
        reverse[1] = 0;
        reverse[2] = - reverse_scale * trans.tar_w * 0.5 + trans.src_w * 0.5;
        reverse[3] = 0;
        reverse[4] = reverse_scale;
        reverse[5] = - reverse_scale * trans.tar_h * 0.5 + trans.src_h * 0.5;
    };

    void init(TransInfo trans){
        float scaled_w = (float)trans.tar_w / trans.src_w;
        float scaled_h = (float)trans.tar_h / trans.src_h;
        forward_scale = (scaled_w < scaled_h ? scaled_w : scaled_h);
        reverse_scale = 1 / forward_scale;
    
        // 计算src->tar和tar->src的仿射矩阵
        calc_forward_matrix(trans);
        calc_reverse_matrix(trans);
    }
};

__device__ void affine_transformation(
    float trans_matrix[6], 
    int src_x, int src_y, 
    float* tar_x, float* tar_y)
{
    *tar_x = trans_matrix[0] * src_x + trans_matrix[1] * src_y + trans_matrix[2];
    *tar_y = trans_matrix[3] * src_x + trans_matrix[4] * src_y + trans_matrix[5];
}

__global__ void resize_warpaffine_BGR2RGB_kernel(
    uint8_t*     tar, 
    uint8_t*     src, 
    TransInfo    trans_info,
    AffineMatrix matrix)
{
    float src_x, src_y;

    // resized之后的图tar上的坐标
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;

    // bilinear interpolation -- 通过逆仿射变换得到计算tar中的x, y所需要的src中的src_x, src_y
    affine_transformation(matrix.reverse, x + 0.5, y + 0.5, &src_x, &src_y);

    // bilinear interpolation -- 计算x,y映射到原图时最近的4个坐标
    int src_x1 = floor(src_x - 0.5);
    int src_y1 = floor(src_y - 0.5);
    int src_x2 = src_x1 + 1;
    int src_y2 = src_y1 + 1;

    if (src_y1 < 0 || src_x1 < 0 || src_y1 > trans_info.src_h || src_x1 > trans_info.src_w) {
        // bilinear interpolation -- 对于越界的坐标不进行计算
    } else {
        // bilinear interpolation -- 计算原图上的坐标(浮点类型)在0~1之间的值
        float tw   = src_x - src_x1;
        float th   = src_y - src_y1;

        // bilinear interpolation -- 计算面积(这里建议自己手画一张图来理解一下)
        float a1_1 = (1.0 - tw) * (1.0 - th);  //右下
        float a1_2 = tw * (1.0 - th);          //左下
        float a2_1 = (1.0 - tw) * th;          //右上
        float a2_2 = tw * th;                  //左上

        // bilinear interpolation -- 计算4个坐标所对应的索引
        int srcIdx1_1 = (src_y1 * trans_info.src_w + src_x1) * 3;  //左上
        int srcIdx1_2 = (src_y1 * trans_info.src_w + src_x2) * 3;  //右上
        int srcIdx2_1 = (src_y2 * trans_info.src_w + src_x1) * 3;  //左下
        int srcIdx2_2 = (src_y2 * trans_info.src_w + src_x2) * 3;  //右下

        // bilinear interpolation -- 计算resized之后的图的索引
        int tarIdx    = (y * trans_info.tar_w  + x) * 3;


        // bilinear interpolation -- 实现bilinear interpolation + BGR2RGB
        tar[tarIdx + 0] = round(
                          a1_1 * src[srcIdx1_1 + 2] + 
                          a1_2 * src[srcIdx1_2 + 2] +
                          a2_1 * src[srcIdx2_1 + 2] +
                          a2_2 * src[srcIdx2_2 + 2]);

        tar[tarIdx + 1] = round(
                          a1_1 * src[srcIdx1_1 + 1] + 
                          a1_2 * src[srcIdx1_2 + 1] +
                          a2_1 * src[srcIdx2_1 + 1] +
                          a2_2 * src[srcIdx2_2 + 1]);

        tar[tarIdx + 2] = round(
                          a1_1 * src[srcIdx1_1 + 0] + 
                          a1_2 * src[srcIdx1_2 + 0] +
                          a2_1 * src[srcIdx2_1 + 0] +
                          a2_2 * src[srcIdx2_2 + 0]);
    }
}

这个 CUDA 核函数实现了图像的 warpAffine 仿射变换操作,结合 bilinear interpolation 双线性插值来对图像进行缩放,并同时执行 BGR 到 RGB 的颜色通道转换,下面我们来详细分析下这个核函数的各个部分:(from ChatGPT)

首先我们来看核函数的各个参数:

__global__ void resize_warpaffine_BGR2RGB_kernel(
    uint8_t*     tar, 
    uint8_t*     src, 
    TransInfo    trans_info,
    AffineMatrix matrix)
  • tar:目标图像的存储数组,输出图像数据会保存在这里
  • src:源图像的存储数组,即输入图像的数据
  • trans_info:包含源图像和目标图像的宽度和高度的结构体
  • matrix:包含仿射变换矩阵以及正向和反向缩放系数的结构体

TransInfo 结构体定义如下:

struct TransInfo{
    int src_w;
    int src_h;
    int tar_w;
    int tar_h;
    TransInfo(int srcW, int srcH, int tarW, int tarH):
        src_w(srcW), src_h(srcH), tar_w(tarW), tar_h(tarH){}
};

功能

  • TransInfo 结构体用于存储源图像和目标图像的宽度和高度信息
  • 它有四个成员变量:
    • src_w:源图像的宽度
    • src_h:源图像的高度
    • tar_w:目标图像的宽度
    • tar_h:目标图像的高度
  • 通过构造函数 TransInfo(int srcW, int srcH, int tarW, int tarH) 可以快速初始化这些参数

用途

  • 该结构体主要用于传递源图像和目标图像的尺寸信息,以便于计算仿射变换矩阵和进行图像缩放

AffineMatrix 结构体定义如下:

struct AffineMatrix{
    float forward[6];
    float reverse[6];
    float forward_scale;
    float reverse_scale;

    void calc_forward_matrix(TransInfo trans){
        forward[0] = forward_scale;
        forward[1] = 0;
        forward[2] = - forward_scale * trans.src_w * 0.5 + trans.tar_w * 0.5;
        forward[3] = 0;
        forward[4] = forward_scale;
        forward[5] = - forward_scale * trans.src_h * 0.5 + trans.tar_h * 0.5;
    };

    void calc_reverse_matrix(TransInfo trans){
        reverse[0] = reverse_scale;
        reverse[1] = 0;
        reverse[2] = - reverse_scale * trans.tar_w * 0.5 + trans.src_w * 0.5;
        reverse[3] = 0;
        reverse[4] = reverse_scale;
        reverse[5] = - reverse_scale * trans.tar_h * 0.5 + trans.src_h * 0.5;
    };

    void init(TransInfo trans){
        float scaled_w = (float)trans.tar_w / trans.src_w;
        float scaled_h = (float)trans.tar_h / trans.src_h;
        forward_scale = (scaled_w < scaled_h ? scaled_w : scaled_h);
        reverse_scale = 1 / forward_scale;
    
        // 计算src->tar和tar->src的仿射矩阵
        calc_forward_matrix(trans);
        calc_reverse_matrix(trans);
    }
};

功能

  • AffineMatrix 结构体用于计算和存储源图像和目标图像之间的仿射变换矩阵及其逆矩阵,以及对应的缩放因子
  • 主要成员变量:
    • foward[6]:正向仿射变换矩阵(源图像到目标图像)
    • reverse[6]:反向仿射变换矩阵(目标图像到源图像)
    • forward_scale:正向缩放系数
    • reverse_scale:反向缩放系数

主要方法

  • calc_forward_matrix(TransInfo trans)
    • 计算正向仿射变换矩阵,即从源图像到目标图像的变换
    • 使用 forward_scale 来调整源图像的大小,并通过调整平移项确保图像居中
  • calc_reverse_matrix(TransInfo trans)
    • 计算反向仿射变换矩阵,即从目标图像到源图像的变换。该矩阵用于将目标图像中的像素位置映射回源图像,以便插值计算
    • 通过反向缩放系数调整目标图像的大小,使其匹配源图像
  • init(TransInfo trans)
    • 初始化正向和反向的缩放比例,并调用 calc_forward_matrixcalc_reverse_matrix 计算正向和反向的仿射变换矩阵
    • forward_scale 是根据目标图像和源图像的宽度和高度的比例来计算的,选择其中较小的缩放因子,这样可以保持纵横比
    • reverse_scaleforward_scale 的倒数,保证反向映射的正确性

用途

  • AffineMatrix 是图像缩放和仿射变换的核心,它的正向矩阵用于图像的放缩操作,反向矩阵用于逆仿射变换(即将目标图像中的坐标映射到源图像中)
  • 通过这个矩阵,我们可以完成图像缩放、旋转、平移等操作

接着我们来看核函数的工作流程,核函数的主要流程是:

  • 使用逆仿射变换计算目标图像像素在源图像中的对应位置
  • 使用双线性插值计算目标图像的像素值,基于源图像四个相邻像素点的加权平均
  • 将像素值从 BGR 格式转换为 RGB 格式

1. 计算目标图像的坐标 xy

int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
  • 这部分代码利用 CUDA 的线程模型,通过 blockIdxthreadIdx 来计算每个线程所处理的目标图像中的像素坐标 (x, y)

2. 逆仿射变换计算源图像的坐标

  • 调用 affine_transformation 函数,利用逆仿射变换矩阵 matrix.reverse 将目标图像坐标 (x, y) 转换为源图像中的浮点坐标 (src_x, src_y)

affine_transformation 函数的定义如下:

__device__ void affine_transformation(
    float trans_matrix[6], 
    int src_x, int src_y, 
    float* tar_x, float* tar_y)
{
    *tar_x = trans_matrix[0] * src_x + trans_matrix[1] * src_y + trans_matrix[2];
    *tar_y = trans_matrix[3] * src_x + trans_matrix[4] * src_y + trans_matrix[5];
}

功能

  • affine_transformation 函数用于在 device 上执行仿射变换,它将源图像中的 (src_x, src_y) 坐标转换为目标图像中的 (tar_x, tar_y) 坐标
  • 该函数通过乘以 2x3 的仿射变换矩阵来完成坐标转换:
    • tar_x = M[0] * src_x + M[1] * src_y + M[2]
    • tar_y = M[3] * src_x + M[4] * src_y + M[5]

详细说明

  • 仿射变换矩阵 trans_matrix[6] 是一个 2x3 的矩阵,它定义了如何将源图像中的坐标映射到目标图像中的坐标,具体公式如下:
    • x ′ = M 00 ⋅ x + M 01 ⋅ y + M 02 x'=M_{00}\cdot x+M_{01}\cdot y+M_{02} x=M00x+M01y+M02
    • y ′ = M 10 ⋅ x + M 11 ⋅ y + M 12 y'=M_{10}\cdot x+M_{11}\cdot y+M_{12} y=M10x+M11y+M12
  • trans_matrix[6] 包含了以下值:
    • M[0] 对应 forward[0]reverse[0],用于缩放
    • M[1]M[3] 用于旋转
    • M[2]M[5] 是平移项,确保图像的中心对齐

用途

  • 该函数在核函数中被调用,通过传入反向仿射矩阵 matrix.reverse,将目标图像的像素坐标映射到源图像中,这是进行双线性插值的基础操作

3. 计算源图像最近的四个相邻像素

int src_x1 = floor(src_x - 0.5);
int src_y1 = floor(src_y - 0.5);
int src_x2 = src_x1 + 1;
int src_y2 = src_y1 + 1;
  • 使用双线性插值,需要计算源图像中与浮点坐标 (src_x, src_y) 最接近的四个整数坐标像素,分别为 (src_x1, src_y1)(src_x2, src_y1)(src_x1, src_y2)(src_x2, src_y2)

4. 边界检查

if (src_y1 < 0 || src_x1 < 0 || src_y1 > trans_info.src_h || src_x1 > trans_info.src_w) {
    // bilinear interpolation -- 对于越界的坐标不进行计算
}
  • 这段代码检查源图像中的四个相邻像素点是否越界(即坐标超出了图像的范围),如果越界,则跳过当前像素的计算

5. 双线性插值权重计算

float tw   = src_x - src_x1;
float th   = src_y - src_y1;
  • 计算 src_xsrc_y 与它们左上角元素 (src_x1, src_y1) 的距离,分别为 twth,这将用于计算插值权重

6. 计算每个邻近像素的插值权重

float a1_1 = (1.0 - tw) * (1.0 - th);  //右下
float a1_2 = tw * (1.0 - th);          //左下
float a2_1 = (1.0 - tw) * th;          //右上
float a2_2 = tw * th;                  //左上
  • 通过双线性插值计算四个相邻像素的权重,插值权重的计算公式基于源图像中相邻像素与目标像素的距离
    • a1_1:左上角像素的权重
    • a1_2:右上角像素的权重
    • a2_1:左下角像素的权重
    • a2_2:右下角像素的权重

7. 计算源图像中四个像素点的索引

int srcIdx1_1 = (src_y1 * trans_info.src_w + src_x1) * 3;  //左上
int srcIdx1_2 = (src_y1 * trans_info.src_w + src_x2) * 3;  //右上
int srcIdx2_1 = (src_y2 * trans_info.src_w + src_x1) * 3;  //左下
int srcIdx2_2 = (src_y2 * trans_info.src_w + src_x2) * 3;  //右下
  • 计算源图像四个相邻像素点在 src 数组中的索引,这里每个像素点有 RGB 三个通道,所以每个索引都乘以 3

8. 计算目标图像中当前像素的索引

int tarIdx = (y * trans_info.tar_w + x) * 3;
  • 计算目标图像中当前像素在 tar 数组中的索引,同样,每个像素有 RGB 三个通道,所以索引乘以 3

9. 双线性插值计算 + BGR2RGB

tar[tarIdx + 0] = round(
    a1_1 * src[srcIdx1_1 + 2] + 
    a1_2 * src[srcIdx1_2 + 2] +
    a2_1 * src[srcIdx2_1 + 2] +
    a2_2 * src[srcIdx2_2 + 2]);

tar[tarIdx + 1] = round(
    a1_1 * src[srcIdx1_1 + 1] + 
    a1_2 * src[srcIdx1_2 + 1] +
    a2_1 * src[srcIdx2_1 + 1] +
    a2_2 * src[srcIdx2_2 + 1]);

tar[tarIdx + 2] = round(
    a1_1 * src[srcIdx1_1 + 0] + 
    a1_2 * src[srcIdx1_2 + 0] +
    a2_1 * src[srcIdx2_1 + 0] +
    a2_2 * src[srcIdx2_2 + 0]);
  • 利用前面计算出的权重和源图像四个像素点的值,对每个颜色通道(B、G、R)进行双线性插值计算,值得注意的是在这个过程中,通道顺序从 BGR 转换为了 RGB
    • tar[tarIdx + 0]:R 通道
    • tar[tarIdx + 1]:G 通道
    • tar[tarIdx + 2]:B 通道

总的来说,这个 CUDA 核函数实现了基于仿射变换的双线性插值,并且将源图像的 BGR 颜色通道转换为目标图像的 RGB 颜色通道

OK,以上就是 warpAffine 实现代码的详细分析了,理解原理之后再来看代码还是比较简单的

结语

本次课程我们学习了 affine transformation 仿射变换,并简单分析了其具体实现的代码,warpAffine 在许多模型的前处理中都有使用到,因此理解它是必不可少的

OK,以上就是 7.2 小节案例的全部内容了,下节我们来学习 7.3 小节 yolov8 模型的部署,敬请期待😄

下载链接

  • tensorrt_starter源码

参考

  • Ubuntu20.04软件安装大全
  • https://github.com/kalfazed/tensorrt_starter
  • 二. CUDA编程入门-双线性插值计算
  • 手写AI/预处理高性能
  • YOLOv5推理详解及预处理高性能实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/873308.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python文件自动化(4)

接上节课内容&#xff0c;在开始正式移动文件到目标文件夹之前&#xff0c;我们需要再思考一个问题。在代码运行之前&#xff0c;阿文的下载文件夹里已经存在一些分类文件夹了&#xff0c;比如图例中“PDF文件”这个文件夹就是已经存在的。这样的话&#xff0c;在程序运行时&am…

SprinBoot+Vue校园数字化图书馆系统的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质…

FreeRTOS任务调度(抢占式、协作式、时间片轮转)

任务调度 文章目录 任务调度前言一、协作式二、时间片轮转三、抢占式总结 前言 FreeRTOS 是一个开源的实时操作系统&#xff0c;它支持多种调度策略&#xff0c;包括协作式&#xff08;cooperative&#xff09;和抢占式&#xff08;preemptive&#xff09;调度。 一、协作式 …

堆排序Java

思路 这个代码还不错 https://blog.csdn.net/weixin_51609435/article/details/122982075 就是从下往上进行调整 1. 如何将数组映射成树 对于下面这颗树&#xff0c;原来的数组是&#xff1a; 好&#xff0c;如果调整的话&#xff0c;我们第一个应该调整的是最下边&#x…

压缩文件隐写

1、伪加密 &#xff08;1&#xff09;zip伪加密 考点&#xff1a;winhex打开压缩包&#xff1b;搜索504b0102(注意不是文件头部&#xff1b;zip文件头部伪504b0304);从50开始&#xff0c;往后面数第9&#xff0c;10个字符为加密字符&#xff0c;将其设置为0000即可变为无加密状…

JAVAEE初阶第七节(中)——物理原理与TCP_IP

系列文章目录 JAVAEE初阶第七节&#xff08;中&#xff09;——物理原理与TCP_IP 文章目录 系列文章目录JAVAEE初阶第七节&#xff08;中&#xff09;——物理原理与TCP_IP 一.应用层重点协议&#xff09;1. DNS2 .NAT3. NAT IP转换过程 4 .NAPT5. NAT技术的缺陷6. HTTP/HTTPS…

野火霸天虎V2学习记录

文章目录 嵌入式开发常识汇总1、嵌入式Linux和stm32之间的区别和联系2、stm32程序下载方式3、Keil5安装芯片包4、芯片封装种类5、STM32命名6、数据手册和参考手册7、什么是寄存器、寄存器映射和内存映射8、芯片引脚顺序9、stm32芯片里有什么10、存储器空间的划分11、如何理解寄…

如何部署Vue+Springboot项目

很多同学在项目上线的部署遇到困难&#xff0c;不懂得怎么部署项目&#xff0c;本文将会带大家手把手从前端部署、java部署来教会大家。 如果项目涉及到了docker相关中间件的环境配置&#xff0c;请参看&#xff1a;https://blog.csdn.net/weixin_73195042/article/details/13…

C#发送正文带图片带附件的邮件

1&#xff0c;开启服务&#xff0c;获取授权码。以QQ邮箱为例&#xff1a; 点击管理服务&#xff0c;进入账号与安全页面 2&#xff0c;相关设置参数&#xff0c;以QQ邮箱为例&#xff1a; 登录时&#xff0c;请在第三方客户端的密码输入框里面填入授权码进行验证。&#xff0…

解决 Ant Design Vue Upload 组件在苹果手机上只能拍照无法选择相册的问题

最近上线发现了这个问题&#xff0c;看别的文档改了很多属性也不行&#xff0c;发现element组件就可以&#xff0c;对比之后就知道问题所在。 原因&#xff1a; 默认情况下&#xff0c;iOS 设备会将 <input type"file"> 的 capture 属性设置为 true&#xff0…

[数据集][目标检测]电动车头盔佩戴检测数据集VOC+YOLO格式4235张5类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;4235 标注数量(xml文件个数)&#xff1a;4235 标注数量(txt文件个数)&#xff1a;4235 标注…

python 正则表达式“.*”和“.*? ”的区别

“.*”和“.*? ”的区别 点号表示任意非换行符的字符&#xff0c;星号表示匹配它前面的字符0次或者任意多次。所以“.*”表示匹配一串任意长度的字符串任意次。这个时候必须在“.*”的前后加其他的符号来限定范围&#xff0c;否则得到的结果就是原来的整个字符串。 “.*? &…

基于SpringBoot校园快递代取系统

基于springbootvue实现的校园快递代取系统&#xff08;源码L文ppt&#xff09;4-049 3系统设计 3.1.1系统结构图 系统结构图可以把杂乱无章的模块按照设计者的思维方式进行调整排序&#xff0c;可以让设计者在之后的添加&#xff0c;修改程序内容…

基于SpringBoot框架和Flask的图片差异检测与展示系统

目录 1. 项目目标 2. 功能需求 &#xff08;1&#xff09;图片上传功能 &#xff08;2&#xff09;差异检测算法 &#xff08;3&#xff09;后端服务 &#xff08;4&#xff09;前端展示 &#xff08;5&#xff09;阿里云服务器存储 &#xff08;6&#xff09;数据库记…

Java:正则表达式 matches

文章目录 正则表达式作用基本用法小结代码 案例&#xff1a;校验用户输入的电话&#xff0c;邮箱&#xff0c;是否合法\\.是什么意思 黑马学习笔记 正则表达式 由一些特定的字符组成&#xff0c;代表的是一个规则 作用 用来校验数据格式是否合法在一段文本中查找满足要求的内…

Elasticsearch:无状态世界中的数据安全

作者&#xff1a;来自 Elastic Henning Andersen 在最近的博客文章中&#xff0c;我们宣布了支持 Elastic Cloud Serverless 产品的无状态架构。通过将持久性保证和复制卸载到对象存储&#xff08;例如 Amazon S3&#xff09;&#xff0c;我们获得了许多优势和简化。 从历史上…

Web3D 技术发展瓶颈在哪里?

Web3D 技术的发展瓶颈主要集中在以下几个方面&#xff1a; 1、性能和优化&#xff1a;尽管现代浏览器和硬件逐步提高了性能&#xff0c;但高质量的3D渲染仍可能导致性能瓶颈。特别是在移动设备上&#xff0c;图形渲染和计算可能会受到限制。建议合理控制好项目资源量&#xff…

实验记录 | 点云处理 | K-NN算法3种实现的性能比较

引言 K近邻&#xff08;K-Nearest Neighbors, KNN&#xff09;算法作为一种经典的无监督学习算法&#xff0c;在点云处理中的应用尤为广泛。它通过计算点与点之间的距离来寻找数据点的邻居&#xff0c;从而有效进行点云分类、聚类和特征提取。本菜在复现点云文章过程&#xff…

详解React setState调用原理和批量更新的过程

1. React setState 调用的原理 setState目录 1. React setState 调用的原理2. React setState 调用之后发生了什么&#xff1f;是同步还是异步&#xff1f;3. React中的setState批量更新的过程是什么&#xff1f; 具体的执行过程如下&#xff08;源码级解析&#xff09;&#x…

基于SpringBoot+Vue+MySQL的宿舍维修管理系统

系统展示 前台界面 管理员界面 维修员界面 学生界面 系统背景 在当今高校后勤管理的日益精细化与智能化背景下&#xff0c;宿舍维修管理系统作为提升校园生活品质、优化资源配置的关键环节&#xff0c;其重要性日益凸显。随着学生规模的扩大及住宿条件的不断提升&#xff0c;宿…