前面幾篇文章介紹了Camera1,Camera2,CameraView和CameraX的使用,對各個API的使用,應該問題不大,不過在真正開發過程當中,也會遇到各類不一樣的問題,本篇文章繼續介紹相機開發過程當中遇到的問題,主要是相機預覽、拍照尺寸,方向,以及圖像數據的處理。php
這裏的尺寸,主要是預覽尺寸、拍照尺寸和顯示預覽畫面的View大小。html
如何獲取預覽尺寸?咱們能夠從cameraview的源碼中獲取到,分爲了Camera1和Camera2。java
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));
}
複製代碼
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
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
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中截取出來的性能
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));
}
複製代碼
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就是攝像頭傳感器方向順時針旋轉到屏幕天然方向的角度。
前置的orientation
270,收集到圖像後(沒有通過鏡像處理),可是要顯示到屏幕上,就要按照屏幕天然方向的座標系來進行顯示,須要順時針旋轉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度後,與屏幕天然方向一致。
最後盜用一張圖來講明:
使用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度。
根據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是一種顏色編碼方法,和它相似的還有RGB顏色編碼方法,主要應用於電視系統和模擬視頻領域。其中YUV表明三個份量,Y 表明明亮度,U 和 V 表示的是色度,色度又定義了顏色的兩個方面:色調和飽和度。將Y與UV分離,沒有UV信息同樣能夠顯示完整的圖像,可是隻能顯示灰度圖。
YUV 圖像的主流採樣方式有以下三種:
盜個圖說明比較清晰,黑點表示採樣該像素點的Y份量,空心圓圈表示採用該像素點的UV份量
有兩種存儲格式,planar和packed。
YUV格式信息能夠參考:YUV pixel formats
根據採樣方式和存儲格式的不一樣,造成了多種YUV格式,常見的YUV格式:
採樣/格式 | |||
---|---|---|---|
YUV422 | YUVY 格式 | UYVY 格式 | YUV422P 格式 |
YUV420 | YUV420P (YV十二、YU12格式) |
YUV420P (NV十二、NV21格式) |
YUVY格式屬於packed存儲格式,相鄰的兩個Y共用其相鄰的兩個U、V
Y0 UO Y1 V0 Y2 U2 Y3 V2
複製代碼
Y0、Y1共用 U0、V0
Y二、Y3共用 U二、V2
UYVY格式也屬於packed存儲格式,與YUYV格式不一樣的是UV的排列順序不同而已
YUV422P格式屬於planar存儲格式,先連續存儲全部像素點的Y,緊接着存儲全部像素點的U,隨後是全部像素點的V
YU12和YV12格式都屬於YUV420P格式,YUV420P是planar存儲格式。先存儲全部Y,而後在存儲U、V。
YU12和YV12的區別在於YU12是先Y再U後V,而YV12是先Y再V後U。
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)