音視頻篇 - Android 圖像處理技術簡介

關於 Android 的音視頻,也能夠叫作多媒體,分紅圖像、聲音和視頻。咱們先從最基本的圖像入手,圖像分紅 2D 和 3D,Android 自身也提供了不少 API 來實現圖像的功能。對於 Android 的圖像內存優化,能夠看我以前的這篇文章:Android應用篇 - 最全圖片相關的優化。android

YUV 簡介

1. YUV 簡介

YUV 是一種顏色編碼方法,常使用在各個視頻處理組件中。 YUV 在對照片或視頻編碼時,考慮到人類的感知能力,容許下降色度的帶寬。git

YUV 是編譯 true-color 顏色空間 (colorspace) 的種類,Y'UV, YUV, YCbCr, YPbPr 等專有名詞均可以稱爲 YUV,彼此有重疊。"Y" 表示明亮度 (Luminance、Luma),"U" 和 "V" 則是色度、濃度 (Chrominance、Chroma)。github

彩色圖像記錄的格式,常見的有 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:二、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...以此類推。bash

至於其餘常見的 YUV 格式有 YUY二、YUYV、YVYU、UYVY、AYUV、Y41P、Y4十一、Y2十一、IF0九、IYUV、YV十二、YVU九、YUV4十一、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 數據是須要鏡像的,參照上面的算法,實現了前置攝像頭的鏡像算法:ide

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/…

相關文章
相關標籤/搜索