C++ opencv 将图片从HWC转CHW数据格式

1. opencv的读取格式

众所周知,opencv读取图片后,在内存中数据是以HWC的顺序进行排列的,但是在深度学习模型中,一般需要将其转为CHW格式(准确来说是NCHW)再进行推断。

在python中,opencv读取后的数据类型是numpy的ndarray,这个时候只要调用numpy的transpose方法就可以解决了:

img_np_t = img_np.transpose(2, 0, 1)

然而,在python中就没这么简单了,虽然在opencv 4.6之后出了一个函数transposeND,但是却有一个限制,即输入必须是单通道的矩阵,因此也无法直接调用。

2. 数据格式与内存

2.1. 数据格式

假设有一张图片img,有三个通道(m1,m2, m3),每个通道有2行2列,如下图所示:

图1

 如果这三个通道是以CHW格式(3*2*2)排列的,则排列后效果如下:

图2

如果这三个通道是以HWC(2*2*3)格式排列的,则排列后效果如下:

图3

可以看出区别还是挺大的,我们的目的就是实现下面的转换:

图4

2.2. 内存

以上面的m1为例,无论m1的形状为2*2还是1*4,只要行数*列数的结果一样,他们在内存中的排列顺序都是一样的,我们可以创建一个简易程序,然后看看在内存中的排列:

int main() {
    cv::Mat m1 = (cv::Mat_<uchar>(2, 2) << 1, 2, 3, 4);
    cv::Mat m1_2  = (cv::Mat_<uchar>(1, 4) << 1, 2, 3, 4);
    return 0;
}

无论m1还是m2_2,查看它们在内存中存储的数据(data指针指向的地址),都是以01020304这种方式进行排列的,如下图所示:

图5 

因此我们可以看看img的不同排列在内存中的实际情况。

CHW:

图6

 HWC:

图7

 因此,要从HWC转为CHW,本质上就是需要将HWC中的内存排列转为CHW中的内存排列。

3. 转换

总的来说,转换有两步:

  • 分离图片通道;
  • 按照通道顺序拼接数据。

在具体实现方面,分离图片通道数据这里使用split函数,拼接数据使用hconcat。

hconcat函数(文档)的主要作用,是将多个矩阵,沿着1轴的方向进行拼接,效果如下:

图8

在内存中的详情如下:

图9

明显不是我们想要的结果。

因此,在分离通道之后,我们还需要将通道数据展平(flatten),然后再使用hconcat进行拼接,实际的代码如下:

cv::Mat hwc2chw(const cv::Mat& src_mat) {
    std::vector<cv::Mat> bgr_channels(3);
    cv::split(src_mat, bgr_channels);
    for (size_t i = 0; i < bgr_channels.size(); i++)
    {
        bgr_channels[i] = bgr_channels[i].reshape(1, 1); // reshape为1通道,1行,n列
    }
    cv::Mat dst_mat;
    cv::hconcat(bgr_channels, dst_mat);
    return dst_mat;
}

其中,reshape(文档)将每个通道进行flatten操作,即从2*2展平为1*4,然后再沿1轴进行拼接,如下图所示:

图10

这个dst_mat的内存数据就是我们想要的结果:

 图11

假如这个时候不需要进行其他操作,直接返回dst_mat就可以了,如果还需要进行基于shape的相关操作,还需要再reshape一次: dst_mat = dst_mat.reshape(3, { 2,2 }); 。

如果是对接其他模型,如Triton backend,就不需要另外转了,因为reshape只是改变了读取数据的方式,并没有对数据进行任何操作,而最后传给Triton的也只是data指针。

当然还有很多方法可以进行转换,如使用vector数组将reshape后的channel按照顺序复制到数组中,其本质也是一样的。

 

4. 参考

https://cloud.tencent.com/developer/ask/sof/107600649

(完)

 

posted @ 2024-08-26 15:37  大师兄啊哈  阅读(149)  评论(0编辑  收藏  举报