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 像素排列

行 \ 列 1 2 3 4
Y 第一行 Y Y Y Y Y Y Y Y
Y 第二行 Y Y Y Y Y Y Y Y
Y 第三行 Y Y Y Y Y Y Y Y
Y 第三行 Y Y Y Y Y Y Y Y
V第一行 V0 V1 V2 V3
U第一行 U0 U1 U2 U3
V第二行 V4 V5 V6 V7
U第二行 U4 U5 U6 U7

瞭解了 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的圖像,對分析當前視頻幀的結構和數據類型還是挺有幫助的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章