NVIDIA的边缘计算的序列板子都配备了视频编码器和解码器,使用解码器硬件解码当然比使用OpenCV+ffmpeg之类的软解码要快多了。使用Jetson Nano的解码程序遇到个问题就是Jetson Nano在存放解码出来的图像的YUV数据时没有完全遵循一般的规范来做。
一般在压缩视频(DV设备生成)中YUV420格式使用较多,YUV420准确的说应该叫YCbCr420,YCbCr是YUV(Y的取值范围为0~255,U的取值范围为-122~122、V的取值范围为-157~157)的缩放和偏移版本,Y的取值范围为16-255,Cb和Cr的取值范围为16-240,YCbCr有4:4:4、4:2:2、4:2:0、4:1:1这几张采样格式,YUV420指YCbCr在水平和垂直方向都对Cb和Cr采样减少为2:1,Y为黑白分量,在水平和垂直方向采样比值都是4,下图左边是H261/H263/MPEG1的采样方式,右图是H264/MPEG2的采样方式:
YUV的数据存放格式分为planar和packed两种。planar格式最常用,它是先连续存放所有像素点的Y分量数据,紧接着存放所有像素点的U分量数据,最后存放所有像素点的V分量数据。packed格式则是每个像素点的Y,U,V数据是交错连续存储的。YUV420中,一个像素点对应一个Y,一个4X4的小方块对应一个U和V,对于YUV420p,也就是说planar格式存放的,先存放完Y分量数据再存放U分量数据,再存放V分量数据,以一个分辨率为8X4的YUV图像为例,它们的420p存放格式如下图:
所以当我们知道一副图像的height和width后,可以立即算出YUV420p格式数据存放占用的空间 size=height*width*3/2,也就是说,这个空间的宽度为width,高度则为 height*3/2。
可是在NVIDIA jetson nano的docode代码里在存放数据时没用完全遵循规范,而是在分配空间时宽度给Y分配的2048,给UV都是分配的1024,而不是图像的实际宽度:
[decode.cpp]
ret = NvBufferGetParams(temp_dma_fd, &parm);
...
int ysize = parm.pitch[0] * parm.height[0]; #pitch[0]=2k
int uvsize = parm.pitch[1] * parm.height[1]; #pitch[1]=1k
pYuvData->pData[0] = new unsigned char[ysize];
pYuvData->pData[1] = new unsigned char[uvsize];
pYuvData->pData[2] = new unsigned char[uvsize];
for (int plane = 0; plane < parm.num_planes; plane++) {
pYuvData->nWidth[plane] = parm.width[plane];
pYuvData->nHeight[plane] = parm.height[plane];
pYuvData->nPitch[plane] = parm.pitch[plane];
图片是1080x1920的,2048这个宽度倒是够了,但是留有空洞,不明白为何要这么做,猜测可能是为了迎合jetson nano的解码器在输出数据时的什么特殊要求吧?如果不知道这点,我们写代码在接受它解码输出的数据时分配的空间按照height*width*3/2大小分配空间后从 pYuvData->pData拷贝数据时仍然以pYuvData->nWidth[plan]作为数据的宽度来拷贝的话,得到的数据就是错的,在使用OpenCV或libyuv把YUV数据转换成BGR数据后看到的图片就是一遍花的。
要解决这个问题,首先要实现一个去空洞的精准拷贝函数来把YUV420p数据的nano存储方式改成标准格式:
bool copyYUV(StNanoYuvData *pYuvData, unsigned char* mem) {
for (int i=0; i
{
memcpy(mem, pYuvData->pData[0]+i*pYuvData->nPitch[0], pYuvData->nWidth[0]);
mem += pYuvData->nWidth[0];
}
for (int i=0; i
{
memcpy(mem, pYuvData->pData[1]+i*pYuvData->nPitch[1], pYuvData->nWidth[1]);
mem += pYuvData->nWidth[1];
}
for (int i=0; i
{
memcpy(mem, pYuvData->pData[2]+i*pYuvData->nPitch[2], pYuvData->nWidth[2]);
mem += pYuvData->nWidth[2];
}
return true;
}
然后把YUV数据转换成BGR数据并显示:
cv::Mat image;
image.create(pYuvData->nHeight[0], pYuvData->nWidth[0], CV_8UC3);
Mat yuvMat;
yuvMat.create(pYuvData->nHeight[0] * 3 / 2, pYuvData->nWidth[0], CV_8UC1);
copyYUV(pYuvData,yuvMat.data);
//用libyuv转换:
libyuv::I420ToRGB24(yuvMat.data ,pYuvData->nWidth[0],yuvMat.data+pYuvData->nWidth[0]*pYuvData->nHeight[0],pYuvData->nWidth[1]/2, yuvMat.data+pYuvData->nWidth[0]*pYuvData->nHeight[0]+pYuvData->nWidth[1]*pYuvData->nHeight[1],pYuvData->nWidth[2]/2,image.data, pYuvData->nWidth[0]*3,pYuvData->nWidth[0]*pYuvData->nHeight[0])
//或用OpenCV转换:
cv::cvtColor(yuvMat, image, CV_YUV2BGR_I420);
//show
cv::imshow("image",image);
cv::waitKey(0)