目标检测预处理的高性能实现

本文是你真的理解高性能吗-预处理高性能_哔哩哔哩_bilibiq的学习笔记!

预处理指的是 把图像变成 tensor 输入到神经网络去的过程。

首先看 YOLOX 的实现:YOLOX/yolox.cpp at main · Megvii-BaseDetection/YOLOX · GitHub

    cv::Mat img = cv::imread(input_image_path);
    int img_w = img.cols;
    int img_h = img.rows;
    cv::Mat pr_img = static_resize(img);
    std::cout << "blob image" << std::endl;

    float* blob;
    blob = blobFromImage(pr_img); // blob 就是输入到神经网络的tensor

cv::Mat static_resize(cv::Mat& img) {
    float r = std::min(INPUT_W / (img.cols*1.0), INPUT_H / (img.rows*1.0));
    // r = std::min(r, 1.0f);
    int unpad_w = r * img.cols;
    int unpad_h = r * img.rows;
    cv::Mat re(unpad_h, unpad_w, CV_8UC3);
    cv::resize(img, re, re.size());
    cv::Mat out(INPUT_H, INPUT_W, CV_8UC3, cv::Scalar(114, 114, 114));
    re.copyTo(out(cv::Rect(0, 0, re.cols, re.rows)));
    return out;
}

缺点:低效,Mat 分配了两次,是CPU实现,效率低。

float* blobFromImage(cv::Mat& img){
    float* blob = new float[img.total()*3];
    int channels = 3;
    int img_h = img.rows;
    int img_w = img.cols;
    for (size_t c = 0; c < channels; c++) 
    {
        for (size_t  h = 0; h < img_h; h++) 
        {
            for (size_t w = 0; w < img_w; w++) 
            {
                blob[c * img_w * img_h + h * img_w + w] =
                    (float)img.at<cv::Vec3b>(h, w)[c];
            }
        }
    }
    return blob;
}

缺点:性能差,3个for循环,CPU实现,效率低。

再看下 YOLOv5实现:https://github.com/wang-xinyu/tensorrtx/commit/e7a5f0acbc5f8879d90f6d387f5fb3e4ec52a1e5

该代码库在 2021年10月19日更新后也是使用了仿射变换在GPU上进行预处理。但我们先看之前的版本。

        for (int b = 0; b < fcount; b++) {
            cv::Mat img = cv::imread(img_dir + "/" + file_names[f - fcount + 1 + b]);
            if (img.empty()) continue;
            cv::Mat pr_img = preprocess_img(img, INPUT_W, INPUT_H); // letterbox BGR to RGB
            int i = 0;
            for (int row = 0; row < INPUT_H; ++row) {
                uchar* uc_pixel = pr_img.data + row * pr_img.step;
                for (int col = 0; col < INPUT_W; ++col) {
                    data[b * 3 * INPUT_H * INPUT_W + i] = (float)uc_pixel[2] / 255.0;
                    data[b * 3 * INPUT_H * INPUT_W + i + INPUT_H * INPUT_W] = (float)uc_pixel[1] / 255.0;
                    data[b * 3 * INPUT_H * INPUT_W + i + 2 * INPUT_H * INPUT_W] = (float)uc_pixel[0] / 255.0;
                    uc_pixel += 3;
                    ++i;
                }
            }
        // Run inference
        auto start = std::chrono::system_clock::now(); // 这里的推理时间都没有考虑预处理,因此是假的推理时间。

两个for循环,效率低。

preprocess_img函数在tensorrtx/utils.h at master · wang-xinyu/tensorrtx · GitHub

static inline cv::Mat preprocess_img(cv::Mat& img, int input_w, int input_h) {
    int w, h, x, y;
    float r_w = input_w / (img.cols*1.0);
    float r_h = input_h / (img.rows*1.0);
    if (r_h > r_w) {
        w = input_w;
        h = r_w * img.rows;
        x = 0;
        y = (input_h - h) / 2;
    } else {
        w = r_h * img.cols;
        h = input_h;
        x = (input_w - w) / 2;
        y = 0;
    }
    cv::Mat re(h, w, CV_8UC3);
    cv::resize(img, re, re.size(), 0, 0, cv::INTER_LINEAR);
    cv::Mat out(input_h, input_w, CV_8UC3, cv::Scalar(128, 128, 128));
    re.copyTo(out(cv::Rect(x, y, re.cols, re.rows)));
    return out;
}

计算上比YOLOX的还复杂,有if 代码可读性差,YOLOX很可能抄袭的该代码。

后期处理:

            cv::Mat img = cv::imread(img_dir + "/" + file_names[f - fcount + 1 + b]);
            for (size_t j = 0; j < res.size(); j++) {
                cv::Rect r = get_rect(img, res[j].bbox);  // 框恢复成图像大小,然后显示出来。
@@ -420,17 +419,22 @@ int main(int argc, char** argv) {
            cv::imwrite("_" + file_names[f - fcount + 1 + b], img);
        }

https://github.com/wang-xinyu/tensorrtx/blob/master/yolov5/common.hpp:get_rect

cv::Rect get_rect(cv::Mat& img, float bbox[4]) {
    float l, r, t, b;
    float r_w = Yolo::INPUT_W / (img.cols * 1.0);
    float r_h = Yolo::INPUT_H / (img.rows * 1.0);
    if (r_h > r_w) {
        l = bbox[0] - bbox[2] / 2.f;
        r = bbox[0] + bbox[2] / 2.f;
        t = bbox[1] - bbox[3] / 2.f - (Yolo::INPUT_H - r_w * img.rows) / 2;
        b = bbox[1] + bbox[3] / 2.f - (Yolo::INPUT_H - r_w * img.rows) / 2;
        l = l / r_w;
        r = r / r_w;
        t = t / r_w;
        b = b / r_w;
    } else {
        l = bbox[0] - bbox[2] / 2.f - (Yolo::INPUT_W - r_h * img.cols) / 2;
        r = bbox[0] + bbox[2] / 2.f - (Yolo::INPUT_W - r_h * img.cols) / 2;
        t = bbox[1] - bbox[3] / 2.f;
        b = bbox[1] + bbox[3] / 2.f;
        l = l / r_h;
        r = r / r_h;
        t = t / r_h;
        b = b / r_h;
    }
    return cv::Rect(round(l), round(t), round(r - l), round(b - t));
}

代码可读性差,难以编写!

性能有多高?

使用warpaffine(仿射变换)实现预处理。

图像预处理的本质是将图像缩小,然后将其移动到特定位置。目标检测器通常按比例缩放+居中以实现预处理。

可以使用 warpaffine 来做。

问题:是否比 resize 慢呢? 并不是,warpaffine需要做插值,resize也要做插值,本质上一样的。

使用warpaffine 用矩阵来描述更加方便,操作简单,预处理和后处理会更加简洁。

两行代码预处理,后处理恢复原图大小,逆矩阵相乘。简洁的! ! !

  • warpaffine 并不会加速,只是逻辑上更清晰。加速的话需要在GPU上实现仿射变换的过程。

关于转换

  • 点的变换,通常存在,缩放、旋转、平移,例如对点P(x, y)进行旋转𝜃度、缩放scale倍、平移ox,oy

旋转变换:

  • 如果用矩阵表示,则有:

目标检测预处理的高性能实现

目标检测预处理的高性能实现

  • 又因为opencv的图像坐标,原点在左上角,y+向下,因此旋转变换矩阵在图像上时是:

目标检测预处理的高性能实现

注意:这里是逆时针旋转,如果是顺时针或者:

目标检测预处理的高性能实现

参考:仿射变换(Affine Transformation)原理及应用(1)_Godswisdom的博客-CSDN博客_affine transformation

import cv2
import numpy as np
cv2.getRotationMatrix2D((0, 0), 60, 1), np.sin(60/180.0*np.pi)

输出:这里忽略第三列。

(array([[ 0.5      ,  0.8660254,  0.       ],
        [-0.8660254,  0.5      ,  0.       ]]),
 0.8660254037844386)

缩放变换

目标检测预处理的高性能实现

注意:公式中x,y 缩放是一样的,这里可以不同。

缩放 + 旋转变换 = 旋转 + 缩放变换

目标检测预处理的高性能实现

核实:

theta = 0.8
scale = 2
rot = np.array([
    [np.cos(theta), np.sin(theta)],
    [-np.sin(theta), np.cos(theta)]
])

sca = np.array([
    [scale, 0],
    [0, scale]
])

np.allclose(rot @ sca, sca @ rot)

# True

翻译转换

目标检测预处理的高性能实现

  • 这时候问题就来了。平移是加法运算,没有办法将其组合成矩阵。我该怎么办?

解决:

齐次坐标定义

用(x, y, w),来表示点P(\frac{x}{w},\frac{y}{w},1)

翻译转换

目标检测预处理的高性能实现

缩放+旋转+平移变换

(注意此时顺序有关,旋转缩放后,再平移的样子,也就是P’ = T x S x R x P)

R:旋转;S:缩放;T:平移;P原来点;{P}'得到的结果

目标检测预处理的高性能实现

旋转矩阵的逆是它的转置,即

目标检测预处理的高性能实现

逆变换,反转变换矩阵

目标检测预处理的高性能实现

对于正常的目标检测推断,通常需要按比例缩放和居中图像

目标检测预处理的高性能实现目标检测预处理的高性能实现

它分为以下三个步骤:

目标检测预处理的高性能实现

目标检测预处理的高性能实现

目标检测预处理的高性能实现

目标检测预处理的高性能实现

由于变换比较简单,所以逆变换可以很容易地写成:

目标检测预处理的高性能实现

作者实现的仿射变换:

        float i2d[6];       // image to dst(network), 2x3 matrix
        float d2i[6];       // dst to image, 2x3 matrix

        void compute(const cv::Size& from, const cv::Size& to){
            float scale_x = to.width / (float)from.width;
            float scale_y = to.height / (float)from.height;

            // 这里取min的理由是
            // 1. M矩阵是 from * M = to的方式进行映射,因此scale的分母一定是from
            // 2. 取最小,即根据宽高比,算出最小的比例,如果取最大,则势必有一部分超出图像范围而被裁剪掉,这不是我们要的
            // **
            float scale = std::min(scale_x, scale_y);

            /**
            这里的仿射变换矩阵实质上是2x3的矩阵,具体实现是
            scale, 0, -scale * from.width * 0.5 + to.width * 0.5
            0, scale, -scale * from.height * 0.5 + to.height * 0.5
            
            这里可以想象成,是经历过缩放、平移、平移三次变换后的组合,M = TPS
            例如第一个S矩阵,定义为把输入的from图像,等比缩放scale倍,到to尺度下
            S = [
            scale,     0,      0
            0,     scale,      0
            0,         0,      1
            ]
            
            P矩阵定义为第一次平移变换矩阵,将图像的原点,从左上角,移动到缩放(scale)后图像的中心上
            P = [
            1,        0,      -scale * from.width * 0.5
            0,        1,      -scale * from.height * 0.5
            0,        0,                1
            ]

            T矩阵定义为第二次平移变换矩阵,将图像从原点移动到目标(to)图的中心上
            T = [
            1,        0,      to.width * 0.5,
            0,        1,      to.height * 0.5,
            0,        0,            1
            ]

            通过将3个矩阵顺序乘起来,即可得到下面的表达式:
            M = [
            scale,    0,     -scale * from.width * 0.5 + to.width * 0.5
            0,     scale,    -scale * from.height * 0.5 + to.height * 0.5
            0,        0,                     1
            ]
            去掉第三行就得到opencv需要的输入2x3矩阵
            **/

            /* 
                 + scale * 0.5 - 0.5 的主要原因是使得中心更加对齐,下采样不明显,但是上采样时就比较明显
                参考:https://www.iteye.com/blog/handspeaker-1545126
            */
            i2d[0] = scale;  i2d[1] = 0;  i2d[2] = -scale * from.width  * 0.5  + to.width * 0.5 + scale * 0.5 - 0.5;
            i2d[3] = 0;  i2d[4] = scale;  i2d[5] = -scale * from.height * 0.5 + to.height * 0.5 + scale * 0.5 - 0.5;
            
            cv::Mat m2x3_i2d(2, 3, CV_32F, i2d);
            cv::Mat m2x3_d2i(2, 3, CV_32F, d2i);
            cv::invertAffineTransform(m2x3_i2d, m2x3_d2i);  // 求逆矩阵
        }

        cv::Mat i2d_mat(){
            return cv::Mat(2, 3, CV_32F, i2d);
        }
    };

核实

origin = np.array([300, 500], dtype=float)
dst = np.array([640, 640], dtype=float)
scale = min(dst/origin)

M = np.array([
    [scale, 0, -(scale * origin[0] * 0.5) + (dst[0] * 0.5)],
    [0, scale, -(scale * origin[1] * 0.5) + (dst[1] * 0.5)],
    [0, 0, 1]
])

# array([[  1.28,   0.  , 128.  ],
#        [  0.  ,   1.28,   0.  ],
#        [  0.  ,   0.  ,   1.  ]])
np.linalg.inv(M)
# array([[   0.78125,    0.     , -100.     ],
#        [   0.     ,    0.78125,    0.     ],
#        [   0.     ,    0.     ,    1.     ]])
k = scale
b1 = M[0, 2]
b2 = M[1, 2]

invM = np.array([
    [1 / k, 0, -b1/k],
    [0, 1 / k, -b2/k]
])
invM
# array([[   0.78125,    0.     , -100.     ],
#        [   0.     ,    0.78125,   -0.     ]])

做一个正变换和逆变换的实验看看

def inv_align(M):
    k = M[0, 0]
    b1 = M[0, 2]
    b2 = M[1, 2]
    return np.array([
        [1/k, 0, -b1/k],
        [0, 1/k, -b2/k]
    ])

def align(image, dst_size):
    
    oh, ow = image.shape[:2]
    dh, dw = dst_size
    scale = min(dw/ow, dh/oh)
    
    M = np.array([
        [scale, 0, -scale * ow * 0.5 + dw * 0.5],
        [0, scale, -scale * oh * 0.5 + dh * 0.5]
    ])
    return cv2.warpAffine(image, M, dst_size), M, inv_align(M)
import matplotlib.pyplot as plt

cat1 = cv2.imread("cat1.png")
acat1, M, inv = align(cat1, (640, 640))

plt.subplot(1, 2, 1)
plt.title("WarpAffine")
plt.imshow(acat1[..., ::-1])

resact1 = cv2.warpAffine(acat1, inv, cat1.shape[:2][::-1])
plt.subplot(1, 2, 2)
plt.title("Restore")
plt.imshow(resact1[..., ::-1])

目标检测预处理的高性能实现

import matplotlib.pyplot as plt

cat2 = cv2.imread("cat2.png")
acat2, M, inv = align(cat2, (640, 640))

plt.subplot(1, 2, 1)
plt.title("WarpAffine")
plt.imshow(acat2[..., ::-1])

resact2 = cv2.warpAffine(acat2, inv, cat2.shape[:2][::-1])
plt.subplot(1, 2, 2)
plt.title("Restore")
plt.imshow(resact2[..., ::-1])

目标检测预处理的高性能实现

线性插值

p0 = 20    # 冷水
p1 = 100   # 热水
pos = 0.6  # 应该多少度

value = (1 - pos) * p0 + pos * p1
value
# 68.0

目标检测预处理的高性能实现

双线性插值

# 0, 0
p0 = 3;        p1 = 5
p2 = 6;        p3 = 8  
#                    1, 1

# x , y
pos = 0.6, 0.8

p0_area = (1 - pos[0]) * (1 - pos[1])
p1_area = pos[0] * (1 - pos[1])
p2_area = (1 - pos[0]) * pos[1]
p3_area = pos[0] * pos[1]

value = p0_area * p0 + p1_area * p1 + p2_area * p2 + p3_area * p3
value
# 6.6

目标检测预处理的高性能实现

实现一个warpaffine,双线性插值

def pyWarpAffine(image, M, dst_size, constant=(0, 0, 0)):
    
    # 注意输入的M矩阵格式,是Origin->Dst
    # 而这里需要的是Dst->Origin,所以要取逆矩阵
    M = cv2.invertAffineTransform(M)
    constant = np.array(constant)
    ih, iw   = image.shape[:2]
    dw, dh   = dst_size
    dst      = np.full((dh, dw, 3), constant, dtype=np.uint8)
    irange   = lambda p: p[0] >= 0 and p[0] < iw and p[1] >= 0 and p[1] < ih
    
    for y in range(dh): # 循环的是输出图的大小,原图大还是一样的快;循环容易cuda核并行
        for x in range(dw):
            
            homogeneous = np.array([[x, y, 1]]).T
            ox, oy = M @ homogeneous
            
            low_ox = int(np.floor(ox))
            low_oy = int(np.floor(oy))
            high_ox = low_ox + 1
            high_oy = low_oy + 1
            
            # p0     p1
            #      o
            # p2     p3
            
            pos = ox - low_ox, oy - low_oy
            p0_area = (1 - pos[0]) * (1 - pos[1])
            p1_area = pos[0] * (1 - pos[1])
            p2_area = (1 - pos[0]) * pos[1]
            p3_area = pos[0] * pos[1]
            
            p0 = low_ox, low_oy
            p1 = high_ox, low_oy
            p2 = low_ox, high_oy
            p3 = high_ox, high_oy
            p0_value = image[p0[1], p0[0]] if irange(p0) else constant
            p1_value = image[p1[1], p1[0]] if irange(p1) else constant
            p2_value = image[p2[1], p2[0]] if irange(p2) else constant
            p3_value = image[p3[1], p3[0]] if irange(p3) else constant
            dst[y, x] = p0_area * p0_value + p1_area * p1_value + p2_area * p2_value + p3_area * p3_value
            # 解锁:resize    透视变换   设计双线性插值的地方   
            # 学习warpAffine,熟练掌握它,然后使用cuda核实现它,前处理速度大幅提升。
            # 交换bgr  rgb
            # normalize ->  -mean /std
            # 1行代码实现normalize , /255.0
            # bgr bgr bgr -> bbb ggg rrr
            # focus 本质是切片
            # focus offset, 1行代码实现focus 性能高
            
    return dst

            
cat1 = cv2.imread("cat1.png")
#acat1_cv, M, inv = align(cat1, (100, 100))
M = cv2.getRotationMatrix2D((0, 0), 30, 0.5)
acat1_cv = cv2.warpAffine(cat1, M, (100, 100))
acat1_py = pyWarpAffine(cat1, M, (100, 100))

plt.figure(figsize=(10, 10))
plt.subplot(1, 2, 1)
plt.title("OpenCV")
plt.imshow(acat1_cv[..., ::-1])

plt.subplot(1, 2, 2)
plt.title("PyWarpAffine")
plt.imshow(acat1_py[..., ::-1])

目标检测预处理的高性能实现

下一步是什么

学习和研究:https://github.com/Guanbin-Huang/tensorRT_Pro_co-comments/blob/main/src/tensorRT/common/preprocess_kernel.cu

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2022年4月11日 下午2:01
下一篇 2022年4月11日