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

来源:https://www.cnblogs.com/glumes/archive/2020/03/14/12492075.html
-Advertisement-
Play Games

在 Android Camera 開發中,兩個比較鬧心的問題就是尺寸和方向了。 其中尺寸指的是: 相機顯示預覽幀的尺寸 相機拍攝幀的尺寸 Android 顯示相機預覽內容的控制項尺寸 而方向指的是 相機顯示預覽幀的方向 相機拍攝幀的方向 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 ,領取學習資源大禮包


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 進程合作 多個進程共同完成一個任務 每個進程有自己執行的一套方案,但不是每個程式都是可以隨便執行的,有時候需要等待,有的進程會給它發一些信號,根據這些信號來決定是否繼續執行 2. 生產者 消費者實例 那個地方需要停?那個地方需要走? 3. 只發信號還不能解決全部問題 問題的關鍵在於消費者僅僅是 ...
  • 1:雙擊安裝包進行安裝。點擊“next”。2:點擊“w accept the termis...”同意條款,並點擊“next”,進行下一步。3:點擊“enthr license information”,點擊“next”。4:在註冊框中分別填入下載的信息product code: 4vkjwhfeh ...
  • 1、簡單文字說明,關鍵位置截圖補充,Samba配置文件中的關鍵參數,使用註釋標明。 2、實驗過程中,出現任何錯誤,詳細描述排錯的過程。 3、實驗完成後,當場演示實驗結果。 ...
  • linux修改root@後面的別名 通過終端登錄伺服器後預設顯示的主機名有的時候是隨機數或者太長 [root@ecs-x-large-9-linux-20221387234818 ~]# 修改方法一: vi /etc/hostname 然後輸入新的主機名,但需重啟才能生效。 方法二: vi ~/.b ...
  • 本文主要介紹Oracle11g,client及PLSQL的安裝過程 一,oracle安裝 安裝環境:虛擬機win7 64 1.點擊目錄中 setup.exe文件 2.配置安全更新中,取消通過my oracle support接收安全更新,點擊下一步按鈕 3.oracle會彈出提示,點擊是按鈕 4.安 ...
  • NoSQL概述 什麼是NoSQL NoSQL不僅僅是SQL,它是Not Only SQL 的縮寫,也是眾多非關係型資料庫的統稱NoSQL和關係型資料庫一樣,也是用來存儲數據的倉庫。 為什麼需要NoSQL? 隨著互聯網的高速發展,數據量、訪問量呈爆髮式式增長。比如12306中國鐵路票務系統。 一年售出 ...
  • 目錄 下載與安裝 1.方法一(本地翻牆) 2.方法二(本地不翻牆) 初始化和配置 1.初始化mysql 2.遠程訪問的設置 3.用戶組許可權的修改 4.開機重啟 ps:一般mysql安裝後會在/var/log/下麵生成一個mysqld.log文件,如果遇到啟動不了或者其他問題,基本都可以在這個log文 ...
  • 註意:無特殊說明,Flutter版本及Dart版本如下: Flutter版本: 1.12.13+hotfix.5 Dart版本: 2.7.0 Form、FormField、TextFormField是表單相關控制項,類似於H5中form。 FormField FormField是一個表單控制項,此控制項包 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...