关于YUV_I420和NV12的区别分析

最近在搞板端部署,于是认真阅读了下OE包里horizon_runtime_sample的示例代码,由于这些示例代码的输入数据都是本地的图片,是bgr/rgb类型,和实际使用时摄像头采集到nv12数据类型不同,所以会有色彩空间转换的操作,把bgr转成nv12,然后再将这份输入数据复制到input_tensor里。

这里我以00_quick_start为例,在 read_image_2_tensor_as_nv12 函数的色彩空间转换这步中,使用了一行代码:cv::cvtColor(mat, yuv_mat, cv::COLOR_BGR2YUV_I420); 我一直以为这里的 YUV_I420 就是NV12了,后来发现有点蹊跷,于是下功夫认真研究了一下。

地平线发布的技术文章 常见图像格式 (horizon.cc) 给了我很多的帮助,大家可以先阅读这篇,对数据类型有个基本的认知。

首先,我们要确定的一点是,无论是nv12还是nv12_separate,在输入给模型推理前,数据排布都应该是yyyyyyyyuvuv这样的形式,即地平线技术文章中提到的 YUV420SP,区别在于nv12使用了一块内存,nv12_separate使用了两块内存。

其次,示例代码使用 cv::cvtColor 接口将BGR转换成的YUV_I420,并不是nv12/YUV420SP,而是 YUV420P,即yyyyyyyyuuvv的排列形式,u和v并不是交替存储,而是先存储u,再存储v。

那么,是什么时候将YUV420P转变成了YUV420SP的呢?答案就在 read_image_2_tensor_as_nv12 函数的后半段:

// copy y data
 auto data = input->sysMem[0].virAddr;
 int32_t y_size = input_h * input_w;
 memcpy(reinterpret_cast<uint8_t *>(data), nv12_data, y_size);

// copy uv data
 int32_t uv_height = input_h / 2;
 int32_t uv_width = input_w / 2;
 uint8_t *nv12 = reinterpret_cast<uint8_t *>(data) + y_size;
 uint8_t *u_data = nv12_data + y_size;
 uint8_t *v_data = u_data + uv_height * uv_width;

 for (int32_t i = 0; i < uv_width * uv_height; i++) {
 if (u_data && v_data) {
      *nv12++ = *u_data++;
      *nv12++ = *v_data++;
    }
  }

这段代码中,变量nv12是input_tensor的地址,变量u_data和v_data是输入数据转变成YUV_I420之后的u分量、v分量地址,通过交替将u和v复制给nv12/input_tensor,实现将YUV_I420转变成nv12。

最后总结:YUV_I420的u和v并不是交替排列的,转换成交替排列的形式之后,才是通常意义上说的nv12!