使用libyuv對YUV數據進行縮放,旋轉,鏡像,裁剪等操作

1.背景

??在Android做過自定義Camera的朋友應該都知道,我們可以通過public void onPreviewFrame(byte[] data, Camera camera)回調中獲取攝像頭采集到的每一幀的數據,但是這個byte[] data的數據格式YUV的,并不能直接給我們進行使用,那么該通過什么樣的方法對這個YUV數據進行處理呢?

2.YUV數據格式介紹

??首先我們來了解什么是YUV數據,當然這方面的文章有很多,在這里我就不詳細的介紹了,大家可以看下這篇文章 : 圖文詳解YUV420數據格式
,在這里我們主要用到的是YUV數據格式是NV21(yuv420sp)和I420(yuv420p),它們都是 4:2:0的格式,唯一的區別就是它們的YUV數據排列不一樣,NV21的排列是YYYYYYYY VUVU =>YUV420SP,而I420的排列是YYYYYYYY UU VV =>YUV420P。
??其實我們知道的NV21和I420的數據格式和數據的排列,我們就可以根據排列方式對其進行一些操作,比如在之前的文章分享幾個Android攝像頭采集的YUV數據旋轉與鏡像翻轉的方法介紹的旋轉鏡像的操作。但是它的效率并不是很高,如果只是簡單的操作單一的YUV數據,那么倒沒有太大影響。但是如果要運用于直播推流的話,要保證推流視頻的幀率,那么對YUV數據處理的耗時就相當的重要。

2.Libyuv庫的介紹

??其實對于YUV數據的處理,Google已經開源了一個叫做libyuv的庫專門用于YUV數據的處理。

2.1 什么是libyuv

??libyuv是Google開源的實現各種YUV與RGB之間相互轉換、旋轉、縮放的庫。它是跨平臺的,可在Windows、Linux、Mac、Android等操作系統,x86、x64、arm架構上進行編譯運行,支持SSE、AVX、NEON等SIMD指令加速。

2.2 Android上如何使用Libyuv

??libyuv并不能直接為Android開發直接進行使用,需要對它進行編譯的操作。在這里介紹的是使用Android Studio的Cmake的方式進行libyuv的編譯操作,首先從官方網站Libyuv上下載libyuv庫,下載的目錄結構如下

libyuv.png

??如果無法下載的話,也可以從我文章最后的demo中去進行拷貝。新鍵Android項目,并且創建的時候勾選項include C++ Support,也就是改android項目支持C,C++的編譯,如果對于Android Stuido如何支持C,C++編譯不清楚的,請自行百度谷歌,這里就不多細說。項目創建之后將下載的libyuv庫直接拷貝到src/main/cpp目錄下
libyuv.png

??修改CMakeLists.txt文件,并在src/main/cpp下創建YuvJni.cpp文件,CMakeLists.txt修改如下

cmake_minimum_required(VERSION 3.4.1)
include_directories(src/main/cpp/libyuv/include)
add_subdirectory(src/main/cpp/libyuv ./build)
aux_source_directory(src/main/cpp SRC_FILE)
add_library(yuvutil SHARED ${SRC_FILE})
find_library(log-lib log)
target_link_libraries(yuvutil ${log-lib} yuv)

??創建文件YuvUtil.java,在這里我添加了三個方法進行yuv數據的操作

public class YuvUtil {

    static {
        System.loadLibrary("yuvutil");
    }

    /**
     * YUV數據的基本的處理
     *
     * @param src        原始數據
     * @param width      原始的寬
     * @param height     原始的高
     * @param dst        輸出數據
     * @param dst_width  輸出的寬
     * @param dst_height 輸出的高
     * @param mode       壓縮模式。這里為0,1,2,3 速度由快到慢,質量由低到高,一般用0就好了,因為0的速度最快
     * @param degree     旋轉的角度,90,180和270三種
     * @param isMirror   是否鏡像,一般只有270的時候才需要鏡像
     **/
    public static native void compressYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int mode, int degree, boolean isMirror);

    /**
     * yuv數據的裁剪操作
     *
     * @param src        原始數據
     * @param width      原始的寬
     * @param height     原始的高
     * @param dst        輸出數據
     * @param dst_width  輸出的寬
     * @param dst_height 輸出的高
     * @param left       裁剪的x的開始位置,必須為偶數,否則顯示會有問題
     * @param top        裁剪的y的開始位置,必須為偶數,否則顯示會有問題
     **/
    public static native void cropYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int left, int top);

    /**
     * 將I420轉化為NV21
     *
     * @param i420Src 原始I420數據
     * @param nv21Src 轉化后的NV21數據
     * @param width   輸出的寬
     * @param width   輸出的高
     **/
    public static native void yuvI420ToNV21(byte[] i420Src, byte[] nv21Src, int width, int height);
}

??同時在前面創建的YuvJni.cpp文件中添加對應的方法

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_compressYUV(JNIEnv *env, jclass type,
                                         jbyteArray src_, jint width,
                                         jint height, jbyteArray dst_,
                                         jint dst_width, jint dst_height,
                                         jint mode, jint degree,
                                         jboolean isMirror) {
}

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_cropYUV(JNIEnv *env, jclass type, jbyteArray src_, jint width,
                                     jint height, jbyteArray dst_, jint dst_width, jint dst_height,
                                     jint left, jint top) {
}

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvI420ToNV21(JNIEnv *env, jclass type, jbyteArray i420Src,
                                           jbyteArray nv21Src,
                                           jint width, jint height) {

}

3.使用Libyuv庫進行YUV數據的操作

??接下來就是要libyuv對yuv數據進行縮放,旋轉,鏡像,裁剪等操作。在libyuv的實際使用過程中,更多的是用于直播推流前對Camera采集到的YUV數據進行處理的操作。對如今,Camera的預覽一般采用的是1080p,并且攝像頭采集到的數據是旋轉之后的,一般來說后置攝像頭旋轉了90度,前置攝像頭旋轉了270度并且水平鏡像。在下面的例子中,就對Camera返回的yuv數據進行相關的處理操作。

3.1 NV21轉化為I420

??對于如何獲取Camera返回的YUV數據,不是本篇文章的重點,不了解的請自行百度谷歌。因為Camera返回的YUV數據只能是NV21和YV12兩種,而libyuv的縮放旋轉鏡像的操作需要的是I420的數據格式,那么第一步就是將NV21(例子中Camera返回數據格式設置的是NV21)轉化為I420了。方法如下:

#include "libyuv.h"
void nv21ToI420(jbyte *src_nv21_data, jint width, jint height, jbyte *src_i420_data) {
    jint src_y_size = width * height;
    jint src_u_size = (width >> 1) * (height >> 1);

    jbyte *src_nv21_y_data = src_nv21_data;
    jbyte *src_nv21_vu_data = src_nv21_data + src_y_size;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;


    libyuv::NV21ToI420((const uint8 *) src_nv21_y_data, width,
                       (const uint8 *) src_nv21_vu_data, width,
                       (uint8 *) src_i420_y_data, width,
                       (uint8 *) src_i420_u_data, width >> 1,
                       (uint8 *) src_i420_v_data, width >> 1,
                       width, height);
}

??首先我們必須得先導入libyuv(#include "libyuv.h"),在這里我們用到的是libyuv::NV21ToI420方法,我們來看下它傳參

// Convert NV21 to I420.  Same as NV12 but u and v pointers swapped.
LIBYUV_API
int NV21ToI420(const uint8* src_y,
               int src_stride_y,
               const uint8* src_vu,
               int src_stride_vu,
               uint8* dst_y,
               int dst_stride_y,
               uint8* dst_u,
               int dst_stride_u,
               uint8* dst_v,
               int dst_stride_v,
               int width,
               int height) {
  return X420ToI420(src_y, src_stride_y, src_stride_y, src_vu, src_stride_vu,
                    dst_y, dst_stride_y, dst_v, dst_stride_v, dst_u,
                    dst_stride_u, width, height);
}

??首先第一個參數src_y指的是NV21數據中的Y的數據,我們知道NV21的數據格式是YYYYYYYY VUVU,同時NV21的數據大小是widthheight3/2,可以知道Y的數據大小是widthheight,而V和U均為widthheight/4。第二個參數src_stride_y表示的是Y的數組行間距,在這里很容易知道是width。以此類推src_vu和src_stride_vu也可以相對應的知道了。對于后面的參數dst_y,dst_stride_y,dst_u,dst_stride_u,dst_v ,dst_stride_v表示分別表示的是輸出的I420數據的YUV三個分量的數據,最后的width和height也就是我們設置的Camera的預覽的width和height了。

3.2 I420數據的縮放和旋轉

??經過上面的NV21轉化為I420操作之后,我們就可以對I420數據進行后續的縮放和旋轉的操作,它們的傳參跟上面的NV21ToI420是類似的,這里就不具體的介紹了。縮放的方法

void scaleI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint dst_width,
               jint dst_height, jint mode) {

    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);
    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jint dst_i420_y_size = dst_width * dst_height;
    jint dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);
    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;

    libyuv::I420Scale((const uint8 *) src_i420_y_data, width,
                      (const uint8 *) src_i420_u_data, width >> 1,
                      (const uint8 *) src_i420_v_data, width >> 1,
                      width, height,
                      (uint8 *) dst_i420_y_data, dst_width,
                      (uint8 *) dst_i420_u_data, dst_width >> 1,
                      (uint8 *) dst_i420_v_data, dst_width >> 1,
                      dst_width, dst_height,
                      (libyuv::FilterMode) mode);
}

??值得注意的是,這邊有一個縮放的模式選擇 (libyuv::FilterMode),它的值分別有0,1,2,3四種,代表不同的縮放模式,在我實際的使用過程中,0的縮放速度是最快的,且遠遠快與其他的3種,并且就縮放的效果來看,以我的肉眼觀察,看不出有什么區別,這里為了保證速度,一般用FilterMode.kFilterNone就好了

typedef enum FilterMode {
  kFilterNone = 0,      // Point sample; Fastest.
  kFilterLinear = 1,    // Filter horizontally only.
  kFilterBilinear = 2,  // Faster than box, but lower quality scaling down.
  kFilterBox = 3        // Highest quality.
} FilterModeEnum;

??旋轉的方法如下,不過在這里要注意的是,因為Camera輸出的數據是需要進行90度或者是270的旋轉,那么要注意的就是旋轉之后width和height也就相反了

void rotateI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint degree) {
    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + src_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + src_i420_y_size + src_i420_u_size;

    if (degree == libyuv::kRotate90 || degree == libyuv::kRotate270) {
        libyuv::I420Rotate((const uint8 *) src_i420_y_data, width,
                           (const uint8 *) src_i420_u_data, width >> 1,
                           (const uint8 *) src_i420_v_data, width >> 1,
                           (uint8 *) dst_i420_y_data, height,
                           (uint8 *) dst_i420_u_data, height >> 1,
                           (uint8 *) dst_i420_v_data, height >> 1,
                           width, height,
                           (libyuv::RotationMode) degree);
    }
}

3.3 libyuv其他的一些操作

??libyuv的操作不僅僅是上面的這些,它還有鏡像,裁剪的一些操作,同時還有一些其他數據格式的轉化和對于的操作。包括rgba與yuv數據的轉化等。在文章中,鏡像和裁剪的操作就不加以敘述了,在demo之中我已經加入了進去了。

4.最后

??最近做直播推流,小視頻的錄制中才接觸到的libyuv庫的使用,網上也有一些相關的文章。但是大多不是很詳細,要么文章中的方法使用過程中有各種各樣的問題,要么就是方法不夠全面和具體。這篇文章也主要是做了一些總結。最后貼上demo的Github地址:https://github.com/hzl123456/LibyuvDemo

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容