天天看點

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

轉載自 林志河 的簡書文章 使用libyuv對YUV資料進行縮放,旋轉,鏡像,裁剪等操作

  在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對YUV資料進行縮放,旋轉,鏡像,裁剪等操作

libyuv.png

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

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

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 {
                 
<span class="hljs-keyword">static</span> {
    System.loadLibrary(<span class="hljs-string">"yuvutil"</span>);
}

<span class="hljs-comment">/**
 * YUV資料的基本的處理
 *
 * <span class="hljs-doctag">@param</span> src        原始資料
 * <span class="hljs-doctag">@param</span> width      原始的寬
 * <span class="hljs-doctag">@param</span> height     原始的高
 * <span class="hljs-doctag">@param</span> dst        輸出資料
 * <span class="hljs-doctag">@param</span> dst_width  輸出的寬
 * <span class="hljs-doctag">@param</span> dst_height 輸出的高
 * <span class="hljs-doctag">@param</span> mode       壓縮模式。這裡為0,1,2,3 速度由快到慢,品質由低到高,一般用0就好了,因為0的速度最快
 * <span class="hljs-doctag">@param</span> degree     旋轉的角度,90,180和270三種
 * <span class="hljs-doctag">@param</span> isMirror   是否鏡像,一般隻有270的時候才需要鏡像
 **/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">native</span> <span class="hljs-keyword">void</span> <span class="hljs-title">compressYUV</span><span class="hljs-params">(<span class="hljs-keyword">byte</span>[] src, <span class="hljs-keyword">int</span> width, <span class="hljs-keyword">int</span> height, <span class="hljs-keyword">byte</span>[] dst, <span class="hljs-keyword">int</span> dst_width, <span class="hljs-keyword">int</span> dst_height, <span class="hljs-keyword">int</span> mode, <span class="hljs-keyword">int</span> degree, <span class="hljs-keyword">boolean</span> isMirror)</span></span>;

<span class="hljs-comment">/**
 * yuv資料的裁剪操作
 *
 * <span class="hljs-doctag">@param</span> src        原始資料
 * <span class="hljs-doctag">@param</span> width      原始的寬
 * <span class="hljs-doctag">@param</span> height     原始的高
 * <span class="hljs-doctag">@param</span> dst        輸出資料
 * <span class="hljs-doctag">@param</span> dst_width  輸出的寬
 * <span class="hljs-doctag">@param</span> dst_height 輸出的高
 * <span class="hljs-doctag">@param</span> left       裁剪的x的開始位置,必須為偶數,否則顯示會有問題
 * <span class="hljs-doctag">@param</span> top        裁剪的y的開始位置,必須為偶數,否則顯示會有問題
 **/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">native</span> <span class="hljs-keyword">void</span> <span class="hljs-title">cropYUV</span><span class="hljs-params">(<span class="hljs-keyword">byte</span>[] src, <span class="hljs-keyword">int</span> width, <span class="hljs-keyword">int</span> height, <span class="hljs-keyword">byte</span>[] dst, <span class="hljs-keyword">int</span> dst_width, <span class="hljs-keyword">int</span> dst_height, <span class="hljs-keyword">int</span> left, <span class="hljs-keyword">int</span> top)</span></span>;

<span class="hljs-comment">/**
 * 将I420轉化為NV21
 *
 * <span class="hljs-doctag">@param</span> i420Src 原始I420資料
 * <span class="hljs-doctag">@param</span> nv21Src 轉化後的NV21資料
 * <span class="hljs-doctag">@param</span> width   輸出的寬
 * <span class="hljs-doctag">@param</span> width   輸出的高
 **/</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">native</span> <span class="hljs-keyword">void</span> <span class="hljs-title">yuvI420ToNV21</span><span class="hljs-params">(<span class="hljs-keyword">byte</span>[] i420Src, <span class="hljs-keyword">byte</span>[] nv21Src, <span class="hljs-keyword">int</span> width, <span class="hljs-keyword">int</span> height)</span></span>;
                

}

  同時在前面建立的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 >> ) * (height >> );
                 
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((<span class="hljs-keyword">const</span> uint8 *) src_nv21_y_data, width,
                   (<span class="hljs-keyword">const</span> uint8 *) src_nv21_vu_data, width,
                   (uint8 *) src_i420_y_data, width,
                   (uint8 *) src_i420_u_data, width &gt;&gt; <span class="hljs-number">1</span>,
                   (uint8 *) src_i420_v_data, width &gt;&gt; <span class="hljs-number">1</span>,
                   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 &gt;&gt; <span class="hljs-number">1</span>) * (height &gt;&gt; <span class="hljs-number">1</span>);
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 &gt;&gt; <span class="hljs-number">1</span>) * (dst_height &gt;&gt; <span class="hljs-number">1</span>);
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((<span class="hljs-keyword">const</span> uint8 *) src_i420_y_data, width,
                  (<span class="hljs-keyword">const</span> uint8 *) src_i420_u_data, width &gt;&gt; <span class="hljs-number">1</span>,
                  (<span class="hljs-keyword">const</span> uint8 *) src_i420_v_data, width &gt;&gt; <span class="hljs-number">1</span>,
                  width, height,
                  (uint8 *) dst_i420_y_data, dst_width,
                  (uint8 *) dst_i420_u_data, dst_width &gt;&gt; <span class="hljs-number">1</span>,
                  (uint8 *) dst_i420_v_data, dst_width &gt;&gt; <span class="hljs-number">1</span>,
                  dst_width, dst_height,
                  (libyuv::FilterMode) mode);
                

}

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

typedef enum FilterMode {
  kFilterNone = ,      // Point sample; Fastest.
  kFilterLinear = ,    // Filter horizontally only.
  kFilterBilinear = ,  // Faster than box, but lower quality scaling down.
  kFilterBox =         // 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 >> ) * (height >> );
                 
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;

<span class="hljs-keyword">if</span> (degree == libyuv::kRotate90 || degree == libyuv::kRotate270) {
    libyuv::I420Rotate((<span class="hljs-keyword">const</span> uint8 *) src_i420_y_data, width,
                       (<span class="hljs-keyword">const</span> uint8 *) src_i420_u_data, width &gt;&gt; <span class="hljs-number">1</span>,
                       (<span class="hljs-keyword">const</span> uint8 *) src_i420_v_data, width &gt;&gt; <span class="hljs-number">1</span>,
                       (uint8 *) dst_i420_y_data, height,
                       (uint8 *) dst_i420_u_data, height &gt;&gt; <span class="hljs-number">1</span>,
                       (uint8 *) dst_i420_v_data, height &gt;&gt; <span class="hljs-number">1</span>,
                       width, height,
                       (libyuv::RotationMode) degree);
}
                

}

3.3 libyuv其他的一些操作

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

4.最後

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