Android OpenCV4.10.0のカメラプレビュー(tutorialを基に)

2024.6.5 2020.1.4 Coskx Lab  

1 はじめに

この資料では,OpenCV sdkの配布zip中のsamples/tutorial-1をもとにAndroidStudioでカメラプレビューするアプリを構築します。
tutorial-1をもとにしているので,Android5.0以降非推奨のhardware.camera APIを使用していますが,camera2 API使用のアプリにも変更できます。
カメラ権限獲得はCameraActivityがやってくれます。

自分でカメラ権限獲得を行う場合は,次のリンク(camera2 APIを用いたプレビューアプリ)を参照してください。
    「OpenCV4.10.0のカメラプレビュー」 ≫ 

OpenCV4.1.0からOpenCV4.1.1になったときに,permission関係で大きな変更があり,4.1.0で使っていたソースコードが4.1.1では動作しなくなりました。
そのときにsamples/tutorialを参考にソースコードを書き換て,トラブルを脱したことがありました。
samples/tutorialを参考に作り直す手順は,今後も必要な時があると思い,このWebページを作成しました。
今回の4.10.0でもCameraBridgeViewBase.javaを中心に大きな変更があり(縦位置も正しく表示できるようになった)ました。(しかし,モジュールを使う側では変更の必要はありませんでした。)

2 使用環境

3 準備

3.1 Android Studio

インストール済とします。

3.2 OpenCVのダウンロード

次のところからアンドロイド版をダウンロードします。
https://opencv.org/releases/
(この説明ではカレントのOpenCV 4.10.0を使っていますが,適当に変えて読みかえてください。)
opencv-4.10.0-android-sdk.zip
がダウンロードされるので,解凍すると
OpenCV-android-sdkという名前のディレクトリができます。
この中には
 ファイル LICENSE
 ファイル README.android
 ディレクトリ samples
 ディレクトリ sdk
が入っています。
OpenCV-android-sdk を適当な場所に置いてください。
(この説明ではCドライブのルートに置きました。)

4 Android Studio で新しいproject

Android Studio で新しいprojectを作ります。新しいプロジェクトでカメラプレビューアプリを作ります。
・ファイルメニュー File -> New -> New Project --> Empty Views Activity
・New Project の名前は OCVpreview とします。(別の名前でもOK)
・OPenCVモジュールでjavaを使用するのでBuild configuration language は Groovy DSL(build.gradle)とします。
・他の設定はそのまま -> Finish

ここで,MainActivity.javaを開くと,先頭がpackage名設定行で,ここに書いてあるのがパッケージ名です。
このWebページでのパッケージ名は「jp.gr.java_conf.coskx.ocvpreview」です。
この後,何回か使いますので,作業中のパッケージ名をメモを取っておくとよいと思います。
出来上がったプロジェクトをそのまま(メニューバーのRun -> Run'App' を選び)実行すると,これまでの経験通り「Hello world!」が表示されます。

5 OpenCVのライブラリをモジュールとして取り込む

OpenCVのライブラリをモジュールとして取り込む作業では,C:\OpenCV-android-sdk\sdkを取り込みます。
(1)android studio メニューバーのFile -> `New -> Import Module を選ぶ。
(2)Source directory に,C:\OpenCV-android-sdk\sdk を指定し,OK。

ここでModule nameボックスが現れるので,Module nameをsdkからOpenCVに変更します。
(sdkのままでも良いのですが,後から見ると意味不明になりますので,変更します。実際にはバージョン番号まで入れると後から,都合がよいと思います。)
変更しなかった時には,この後もモジュール名は「sdk」のままになります。

ここで次のエラーが表示されるでしょう。

Build file '.......AndroidStudioProjects\OCVpreview\OpenCV\build.gradle' line: 93

A problem occurred evaluating project ':OpenCV'.
> Plugin with id 'kotlin-android' not found.


そしてそのエラーの生じているファイル(OpenCV\build.gradle,あるいはbuild.gradle(:OpenCV))も自動的に開かれます。次のように対処します。

(A) kotlinは使用しないとき
Gradle Script/build.gradle(Module OpenCV)

apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'kotlin-android'

apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
//apply plugin: 'kotlin-android'
のようにコメントアウトしておきます。

(B) kotlinを使用するとき
Gradle Script/build.gradle(Project)の
buildscript {
中の
dependencies {
の中に
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31"
を加えます。(バージョンは最新の番号にしてください)

なお,この段階で,作業中のファイル(OpenCV\build.gradle,あるいはbuild.gradle(:OpenCV))内の compileSdkVersion targetSdkVersion などを新しい記述に変更し, 値も最新のバージョン(2024.1.17時点では34)に変更しておくと良いと思います。

 compileSdkVersion 31 → compileSdk 34
 minSdkVersion 21 → minSdk 24
 targetSdkVersion 31 → targetSdk 34
targetSdk 34 のところに警告が出ますが無視して進みます。

6 依存関係の設定

プロジェクトOCVCameraがOpenCVのモジュールを使うこと(に依存していること)を設定します。ただし,OpenCVのモジュール名は「OpenCV」となっています。

(1)メニューバーのFile -> Project Structure を選ぶ。

(2)左側のDependenciesを選び,Modulesでapp を選ぶ。
(この段階ではModule appのDeclared Dependencies中にモジュール「OpenCV」は無い)


(3)Declared Dependencies の+-と書いてあるところで,+(add) をクリックし、3: Module dependenciy を選ぶ。


(4)Add Module Dependenciesダイアログで モジュール「OpenCV」にチェックを入れて(選らんで),OK。


 Module appのDeclared Dependencies中のapp中にモジュール「OpenCV」が増えている。
(プロジェクトOCVCameraのappがモジュール「OpenCV」を使うことが設定された。)


(5)OK。
 (Syncが終了するまで待つ 黄色の帯にTry Againが表示されたらそれをクリック)
Gradle Script/build.gradle(Module.app)のDependencies内に
 implementation project(':OpenCV')
が追加されていればOK。

Android Studio左側project中にapp, OpenCVが並んで表示されたらOK


★ 実は,上記の(1)から(5)の作業は,
Gradle Script/build.gradle(Module.app)のDependencies内に
 implementation project(':OpenCV')
を追加する作業のようなので,この1行を追加しSyncすれば,それだけで良いようです。

7 OCVpreviewの編集

OCVpreviewのProjectでは次の3点を編集します。

7.1 AndroidManifast.xmlの編集

app/manifests/AndroidManifast.xmlにおいて,カメラを使うことを宣言します。次のように6行追加になります。

AndroidManifast.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!--↓追加 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-feature android:name="android.hardware.camera2" android:required="false"/>
    <uses-feature android:name="android.hardware.camera2.autofocus" android:required="false"/>
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    <!--↑追加-->

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.OCVpreview"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

7.2 activity_main.xmlの編集

Res/Layout/activity_main.xmlを編集します。 「Hello World!」を表示するような画面設定(Textview)を消して,カメラビューを表示する設定に直します。
変更部分は,チュートリアルのxmlから持ってきました。(app:camera_id="back" を追加してあります。)

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">

    <!--↓変更-->
    <org.opencv.android.JavaCameraView
        android:id="@+id/camera_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        tools:layout_editor_absoluteX="0dp"
        tools:layout_editor_absoluteY="0dp"
        app:camera_id="back"
        app:show_fps="true"/>
    <!--↑変更-->

</androidx.constraintlayout.widget.ConstraintLayout>

ここで,layoutの設定にactivity_main.xmlという名前のファイルを使っています。
その中の表示領域のIDとしてcamera_viewという名前を使いました。
この2つの名前を後でMainActivity.javaの編集で使います。
org.opencv.android.JavaCameraViewのところをorg.opencv.android.JavaCamera2Viewに書き換えると,hardware,camera API使用のアプリからcamera2 API使用のアプリになります。
(app:camera_id="back" のところを app:camera_id="front" と変更すると,スクリーン側のカメラが使えます。)

7.3 MainActivity.javaの編集

C:\OpenCV-android-sdk\samples\tutorial-1-camerapreview\src\org\opencv\samples\tutorial1.java
の中身を作業中のProjectのMainActivity.java内に上書きコピーして,必要な名前の変更をする手順で作業します。

●最初にtutorial1.javaの2行目以降をMainActivity.javaの2行目以降に上書きコピーします。
そうすると,先頭行のパッケージ名(package)は残ります。
●次に必要な名前の書換をします。
1)Activityの名前を元のMainActivityに戻します。
 MainActivity.java中のすべての「Tutorial1Activity」という名前を「MainActivity」に置換します。
2)onCreate中のlayoutのIDの記述の整合性を取ります。(layout/activity_main.xmlへの依存)
 setContentView(R.layout.tutorial1_surface_view);
 を
 setContentView(R.layout.activity_main);
3)onCreate中のViewのIDの記述の整合性を取ります。(layout/activity_main.xml内の「camera_view」への依存)
 mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.tutorial1_activity_java_surface_view);
 を
 mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.camera_view);
4)この手順では起こらないと思いますが,もし,未解決の名前(赤字)が出てきたら,未解決の名前の解決をstudioの自動修正機能の助けを借りて Alt+Enterで行います。(赤字がなくなるまで)

OpenCV4.10.0(4.1.1以降なら同様)の場合は次のようなMainActivity.javaができます。

MainActivity.java
package jp.gr.java_conf.coskx.ocvpreview;
import org.opencv.android.CameraActivity;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.Mat;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;

import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.Toast;

import java.util.Collections;
import java.util.List;

public class MainActivity extends CameraActivity implements CvCameraViewListener2 {
    private static final String TAG = "OCVSample::Activity";

    private CameraBridgeViewBase mOpenCvCameraView;

    public MainActivity() {
        Log.i(TAG, "Instantiated new " + this.getClass());
    }

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "called onCreate");
        super.onCreate(savedInstanceState);

        //! [ocv_loader_init]
        if (OpenCVLoader.initLocal()) {
            Log.i(TAG, "OpenCV loaded successfully");
        } else {
            Log.e(TAG, "OpenCV initialization failed!");
            (Toast.makeText(this, "OpenCV initialization failed!", Toast.LENGTH_LONG)).show();
            return;
        }
        //! [ocv_loader_init]

        //! [keep_screen]
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        //! [keep_screen]

        setContentView(R.layout.activity_main);

        mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.camera_view);

        mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);

        mOpenCvCameraView.setCvCameraViewListener(this);
    }

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

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

    @Override
    protected List<? extends CameraBridgeViewBase> getCameraViewList() {
        return Collections.singletonList(mOpenCvCameraView);
    }

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

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

    @Override
    public void onCameraViewStopped() {
    }

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

ここまでで,buildすると,とりあえず動作するはずです。


8 bulidと実行

不要なimportや不要な変数名もあります。Android Studioではそれらがグレーの文字になっています。消去しても構いません。

onCameraFrame() はカメラが1フレーム取得するたびに呼ばれる関数です。
引数(inputFrame)にカメラからの入力フレーム画像が入り,戻り値で表示したいMatを戻します。
Mat mMatを用意して,初期化と廃棄をするようにして,関数onCameraFrame()を次のように変更すると,ネガポジ反転で表示します。
複数の表示方法の中から1つだけ選ぶことができます。

    public void onCameraViewStarted(int width, int height) {
        mMat = new Mat(height, width, CvType.CV_8UC4);
    }

    public void onCameraViewStopped() {
        mMat.release();
    }

    private Mat mMat;

    public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
        //mMat= inputFrame.rgba();    //color
        //mMat= inputFrame.gray();    //grayscale
        Core.bitwise_not(inputFrame.rgba(), mMat); //reversed
        //Core.bitwise_not(inputFrame.gray(), mMat); //grayscale reversed
        //Imgproc.Canny(inputFrame.gray(), mMat, 100, 200); //grayscale canny filtering
        //Imgproc.threshold(inputFrame.gray(), mMat, 0.0, 255.0, Imgproc.THRESH_OTSU); //grayscale binarization with Ohtsu
        return mMat;
    }

カラー画像 グレースケール画像
カラー画像リバース グレースケール画像リバース
グレースケール画像Cannyフィルタ グレースケール画像二値化
この変更では,MainActivity.javaの先頭部分に
 ・import org.opencv.core.Core;
 ・import org.opencv.core.Mat;
 ・import org.opencv.imgproc.Imgproc;
が必要になりますが,android studioがうまく解決してくれます。

9 カメラのパーミッション

カメラのパーミッションのユーザからの取得は,CameraActivityのOnStart()が舞台裏でやってくれるので,明示的な記述は不要になっています。
protected List<? extends CameraBridgeViewBase> getCameraViewList() のオーバーライドが必要なだけです。
参考
 ・CameraActivity.javaはOpenCV/java/org.opencv/androidにあります。
 ・CameraActivity.javaはOpenCV4.1.1より使われるようになったようです。

10 終わりに

・Android StudioでOpenCV4.10.0のチュートリアルをもとにカメラプレビューテストアプリを作る手順をまとめました。
・OpenCVのチュートリアルをもとににする手順なので,OpenCVバージョン変更への対応は柔軟だと思います。