使用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.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 {
<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 >> 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((<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 = 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;

<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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值