这段时间,在做一个动态配置录相预览帧上的字符样式以及颜色等等的功能。因为要移植到几个不同的项目上,刚好这几个项目的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