音视频篇 - Android 图像处理技术简介

2,607 阅读14分钟

关于 Android 的音视频,也可以叫做多媒体,分成图像、声音和视频。我们先从最基本的图像入手,图像分成 2D 和 3D,Android 自身也提供了很多 API 来实现图像的功能。对于 Android 的图像内存优化,可以看我之前的这篇文章:Android应用篇 - 最全图片相关的优化。

YUV 简介

1. YUV 简介

YUV 是一种颜色编码方法,常使用在各个视频处理组件中。 YUV 在对照片或视频编码时,考虑到人类的感知能力,允许降低色度的带宽。

YUV 是编译 true-color 颜色空间 (colorspace) 的种类,Y'UV, YUV, YCbCr, YPbPr 等专有名词都可以称为 YUV,彼此有重叠。"Y" 表示明亮度 (Luminance、Luma),"U" 和 "V" 则是色度、浓度 (Chrominance、Chroma)。

彩色图像记录的格式,常见的有 RGB、YUV、CMYK 等。彩色电视最早的构想是使用 RGB 三原色来同时传输,这种设计方式是原来黑白带宽的 3 倍,在当时并不是很好的设计。RGB 诉求于人眼对色彩的感应,YUV 则着重于视觉对于亮度的敏感程度,Y 代表的是亮度,UV 代表的是彩度 (因此黑白电影可省略 UV,相近于 RGB),分别用 Cr 和 Cb 来表示,因此 YUV 的记录通常以Y:UV 的格式呈现。

2. 常用的 YUV 格式

为节省带宽起见,大多数 YUV 格式平均使用的每像素位数都少于 24 位。主要的抽样 (subsample) 格式有 YCbCr4:2:0、YCbCr4:2:2、YCbCr4:1:1 和 YCbCr4:4:4。YUV 的表示法称为 A:B:C 表示法:

  • 4:4:4 表示完全取样。
  • 4:2:2 表示 2:1 的水平取样,垂直完全采样。
  • 4:2:0 表示 2:1 的水平取样,垂直 2:1 采样。
  • 4:1:1 表示 4:1 的水平取样,垂直完全采样。

最常用 Y:UV 记录的比重通常 1:1 或 2:1,DVD-Video 是以 YUV4:2:0 的方式记录,也就是我们俗称的 I420,YUV4:2:0 并不是说只有 U (即 Cb) , V(即 Cr) 一定为 0,而是指 U:V 互相援引,时见时隐,也就是说对于每一个行,只有一个 U 或者 V 分量,如果一行是 4:2:0 的话,下一行就是 4:0:2,再下一行是 4:2:0...以此类推。

至于其他常见的 YUV 格式有 YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420 等。

3. YUV 操作

比如在做直播或者美颜相机的时候,因为需要添加美白,滤镜,AR 贴图等效果。所以不能简单的使用 SufaceView 加 Camera 的方式进行数据的采集,而是需要对 Camera 采集到的 YUV 数据进行相关的处理之后然后再进行推流的操作,YUV 数据的返回接口。

  @Override
  public void onPreviewFrame(byte[] data, Camera camera) {
   
  }

Android 摄像头采集的数据都是有一定的旋转的。一般前置摄像头有 270 度的旋转,后置摄像头有 90 的旋转。所以要对 YUV 数据进行一定旋转操作,同时对于前置摄像头的数据还要进行镜像翻转的操作。网上一般比较多的算法是关于旋转的:

  private byte[] rotateYUVDegree90(byte[] data, int imageWidth, int imageHeight) {
      byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
      // Rotate the Y luma
      int i = 0;
      for (int x = 0; x < imageWidth; x++) {
          for (int y = imageHeight - 1; y >= 0; y--) {
              yuv[i] = data[y * imageWidth + x];
              i++;
          }
      }
      // Rotate the U and V color components
      i = imageWidth * imageHeight * 3 / 2 - 1;
      for (int x = imageWidth - 1; x > 0; x = x - 2) {
          for (int y = 0; y < imageHeight / 2; y++) {
              yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
              i--;
              yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
              i--;
          }
      }
      return yuv;
  }
   private byte[] rotateYUVDegree270(byte[] data, int imageWidth, int imageHeight) {
      byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
      // Rotate the Y luma
      int i = 0;
      for (int x = imageWidth - 1; x >= 0; x--) {
          for (int y = 0; y < imageHeight; y++) {
              yuv[i] = data[y * imageWidth + x];
              i++;
          }
      }// Rotate the U and V color components
      i = imageWidth * imageHeight;
      for (int x = imageWidth - 1; x > 0; x = x - 2) {
          for (int y = 0; y < imageHeight / 2; y++) {
              yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
              i++;
              yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
              i++;
          }
      }
      return yuv;
  }

上述两个算法分别用于 90 度旋转 (后置摄像头) 和 270 度旋转 (前置摄像头),但是对于前置摄像头的 YUV 数据是需要镜像的,参照上面的算法,实现了前置摄像头的镜像算法:

  private byte[] rotateYUVDegree270AndMirror(byte[] data, int imageWidth, int imageHeight) {
      byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
      // Rotate and mirror the Y luma
      int i = 0;
      int maxY = 0;
      for (int x = imageWidth - 1; x >= 0; x--) {
          maxY = imageWidth * (imageHeight - 1) + x * 2;
          for (int y = 0; y < imageHeight; y++) {
              yuv[i] = data[maxY - (y * imageWidth + x)];
              i++;
          }
      }
      // Rotate and mirror the U and V color components
      int uvSize = imageWidth * imageHeight;
      i = uvSize;
      int maxUV = 0;
      for (int x = imageWidth - 1; x > 0; x = x - 2) {
          maxUV = imageWidth * (imageHeight / 2 - 1) + x * 2 + uvSize;
          for (int y = 0; y < imageHeight / 2; y++) {
              yuv[i] = data[maxUV - 2 - (y * imageWidth + x - 1)];
              i++;
              yuv[i] = data[maxUV - (y * imageWidth + x)];
              i++;
          }
      }
      return yuv;
  }

其实对于 YUV 数据的处理,Google 已经开源了一个叫做 libyuv 的库专门用于 YUV 数据的处理。libyuv 并不能直接为 Android 开发直接进行使用,需要对它进行编译的操作。

libyuv 可以对 YUV 数据进行缩放,旋转,镜像,裁剪、转化成 RGBA 等操作。在 libyuv 的实际使用过程中,更多的是用于直播推流前对 Camera采集到的 YUV 数据进行处理的操作。对如今,Camera 的预览一般采用的是 1080p,并且摄像头采集到的数据是旋转之后的,一般来说后置摄像头旋转了 90 度,前置摄像头旋转了 270 度并且水平镜像。github 上有一个 demo: github.com/hzl123456/L…

当然关于 YUV 转换其他格式,可以自己手动实现,也可以使用其他框架的现成方法。

JNI:

  bool YV12ToBGR24_Native(unsigned char* pYUV,unsigned char* pBGR24,int width,int height)
  {
      if (width < 1 || height < 1 || pYUV == NULL || pBGR24 == NULL)
          return false;
      const long len = width * height;
      unsigned char* yData = pYUV;
      unsigned char* vData = &yData[len];
      unsigned char* uData = &vData[len >> 2];
 
      int bgr[3];
      int yIdx,uIdx,vIdx,idx;
      for (int i = 0;i < height;i++){
          for (int j = 0;j < width;j++){
              yIdx = i * width + j;
              vIdx = (i/2) * (width/2) + (j/2);
              uIdx = vIdx;
        
              bgr[0] = (int)(yData[yIdx] + 1.732446 * (uData[vIdx] - 128));                                    // b分量
              bgr[1] = (int)(yData[yIdx] - 0.698001 * (uData[uIdx] - 128) - 0.703125 * (vData[vIdx] - 128));    // g分 
   量
              bgr[2] = (int)(yData[yIdx] + 1.370705 * (vData[uIdx] - 128));                                    // r分量
 
              for (int k = 0;k < 3;k++){
                  idx = (i * width + j) * 3 + k;
                  if(bgr[k] >= 0 && bgr[k] <= 255)
                      pBGR24[idx] = bgr[k];
                  else
                      pBGR24[idx] = (bgr[k] < 0)?0:255;
              }
          }
      }
     return true;
  }

OpenCV:

  bool YV12ToBGR24_OpenCV(unsigned char* pYUV,unsigned char* pBGR24,int width,int height)
  {
      if (width < 1 || height < 1 || pYUV == NULL || pBGR24 == NULL)
          return false;
      Mat dst(height,width,CV_8UC3,pBGR24);
      Mat src(height + height/2,width,CV_8UC1,pYUV);
      cvtColor(src,dst,CV_YUV2BGR_YV12);
      return true;
  }

FFmpeg:

  bool YV12ToBGR24_FFmpeg(unsigned char* pYUV,unsigned char* pBGR24,int width,int height)
  {
      if (width < 1 || height < 1 || pYUV == NULL || pBGR24 == NULL)
          return false;
      //int srcNumBytes,dstNumBytes;
      //uint8_t *pSrc,*pDst;
      AVPicture pFrameYUV,pFrameBGR;
    
      //pFrameYUV = avpicture_alloc();
      //srcNumBytes = avpicture_get_size(PIX_FMT_YUV420P,width,height);
      //pSrc = (uint8_t *)malloc(sizeof(uint8_t) * srcNumBytes);
      avpicture_fill(&pFrameYUV,pYUV,PIX_FMT_YUV420P,width,height);
 
      //U,V互换
      uint8_t * ptmp=pFrameYUV.data[1];
      pFrameYUV.data[1]=pFrameYUV.data[2];
      pFrameYUV.data [2]=ptmp;
 
      //pFrameBGR = avcodec_alloc_frame();
      //dstNumBytes = avpicture_get_size(PIX_FMT_BGR24,width,height);
      //pDst = (uint8_t *)malloc(sizeof(uint8_t) * dstNumBytes);
      avpicture_fill(&pFrameBGR,pBGR24,PIX_FMT_BGR24,width,height);
 
      struct SwsContext* imgCtx = NULL;
      imgCtx = 
  sws_getContext(width,height,PIX_FMT_YUV420P,width,height,PIX_FMT_BGR24,SWS_BILINEAR,0,0,0);
 
      if (imgCtx != NULL){
        
  sws_scale(imgCtx,pFrameYUV.data,pFrameYUV.linesize,0,height,pFrameBGR.data,pFrameBGR.linesize);
          if(imgCtx){
              sws_freeContext(imgCtx);
              imgCtx = NULL;
          }
          return true;
      }
      else{
          sws_freeContext(imgCtx);
          imgCtx = NULL;
          return false;
      }
  }

4. YuvImage

Android YuvImage 包含四元组的 YUV 数据,contains YUV data and provides a method that compresses a region of the YUV data to a Jpeg,提供了一个向 jpeg 格式压缩的方法。

  public static @Nullable byte[] convertNv21ToJpeg(byte[] nv21, int w, int h, Rect rect){
      if(nv21 == null) return null;
      ByteArrayOutputStream outputSteam = new ByteArrayOutputStream();
      YuvImage image = new YuvImage(nv21, ImageFormat.NV21, w, h, null);
      image.compressToJpeg(rect, 70, outputSteam);
      return outputSteam.toByteArray();
  }

但是 YuvImage.compressToJpeg 存在 native 级别的内存泄漏:blog.csdn.net/q979713444/…

Camera、Camera2 的简介

在 Google 推出 Android 5.0 的时候,Android Camera API 版本升级到了 API2 (android.hardware.camera2),之前使用的API1 (android.hardware.camera) 就被标为 Deprecated 了。Camera API2 相较于 API1 有很大不同, 并且 API2 是为了配合 HAL3 进行使用的,API2 有很多 API1 不支持的特性,比如:

  • 更先进的 API 架构。
  • 可以获取更多的帧 (预览/拍照) 信息以及手动控制每一帧的参数。
  • 对 Camera 的控制更加完全 (比如支持调整 focus distance,剪裁预览/拍照图片)。
  • 支持更多图片格式 (yuv/raw) 以及高速连拍。

Camera2 API 相比原来 android.hardware.Camera API 在架构上有了很大的改变,虽然让手机拍照功能更加强大,但同时也增加了开发复杂度。感兴趣的可以看看这篇文章,分析了 Camera2 的架构与使用:www.jianshu.com/p/d83161e77…

SurfaceView、TextureView、SurfaceTexture、GLSurfaceView 对比

1. SurfaceView

SurfaceView 继承自 View,并提供了一个独立的绘图层,你可以完全控制这个绘图层,比如说设定它的大小,所以 SurfaceView可以嵌入到 View 结构树中,需要注意的是,由于 SurfaceView 直接将绘图表层绘制到屏幕上,所以和普通的 View 不同的地方就在与它不能执行 Transition,Rotation,Scale 等转换,也不能进行 Alpha 透明度运算。

SurfaceView 的 Surface 排在 Window 的 Surface (也就是 View 树所在的绘图层) 的下面,SurfaceView 嵌入到 Window 的 View结构树中就好像在 Window 的 Surface 上强行打了个洞让自己显示到屏幕上,而且 SurfaceView 另起一个线程对自己的 Surface进行刷新。需要注意的是 SurfaceHolder.Callback 的所有回调方法都是在主线程中回调的。

SurfaceView、SurfaceHolder、Surface 的关系可以概括为以下几点:

  • SurfaceView 是拥有独立绘图层的特殊 View。
  • Surface 就是指 SurfaceView 所拥有的那个绘图层,其实它就是内存中的一段绘图缓冲区。
  • SurfaceView 中具有两个 Surface,也就是我们所说的双缓冲机制。
  • SurfaceHolder 顾名思义就是 Surface 的持有者,SurfaceView 就是通过 SurfaceHolder 来对 Surface 进行管理控制的。并且 SurfaceView.getHolder() 方法可以获取 SurfaceView 相应的 SurfaceHolder。
  • Surface 是在 SurfaceView 所在的 Window 可见的时候创建的。我们可以使用 SurfaceHolder.addCallback() 方法来监听 Surface 的创建与销毁的事件。

Surface 的渲染可以放到单独线程去做,渲染时可以有自己的 GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个 Surface 不在 View hierachy 中,它的显示也不受 View 的属性控制,所以不能进行平移,缩放等变换,也不能放在其它 ViewGroup 中,一些 View 中的特性也无法使用。

2. TextureView

TextureView 专门用来渲染像视频或 OpenGL 场景之类的数据的,而且 TextureView 只能用在具有硬件加速的 Window 中,如果使用的是软件渲染,TextureView 将什么也不显示。也就是说对于没有 GPU 的设备,TextureView 完全不可用。

TextureView 有两个相关类 SurfaceTexture、Surface,下面说明一下几者相关的特点:

  • Surface 就是 SurfaceView 中使用的 Surface,就是内存中的一段绘图缓冲区。
  • SurfaceTexture 用来捕获视频流中的图像帧的,视频流可以是相机预览或者视频解码数据。SurfaceTexture 可以作为android.hardware.camera2, MediaCodec, MediaPlayer, 和 Allocation 这些类的目标视频数据输出对象。可以调用updateTexImage() 方法从视频流数据中更新当前帧,这就使得视频流中的某些帧可以跳过。
  • TextureView 可以通过 getSurfaceTexture() 方法来获取 TextureView 相应的 SurfaceTexture。但是最好的方式还是使用TextureView.SurfaceTextureListener 监听器来对 SurfaceTexture 的创建销和毁进行监听,因为 getSurfaceTexture() 可能获取的是空对象。

3. GLSurfaceView

GLSurfaceView 作为 SurfaceView 的补充,可以看作是 SurfaceView 的一种典型使用模式。在 SurfaceView 的基础上,它加入了EGL 的管理,并自带了渲染线程。另外它定义了用户需要实现的 Render 接口,提供了用 Strategy pattern 更改具体 Render 行为的灵活性。作为 GLSurfaceView 的 Client,只需要将实现了渲染函数的 Renderer 的实现类设置给 GLSurfaceView 即可。

4. SurfaceTexture

SurfaceTexture 和 SurfaceView 不同的是,它对图像流的处理并不直接显示,而是转为 GL 外部纹理,因此可用于图像流数据的二次处理 (如 Camera 滤镜,桌面特效等)。比如 Camera 的预览数据,变成纹理后可以交给 GLSurfaceView 直接显示,也可以通过 SurfaceTexture 交给 TextureView 作为 View heirachy 中的一个硬件加速层来显示。首先,SurfaceTexture 从图像流 (来自Camera 预览,视频解码,GL 绘制场景等) 中获得帧数据,当调用 updateTexImage() 时,根据内容流中最近的图像更新SurfaceTexture 对应的 GL 纹理对象,接下来,就可以像操作普通 GL 纹理一样操作它了。

5. 整理对比

SurfaceView

继承自 View,拥有 View 的大部分属性,但是由于 holder 的存在,不能设置透明度。

  • 优点:可以在一个独立的线程中进行绘制,不会影响主线程,使用双缓冲机制,播放视频时画面更流畅。
  • 缺点:surface 的显示不受 View 属性的控制,不能将其放在 ViewGroup 中,SurfaceView 不能嵌套使用。
    GlSurfaceView

GlSurfaceView 继承自 SurfaceView 类,专门用来显示 OpenGL 渲染的,简单理解可以显示视频,图像及 3D 场景。

SurfaceTexture

和 SurfaceView 功能类似,区别是,SurfaceTexure 可以不显示在界面中。使用 OpenGL 对图片流进行美化,添加水印,滤镜这些操作的时候我们都是通过 SurfaceTexure 去处理,处理完之后再通过 GlSurfaceView 显示。缺点,可能会导致个别帧的延迟。本身管理着 BufferQueue,所以内存消耗会多一点。

TextureView

TextureView 同样继承自 View,必须在开启硬件加速的设备中使用 (保守估计目前 90% 的 Android 设备都开启了),TextureView 通过 setSurfaceTextureListener 的回调在子线程中进行更新 UI。

  • 优点:支持动画效果。
  • 缺点:在 5.0 之前在主线程渲染,在 5.0 之后在单独线程渲染。

OpenGL ES 简介

1. 什么是 OpenGL ES?

OpenGL (全写 Open Graphics Library) 是指定义了一个跨编程语言、跨平台的编程接口规格的专业的图形程序接口。它用于三维图像 (二维的亦可),是一个功能强大,调用方便的底层图形库。

OpenGL 在不同的平台上有不同的实现,但是它定义好了专业的程序接口,不同的平台都是遵照该接口来进行实现的,思想完全相同,方法名也是一致的,所以使用时也基本一致,只需要根据不同的语言环境稍有不同而已。OpenGL 这套 3D 图形 API 从1992 年发布的 1.0 版本到目前最新 2014 年发布的 4.5 版本,在众多平台上多有着广泛的使用。

OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三维图形 API 的子集,针对手机、PDA 和游戏主机等嵌入式设备而设计。

OpenGL ES 相对于 OpenGL 来说,减少了许多不是必须的方法和数据类型,去掉了不必须的功能,对代价大的功能做了限制,比 OpenGL 更为轻量。在 OpenGL ES 的世界里,没有四边形、多边形,无论多复杂的图形都是由点、线和三角形组成的,也去除了 glBegin/glEnd 等方法。

2. OpenGL ES 可以做什么?

OpenGL ES 是手机、PDA 和游戏主机等嵌入式设备三维 (二维也包括) 图形处理的 API,当然是用来在嵌入式设备上的图形处理了,OpenGL ES 强大的渲染能力使其成为我们在嵌入式设备上进行图形处理的优良选择。我们经常使用的场景有:

  • 图片处理。比如图片色调转换、美颜等。
  • 摄像头预览效果处理。比如美颜相机、恶搞相机等。
  • 视频处理。摄像头预览效果处理可以,这个自然也不在话下了。
  • 3D 游戏。比如神庙逃亡、都市赛车等。
3. OpenGL ES 版本及 Android 支持情况

OpenGL ES 当前主要版本有 1.0/1.1/2.0/3.0/3.1。这些版本的主要情况如下:

  • OpenGL ES 1.0 是基于OpenGL 1.3 的,OpenGL ES 1.1 是基于 OpenGL 1.5 的。Android 1.0 和更高的版本支持这个 API 规范。OpenGL ES 1.x 是针对固定硬件管线的。
  • OpenGL ES 2.0 是基于 OpenGL 2.0 的,不兼容 OpenGL ES 1.x。Android 2.2 (API 8) 和更高的版本支持这个 API 规范。OpenGL ES 2.x 是针对可编程硬件管线的。
  • OpenGL ES 3.0 的技术特性几乎完全来自 OpenGL 3.x 的,向下兼容 OpenGL ES 2.x。Android 4.3 (API 18) 及更高的版本支持这个 API 规范。
  • OpenGL ES 3.1 基本上可以属于 OpenGL 4.x 的子集,向下兼容 OpenGL ES 3.0/2.0。Android 5.0 (API 21) 和更高的版本支持这个 API 规范。

GPUImage

GPUImage 是 iOS 下一个开源的基于 GPU 的图像处理库,提供各种各样的图像处理滤镜,并且支持照相机和摄像机的实时滤镜。GPUImage for Android 是它在 Android 下的实现,同样也是开源的。其中提供了几十多种常见的图片滤镜 API,且其机制是基于 GPU 渲染,处理速度相应也比较快,是一个不错的图片实时处理框架。

github 地址:github.com/CyberAgent/…