在 Android Camera 開發中,兩個比較鬧心的問題就是尺寸和方向了。 其中尺寸指的是: 相機顯示預覽幀的尺寸 相機拍攝幀的尺寸 Android 顯示相機預覽內容的控制項尺寸 而方向指的是 相機顯示預覽幀的方向 相機拍攝幀的方向 Android 手機自身的方向 在開發中要處理好這三個方向和三個 ...
在 Android Camera 開發中,兩個比較鬧心的問題就是尺寸和方向了。
其中尺寸指的是:
- 相機顯示預覽幀的尺寸
- 相機拍攝幀的尺寸
- Android 顯示相機預覽內容的控制項尺寸
而方向指的是
- 相機顯示預覽幀的方向
- 相機拍攝幀的方向
- Android 手機自身的方向
在開發中要處理好這三個方向和三個尺寸各自的關係才行,這裡以 Camera 1.0 版本的 API 作為示例,參考了 Google 的開源項目:cameraview 和 android-Camera2Basic 。
## 尺寸
相機作為硬體設備,可以提供兩類尺寸:
- 預覽幀尺寸
- 拍攝幀尺寸
預覽幀尺寸
通過 getSupportedPreviewSizes
方法可以得到支持的預覽幀的尺寸集合。
private final SizeMap mPreviewSizes = new SizeMap();
mCamera = Camera.open(mCameraId);
mCameraParameters = mCamera.getParameters();
// Supported preview sizes
mPreviewSizes.clear();
for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
mPreviewSizes.add(new Size(size.width, size.height));
}
而 mPreviewSizes
是 SizeMap
類型,查看源碼,實際上就是在添加預覽幀尺寸的長和寬時,還計算了他們的長寬比,並保存了起來,存儲長寬比的結構可以是一對多的關係,也就是長寬比相同,長和寬的尺寸可以有多種,只要他們最後約分後的比例相同。
// 同一個長寬比,對應多個尺寸
private final ArrayMap<AspectRatio, SortedSet<Size>> mRatios = new ArrayMap<>();
比如,尺寸是 1920 * 1080 和 1280 * 720 的長寬比都是 16 : 9,而尺寸是 800 * 600 和 640 * 480 的的長寬比都是 4 : 3。
在這裡說法是 長寬比,也有說話是 寬高比 的。實際上都是相同的,都是將手機橫放時的,較長的那一邊比較短的那一邊的值。
在計算長寬比時,需要求出寬和高數值的最大公約數,這樣才能進行約分計算,根據歐幾裡得演算法,又叫做輾轉相除法
:兩個整數的最大公約數等於其中較小的那個數和兩個數相除餘數的最大公約數。轉換成代碼如下:
// a > b
private static int gcd(int a, int b) {
while (b != 0) {
int c = b;
b = a % b;
a = c;
}
return a;
}
拍攝幀尺寸
通過 getSupportedPictureSizes
方法可以得到支持的拍攝幀的尺寸集合。
// Supported picture sizes;
private final SizeMap mPictureSizes = new SizeMap();
mPictureSizes.clear();
for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
mPictureSizes.add(new Size(size.width, size.height));
}
存儲結構和預覽幀相似,在得到尺寸集合時,也計算了它們對應的長寬比。
而 Android 顯示相機預覽內容的控制項尺寸,在控制項對應的方法中可以拿到它的 Width 和 Height 。
計算寬高比
有了這三類尺寸,接下來就是要如何處理了。
為了在預覽和拍攝時,圖像不會出現拉伸現象,預覽幀的長寬比最好和顯示控制項的長寬比一致,並且拍攝幀的長寬比也和預覽幀和顯示控制項的長寬比一致,總之三者的長寬比最好是一致的,才會有最好的預覽和拍攝效果
因為手機預覽控制項的圖像 是由 相機預覽幀 根據 控制項大小 縮放得來的,當長寬比不一致時必然會導致預覽圖像變形。而預覽幀的長寬比和拍攝幀的長寬比不一致的話,又會導致拍攝的圖片變形拉伸。
在 cameraview 的源碼中,首先設定了預設的寬高比為 4 : 3 。
AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(4, 3);
根據這一長寬比,可以從預覽幀的尺寸集合中得到那些符合的尺寸列表,再從那些尺寸列表中找到寬和高都剛好大於預覽控制項的寬高的。若是小於預覽控制項的寬高則會導致圖像被拉伸了。
SortedSet<Size> sizes = mPreviewSizes.sizes(mAspectRatio);
if (sizes == null) { // Not supported
mAspectRatio = chooseAspectRatio();
// 根據選定的長寬比得到對應的支持的尺寸集合
sizes = mPreviewSizes.sizes(mAspectRatio);
}
// 和預覽控制項的尺寸相比較,從尺寸集合中找到合適的尺寸
Size size = chooseOptimalSize(sizes);
// 把找到的合適尺寸,設置給相機的預覽幀
mCameraParameters.setPreviewSize(size.getWidth(), size.getHeight());
具體找到合適的預覽幀尺寸大小的代碼如下:
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;
}
註意到,當屏幕處於橫屏模式式,預覽控制項的寬和高就發生變換了,要相互調換。
找到合適的預覽幀的尺寸後,就可以設置給相機了。
而相機拍攝幀的尺寸,也是要根據長寬比來選定。
final Size pictureSize = mPictureSizes.sizes(mAspectRatio).last();
在寬高比一定的情況下,拍攝幀往往選擇尺寸最大的,那樣拍攝的圖片更清楚,這也是為什麼最後使用 last
方法。
這樣一來,在確定好了寬高比的情況下,就可以設置對應的尺寸了。
在 Google 的 android-Camera2Basic 工程中,也有這樣一段設置尺寸的代碼,不同的它是根據拍攝的圖片的最大尺寸確定好了長寬比,而不是預設選擇普遍的 4 : 3 的比例,之後在此基礎之上才進行設置。
Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
new CompareSizesByArea());
可以看到,在相機中設置寬高比還是非常重要的一個環節。
## 方向
搞定了尺寸問題,還剩下方向了。
相機有兩種方向需要處理:
- 預覽幀方向
- 拍攝幀方向
為了獲得更好的相機體驗,要處理好預覽幀和拍攝幀的方向,保證通過手機屏幕看到的內容都是方向正常的。
首先要明確手機的自然方向:
- 當手機屏幕
豎立時的自然方向
,此時,坐標原點位於左上角,向右為 X 軸正方向,向下為 Y 軸正方向,寬比高短。 - 當手機屏幕
橫放時的自然方向
,此時,坐標原點位於左上角,向右為 X 軸正方向,向下為 Y 軸正方向,寬比高長。
預覽幀方向
而相機的圖像數據是來自相機硬體圖像感測器的,感測器被固定在手機上後有一個預設的取景方向:坐標原點位於手機逆時針橫放時的左上角,即與橫屏應用的屏幕 X 方向一致。也就是與豎屏應用的屏幕 X 方向呈 90 度角。
這裡盜圖幾張:
所以,對於橫屏應用來說,屏幕的自然方向和相機的圖像感測器方向一致,因此看到的圖像是正的。而對於豎屏應用來說,預覽圖像就側過來了。需要將預覽圖像順時針旋轉 90 度角才可以正常預覽圖像。
橫屏拍攝結果:
豎屏拍攝結果:
關於相機的預覽方向和屏幕自然方向存在 90 度角的偏差,在 Camera 的 orientation
屬性中也有說明:
orientation 表示相機圖像的方向。它的值是相機圖像順時針旋轉到設備自然方向一致時的圖像,它可能是 0、90、180、270 四種。
對於豎屏應用來說,後置相機感測器是橫屏安裝的,當你面向屏幕時,如果後置相機感測器頂邊和設備自然方向的右邊是平行的,那麼後置相機的 orientation 是 90。如果是前置相機感測器頂邊和設備自然方向的右邊是平行的,則前置相機的 orientation 是 270 。
對於前置和後置相機感測器 orientation 是不同的,在不同的設備上也可能會有不同的值。
在沒有限定 Activity 方向時,採用官方推薦的代碼來設置方向:
public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:
degrees = 270;
break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
首先計算得到設備逆時針旋轉的角度,對於後置攝像頭感測器的計算:(info.orientation - degrees + 360) % 360 。
因為攝像頭圖像方向要恢復到自然方向需要順時針旋轉,而屏幕逆時針旋轉正好抵掉了攝像頭的旋轉,所以兩者相減,然後再加上 360 取模運算。
對於前置攝像頭感測器,因為在使用前置攝像頭時,從屏幕豎直方向看到的往往是一個鏡像,這是因為攝像頭硬體對圖像做了水平翻轉,也就是將圖像內容對著豎直方向對調了,相當於預先旋轉了 180 度。之後再只需要旋轉 90 度就可以到自然方向了,只不過是個鏡像,即左右翻轉了。
需要註意的一點是,在 API 14 之前,調用 setDisplayOrientation 方法時要先關閉預覽。
最後盜圖更清晰明瞭一下:
拍攝幀方向
確定了預覽時的方向,還需要確定拍攝時的方向。
通過 Camera.Parameters.setRotation 函數可以設置相機最終拍出的圖片方向。
官方的推薦代碼:
public void onOrientationChanged(int orientation) {
if (orientation == ORIENTATION_UNKNOWN) {
return;
}
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
orientation = (orientation + 45) / 90 * 90;
int rotation = 0;
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else { // back-facing camera
rotation = (info.orientation + orientation) % 360;
}
mParameters.setRotation(rotation);
}
計算旋轉的方向,需要考慮到當前屏幕的方向和相機的方向。
OrientationEventListener 和 Camera.orientation 一起配合使用。當屏幕方向改變時,OrientationEventListener 會收到相應的通知,在 onOrientationChanged 的回調方法中去改變相機的拍攝方向,實際上在相機預覽方向的改變也是在該回調方法中進行的。
onOrientationChanged 方法的返回值是從 0 ~ 359。而 setRotation 的值只能是 0、90、180、270。所以需要對屏幕方向的 orientation 做一個類似四捨五入的操作。
當然也可以在此回調中根據 Display 類的 getRotation 方法得到方向就行,總之就是有一個回調的通知,然後在此改變屏幕拍攝和預覽的方向。
對於前置攝像頭,攝像頭的 orientation 和屏幕方向的 orientation 兩個之差即為要旋轉的角度;對於後置攝像頭,兩者之和即為要旋轉的角度。
到這裡,就對攝像頭開發中的尺寸和方向設置有個更清晰的認識了。
## 參考
- https://blog.csdn.net/Tencent_Bugly/article/details/53375311
- https://blog.csdn.net/daiqiquan/article/details/40650055
- https://zhuanlan.zhihu.com/p/20559606
- http://javayhu.me/blog/2017/09/25/camera-development-experience-on-android/
歡迎關註,持續更新,公眾號回覆:OpenGL ,領取學習資源大禮包