Android 相機開發中的尺寸和方向問題


在 Android Camera 開發中,兩個比較鬧心的問題就是尺寸和方向了。

其中尺寸指的是:

  • 相機顯示預覽幀的尺寸
  • 相機拍攝幀的尺寸
  • Android 顯示相機預覽內容的控件尺寸

而方向指的是

  • 相機顯示預覽幀的方向
  • 相機拍攝幀的方向
  • Android 手機自身的方向

在開發中要處理好這三個方向和三個尺寸各自的關系才行,這里以 Camera 1.0 版本的 API 作為示例,參考了 Google 的開源項目:cameraviewandroid-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));
        }

mPreviewSizesSizeMap 類型,查看源碼,實際上就是在添加預覽幀尺寸的長和寬時,還計算了他們的長寬比,並保存了起來,存儲長寬比的結構可以是一對多的關系,也就是長寬比相同,長和寬的尺寸可以有多種,只要他們最后約分后的比例相同。

    // 同一個長寬比,對應多個尺寸
    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 度角。

這里盜圖幾張:

back_camera_coordinate

所以,對於橫屏應用來說,屏幕的自然方向和相機的圖像傳感器方向一致,因此看到的圖像是正的。而對於豎屏應用來說,預覽圖像就側過來了。需要將預覽圖像順時針旋轉 90 度角才可以正常預覽圖像。

橫屏拍攝結果:

landscape_camera

豎屏拍攝結果:

portrait

關於相機的預覽方向和屏幕自然方向存在 90 度角的偏差,在 Camera 的 orientation屬性中也有說明:

camera_orientation_description

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 兩個之差即為要旋轉的角度;對於后置攝像頭,兩者之和即為要旋轉的角度。

到這里,就對攝像頭開發中的尺寸和方向設置有個更清晰的認識了。

參考


  1. https://blog.csdn.net/Tencent_Bugly/article/details/53375311
  2. https://blog.csdn.net/daiqiquan/article/details/40650055
  3. https://zhuanlan.zhihu.com/p/20559606
  4. http://javayhu.me/blog/2017/09/25/camera-development-experience-on-android/

歡迎關注,持續更新,公眾號回復:OpenGL ,領取學習資源大禮包


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM