OpenCV4.9.0のポートレート(縦向き)カメラプレビュー
(Android Studio)Camera2 API

2024.1.17 2020.5.3 Coskx Lab  

1 はじめに

OpenCV4.9.0 android-sdkを使って,デバイスが縦位置でも横位置でも正しくカメラプレビューするアプリを作ります。

android-sdk中のJavaCamera2View.javaを使わずに修正版のJavaCamera2ViewPlus.javaを使います。OpenCV android-sdkのソースファイルには手を付けません。


OpenCV android-sdkを使ってチュートリアル通りにカメラプレビューするアプリを作ると,デバイスが横位置(Landscape)の場合にはうまく動作しますが,デバイスを縦位置(portrait)にすると画像が横倒しになり,小さく表示されてしまいます。

androidデバイスではカメラ(イメージセンサ)が横位置(Landscape 幅>高さ)でデバイスに組み込まれていて,画像データも横長の長方形で得られるため,デバイスを横位置(幅<高さ)にすると,問題なく表示が可能です。しかし,デバイスを縦位置にすることは考慮されていません。

デバイスを縦位置で使うときは,横位置カメラ(Landscape)から得られた画像を90度回転させなければなりません。
画像を回転させるのは,結構負荷が大きく,FPSを低下させます。横位置表示よりFPSが半分くらいになります。
画質をちょっと犠牲にするとFPSの低下を抑えることができます。
そのため,デバイスを縦位置で使うときには最大プレビューサイズを設定して画質を優先するか,小さめのプレビューサイズを設定してFPSを優先するかを選べるようにするのが良いようです。

camera2 APIを使用しているJavaCamera2View.javaを拡張したJavaCamera2ViewPlus.javaを使用します。

2 使用環境

3 準備

まず,新規プロジェクトを「OCVprePlus」の名前でEmpty Views Activityとして作ることにします。
javaを使用するのでBuild configuration language は Groovy DSL(build.gradle)とします。
パッケージ名が「jp.gr.java_conf.coskx.ocvpreplus」になったとします。(作業では読み替えて下さい。)
OpenCV4.9.0のカメラプレビュー」記事の手順で次の作業を行います。
 (1)OpenCVモジュールを取り込みます。
 (2)AndroidManifast.xmlを修正します。
その続きからの作業になります。

4 追加作業

4.1 projectに新しいviewクラスに差し替え

Projectのところで,JavaCamera2ViewPlus.javaをapp/java/xxxxxxxxxxxxxxxxのMainActivity.javaと同じフォルダに入れます。

JavaCamera2ViewPlus.javaのダウンロード


MainActivity.javaとJavaCamera2ViewPlus.javaがProjectのところに並んで見えたら成功です。
(先頭行のパッケージ名はプロジェクトに合わせてください。)




4.2 activity_main.xmlの編集

表示するビューのクラスをJavaCamera2ViewPlusに設定します。

OpenCV4.9.0のカメラプレビュー」をもとに,Res/Layout/activity_main.xmlを編集します。
「org.opencv.android.JavaCameraView」のところを, 「xxxxxxxxx.JavaCamera2ViewPlus」にします。
xxxxxxxxxのところは,現在作成中のパッケージ名を書きます。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- JavaCamera2ViewPlus is placed -->
    <jp.gr.java_conf.coskx.ocvpreplus.JavaCamera2ViewPlus
        android:id="@+id/camera_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:camera_id="back"
        app:show_fps="true"/>

</androidx.constraintlayout.widget.ConstraintLayout>

(app:camera_id="back" のところを app:camera_id="front" と変更すると,フロントカメラが使えます。)


4.3 MainActivity.javaの編集

OpenCV4.9.0のカメラプレビュー」をもとに,次のように編集します。パッケージ名は作業中のものに合わせてください。

onCreate内で画像回転作業負荷を低減するため,previewに所望のサイズ(解像度)を設定できるように修正しています。
 mOpenCvCameraView.setMaxFrameSize(960,720);
常に第1引数>第2引数で設定します。
previewサイズを自動で決定してもらうときはこの行は不要です。
この行以外は,「OpenCV4.9.0のカメラプレビュー」と全く同じです。

package jp.gr.java_conf.coskx.ocvpreplus;

import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;
import org.opencv.core.Mat;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import static android.Manifest.permission.CAMERA;

public class MainActivity extends Activity implements CvCameraViewListener2 {
    private static final String TAG = "MainActivity";
    private static final int CAMERA_PERMISSION_REQUEST_CODE = 200;

    private CameraBridgeViewBase mOpenCvCameraView;

    @Override
    protected void onStart() {
        super.onStart();
        boolean havePermission = true;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(CAMERA) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
                havePermission = false;
            }
        }
        if (havePermission) {
            mOpenCvCameraView.setCameraPermissionGranted();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            mOpenCvCameraView.setCameraPermissionGranted();
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "called onCreate");
        super.onCreate(savedInstanceState);
        System.loadLibrary("opencv_java4");

        setContentView(R.layout.activity_main);
        mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.camera_view);
        mOpenCvCameraView.setCvCameraViewListener(this);

        //requesting previewsize option (maxWidth > maxHeight)
        mOpenCvCameraView.setMaxFrameSize(960, 720);
    }

    @Override
    public void onResume() {
        super.onResume();
        mOpenCvCameraView.enableView();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    public void onCameraViewStarted(int width, int height) {}

    public void onCameraViewStopped() {}

    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        return inputFrame.rgba();
    }
}

5 何をしているのか

ここでは,OpenCVモジュール中のJavaCamera2View.javaの作業とJavaCamera2ViewPlusの作業を比較します。

5.1 幅・高さの概念と,画像データ

説明の準備として,4つの「幅×高さ」の概念を使います。
デバイススクリーンの幅・高さの概念はデバイスの向きによって変化するはずです。
しかしカメラの幅・高さはカメラに固定した概念になっていて,変化しません。
JavaCamera2View.javaとJavaCamera2ViewPlusの持つ4つのサイズ概念について見てみます。

また,カメラ画像データはpreviewsizeの横長長方形で得られるので,デバイスを横向き(landscape)の時にはそのまま使えますが, 縦向き(portrait)の時には,90度回転した画像データを受け取ることになり,画像データを90度回転させないと正しく表示できません。

5.2 JavaCamera2View.javaでの作業
    (OpenCVモジュールに付属,横位置のみで正しく動作)


「5.1」の4つのサイズに関する作業はすべてconnectCamera()で行われています。
connectCameraはconnectCamera(int width, int height)のように呼び出されますが,このwidth,heightがavailablesizeです。
calcPreviewSize()でavailablesizeをもとに,カメラにとって都合の良いpreviewsizeを設定してもらいます。(ここに問題があります)
そして,previewsizeをそのままframesizeにしています。(ここにも問題があります)
その後,framesizeをdisplaysizeにするための倍率mScaleを求めています。
CameraBridgeViewBase中でmScale倍されてdisplaysizeに変換されてスクリーンのユーザー領域に表示されます。

この操作においては,デバイスを横向きにした場合にはデバイスの幅・高さとカメラの幅・高さの概念は共通ですが, デバイスを縦にしたときは,デバイスでの幅・高さの概念とカメラの幅・高さの概念がずれてしまうので,正しいpreviewsizeを選ぶことができず, また受け取った画像データも90度回転したものを受け取るため,うまく動作しません。

例 P10Lite
previewsize候補:3968x2976,1920x1080,1440x1080,1280x960,1280x720,960x720, 960x544,720x720,640x480,352x288,320x240,208x144,176x144
(イメージセンサの画素は3968x2976ですが,実際に切り出す画素数を上記の中から選ぶことができます。 切り出しはイメージセンサの中央部分から行われます。)

〇 横位置の場合
availablesize:1776x1008 (利用できるスクリーンの幅x高,横位置なので幅>高になっている)
previewsize:1280x960  (カメラの作成するビュー幅x高,内蔵カメラは横向きなので必ず幅>高になっている)
framesize:1280x960   (本体に表示するために用意するビュー幅x高,previewsizeと同じ)
mScale:1.05
displaysize:1344x1008 availablesizeと比べて幅方向に余白432pix
FPS:25

〇 縦位置の場合
availablesize:1080x1740 (利用できるスクリーンの幅x高,縦位置なので幅<高になっている)
previewsize:960x720  (カメラの作成するビュー幅x高,内蔵カメラは横向きなので必ず幅>高になっている)
framesize:960x720   (本体に表示するために用意するビュー幅x高,previewsizeと同じ)
mScale:1.125
displaysize:1080x810   availablesizeと比べて高さ方向に大きな余白930pix
(カメラの縦横と本体の縦横が90度ずれているおり,そのまま表示しているため,画像の向きが不正になる)
FPS:25


5.3 JavaCamera2ViewPlus.javaでの作業
    (横位置・縦位置で正しく動作するように修正したもの)


「5.1」の4つのサイズに関する修正作業はすべてconnectCamera()内で行われています。
connectCameraはconnectCamera(int width, int height)のように呼び出されますが,このwidth,heightがavailablesizeです。
width<heightの値がえられたら,デバイスが縦位置にあることがわかります。
デバイスが縦位置のときは,calcPreviewSize()に与える幅・高さは幅>高さでなければならないので,availablesizeの幅・高さを longside,shortsideとしてcalcPreviewSize()に与える準備をします。

    isportrait = width<height;
    int longside, shortside;
    if (isportrait) {
        longside = height;
        shortside = width;
    } else {
        longside = width;
        shortside = height;
    }

そして,longside,shortsideをもとに,カメラにとって都合の良いpreviewsizeをcalcPreviewSize(longside, shortside);で設定してもらいます。
ただし,previewsize(CameraBridgeViewBaseの変数)を設定しているところはここでは見えません。

        boolean needReconfig = calcPreviewSize(longside, shortside);

求められたpreviewsize(CameraBridgeViewBaseの変数)は必ず幅>高さなので,デバイスが横位置のときは,framesizeにはpreviewsizeの値をそのまま使い, デバイスが縦位置のときは,framesizeにはpreviewsizeを幅・高さを逆にして使います。

        if (isportrait) {
            mFrameWidth = mPreviewSize.getHeight();
            mFrameHeight = mPreviewSize.getWidth();
        } else {
            mFrameWidth = mPreviewSize.getWidth();
            mFrameHeight = mPreviewSize.getHeight();
        }

その後,availablesizeとframesizeから,framesizeをdisplaysizeにするための倍率mScaleを求めています。
ここはJavaCamera2View.javaと同じ記述になります。

        if ((getLayoutParams().width == LayoutParams.MATCH_PARENT) && (getLayoutParams().height == LayoutParams.MATCH_PARENT))
            mScale = Math.min(((float)height)/mFrameHeight, ((float)width)/mFrameWidth);
        else
            mScale = 0;

デバイスが縦位置のときは,得られた画像データを90度回転して表示します。(回転すると幅・高さがframesizeに一致します。)
画像の回転は,JavaCamera2ViewPlus.javaのclass JavaCamera2Frame内で行います。
画像がgrayscaleのときは,Mat gray()で,rgbaのときはMat rgba()で画像の回転を行っています。
CameraBridgeViewBase中でmScale倍されてdisplaysizeに変換されてスクリーンのユーザー領域に表示されます。

例 P10Lite
CameraFrameSize候補:3968x2976,1920x1080,1440x1080,1280x960,1280x720, 960x720,960x544,720x720,640x480,352x288,320x240,208x144,176x144

〇 横位置の場合(最大PreviewSize)
availablesize:1776x1008
previewsize:1280x960
framesize:1280x960
mScale:1.05
displaysize:1344x1008 availablesizeと比べて幅方向に余白432pix
FPS:約25

〇 横位置の場合(960x720程度を所望したとき)
availablesize:1776x1008
previewsize:960x720
framesize:960x720
mScale:1.4
displaysize:1344x1008 availablesizeと比べて幅方向に余白432pix
FPS:約25  (改善されない 当該機種ではこれが最高性能か?)

〇 縦位置の場合(最大PreviewSize)
availablesize:1080x1740
previewsize:1440x1080
framesize:1080x1440 (previewsizeと幅・高さが逆になっている)
mScale:1.0
displaysize:1080x1440 availablesizeと比べて高さ方向に余白300pix
FPS:約15

〇 縦位置の場合(720x960程度を所望したとき)
availablesize:1080x1740
previewsize:960x720
framesize:720x960 (previewsizeと幅・高さが逆になっている)
mScale:1.5
displaysize:1080x1440 availablesizeと比べて高さ方向に余白300pix
FPS:約20  (すこし改善されている)


9 追加テスト

Pixel4aを入手したので,これもテストしてみました。

Pixel4a
最大サイズ
横位置 displaysize:1920x 960 FPS:30
縦位置 displaysize:1080x1920 FPS:26
やや小さめサイズ
横位置 displaysize:1280x 720 FPS:30
縦位置 displaysize:1080x1440 FPS:30
小さめサイズ
横位置 displaysize: 800x 600 FPS:30
縦位置 displaysize: 600x 800 FPS:30

さすがに最大サイズの縦位置では30FPSにはならなかったものの,ハードウエアの進歩が感じられました。

10 画面の余白(余黒?)をなくして,全画面に表示したい

スクリーンのユーザ領域の縦横比とカメラの制約によるプレビューサイズの縦横比が異なるため, 表示に余白が生じます。画像処理用途では,「画面の余白(余黒?)をなくして,全画面に表示したい」 という要求はないと思いますが,見かけはきれいな方がよいと思う場合もあるでしょう。 その場合は 表示倍率を大きくして,プレビューの一部を捨てると全画面表示にすることができます。
JavaCamera2ViewPlus.javaのメソッドconnectCamera()で,
Math.min(((float)height)/mFrameHeight, ((float)width)/mFrameWidth);

Math.max(((float)height)/mFrameHeight, ((float)width)/mFrameWidth);
に変更することで実現できます。表示倍率を大きくして,縦横で長い側に合うようにして表示画像を画面からはみ出させています。
これは表示が変わるだけで,MainActivityのonCameraFrame()で扱っている元のMatの画像情報は変化しません。



11 常に正しい向きで表示させる仕組み

デバイスを縦位置にして使っても横位置にして使っても,常に正しい向きで表示させるには,「 デバイスの回転角」,「カメラ光軸の方向(カメラの正面方向)」,「カメラ(イメージセンサ)の設置角 (デバイスに対してどの向きにカメラがついているか)」の3つの値が必要になります。

デバイスの回転角
 通常の使い方では,デバイスを縦(portrait)に構えるので,それが初期位置で0度になります。 デバイスを時計回りに倒して横向き(landscape)にしたときが90度,初期位置から反時計回りに倒して横向きにしたときが270度になります。

カメラ光軸の方向
 バックカメラの向きを0度としたとき,フロントカメラの向きは180度と考えるのが都合がよいでしょう。

カメラ(イメージセンサ)の設置角
 イメージセンサは横向き(landscape)についているので, 横長カメラの上辺がデバイスの3時方向のときカメラの設置角は90度,デバイスの9時方向のとき270度になります。
この3つの情報を得て,画像を回転させる角度を求めているのが,クラスJavaCamera2ViewPlusのメソッドgetRotationIndex()になります。 この関数の返す値は,角度を90で除した値が使われています。
そしてこの「回転させる角度」を使って,rgb()あるいはgray()のメソッド中で, 画像(Mat形式の画像)を正しく回転させて表示に使えるようにしています。

12 終わりに

・Android Studioで「OpenCV4.9.0のカメラプレビュー」をもとに縦位置でも正しく表示するカメラプレビューアプリを作る手順をまとめました。
・縦位置の場合,画像の回転操作が入るので,プレビューサイズが大きいとどうしてもFPSが下がってしまいます。 しかし,プレビューサイズ(解像度)を小さくすると改善することができます。