Android Camera-相機尺寸、方向和圖像數據

前面幾篇文章介紹了Camera1,Camera2,CameraView和CameraX的使用,對各個API的使用,應該問題不大,不過在真正開發過程當中,也會遇到各類不一樣的問題,本篇文章繼續介紹相機開發過程當中遇到的問題,主要是相機預覽、拍照尺寸,方向,以及圖像數據的處理。php

尺寸

這裏的尺寸,主要是預覽尺寸、拍照尺寸和顯示預覽畫面的View大小。html

預覽尺寸

如何獲取預覽尺寸?咱們能夠從cameraview的源碼中獲取到,分爲了Camera1和Camera2。java

Camera1

mCameraParameters = mCamera.getParameters();
// Supported preview sizes
mPreviewSizes.clear();
for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
    Log.d("DEBUG", "###### SupportedPreviewSizes: width=" + size.width + ", height="
            + size.height);
    mPreviewSizes.add(new Size(size.width, size.height));
}
複製代碼

Camera2

mPreviewSizes.clear();
for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
    int width = size.getWidth();
    int height = size.getHeight();
    if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
        mPreviewSizes.add(new Size(width, height));
    }
}
複製代碼

不一樣的廠商和系統所支持的預覽尺寸是不同,下面是紅米Note 5A手機上支持的全部預覽尺寸:android

SupportedPreviewSizes: width=1280, height=720
SupportedPreviewSizes: width=960, height=720
SupportedPreviewSizes: width=864, height=480
SupportedPreviewSizes: width=800, height=480
SupportedPreviewSizes: width=768, height=432
SupportedPreviewSizes: width=720, height=480
SupportedPreviewSizes: width=640, height=640
SupportedPreviewSizes: width=640, height=480
SupportedPreviewSizes: width=480, height=640
SupportedPreviewSizes: width=640, height=360
SupportedPreviewSizes: width=576, height=432
SupportedPreviewSizes: width=480, height=360
SupportedPreviewSizes: width=480, height=320
SupportedPreviewSizes: width=384, height=288
SupportedPreviewSizes: width=352, height=288
SupportedPreviewSizes: width=320, height=240
SupportedPreviewSizes: width=240, height=320
SupportedPreviewSizes: width=240, height=160
SupportedPreviewSizes: width=176, height=144
SupportedPreviewSizes: width=144, height=176
SupportedPreviewSizes: width=160, height=120
複製代碼

這裏尺寸的比例通常都是4:三、16:9,其餘比例是在此基礎上裁剪出來的git

選取預覽尺寸

在相同寬高比下,選擇最接近View的寬高,避免過大的預覽尺寸, 形成性能損耗, 引發預覽卡頓。
cameraview源碼中,默認定義的寬高比AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(4, 3)github

Camera1

private Size chooseOptimalSize(SortedSet<Size> sizes) {
    if (!mPreview.isReady()) { // Not yet laid out
        return sizes.first(); // Return the smallest size
    }
    int desiredWidth;
    int desiredHeight;
    final int surfaceWidth = mPreview.getWidth();
    final int surfaceHeight = mPreview.getHeight();
    if (isLandscape(mDisplayOrientation)) {
        desiredWidth = surfaceHeight;
        desiredHeight = surfaceWidth;
    } else {
        desiredWidth = surfaceWidth;
        desiredHeight = surfaceHeight;
    }
    Size result = null;
    for (Size size : sizes) { // Iterate from small to large
        if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {
            return size;

        }
        result = size;
    }
    return result;
}
複製代碼

區分了橫豎屏,而後獲得尺寸中寬和高等於或者大於View的寬高的尺寸。app

Camera2

private Size chooseOptimalSize() {
    int surfaceLonger, surfaceShorter;
    final int surfaceWidth = mPreview.getWidth();
    final int surfaceHeight = mPreview.getHeight();
    if (surfaceWidth < surfaceHeight) {
        surfaceLonger = surfaceHeight;
        surfaceShorter = surfaceWidth;
    } else {
        surfaceLonger = surfaceWidth;
        surfaceShorter = surfaceHeight;
    }
    SortedSet<Size> candidates = mPreviewSizes.sizes(mAspectRatio);

    // Pick the smallest of those big enough
    for (Size size : candidates) {
        if (size.getWidth() >= surfaceLonger && size.getHeight() >= surfaceShorter) {
            return size;
        }
    }
    // If no size is big enough, pick the largest one.
    return candidates.last();
}
複製代碼

先判斷View寬高,區分其中較大值和較小值,而後再獲得尺寸中寬和高大於或者等於View的較大值和較小值的尺寸。post

拍照尺寸

代碼也是從cameraview中截取出來的性能

Camera1

mPictureSizes.clear();
for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
    Log.d("DEBUG", "###### SupportedPictureSizes: width=" + size.width + ", height="
            + size.height);
    mPictureSizes.add(new Size(size.width, size.height));
}
複製代碼

Camera2

protected void collectPictureSizes(SizeMap sizes, StreamConfigurationMap map) {
    for (android.util.Size size : map.getOutputSizes(ImageFormat.JPEG)) {
        mPictureSizes.add(new Size(size.getWidth(), size.getHeight()));
    }
}
複製代碼

在紅米Note 5A手機支持的拍照尺寸:ui

SupportedPictureSizes: width=4160, height=3120
SupportedPictureSizes: width=4160, height=2340
SupportedPictureSizes: width=4096, height=3072
SupportedPictureSizes: width=4096, height=2304
SupportedPictureSizes: width=4000, height=3000
SupportedPictureSizes: width=3840, height=2160
SupportedPictureSizes: width=3264, height=2448
SupportedPictureSizes: width=3200, height=2400
SupportedPictureSizes: width=2976, height=2976
SupportedPictureSizes: width=2592, height=1944
SupportedPictureSizes: width=2592, height=1458
SupportedPictureSizes: width=2688, height=1512
SupportedPictureSizes: width=2304, height=1728
SupportedPictureSizes: width=2048, height=1536
SupportedPictureSizes: width=2336, height=1314
SupportedPictureSizes: width=1920, height=1080
SupportedPictureSizes: width=1600, height=1200
SupportedPictureSizes: width=1440, height=1080
SupportedPictureSizes: width=1280, height=960
SupportedPictureSizes: width=1280, height=768
SupportedPictureSizes: width=1280, height=720
SupportedPictureSizes: width=1200, height=1200
SupportedPictureSizes: width=1024, height=768
SupportedPictureSizes: width=800, height=600
SupportedPictureSizes: width=864, height=480
SupportedPictureSizes: width=800, height=480
SupportedPictureSizes: width=720, height=480
SupportedPictureSizes: width=640, height=480
SupportedPictureSizes: width=640, height=360
SupportedPictureSizes: width=480, height=640
SupportedPictureSizes: width=480, height=360
SupportedPictureSizes: width=480, height=320
SupportedPictureSizes: width=352, height=288
SupportedPictureSizes: width=320, height=240
SupportedPictureSizes: width=240, height=320
複製代碼

這裏尺寸的比例通常也是4:三、16:9

選取拍照尺寸

Camaer1和Camera2都是同樣的邏輯,選取固定寬高比例中的最大尺寸,這樣拍攝的圖片最清晰。

Size largest = mPictureSizes.sizes(mAspectRatio).last();
複製代碼

方向

這裏的設置方向有兩種:圖像預覽方向和拍照方向。在這以前,須要先介紹幾個概念:

  • 屏幕座標方向
  • 設備天然方向
  • 攝像頭傳感器方向
  • 相機預覽方向

屏幕座標方向

在Android系統中,以屏幕左上角爲座標系統的原點(0,0)座標,該座標系是固定不變的,不會由於設備方向的變化而改變。

屏幕座標方向

屏幕天然方向

每一個設備都有一個天然方向,手機和平板天然方向不同,如圖所示,這裏盜個圖:

設備天然方向
默認狀況下,平板的天然方向是橫屏,而手機的天然方向是豎屏方向。Android系統能夠經過View的 OrientationEventListener監聽設備方向,回調方法:

abstract public void onOrientationChanged(int orientation);
複製代碼

onOrientationChanged返回0到359的角度,其中0表示天然方向。

攝像頭傳感器方向

攝像頭傳感器方向
手機相機的圖像數據都是來自於攝像頭硬件的圖像傳感器,這個傳感器在被固定到手機上後有一個默認的取景方向,方向通常是和手機橫屏方向一致,如上圖所示。

相機預覽方向

將攝像頭傳感器捕獲的圖像,顯示在屏幕上的方向,就是相機預覽方向。默認狀況下,和攝像頭傳感器方向一致,能夠經過Camera API進行改變。
Camaer1可使用setDisplayOrientation設置預覽方向,Camera2則能夠經過TextureView來實現。
不一樣的攝像頭位置,orientation是不同的,orientation就是攝像頭傳感器方向順時針旋轉到屏幕天然方向的角度。

後置

後置的orientation90

後置
對橫屏來講,屏幕的天然方向和相機的攝像頭傳感器方向一致的。
對豎屏來講,看到的圖像逆時針旋轉了90度,所以預覽方向須要順時針旋轉90度,才能與屏幕的天然方向保持一致。

前置

前置的orientation270,收集到圖像後(沒有通過鏡像處理),可是要顯示到屏幕上,就要按照屏幕天然方向的座標系來進行顯示,須要順時針旋轉270度,才能和設備天然方向一致。預覽的時候,作了鏡像處理,因此只須要順時針旋轉90度,就能和設置天然方向一致。
那麼Camera1和Camera2具體設置預覽方向的代碼,來自cameraview

Camera1

private int calcDisplayOrientation(int screenOrientationDegrees) {
    if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        return (360 - (mCameraInfo.orientation + screenOrientationDegrees) % 360) % 360;    // compensate the mirror
    } else {  // back-facing
        return (mCameraInfo.orientation - screenOrientationDegrees + 360) % 360;
    }
}
複製代碼

代碼中區分了前置和後置攝像頭。

  • 後置:(mCameraInfo.orientation - screenOrientationDegrees + 360) % 360,恢復到天然方向須要順時針旋轉,而屏幕逆時針旋轉正好抵掉了攝像頭的旋轉,二者差值+360取模。
  • 前置:(mCameraInfo.orientation + screenOrientationDegrees) % 360,屏幕豎直方向看到的是一個鏡像,360-(mCameraInfo.orientation + screenOrientationDegrees) % 360,順時針旋轉這個差值能夠到天然方向,只不過這是個鏡像,左右翻轉了

Camera2 使用的TextureView的setTransform進行旋轉,並有區分橫豎屏。

/** * Configures the transform matrix for TextureView based on {@link #mDisplayOrientation} and * the surface size. */
void configureTransform() {
    Matrix matrix = new Matrix();
    if (mDisplayOrientation % 180 == 90) {
        final int width = getWidth();
        final int height = getHeight();
        // Rotate the camera preview when the screen is landscape.
        matrix.setPolyToPoly(
                new float[]{
                        0.f, 0.f, // top left
                        width, 0.f, // top right
                        0.f, height, // bottom left
                        width, height, // bottom right
                }, 0,
                mDisplayOrientation == 90 ?
                        // Clockwise
                        new float[]{
                                0.f, height, // top left
                                0.f, 0.f, // top right
                                width, height, // bottom left
                                width, 0.f, // bottom right
                        } : // mDisplayOrientation == 270
                        // Counter-clockwise
                        new float[]{
                                width, 0.f, // top left
                                width, height, // top right
                                0.f, 0.f, // bottom left
                                0.f, height, // bottom right
                        }, 0,
                4);
    } else if (mDisplayOrientation == 180) {
        matrix.postRotate(180, getWidth() / 2, getHeight() / 2);
    }
    mTextureView.setTransform(matrix);
}
複製代碼

拍照方向

設置預覽方向並不會改變拍出照片的方向。
對於後置相機,相機採集到的圖像和相機預覽的圖像是同樣的,只須要旋轉後置相機orientation度。 對於前置相機來講,相機預覽的圖像和相機採集到的圖像是鏡像關係。
採集的圖像:順時針旋轉270度後,與屏幕天然方向一致。
預覽的圖像:順時針旋轉90度後,與屏幕天然方向一致。
最後盜用一張圖來講明:

拍照方向

Camera1

使用mCameraParameters.setRotation()設置拍照後圖像方向:

mCameraParameters.setRotation(calcCameraRotation(displayOrientation));
......
/** * Calculate camera rotation * * This calculation is applied to the output JPEG either via Exif Orientation tag * or by actually transforming the bitmap. (Determined by vendor camera API implementation) * * Note: This is not the same calculation as the display orientation * * @param screenOrientationDegrees Screen orientation in degrees * @return Number of degrees to rotate image in order for it to view correctly. */
private int calcCameraRotation(int screenOrientationDegrees) {
    if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        return (mCameraInfo.orientation + screenOrientationDegrees) % 360;
    } else {  // back-facing
        final int landscapeFlip = isLandscape(screenOrientationDegrees) ? 180 : 0;
        return (mCameraInfo.orientation + screenOrientationDegrees + landscapeFlip) % 360;
    }
}
複製代碼

相機採集到的圖像,只須要旋轉相機orientation度。

Camera2

根據CameraCharacteristics.SENSOR_ORIENTATION,使用captureRequest設置了JPEG圖像的旋轉方向。

// Calculate JPEG orientation.
@SuppressWarnings("ConstantConditions")
int sensorOrientation = mCameraCharacteristics.get(
        CameraCharacteristics.SENSOR_ORIENTATION);
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
        (sensorOrientation +
                mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) +
                360) % 360);
複製代碼

圖像數據

Android Camera默認返回的數據格式是NV21。Camera1經過mParameters.setPreviewFormat()設置,Camera2經過ImageReader.newInstance()設置。
ImageFormat枚舉了不少種圖片格式,其中ImageFormat.NV21和ImageFormat.YV12是官方推薦的格式,NV2一、YV12格式都屬於 YUV 格式,也能夠表示爲YCbCr,Cb、Cr的含義等同於U、V。

YUV

YUV是一種顏色編碼方法,和它相似的還有RGB顏色編碼方法,主要應用於電視系統和模擬視頻領域。其中YUV表明三個份量,Y 表明明亮度,U 和 V 表示的是色度,色度又定義了顏色的兩個方面:色調和飽和度。將Y與UV分離,沒有UV信息同樣能夠顯示完整的圖像,可是隻能顯示灰度圖。

YUV採樣格式

YUV 圖像的主流採樣方式有以下三種:

  • YUV 4:4:4 採樣:每個Y對應一組UV份量
  • YUV 4:2:2 採樣:每兩個Y共用一組UV份量
  • YUV 4:2:0 採樣:每四個Y共用一組UV份量

盜個圖說明比較清晰,黑點表示採樣該像素點的Y份量,空心圓圈表示採用該像素點的UV份量

YUV

YUV存儲格式

有兩種存儲格式,planar和packed。

  • planar:先連續存儲全部像素點的Y,緊接着存儲全部像素點的U,隨後是全部像素點的V
  • packed:每一個像素點的Y,U,V是連續交替存儲

YUV格式信息能夠參考:YUV pixel formats
根據採樣方式和存儲格式的不一樣,造成了多種YUV格式,常見的YUV格式:

採樣/格式
YUV422 YUVY 格式 UYVY 格式 YUV422P 格式
YUV420 YUV420P
(YV十二、YU12格式)
YUV420P
(NV十二、NV21格式)

YUVY格式

YUVY格式屬於packed存儲格式,相鄰的兩個Y共用其相鄰的兩個U、V

Y0 UO Y1 V0 Y2 U2 Y3 V2
複製代碼

Y0、Y1共用 U0、V0
Y二、Y3共用 U二、V2

UYVY格式

UYVY格式也屬於packed存儲格式,與YUYV格式不一樣的是UV的排列順序不同而已

YUV422P格式

YUV422P格式屬於planar存儲格式,先連續存儲全部像素點的Y,緊接着存儲全部像素點的U,隨後是全部像素點的V

YV十二、YU12格式

YU12和YV12格式都屬於YUV420P格式,YUV420P是planar存儲格式。先存儲全部Y,而後在存儲U、V。
YU12和YV12的區別在於YU12是先Y再U後V,而YV12是先Y再V後U。

NV十二、NV21格式

NV十二、NV21格式YUV420SP格式,YUV420SP也是planar存儲格式。先存儲全部Y,而後按照UV或者VU的交替順序進行存儲。
NV12格式先存儲Y,而後UV再進行交替存儲。
NV21格式則是先存儲Y,而後VU再進行交替存儲。
最後盜用一個數據格式的總結:

YV21: YYYYYYYY UU VV => YUV420P   
YV12: YYYYYYYY VV UU => YUV420P   
NV12: YYYYYYYY UV UV => YUV420SP   
NV21: YYYYYYYY VU VU => YUV420SP
複製代碼

Android Camera 默認數據格式是 NV21,Camera1直接設置mParameters.setPreviewFormat(ImageFormat.NV21),而後拍照回調中的 raw data 數據返回就是 NV21的。
Camera2經過ImageReader.newInstance()設置,可是不能直接設置格式ImageFormat.NV21,在源碼中有段代碼:

if (format == ImageFormat.NV21) {
    throw new IllegalArgumentException(
            "NV21 format is not supported");
}
複製代碼

在最新的ImageFormat.NV21上有說明:

YCrCb format used for images, which uses the NV21 encoding format.   
This is the default format for android.hardware.Camera preview images,
when not otherwise set with android.hardware.Camera.Parameters.setPreviewFormat(int).
For the android.hardware.camera2 API, the YUV_420_888 format is recommended for YUV output instead.
複製代碼

Camera2建議使用YUV_420_888來替代,因此要獲得NV21的數據須要進行數據轉化,具體能夠參考Image類淺析(結合YUV_420_888)

參考

相關文章
相關標籤/搜索