YUV420之YV12格式以及yuv422格式的显示

        这段时间,在做一个动态配置录相预览帧上的字符样式以及颜色等等的功能。因为要移植到几个不同的项目上,刚好这几个项目的camera原始预览数据格式,一个为yv12,一个yuv422,所以将这两种格式都做了送显的处理。先上一段传统的代码,也就是网上流行的给camera帧打上时间戳的代码:

DisplayClient::
addPreviewTimestamps(sp<StreamImgBuf>const& pCameraImgBuf)
{
      int width = pCameraImgBuf->getImgWidth();
      int height = pCameraImgBuf->getImgHeight();
      //ALOGD("timestamp videoSize  width : %d, height : %d",width,height);
      bool is1080P = width > 1280;
      int word_width = is1080P? digital_1080_d_width : digital_720_d_width;
      int word_height = is1080P? digital_1080_d_height : digital_720_d_height;
      int word_gap = is1080P? digital_1080_gap_d_width : gap_720_d_width;
      uint8_t* _ptr=(uint8_t *)pCameraImgBuf->getVirAddr();
      char isTimestampOffset[PROPERTY_VALUE_MAX];
      int    offset         = height - 100;
      property_get("com.spt.stampoffset.switch",isTimestampOffset,"0");
      if('1' == *isTimestampOffset){
	  offset =  55;
	  }
      int    margin_left      = width - 18 * word_width - 100;

      if ( NULL != _ptr )
      {
          char        dateTime[] = "2014-05-29 03:16:78";
            time_t          timer;
            struct tm     * t_tm;

            time( &timer );
            t_tm = localtime( &timer );
            memset( dateTime, 0, sizeof(dateTime) );
            sprintf( dateTime, "%4d-%02d-%02d %02d:%02d:%02d", t_tm->tm_year + 1900, t_tm->tm_mon + 1, t_tm->tm_mday, t_tm->tm_hour, t_tm->tm_min, t_tm->tm_sec );

            int digitalNums[10 + 8 + 1 + 1] = { -1 }; /* 10:-       11 ::      12:blank */
            memset( digitalNums, -1, sizeof(digitalNums) );
            for ( int i = 0; i < strlen( dateTime ); i++ )
            {
                char num = dateTime[i];
                if ( ('0' <= num) && (num <= '9') )
                {
                    digitalNums[i] = num - '0';
                }else if ( num == '-' )
                {
                    digitalNums[i] = 10;
                }else if ( num == ':' )
                {
                    digitalNums[i] = 11;
                }else if ( num == ' ' )
                {
                    digitalNums[i] = 12;
                }
            }

            for ( int j = 0; j < word_height; j++ )
            {
                for ( int k = 0; k < 10 + 1 + 8; k++ )
                {
                    const unsigned char* str = (digitalNums[k] < 12 && digitalNums[k] != -1) ? (is1080P? DigitalArray_1080_d[digitalNums[k]] : DigitalArray_node_d[digitalNums[k]]) : NULL;
                    if ( str != NULL )
                    {
                        for ( int h = 0; h < word_width; h++ )
                        {
                            if ( *(str + (word_height - 1 - j) * word_width + h) != 0x00 )
                            {
                                        const int     offset_pixel     = offset * width + margin_left + j * width + k * (word_gap + word_width) + h;
                                        const int     offset_adr    = (int) (offset_pixel * 1);
                                        *(_ptr + offset_adr)        = 0xff;
                                        //*(_ptr + offset_adr + 1)      = 0xff;

                            }
                        }
                    }
                }
            }
	     //add by mcjerdy specified timestamp end
      }
}

        这段代码的核心原理,就是从字符数组里取出编码成了yuv422或yv12的一个个字节,来替换对应位置的内容。我们现在要做的工作,也就是这样。只不过上面这段代码是没有加颜色的,也就是只画了Y(灰度)数据,所以算法很简单。而我们要将颜色也画上去,那么就还需要将对应的u、v分量也给画上去,算法自然也就不同了。

        再来先讲下yuv数据的格式,YU12和YV12属于YUV420格式,也是一种Plane模式,将Y、U、V分量分别打包,依次存储。其每一个像素点的YUV数据提取遵循YUV420格式的提取方式,即4个Y分量共用一组UV。

        NV12和NV21属于YUV420格式,是一种two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane。

        

在YUV420中,一个像素点对应一个Y,一个4X4的小方块对应一个U和V。对于所有YUV420图像,它们的Y值排列是完全相同的,因为只有Y的图像就是灰度图像。YUV420sp与YUV420p的数据格式它们的UV排列在原理上是完全不同的。420p它是先把U存放完后,再存放V,也就是说UV它们是连续的。而420sp它是UV、UV这样交替存放的。(见下图) 有了上面的理论,我就可以准确的计算出一个YUV420在内存中存放的大小。 width * hight =Y(总和) U = Y / 4   V = Y / 4

所以YUV420 数据在内存中的长度是 width * hight * 3 / 2,

假设一个分辨率为8X4的YUV图像,它们的格式如下图:

YUV420sp格式如下图 

YUV420p数据格式如下图

        从上图可以看出,yuv420sp和yuv420p的存储方式,基本相同,只是yuv420sp的uv是交替存储的,而yuv420p的uv是分开存储的。我们要处理的yv12,就是属于yuv420p的一种,不过yv12是先存的全部Y,然后再存全部的V,最后再存全部的U,这个顺序不能弄乱了。

        在yv12中,所有 Y 样例都会作为不带正负号的 char 值组成的数组首先显示在内存中。此数组后面紧接着所有 V (Cr) 样例。V 平面的跨距为 Y 平面跨距的一半,V 平面包含的行为 Y 平面包含行的一半。V 平面后面紧接着所有 U (Cb) 样例,它的跨距和行数与 V 平面相同, 见下图:

        有了上面的基础,我们再来说说加yv12时间戳水印的事。因为我们camera出来的原始数据就是yv12的,所以我们要用来替换的数字图片数组,必定也是转成了yv12的无符号字符数组unsigned char ptr[]。也就是数且的前面w*h个字节,存储的是Y数据。后面紧接着从ptr[w*h - 1]开始,一共存储了w/2 * h/2个V字符。再从ptr[w*h + w/2 * h/2 -1]开始,存储剩下的w/2 * h/2个U字符。 以width=4, height=8为例,总大小为4*8*1.5=32*1.5=48个字节。 ptr[0]~ptr[31]存储的是Y数据, ptr[32]~ptr[39]存储的是V数据,ptr[40]~ptr[47]存储的是U数据。好了,接下来上画yv12的代码:

inline void DisplayClient::fill_yv12( int x,int y, unsigned char* camera_ptr,int cameraWidth,int cameraHeight,unsigned char* pic_ptr, int picWidth,int picHeight )
{
    int     offset_pixel = 0;
    int index = 0;
    for ( int j = 0; j < picHeight; j++ )
    {
        for ( int h = 0; h < picWidth; h++ )
        {
            offset_pixel     = y * cameraWidth + x + j * cameraWidth + h;
            index  = j*picWidth+h;
            *(camera_ptr + offset_pixel) = pic_ptr[index];                            
        }
    }
}

       这个fill_yv12函数,每调一次,只单独画Y、U、V这三个分量中的一个。x, y是指从一帧图片的哪个座标开始画, camera_ptr是这一帧图片的起始地址,cameraWidth是一帧的宽度, cameraHeight是帧的高度,pic_ptr是用来替换帧像素的图片,比如对应的“0”、“1”等数据图片的地址, picWidth、picHeight是数字图片的宽高。

        调用fill_yv12的代码如下:

uint8_t* _ptr=(uint8_t *)pCameraImgBuf->getVirAddr();
int half_height = picHieght/2;
int half_width = picWidth/2;
int half_camera_height = mCameraHeight/2;
int half_camera_widht = mCameraWidth/2;
int half_x = x/2;
int half_y = y/2;
int pic_start_pos = y * mCameraWidth + x;
uint8_t* v_start_ptr = _ptr+(mCameraWidth*mCameraHeight);
uint8_t* u_start_ptr = v_start_ptr + mCameraWidth/2 * mCameraHeight/2;
unsigned char* pic_v_ptr = prefix+(picWidth*picHieght);
unsigned char* pic_u_ptr = pic_v_ptr + half_width*half_height;                        
//画Y
fill_prefix_yv12(x, y, _ptr, mCameraWidth, mCameraHeight, prefix, picWidth, picHieght);
//画v
fill_prefix_yv12(half_x, half_y, v_start_ptr, half_camera_widht, half_camera_height, pic_u_ptr, half_width, half_height);
//画U
fill_prefix_yv12(half_x, half_y, u_start_ptr, half_camera_widht, half_camera_height, pic_v_ptr, half_width, half_height);                   

        为了让大家有个更直观的理解,再上一个从yuv字符数组里取uv分量的函数:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 获取 UV 分量
typedef unsigned char UCHAR, BYTE, *PUCHAR, *PBYTE;
VOID CRawImage::GetUV(PBYTE pbX, PBYTE *ppbU, PBYTE *ppbV)
{
	_Assert(ppbU && ppbV);

	if (m_csColorSpace == CS_YV12)
	{
		*ppbV = pbX + m_uWidth * m_uHeight;
		*ppbU = *ppbV + m_uWidth/2 * m_uHeight / 2;
	}
}

        总之一句话,画yuv字符时,先画y的值,然后盏V、U的值。 画V、U的值的时候,对应的x、y坐标,以及宽高都为y的一半。

        好了,上面入是画yv12的代码。 下面再说一下画yuv422的的方法,准确来说,是YUYV,它是Y1U0, Y2V0, Y3U1, Y4U1这样yuv交替存储的, 相邻的两个Y共用其相邻的两个U、V。对应的还有yuv422p,YUV422P也属于YUV422的一种,它是一种Plane模式,即平面模式,并不是将YUV数据交错存储,而是先存放所有的Y分量,然后存储所有的U(Cb)分量,最后存储所有的V(Cr)分量,YUV422占用内存空间 = w * h * 2。

         有了上面这些概念,再来上画yuv422,也即yuyv的代码:

inline void fill_yuv422(uint8_t* camera_ptr, unsigned char* pic_ptr, int y, int x, int bitsPerPixe)
{
    int index = 0;
    for ( int j = 0; j < mWord_height; j++ )
    {
        for ( int h = 0; h < mPrefixWidth; h++ )
        {
            const int     offset_pixel     = y * mCameraWidth + x + j * mCameraWidth + h;
            const int     offset_adr    = (int) (offset_pixel * bitsPerPixe);
            index = j*mPrefixWidth*2+h*2;
            *(camera_ptr + offset_adr) = pic_ptr[index];
            if(index+3 >= mWord_height*mPrefixWidth*2)
            {
                //如果颜色显示正常,就用下面这条代码
                *(camera_ptr + offset_adr + 1) = pic_ptr[index+1];
            }
            else
            {
                //如果颜色反了,则可以用下的代码,将u和v分量的位置换一下。
                *(camera_ptr + offset_adr + 1) = pic_ptr[index+3];
            }                               
        }
    }
}

       fill_yuv422的参数camera_ptr,是指帧图片的地址,  pic_ptr是数字图片的地址, y, x是要画的数字图片的座标, bitsPerPixe是指每一个像素点占几个字节。当为yuv422时,每一个像素点占两个字节。 *(camera_ptr + offset_adr) = pic_ptr[index];这一行是画Y数据。 下面的是画U和V

       上面的图片引用到了https://blog.csdn.net/tq384998430/article/details/70227199https://blog.csdn.net/laikaikai/article/details/89377306这两篇博客里的资源。

猜你喜欢

转载自blog.csdn.net/xuhui_7810/article/details/102969639