Android OpenCV4.11.0のカメラプレビュー(kotlin)

2025.2.22 2020.1.4 Coskx Lab  

1 はじめに

OpenCV4.11.0 android-sdkのcamera2API対応クラスを使用して,カメラプレビューアプリを作ります。
OpenCVのライブラリモジュールはjavaで記述されていますが,MainActivityをkotlinでの記述としました。
Android5.0以降ではhardware.camera APIは非推奨になっています。
そのためhardware.camera2 APIを使用します。

このkotlin版は,次のjava版から作成されています。
    OpenCV4.11.0のカメラプレビュー(java) ≫ 

2 使用環境

3 準備

3.1 Android Studio

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

3.2 OpenCVのダウンロード

次のところからアンドロイド版をダウンロードします。
https://opencv.org/releases/
(この説明ではカレントのOpenCV 4.11.0を使っていますが,適当に変えて読みかえてください。)
opencv-4.11.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 の名前は OcvKotTest とします。(別の名前でもOK)
・AppおよびOPenCVモジュールでkotlinを使用するので,
   Language → kotlin
   Build configuration language → Kotlin DSL(build.gradle.kts)
 とします。
・パッケージ名は「jp.gr.java_conf.coskx.ocvkottest」(この文書での)です。デフォルトのパッケージ名のままでもOKです。
 パッケージ名はこの後,何回か出てきます。
・他の設定はそのまま -> Finish

ここで,MainActivity.ktを開くと,先頭がpackage名設定行で,ここに書いてあるのがパッケージ名です。
この文書でのパッケージ名は「jp.gr.java_conf.coskx.ocvkottest」です。
この後,何回か使いますので,作業中のパッケージ名をメモを取っておくとよいと思います。
出来上がったプロジェクトはそのまま(メニューバーの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」のままになります。

6 gradleファイルの修正

ここではSDKバージョンの設定をそろえる作業と,compileOptions,kotlinOptionsの整合性を合わせる作業を行います。

6.1 ファイルOpenCV\build.gradle,あるいはbuild.gradle(:OpenCV)内の compileSdkVersion targetSdkVersion などを
ファイルapp\build.gradle.kts,あるいはbuild.gradle.kts(:app)
と一致させておきます。

android {
    namespace 'org.opencv'
    compileSdk = 35

    defaultConfig {
        minSdk = 24
        targetSdk = 35

その下の方のcompileOptionsのところの設定を「JavaVersion.VERSION_21」に変更します。 これをしておかないと,コンパイル時に設定が合っていない(compileDebugKotlinは21だよ)と叱られます。
(この振る舞いはandroid studioの更新に伴って変化しているようです。API34では17でした。)

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_21
        targetCompatibility JavaVersion.VERSION_21
    }

targetSdk 35 のところに警告が出ますが,そのままにしておきます。
右上の青い字の「Sync Now」でsyncします。

6.2 ファイルapp\build.gradle.kts,あるいはbuild.gradle.kts(:app)内のcompileOptionsを「JavaVersion.VERSION_21」にし, kotlinOptionsの設定を「jvmTarget = "21"」に変更します。

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }
    kotlinOptions {
        jvmTarget = "21"
    }

右上の青い字の「Sync Now」でsyncします。

7 依存関係の設定

ここでの説明用画面の表示が古いままのものですが,似ているので読み替えてください。

プロジェクトOcvKotTestが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」が増えている。
(プロジェクトOcvKotTestの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すれば良いようです。

8 OcvKotTestの編集

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

8.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.OcvKotTest"
        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>

8.2 activity_main.xmlの編集

app/res/layout/activity_main.xmlを編集します。 「Hello World!」を表示するような画面設定(Textview)を消して,camera2APIを使用したJavaCamera2Viewのカメラビューを表示する設定に直します。

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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- JavaCamera2View was placed instead of TextView -->
    <org.opencv.android.JavaCamera2View
        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>

ここで,layoutの設定にactivity_main.xmlという名前のファイルを使っています。
その中の表示領域のIDとしてcamera_viewという名前を使いました。
この2つの名前を後でMainActivity.ktの編集で使います。
(app:camera_id="back" のところを app:camera_id="front" と変更すると,スクリーン側のカメラが使えます。)

8.3 MainActivity.ktの編集

(1) onCreate()中でlayoutのIDの記述の整合性を取る。(layout/activity_main.xmlへの依存)

(2) onCreate()中でOpenCVをロードする。
(3) パーミッションを取得し,mOpenCvCameraViewに取得を知らせる。
 onStart()中でパーミッションを確認

MainActivity.kt
package jp.gr.java_conf.coskx.ocvkottest

import android.Manifest.permission.CAMERA
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.SurfaceView
import android.view.WindowManager
import android.widget.Toast
import org.opencv.android.CameraBridgeViewBase
import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame
import org.opencv.android.OpenCVLoader
import org.opencv.core.Mat

class MainActivity : Activity(), CameraBridgeViewBase.CvCameraViewListener2 {
    private val TAG: String = "OCVSample::Activity"
    private val CAMERA_PERMISSION_REQUEST_CODE: Int = 200
    private var mOpenCvCameraView: CameraBridgeViewBase? = null

    override fun onStart() {
        super.onStart()
        var havePermission = true
        if (checkSelfPermission(CAMERA) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf<String>(CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
            havePermission = false
        }
        if (havePermission) {
            mOpenCvCameraView!!.setCameraPermissionGranted()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
        ) {
            if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                mOpenCvCameraView!!.setCameraPermissionGranted()
            }
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val result: Boolean = OpenCVLoader.initLocal()
        val versionString: String = openCVVersionDisplay(result)
        val toast = Toast.makeText(applicationContext, versionString, Toast.LENGTH_LONG)
        toast.show()

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

        mOpenCvCameraView = findViewById(R.id.camera_view)

        mOpenCvCameraView!!.setVisibility(SurfaceView.VISIBLE)

        mOpenCvCameraView!!.setCvCameraViewListener(this)

        //mOpenCvCameraView!!.setMaxFrameSize(1280, 720)

    }

    private fun openCVVersionDisplay(arg: Boolean): String {
        return when (arg) {
            true -> "OpenCV Version: " + OpenCVLoader.OPENCV_VERSION
            false -> "ERROR: OpenCV Load Failed"
        }
    }

    public override fun onPause() {
        super.onPause()
        mOpenCvCameraView!!.disableView()
    }

    public override fun onResume() {
        super.onResume()
        mOpenCvCameraView!!.enableView()
    }

    public override fun onDestroy() {
        super.onDestroy()
        mOpenCvCameraView!!.disableView()
    }

    override fun onCameraViewStarted(width: Int, height: Int) {
    }

    override fun onCameraViewStopped() {
    }

    override fun onCameraFrame(inputFrame: CvCameraViewFrame): Mat {
        return inputFrame.rgba()
    }

}

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

プレビュー表示が,スマホなどのデバイスの画面を有効に使っていない(画面に比べて狭い)と感じたら,
もう少し広くすることができます。

override fun onCreate(savedInstanceState: Bundle?) {
 :
}

の最後の行は

mOpenCvCameraView!!.setCvCameraViewListener(this)

ですが,その直後に

//requesting preview size option (maxWidth > maxHeight)
mOpenCvCameraView!!.setMaxFrameSize(1280, 720)

を加えてみてください。画面サイズを1280x720に近い都合の良いサイズで表示してくれます。
あまり大きな値にするとfps(表示速度)が遅くなります。


9 画像処理例

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

    override fun onCameraViewStarted(width: Int, height: Int) {
        mMat = Mat(height, width, CvType.CV_8UC4)
    }

    override fun onCameraViewStopped() {
        mMat!!.release()
    }

    private var mMat: Mat? = null

    override fun onCameraFrame(inputFrame: CvCameraViewFrame): Mat? {
        //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.ktの先頭部分に
 ・import org.opencv.core.Core
 ・import org.opencv.core.Mat
 ・import org.opencv.imgproc.Imgproc
が必要になりますが,android studioがうまく解決してくれます。

10 終わりに

・Android Studio kotlinでOpenCV4.11.0のカメラプレビューテストアプリを作る手順をまとめました。





補足1 アクションバーを付ける

MainActivityはActivityからextendsされているため,アクションバーなどAppCompatActivityの機能がありません。
必要ならActivityをAppCompatActivityからextendsします。(ただし,横位置の場合表示領域が狭くなります。)

補足2 JavaCameraView, JavaCamera2View, CameraBridgeViewBase

OpenCVのandroid向けライブラリでは,java/org.opencv/androidの中にカメラビュー向けのクラスがあります。
CameraBridgeViewBaseはSurfaceViewを継承していて,カメラのハードウェアに依存しない(デバイスの表示機能)部分を担当しています。

カメラのハードウエアに近い部分は,JavaCameraViewあるいはJavaCamera2Viewが担当していています。
JavaCameraView, JavaCamera2ViewはともにCameraBridgeViewBaseを継承していて,カメラのハードウェアに依存している部分を担当しています。
カメラビューアプリではJavaCameraView, JavaCamera2Viewのうちどちらか片方が使用されます。

JavaCameraViewは古いAPI(hardware.Camera)が使用されていて,Android5.0(Lollipop)以降(APIレベル21以降)では非推奨となっています。
android studioでJavaCameraView.javaをみると,取り消し線だらけになります。)

JavaCamera2ViewはJavaCameraViewと同等の役割を果たしていて,APIレベル21以降でも問題なく使用できるAPI(hardware.camera2)が使われています。