Android libyuv应用系列(一)Android常用的几种格式:NV21/NV12/YV12/YUV420P的区别

项目中接触到图像处理这部分,需求是将手机摄像头采集的原始帧进行 Rotate (旋转)、Scale(拉伸)和 format convert(格式转换),无奈对此的了解甚少于是网上查阅资料恶补了一顿,完事后将最近所学总结一下以方便之后的人别踩太多。

首先想要了解YUV为何物,请猛戳:Video Rendering with 8-Bit YUV Formats 链接中微软已经写的很详细了,国内大部分文章都是翻译这篇文章的,如果还有疑问的同学可以参考下面这些大神的博客:

从上面的文章中应该都会对YUV有所了解和认识了。需要注意的是,在 Android SDK <= 20 Android5.0 LOLLIPOP 版本中 Google 支持的 Camera Preview Callback 的YUV常用格式有两种:

先贴一段微软的叙述:

4:2:0 Formats, 12 Bits per Pixel

Four 4:2:0 12-bpp formats are recommended, with the following FOURCC codes:

  • IMC2

  • IMC4

  • YV12

  • NV12

In all of these formats, the chroma channels are subsampled by a factor of two in both the horizontal and vertical dimensions.

YV12

All of the Y samples appear first in memory as an array of unsigned char values. This array is followed immediately by all of the V (Cr) samples. The stride of the V plane is half the stride of the Y plane, and the V plane contains half as many lines as the Y plane. The V plane is followed immediately by all of the U (Cb) samples, with the same stride and number of lines as the V plane (Figure 12).

NV12

All of the Y samples are found first in memory as an array of unsigned char values with an even number of lines. The Y plane is followed immediately by an array of unsigned char values that contains packed U (Cb) and V (Cr) samples, as shown in Figure 13. When the combined U-V array is addressed as an array of little-endian WORD values, the LSBs contain the U values, and the MSBs contain the V values. NV12 is the preferred 4:2:0 pixel format for DirectX VA. It is expected to be an intermediate-term requirement for DirectX VA accelerators supporting 4:2:0 video.

从上可知 YV12 和 NV12 所占内存是 12bits / Pixel,每个 Y 就是一个像素点,注意红色加粗的叙述,YUV 值在内存中是按照数组的形式存放的,而由于 YV12 和 NV21 都是属于 Planar 格式,也就是 Y 值和 UV 值是独立采样的:

In a planar format, the Y, U, and V components are stored as three separate planes.

在 planar 的格式中, Y, U, V 值是单独存储在三个分离的平面中的。

既然 Y、U、V 值都是独立的,那就意味着我们可以分别处理相应的值,比如在YV12中,排列方式如下表所示,每4个 Y 共用一对 UV 值,而 U、V 值又是按照横排排列(下面是 YV12 格式中,宽为16,高为4像素的排列)。

YV12 中 16 x 4 像素排列

行 \ 列1234
Y 第一行Y YY YY YY Y
Y 第二行Y YY YY YY Y
Y 第三行Y YY YY YY Y
Y 第三行Y YY YY YY Y
V第一行V0V1V2V3
U第一行U0U1U2U3
V第二行V4V5V6V7
U第二行U4U5U6U7

了解了 YUV 值的结构我们就可以任性的对此图像做 Rotate,scale等等。这里我以480*270 (16:9)的一张原始帧图像举例,贴出部分代码示例:
CameraPreviewFrame.java:

/**
* 获取preview的原始帧 
* 这里有个前提,因为Android camera preview默认格式为NV21的,所以需要
* 调用setPreviewFormat()方法设置为我们需要的格式
*/
@Override
public void onPreviewFrame(byte[] data, Camera camera) {// 假设这里的data为480x270原始帧
        String SRC_FRAME_WIDTH = 480;
        String SRC_FRAME_HEIGHT = 270;
        String DES_FRAME_WIDTH = 480;
        String DES_FRAME_HEIGHT = 270;
        // 此处将data数组保存在了指定的路径,保存类型为jpeg格式,但是普通的图片浏
        // 览器是无法打开的,需要使用RawViewer等专业的工具打开。
        // 定义与原始帧大小一样的outputData,因为YUV420所占内存是12Bits/Pixel,
        // 每个Y为一个像素8bit=1Byte,U=2bit=1/4(Byte),V= 2bit =1/4(Byte),
        // Y值数量为480*270,则U=V=480*270*(1/4)
        byte[] outputData = new byte[DES_FRAME_WIDTH * DES_FRAME_HEIGHT * 3 / 2]; 
        // call the JNI method to rotate frame data clockwise 90 degrees
        YuvUtil.DealYV12(data, outputData, SRC_FRAME_WIDTH, SRC_FRAME_HEIGHT, 90);
        saveImageData(outputData);
    }
}

    	// save image to sdcard path: Pictures/MyTestImage/
public void saveImageData(byte[] imageData) {
        File imageFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
        if (imageFile == null) {
            return;
        }
        try {
            FileOutputStream fos = new FileOutputStream(imageFile);
            fos.write(imageData);
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            Log.e(TAG, "File not found: " + e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "Error accessing file: " + e.getMessage());
        }
    }

public static File getOutputMediaFile(int type) {
  		File imageFileDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "MyYuvImage");
        if (!imageFileDir.exists()) {
            if (!imageFileDir.mkdirs()) {
                Log.e(TAG, "can't makedir for imagefile");
                return null;
            }
        }
        // Create a media file name
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        File imageFile;
        if (type == MEDIA_TYPE_IMAGE) {
            imageFile = new File(imageFileDir.getPath() + File.separator +
                    "IMG_" + timeStamp + ".jpg");
        } else if (type == MEDIA_TYPE_VIDEO) {
            imageFile = new File(imageFileDir.getPath() + File.separator +
                    "VID_" + timeStamp + ".mp4");
        } else {
            return null;
        }
        return imageFile;
}

上面的代码中可以看到我调用了JNI的方法YuvUtil.RotateYV12()

YuvUtil.java

public class YuvUtil {
    // 初始化,为data分配相应大小的内存
    public static native void initYV12(int length, int scale_length);
    
    public static native void DealYV12(byte[] src_data, byte[] dst_data, int width, int height, int rotation);
}

对应的Jni的C代码如下:
com_example_jni_YuvUtil.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class _Included_com_example_jni_YuvUtil */

#ifndef _Included_com_example_jni_YuvUtil
#define _Included_com_example_jni_YuvUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class:     com_example_jni_YuvUtil
* Method:    initYV12
* Signature: (II)V
    */
   JNIEXPORT void JNICALL Java_com_example_jni_YuvUtil_initYV12
   (JNIEnv *, jclass, jint, jint);

/*
* Class:     com_example_jni_YuvUtil
* Method:    DealYV12
* Signature: ([B[BIIIII)V
    */
   JNIEXPORT void JNICALL Java_com_example_jni_YuvUtil_DealYV12
   (JNIEnv *, jclass, jbyteArray, jbyteArray, jint, jint, jint, jint, jint);


#ifdef __cplusplus
}
#endif
#endif

com_example_jni_YuvUtil.c

#include "com_example_jni_YuvUtil.h"
#include <android/log.h>
#include <string.h>
#include <jni.h>
#include <stdlib.h>

#define TAG "jni-log-jni" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型

char *input_src_data, *output_src_data, *src_y_data,
     *src_u_data, *src_v_data, *dst_y_data, *dst_v_data;
int src_data_width, src_data_height, len_src;

/*
* Class: com_example_jni_YuvUtil
*/
JNIEXPORT void JNICALL Java_com_example_jni_YuvUtil_initYV12
   (JNIEnv *env, jclass jcls, jint length, jint scaleDataLength) {
   len_src = length;
   len_scale = scaleDataLength;
   LOGD("########## len_src  = %d, len_scale = %d \n", len_src, len_scale);

input_src_data = malloc(sizeof(char) * len_src);
LOGD("########## input_src_data  = %d \n", input_src_data);

src_y_data = malloc(sizeof(char) * (len_src * 2 / 3));
src_u_data = malloc(sizeof(char) * (len_src / 6));
src_v_data = malloc(sizeof(char) * (len_src / 6));

dst_y_data = malloc(sizeof(char) * (len_src * 2 / 3));
dst_u_data = malloc(sizeof(char) * (len_src / 6));
dst_v_data = malloc(sizeof(char) * (len_src / 6));

}

JNIEXPORT void JNICALL Java_com_example_jni_YuvUtil_DealYV12
(JNIEnv *env, jclass jcls, jbyteArray src_data,
    jbyteArray dst_data, jint width, jint height, jint rotation, jint dst_width, jint dst_height) {
src_data_width = width;
src_data_height = height;

// 将src_data的数据传给input_src_data
(*env)->GetByteArrayRegion (env, src_data, 0, len_src, (jbyte*)(input_src_data));

/*以下三个memcpy分别将Y、U、V值从src_data中提取出来,将YUV值分别scale或者rotate,则可得到对应格式的图像数据*/
// get y plane
memcpy(src_y_data, input_src_data , (len_src * 2 /3));
// get u plane
memcpy(src_u_data, input_src_data + (len_src * 2 / 3), len_src / 6);
// get v plane
memcpy(src_v_data, input_src_data + (len_src * 5 / 6 ), len_src / 6);
/*获取yuv三个值的数据可以做相应操作*/
// ......... 
// .........
// 例:将Y值置为0,则得到没有灰度的图像;
memset(input_src_data + src_data_width * src_data_height, 0, src_data_width * src_data_height);

// 将input_src_data的数据返回给dst_data输出
// output to the dst_data
(*env)->SetByteArrayRegion (env, dst_data, 0, len_src, (jbyte*)(input_src_data));

}

/**
* free memory
*/
JNIEXPORT void JNICALL Java_com_example_jni_YuvUtil_ReleaseYV12
(JNIEnv *env , jclass jcls) {
free(output_src_data);
free(input_src_data);
}

RawViewer

一个查看YUV原始帧文件的工具,可以根据自定义的宽高、YUV格式显示出当前YUV的图像,对分析当前视频帧的结构和数据类型还是挺有帮助的。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: uyvy是一种视频压缩格式,其中每个像素的颜色信息被压缩为16位,包含了U、Y和V的数据。而nv12和yv12则是uyvy格式的两种常见的解压缩格式。 在uyvy转nv12时,uyvy中的U和V的数据会被分离出来,并交错地存放在nv12中。换句话说,连续两个像素共享一个U和一个V分量。同时,每个像素的亮度被保留在nv12中。nv12具有与uyvy相同的颜色信息,但存储方式不同,这种转换通常用于视频压缩和传输。 而uyvy转yv12时,uyvy中的U和V数据也会被分离,但与nv12不同,yv12的U和V数据在存储时是分开存放的,即分别存储U平面和V平面。同时,每个像素的亮度也被保留在yv12中。yv12与uyvy和nv12相比,存储方式更加灵活,使得处理软件更容易对图像进行操作。 总之,uyvy转nv12时U和V数据被交错存放在每两个像素之间,而uyvy转yv12时U和V数据分别存放在不同的平面上。这种转换可以根据具体需求选择合适的存储格式,提供更高的灵活性和图像处理能力。 ### 回答2: uyvy是一种YUV色彩空间的格式,其中"u"表示色度(chrominance)分量,"y"表示亮度(luminance)分量,"v"表示色度分量。UYVY格式中,每四个像素共用两对UV值,因此每个像素由两个Y值和一个UV值组成。 要将uyvy格式转换成nv12格式,需要先了解两者的存储方式。uyvy格式的像素按照YUVYUVYUV...的顺序存储,而nv12格式的像素按照YYYY...UVUVUV...的顺序存储。因此,转换的过程中需要将uyvy中的UV值依次排列,然后将这些值与对应的Y值一一对应,得到nv12格式的像素。 同样地,要将uyvy格式转换成yv12格式,需要了解两者的存储方式。uyvy格式的像素按照YUVYUVYUV...的顺序存储,而yv12格式的像素按照YYYY...UUUUVVVV...的顺序存储。因此,转换的过程中需要将uyvy中的UV值分离并按照顺序存储,再将这些值与对应的Y值一一对应,得到yv12格式的像素。 总结起来,uyvy转nv12和yv12的过程都需要将UYVY值中的UV分量重新排列,并与对应的Y分量一一对应,得到不同格式的像素。 ### 回答3: UYVY转NV12和YV12是视频色彩格式的转换过程。UYVY是一种16位的色彩格式,其中U、Y、V分别代表亮度(Y)和色度(U、V)的采样值。NV12和YV12是常用YUV色彩格式,也是视频编码和解码中常见的格式。 UYVY转NV12的过程如下:首先,将UYVY中的16位数据拆分成Y、UV两个平面。然后,将UV平面的数据重新采样并转换为NV12格式,其中UV的采样方式是每4个像素共享一组UV采样值,即4:2:0采样。最后,将Y平面和转换后的UV平面合并成NV12格式的图像。 UYVY转YV12的过程类似:同样是将UYVY中的16位数据拆分成Y、UV两个平面。接着,将UV平面的数据重新采样并转换为YV12格式,其中UV的采样方式同样是4:2:0采样。最后,将Y平面和转换后的UV平面合并成YV12格式的图像。 这样,UYVY格式的视频就可以转换为NV12或YV12格式的视频,供后续的处理或存储使用。转换过程中的采样方式和数据组织方式的不同,会影响视频的质量和处理效果。因此,在实际应用中,需要根据具体的需求和设备要求选择合适的色彩格式进行转换和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值