首页 文章详情

浅析 YUV 颜色空间

字节流动 | 332 2022-01-13 02:44 0 0 0
UniSMS (合一短信)

1. YUV存储格式与采样

1.1 YUV存储格式

YUV是一种亮度信号Y和色度信号U、V是分离的色彩空间,它主要用于优化彩色视频信号的传输,使其向后相容老式黑白电视。

其中“Y”表示明亮度(Luminance或Luma),也就是灰阶值;而“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。 

YUV格式分为两种类型:Packed类型和Planar类型。

  • Packed类型:是将YUV分量存在在同一个数组中,每个像素点的Y、U、V是连续交错存储的;

  • Planar类型:是将YUV分量分别存放到三个独立的数组中,且先连续存储所有像素点的Y,紧接着存储所有像素点的U,最后是所有像素点的V。

1.2 YUV采样方式

YUV码流的存储格式与采样方式密切相关,目前主流的采样方式有如下三种:YUV444、YUV422、YUV420。

其中,YUV444采样是每一个Y对应一组UV分量,每个像素(YUV)占32Bits;YUV422采样是每两个Y共用一组UV分量,每个像素占16bits(Y占8bits、UV分量占8bits);YUV420采样是每四个Y共用一组UV分量,每个像素(YUV)占16bits或者12bits。

三种采样格式表示如下图:

2. 常见YUV420颜色格式内存分析

2.1 摄像头设备输出的颜色格式

考虑到摄像头设备种类很多,比如手机摄像头、USB摄像头、WIFI摄像头等,输出图像涉及到的YUV颜色格式也有所区别,因此这里我就选择开发中时常接触的手机摄像头来讲解。

关于手机自带Camera采集图像的YUV颜色格式,可以通过Camera.Parameters.getSupportedPreviewFormats ()方法获取Camera支持的颜色格式。API介绍如下:

从getSupportedPreviewFormats ()方法可知,手机自带Camera主要支持NV21和YV12两种YUV颜色格式,其中,YV12引入于API12。下面我们着重分析下这两种格式的特征以及内存存储特点:
(1) NV21:YYYYYYYY VUVU
从android/graphics/ImageFormat.NV21中可知,NV21是手机自带Camera默认的预览格式,即采集图像颜色格式为NV21。

NV21是一种半平面(Semi Planner)格式,Y分量占一个平面空间,VU交叉存储占一个平面空间,4个像素的Y分量共用一个VU分量。示意图如下:


在内存中的存储:

(2) YV12:YYYYYYYY VV UU

从android/graphics/ImageFormat.YV12中可知,YV12是手机自带Camera支持的颜色格式。

YV12是一种平面格式(Planner),Y分量、U分量、V分量分别占一个平面空间,4个像素的Y分量共用一个VU分量。

示意图如下: 在内存中的存储:

2.2 编码器需输入的颜色格式

诸如USB摄像头、手机Camera采集的原始图片通常为YUV420格式,根据YUV420采样方式可知,每个像素占3/2个字节(一个像素由Y、U、V分量组成。

其中Y分量占一个字节,U、V分量为Y分量的1/4),那么一张1280x720分辨率的图片大小为1280*720*1.5字节=1382400字节,约为1.3MB。

假设视频传输的帧率为20fps,那么1秒钟需要传输26.36MB,对于目前现有的带宽条件来说,压力是非常大的,因此我们需要对原始图像进行压缩编码以减少原始图像的大小而又能够保留可用信息。

在Android中,提供了MediaCodec这个API实现硬编码,它将调用底层编码器硬件实现对原始YUV图像进行压缩编码,在使用MediaCodec时我们需要编码器支持的颜色格式。

MediaCodecInfo.CodecCapabilities常见的颜色格式如下: MediaCodecInfo.CodecCapabilities中枚举出来的格式比较多,我们就分析下在编解码过程中常用的几种,有COLOR_FormatYUV420Planar、COLOR_FormatYUV420SemiPlanar、COLOR_FormatYUV420PackedPlanar。

(1) COLOR_FormatYUV420Planar:YYYYYYYY UU VV

COLOR_FormatYUV420Planar是一种平面格式(Planner),Y分量、U分量、V分量分别占一个平面空间,4个像素的Y分量共用一个VU分量。示意图如下: 在内存中的存储:

(2) COLOR_FormatYUV420SemiPlanar:YYYYYYYY UVUV

COLOR_FormatYUV420SemiPlanar是一种半平面格式(semi Planner),Y分量占一个平面空间,UV交差存储占一个平面空间,4个像素的Y分量共用一个UV分量。示意图如下:

在内存中的存储:

最后,我们在总结下上述几种常见的YUV420颜色格式之间的关系与区别:NV21、COLOR_FormatYUV420SemiPlanar(简称YUV420SP)均属于半平面格式(semi planner),即Y分量占一个平面,U、V分量共用一个平面,前者V位于U前面(YYYYYYYY VUVU),后者U位于V前面(YYYYYYYY UVUV);

YV12、COLOR_FormatYUV420Planar(简称YUV420P)均属于平面格式(planner),即Y分量、U分量、V分量分别占用一个平面,前者V位于U前面(YYYYYYYY VV UU),后者U位于V前者(YYYYYYYY UU VV)。

注:COLOR_FormatYUV420SemiPlanar还有另一种称谓,即NV12;COLOR_FormatYUV420Planar还有一种称谓,即I420,实际上它就是标准的YUV420格式。

半平面(semi planner)格式:NV21:YYYYYYYY VUVU COLOR_FormatYUV420SemiPlanar:YYYYYYYY UVUV 其中,COLOR_FormatYUV420SemiPlanar、YUV420SP、NV12是同概念

平面(planner)格式:YV12:YYYYYYYY VV UU COLOR_FormatYUV420Planar:YYYYYYYY UU VV(标准YUV格式)
其中,COLOR_FormatYUV420Planar、I420是同概念

3.  YUV颜色格式处理代码实现

有过硬编码开发经验的朋友应该熟悉,当我们在使用Camera获取摄像头采集的数据时,需要为其指定Preview Format,即摄像头设备采集的原始YUV图像颜色格式,通常默认为ImageFormat.NV21,也可以设置为ImageFormat.YV12。

如果我们使用硬编码器(MediaCodec)对摄像头采集的原始图像压缩编码为H.264数据流,也需要指定硬编码器支持哪种YUV颜色格式数据作为输入,常见格式有COLOR_FormatYUV420SemiPlanar、COLOR_FormatYUV420Planar等。

由于摄像头输出的原始图像格式和编码器所需输入图像颜色格式不一致,就会导致编码后的数据出现图像颜色显示不正常,或称花屏现象。

基于此,我们就需要在给编码器“喂”数据之前,将Camera采集的YUV图像转换为编码器所需的颜色格式。

3. 1 NV21与COLOR_FormatYUV420SemiPlanar转换

  • NV21在内存中的存储:


  • COLOR_FormatYUV420SemiPlanar在内存中的存储:


从NV21和COLOR_FormatYUV420SemiPlanar的内存存储结构来看,它们均属于半平面模式,在转换时我们只需要调换下U、V分量的顺序即可。这里提供两种实现方法,即Java层实现、C/C++实现,代码如下:

(1) Java代码实现

// YYYYYYYY VUVU  --> YYYYYYYY UVUV
// 将NV21转换为Yuv420sp
public static byte[] nv21ToYuv420sp(byte[] src, int width, int height) {
int yLength = width * height;
int uLength = yLength / 4;
int vLength = yLength / 4;
int frameSize = yLength + uLength + vLength;
byte[] yuv420sp = new byte[frameSize];
// Y分量
System.arraycopy(src, 0, yuv420sp, 0, yLength);
for (int i = 0; i < yLength/4; i++) {
// U分量
yuv420sp[yLength + 2 * i] = src[yLength + 2*i+1];
// V分量
yuv420sp[yLength + 2*i+1] = src[yLength + 2*i];
}
return yuv420sp;
}

// YYYYYYYY UVUV(yuv420sp)--> YYYYYYYY VUVU(nv21)
// 将YUV420SemiPlanner转换为NV21
public static byte[] yuv420spToNV21(byte[] src, int width, int height) {
int yLength = width * height;
int uLength = yLength / 4;
int vLength = yLength / 4;
int frameSize = yLength + uLength + vLength;
byte[] nv21 = new byte[frameSize];
// Y分量
System.arraycopy(src, 0, nv21, 0, yLength);
for (int i = 0; i < yLength/4; i++) {
// U分量
nv21[yLength + 2*i +1] = src[yLength+2*i];
// V分量
nv21[yLength + 2*i] = src[yLength + 2*i + 1];
}
return nv21;
}

(2) C/C++代码实现

JNIEXPORT jint JNICALL
Java_com_jiangdg_natives_YuvUtils_nativeNV21ToYUV420p(JNIEnv *env, jclass type, jbyteArray jarray_,
jint width, jint height)

{
jbyte *srcData = env->GetByteArrayElements(jarray_, NULL);
jsize srcLen = env->GetArrayLength(jarray_);
int yLength = width * height;
int uLength = yLength / 4;
// 开辟一段临时内存空间
char *c_tmp = (char *)malloc(srcLen);
// 拷贝Y分量
memcpy(c_tmp,srcData,yLength);
int i = 0;
for(i=0; i<yLength/4; i++) {
// U分量
c_tmp[yLength + 2 * i] = srcData[yLength + 2*i+1];
// V分量
c_tmp[yLength + 2*i+1] = srcData[yLength + 2*i];
}
// 将c_tmp的数据覆盖到jarray_
env->SetByteArrayRegion(jarray_,0,srcLen,(jbyte *)c_tmp);
env->ReleaseByteArrayElements(jarray_, srcData, 0);
// 释放临时内存
free(c_tmp);
}

3. 2 NV21与COLOR_FormatYUV420Planar转换

  • NV21在内存中的存储:


  • COLOR_FormatYUV420Planar在内存中的存储:


从NV21和COLOR_FormatYUV420Planar的内存存储结构来看,前者属于半平面模式,后者属于平面模式,在转换时我们只需要抽取U、V分量,并按要求排列。这里提供两种实现方法,即Java层实现、C/C++实现,代码如下:

(1) Java代码实现

// YYYYYYYY UU VV --> YYYYYYYY VUVU
// 将YUV420Planner(I420)转换为NV21
public static byte[] yuv420pToNV21(byte[] src, int width, int height) {
int yLength = width * height;
int uLength = yLength / 4;
int vLength = yLength / 4;
int frameSize = yLength + uLength + vLength;
byte[] nv21 = new byte[frameSize];

System.arraycopy(src, 0, nv21, 0, yLength); // Y分量
for (int i = 0; i < yLength / 4; i++) {
// U分量
nv21[yLength + 2*i + 1] = src[yLength + i];
// V分量
nv21[yLength + 2*i] = src[yLength + uLength + i];
}
return nv21;
}

// YYYYYYYY VUVU ---> YYYYYYYY UU VV
// 将nv21转换为yuv420p(I420)
public static byte[] nv21ToYuv420p(byte[] src, int width, int height) {
int yLength = width * height;
int uLength = yLength / 4;
int vLength = yLength / 4;
int frameSize = yLength + uLength + vLength;
byte[] yuv420p = new byte[frameSize];
// Y分量
System.arraycopy(src, 0, yuv420p, 0, yLength);
for (int i = 0; i < yLength/4; i++) {
// U分量
yuv420p[yLength + i] = src[yLength + 2*i + 1];
// V分量
yuv420p[yLength + uLength + i] = src[yLength + 2*i];
}
return yuv420p;
}

(2) C/C++代码实现

JNIEXPORT jint JNICALL
Java_com_jiangdg_natives_YuvUtils_nativeNV21ToYUV420p(JNIEnv *env, jclass type, jbyteArray jarray_,
jint width, jint height)

{
jbyte *srcData = env->GetByteArrayElements(jarray_, NULL);
jsize srcLen = env->GetArrayLength(jarray_);
int yLength = width * height;
int uLength = yLength / 4;
// 开辟一段临时内存空间
char *c_tmp = (char *)malloc(srcLen);
// 拷贝Y分量
memcpy(c_tmp,srcData,yLength);
int i = 0;
for(i=0; i<yLength/4; i++) {
// U分量
c_tmp[yLength + i] = srcData[yLength + 2*i + 1];
// V分量
c_tmp[yLength + uLength + i] = srcData[yLength + 2*i];
}
// 将c_tmp的数据覆盖到jarray_
env->SetByteArrayRegion(jarray_,0,srcLen,(jbyte *)c_tmp);
env->ReleaseByteArrayElements(jarray_, srcData, 0);
// 释放临时内存
free(c_tmp);
}

3.3 NV21与YV12转换

  • NV21在内存中的存储:


  • YV12在内存中的存储:


从NV21和YV12的内存存储结构来看,前者属于半平面模式,后者属于平面模式,在转换时我们只需要抽取U、V分量,并按要求排列。这里提供两种实现方法,即Java层实现、C/C++实现,代码如下:


(1) Java代码实现

// YYYYYYYY VV UU --> YYYYYYYY VUVU
// 将YV12转换为NV21
public static byte[] yv12ToNV21(byte[] src, int width, int height) {
int yLength = width * height;
int uLength = yLength / 4;
int vLength = yLength / 4;
int frameSize = yLength + uLength + vLength;
byte[] nv21 = new byte[frameSize];

System.arraycopy(src, 0, nv21, 0, yLength); // Y分量
for (int i = 0; i < yLength / 4; i++) {
// U分量
nv21[yLength + 2*i + 1] = src[yLength + vLength + i];
// V分量
nv21[yLength + 2*i] = src[yLength + i];
}
return nv21;
}

(2) C/C++代码实现

JNIEXPORT jint JNICALL
Java_com_jiangdg_natives_YuvUtils_nativeYV12ToNV21
(JNIEnv *env, jclass type, jbyteArray jarray_,jint width, jint height)
{
jbyte *srcData = env->GetByteArrayElements(jarray_, NULL);
jsize srcLen = env->GetArrayLength(jarray_);
int yLength = width * height;
int vLength = yLength / 4;
// 开辟一段临时内存空间
char *c_tmp = (char *)malloc(srcLen);
// YYYYYYYY VV UU --> YYYYYYYY VUVU
// 拷贝Y分量
memcpy(c_tmp,srcData,yLength);
int i = 0;
for(i=0; i<yLength/4; i++) {
// U分量
c_tmp[yLength + 2*i + 1] = srcData[yLength + vLength + i];
// V分量
c_tmp[yLength + 2*i] = srcData[yLength + i];
}
// 将c_tmp的数据覆盖到jarray_
env->SetByteArrayRegion(jarray_,0,srcLen,(jbyte *)c_tmp);
env->ReleaseByteArrayElements(jarray_, srcData, 0);
// 释放临时内存
free(c_tmp);
}

注:迟点给出Android工程示例。

来源:https://juejin.cn/post/7032087760835969054



-- END --


进技术交流群,扫码添加我的微信:Byte-Flow



获取视频教程和源码



推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

一文掌握 YUV 图像的基本处理

带你走进程序员眼中的“像素”世界,深入理解图显系统中的 RGB 与 YUV

YUV 格式详解 - 史上最全

10bit YUV(P010)的存储结构和处理

10bit YUV 数据在内存中的存储格式

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter