移動端視頻進階(一):攝像頭視頻回調數據格式淺析

前言

最近一段時間,接觸到移動端音視頻通話相關的內容,主要是結合OpenCV,TensorFlow等做一些視頻數據的分析,檢測工作。中間碰到大量的問題,入坑了算是,這裏總結一下!

攝像頭數據回調

關於移動端調用攝像頭的相關內容,這裏就不多說了,我們直接來看回調得到的數據!

iOS

我們設置了AVCaptureVideoDataOutputSampleBufferDelegate代理後,就在下邊方法中拿到的攝像頭中的一幀數據CMSampleBufferRef

- (void)captureOutput:(AVCaptureOutput *)captureOutput
    didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
           fromConnection:(AVCaptureConnection *)connection

那麼這一幀數據是什麼格式呢?取決於我們在採集時候的設置:

NSDictionary *rgbOutputSettings = @{
      (__bridge NSString*)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)
  };
[self.videoDataOutput setVideoSettings:rgbOutputSettings];

這裏設置的kCVPixelFormatType_32BGRA就是得到的數據的格式(32位BGRA)!
還有其他常用的格式,比如:kCVPixelFormatType_420YpCbCr8BiPlanarFullRangekCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,這兩種都是NV12格式!

根據源碼中的註釋,可以知道kCVPixelFormatType_420YpCbCr8BiPlanarFullRange8位雙平面組件Y'CbCr比例爲4:2:0,全範圍(亮度=[0,255] 色度=[1,255])
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange8位雙平面組件Y'CbCr比例爲4:2:0,視頻範圍(亮度=[16,235] 色度=[16,240])

Android

Camera

在5.0之前,使用CameraAPI,需要實現Camera.PreviewCallback接口,在回調方法onPreviewFrame中拿到數據:

@Override
public void onPreviewFrame(final byte[] bytes, final Camera camera) {
	....
}

這裏的bytes數組就是得到yuv420sp格式的數據,也稱爲NV21格式。這是CameraAPI的默認預覽格式。當然我們也可以指定預覽的輸出格式:

android.hardware.Camera.Parameters#setPreviewFormat(ImageFormat.NV21)}

具體格式都在ImageFormat類裏邊!
我們也可以通過調用android.hardware.Camera.Parameters#getSupportedPreviewFormats()來查看CameraAPI支持的預覽格式!

Camera2

在5.0之後,Camera被廢棄,使用Camera2API,需要實現OnImageAvailableListener接口,在回調方法onImageAvailable中拿到數據

@Override
public void onImageAvailable(final ImageReader reader) {
	final Image image = reader.acquireLatestImage();
	....
}

調用reader.acquireLatestImage()方法,就得到一個Image對象。
這裏就要注意了,這個Image中存放的數據格式默認是谷歌自家的YUV_420_888,根據文檔描述這是一種新的YUV格式YUV420Flexible。而且不再支持CameraAPI中預覽回調默認的NV21格式

這塊也是折騰好久啊,我們這裏簡單來了解一下:

final Plane[] planes = image.getPlanes();
yRowStride = planes[0].getRowStride();
final int uvRowStride = planes[1].getRowStride();
final int uvPixelStride = planes[1].getPixelStride();

YUV_420_888類型,其表示YUV420格式的集合,888表示Y、U、V分量中每個顏色佔8bit。Image將三個分量存儲在三個Plane類中,通過getPlanes()方法得到一個Plane數組,plane[0]存放Y分量,plane[1]存放U分量,plane[2]存放V分量。還有那麼既然是YUV格式,Y分量就一定是連續存儲的,那麼重點就在U、V分量在數組中是如何的?!

下面瞭解一下Plane類型的兩個屬性rowStridepixelStride
rowStride:則是行跨度,就是每行存放的數據量。
pixelStride:指像素跨度,即在一個平面中,U/V分量的取值間隔,這裏我們可以知道即使UV分量分別存儲在不同平面中,他們也不一定是連續存儲的(即PixelStride不總是爲1)

舉個例子,比如一個4x4的圖片:
我們知道一個NV21格式的數據,如果在Camera的回調中,byte[]數組中分量以

YYYYYYYYYYYYYYYYVUVUVUVU

的形式存儲,而在Camera2得到的Image中,由於YUV_420_888格式強制分離了UV分量,這時Plane的屬性:

分量 rowStride pixelStride data
Y分量 4 1 YYYYYYYYYYYYYYYY
U分量 4 2 U_U_U_U_
V分量 4 2 V_V_V_V_

關於Camera2得到的YUV_420_888格式更多內容,可以參考這篇文章:
Android: Image類淺析(結合YUV_420_888)

轉換

在使用TensorFlow時,一般要求輸入的圖片格式爲RGB,下邊說一下如何將上述獲得的數據轉換爲RGB格式!
此轉換來自於TensorFlow Lite的object_detection例子
首先對於YUV格式的數據,在得到每一個分量上的byte數組後,轉換有一個公式:

private static int YUV2RGB(int y, int u, int v) {
    // This value is 2 ^ 18 - 1, and is used to clamp the RGB values before their ranges
    // are normalized to eight bits.
    static final int kMaxChannelValue = 262143;


    // Adjust and check YUV values
    y = (y - 16) < 0 ? 0 : (y - 16);
    u -= 128;
    v -= 128;

    // This is the floating point equivalent. We do the conversion in integer
    // because some Android devices do not have floating point in hardware.
    // nR = (int)(1.164 * nY + 2.018 * nU);
    // nG = (int)(1.164 * nY - 0.813 * nV - 0.391 * nU);
    // nB = (int)(1.164 * nY + 1.596 * nV);
    int y1192 = 1192 * y;
    int r = (y1192 + 1634 * v);
    int g = (y1192 - 833 * v - 400 * u);
    int b = (y1192 + 2066 * u);

    // Clipping RGB values to be inside boundaries [ 0 , kMaxChannelValue ]
    r = r > kMaxChannelValue ? kMaxChannelValue : (r < 0 ? 0 : r);
    g = g > kMaxChannelValue ? kMaxChannelValue : (g < 0 ? 0 : g);
    b = b > kMaxChannelValue ? kMaxChannelValue : (b < 0 ? 0 : b);

    return 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
  }

然後,對於NV21格式的Camera回調:

    //input即爲攝像頭回調byte[]數組,output爲int[] 轉換後的rgb輸出
    //width 爲圖片寬度    height爲圖片高度
	final int frameSize = width * height;
    for (int j = 0, yp = 0; j < height; j++) {
      int uvp = frameSize + (j >> 1) * width;
      int u = 0;
      int v = 0;

      for (int i = 0; i < width; i++, yp++) {
        int y = 0xff & input[yp];
        if ((i & 1) == 0) {
          v = 0xff & input[uvp++];
          u = 0xff & input[uvp++];
        }

        output[yp] = YUV2RGB(y, u, v);
      }
    }

對於Camera2Image
首先得到yuv各個分量的byte[]:

protected void fillBytes(final Plane[] planes, final byte[][] yuvBytes) {
    // Because of the variable row stride it's not possible to know in
    // advance the actual necessary dimensions of the yuv planes.
    for (int i = 0; i < planes.length; ++i) {
      final ByteBuffer buffer = planes[i].getBuffer();
      if (yuvBytes[i] == null) {
        LOGGER.d("Initializing buffer %d at size %d", i, buffer.capacity());
        yuvBytes[i] = new byte[buffer.capacity()];
      }
      buffer.get(yuvBytes[i]);
    }
}

private byte[][] yuvBytes = new byte[3][];

final Plane[] planes = image.getPlanes();
fillBytes(planes, yuvBytes);

然後得到stride:

yRowStride = planes[0].getRowStride();
final int uvRowStride = planes[1].getRowStride();
final int uvPixelStride = planes[1].getPixelStride();

最後轉換:

    /**
     * width 爲圖片寬度
     * height 爲圖片高度
     * yRowStride 爲y分量的行跨度
     * uvRowStride 爲uv分量的行跨度
     * uvPixelStride 爲uv分量的像素跨度
     * yData,uData,vData 分別對應yuvBytes[0],yuvBytes[1],yuvBytes[2]
     * out[] 爲輸出的rgb int[]數組
     */
    int yp = 0;
    for (int j = 0; j < height; j++) {
      int pY = yRowStride * j;
      int pUV = uvRowStride * (j >> 1);

      for (int i = 0; i < width; i++) {
        int uv_offset = pUV + (i >> 1) * uvPixelStride;

        out[yp++] = YUV2RGB(0xff & yData[pY + i], 0xff & uData[uv_offset], 0xff & vData[uv_offset]);
      }
    }

結語

好了,以上就是關於移動端獲取攝像頭回調數據的相關內容,在下一篇我們再去了解關於和OpenCV中mat的轉換和相關操作,以及豎屏時,圖片逆時針旋轉90度的問題!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章