1、背景
在Android做过自定义Camera的朋友应该都知道,我们可以通过public void onPreviewFrame(byte[] data, Camera camera)回调中获取摄像头采集到的每一帧的数据,但是这个byte[] data的数据格式YUV的,并不能直接给我们进行使用,那么该通过什么样的方法对这个YUV数据进行处理呢?
2、YUV数据格式介绍
首先我们来了解什么是YUV数据,当然这方面的文章有很多,在这里我就不详细的介绍了,大家可以看下这篇文章 :
一文读懂 YUV 的采样与格式
在这里我们主要用到的是YUV数据格式是NV21(yuv420sp)和I420(yuv420p),它们都是 4:2:0的格式,唯一的区别就是它们的YUV数据排列不一样,
NV21的排列是YYYYYYYY VUVU =>YUV420SP,
而I420的排列是YYYYYYYY UU VV =>YUV420P。
其实我们知道的NV21和I420的数据格式和数据的排列,我们就可以根据排列方式对其进行一些操作,但是它的效率并不是很高,如果只是简单的操作单一的YUV数据,那么倒没有太大影响。但是如果要运用于直播推流的话,要保证推流视频的帧率,那么对YUV数据处理的耗时就相当的重要。
Libyuv库的介绍
其实对于YUV数据的处理,Google已经开源了一个叫做libyuv的库专门用于YUV数据的处理。
什么是libyuv
libyuv是Google开源的实现各种YUV与RGB之间相互转换、旋转、缩放的库。它是跨平台的,可在Windows、Linux、Mac、Android等操作系统,x86、x64、arm架构上进行编译运行,支持SSE、AVX、NEON等SIMD指令加速。
Android上如何使用Libyuv
libyuv并不能直接为Android开发直接进行使用,需要对它进行编译的操作。在这里介绍的是使用Android Studio的Cmake的方式进行libyuv的编译操作,首先从官方网站Libyuv上下载libyuv库,下载的目录结构如下
如果无法下载的话,也可以从我文章最后的demo中去进行拷贝。新键Android项目,并且创建的时候勾选项include C++ Support,也就是改android项目支持C,C++的编译,如果对于Android Stuido如何支持C,C++编译不清楚的,请自行百度谷歌,这里就不多细说。
项目创建之后将下载的libyuv库直接拷贝到src/main/cpp目录下
修改CMakeLists.txt文件,并在src/main/cpp下创建YuvJni.cpp文件,CMakeLists.txt修改如下
cmake_minimum_required(VERSION3.4.1)include_directories(src/main/cpp/libyuv/include)add_subdirectory(src/main/cpp/libyuv./build)aux_source_directory(src/main/cppSRC_FILE)add_library(yuvutilSHARED${SRC_FILE})find_library(log-liblog)target_link_libraries(yuvutil${log-lib}yuv)
创建文件YuvUtil.java,在这里我添加了三个方法进行yuv数据的操作
publicclassYuvUtil{static{System.loadLibrary("yuvutil");}/***YUV数据的基本的处理**@paramsrc原始数据*@paramwidth原始的宽*@paramheight原始的高*@paramdst输出数据*@paramdst_width输出的宽*@paramdst_height输出的高*@param mode 压缩模式。这里为0,1,2,3 速度由快到慢,质量由低到高,一般用0就好了,因为0的速度最快*@paramdegree旋转的角度,90,180和270三种*@paramisMirror是否镜像,一般只有270的时候才需要镜像**/publicstaticnativevoidcompressYUV(byte[]src,intwidth,intheight,byte[]dst,intdst_width,intdst_height,intmode,intdegree,booleanisMirror);/***yuv数据的裁剪操作**@paramsrc原始数据*@paramwidth原始的宽*@paramheight原始的高*@paramdst输出数据*@paramdst_width输出的宽*@paramdst_height输出的高*@paramleft裁剪的x的开始位置,必须为偶数,否则显示会有问题*@paramtop裁剪的y的开始位置,必须为偶数,否则显示会有问题**/publicstaticnativevoidcropYUV(byte[]src,intwidth,intheight,byte[]dst,intdst_width,intdst_height,intleft,inttop);/***将I420转化为NV21**@parami420Src原始I420数据*@paramnv21Src转化后的NV21数据*@paramwidth输出的宽*@paramwidth输出的高**/publicstaticnativevoidyuvI420ToNV21(byte[]i420Src,byte[]nv21Src,intwidth,intheight);}
同时在前面创建的YuvJni.cpp文件中添加对应的方法
extern"C"JNIEXPORTvoidJNICALLJava_com_libyuv_util_YuvUtil_compressYUV(JNIEnv*env,jclasstype,jbyteArraysrc_,jintwidth,jintheight,jbyteArraydst_,jintdst_width,jintdst_height,jintmode,jintdegree,jbooleanisMirror){}extern"C"JNIEXPORTvoidJNICALLJava_com_libyuv_util_YuvUtil_cropYUV(JNIEnv*env,jclasstype,jbyteArraysrc_,jintwidth,jintheight,jbyteArraydst_,jintdst_width,jintdst_height,jintleft,jinttop){}extern"C"JNIEXPORTvoidJNICALLJava_com_libyuv_util_YuvUtil_yuvI420ToNV21(JNIEnv*env,jclasstype,jbyteArrayi420Src,jbyteArraynv21Src,jintwidth,jintheight){}
3、使用Libyuv库进行YUV数据的操作
接下来就是要libyuv对yuv数据进行缩放,旋转,镜像,裁剪等操作。
在libyuv的实际使用过程中,更多的是用于直播推流前对Camera采集到的YUV数据进行处理的操作。
对如今,Camera的预览一般采用的是1080p,并且摄像头采集到的数据是旋转之后的,一般来说后置摄像头旋转了90度,前置摄像头旋转了270度并且水平镜像。在下面的例子中,就对Camera返回的yuv数据进行相关的处理操作。
NV21转化为I420
对于如何获取Camera返回的YUV数据,不是本篇文章的重点,不了解的请自行百度谷歌。
因为Camera返回的YUV数据只能是NV21和YV12两种,而libyuv的缩放旋转镜像的操作需要的是I420的数据格式,那么第一步就是将NV21(例子中Camera返回数据格式设置的是NV21)转化为I420了。
方法如下:
#include"libyuv.h"voidnv21ToI420(jbyte*src_nv21_data,jintwidth,jintheight,jbyte*src_i420_data){jintsrc_y_size=width*height;jintsrc_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((constuint8*)src_nv21_y_data,width,(constuint8*)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方法,我们来看下它传参
//ConvertNV21toI420.SameasNV12butuandvpointersswapped.LIBYUV_APIintNV21ToI420(constuint8*src_y,intsrc_stride_y,constuint8*src_vu,intsrc_stride_vu,uint8*dst_y,intdst_stride_y,uint8*dst_u,intdst_stride_u,uint8*dst_v,intdst_stride_v,intwidth,intheight){returnX420ToI420(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是类似的,这里就不具体的介绍了。
缩放的方法
voidscaleI420(jbyte*src_i420_data,jintwidth,jintheight,jbyte*dst_i420_data,jintdst_width,jintdst_height,jintmode){jintsrc_i420_y_size=width*height;jintsrc_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;jintdst_i420_y_size=dst_width*dst_height;jintdst_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((constuint8*)src_i420_y_data,width,(constuint8*)src_i420_u_data,width>>1,(constuint8*)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就好了。
typedefenumFilterMode{kFilterNone=0,//Pointsample;Fastest.kFilterLinear=1,//Filterhorizontallyonly.kFilterBilinear=2,//Fasterthanbox,butlowerqualityscalingdown.kFilterBox=3//Highestquality.}FilterModeEnum;
旋转的方法如下,不过在这里要注意的是,因为Camera输出的数据是需要进行90度或者是270的旋转,那么要注意的就是旋转之后width和height也就相反了。
voidrotateI420(jbyte*src_i420_data,jintwidth,jintheight,jbyte*dst_i420_data,jintdegree){jintsrc_i420_y_size=width*height;jintsrc_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((constuint8*)src_i420_y_data,width,(constuint8*)src_i420_u_data,width>>1,(constuint8*)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);}}
libyuv其他的一些操作
libyuv的操作不仅仅是上面的这些,它还有镜像,裁剪的一些操作,同时还有一些其他数据格式的转化和对于的操作。包括rgba与yuv数据的转化等。在文章中,镜像和裁剪的操作就不加以叙述了,在demo之中我已经加入了进去了。
最后
最近做直播推流,小视频的录制中才接触到的libyuv库的使用,网上也有一些相关的文章。
但是大多不是很详细,要么文章中的方法使用过程中有各种各样的问题,要么就是方法不够全面和具体。这篇文章也主要是做了一些总结。
最后贴上demo的Github地址:/hzl123456/LibyuvDemo
作者:这真不是玩笑
链接:/p/bd0feaf4c0f9
技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。
推荐阅读:
音视频面试基础题
OpenGL ES 学习资源分享
一文读懂 YUV 的采样与格式
OpenGL 之 GPUImage 源码分析
推荐几个堪称教科书级别的 Android 音视频入门项目
觉得不错,点个在看呗~