5
0

【JetpackCompose】拝啓。カメラ映像の写真の背景と前景を分けたいです。【ML Kit】

Last updated at Posted at 2023-12-17

はじめに

こんにちは。私はAndroidを趣味として開発しているまっつー🀄です。
最近Googleの勢いが半端ないですよね!Google PaLMがGeminiに進化するなど去年からこんなにも基板モデルがすごく成長しているのを見て僕もワクワクが止まりません!!!
(小声・・・でもちょっと最近AIが突っ走っているのは将来まずいと思いますが...)
そんなワクワクのなか、最近のNow in Android(なうい)で前からあった機能が公式で発表されていることを発見したので今更ながら開発方法をまとめてみました。

Modifier.Nodeを使って開発したかったのですが、わざわざ使ってまで開発するところがありませんでした...

本記事のゴール

この動画のような機能が作れます。
動画.gif
二つの物体をリアルタイムに認識できています。複数認識できます。
また認識した物体の背景を切り抜いて表示することができます。
ちなみに公式ドキュメントを読むとわかるのですが、このAPIはリアルタイムで認識することを推奨していません!ワンショット(と聞けばあの曲)で認識をおすすめしています。

本記事のサンプルコードがありますので早見するかたはどうぞ!
https://github.com/Takuchan/DelSelfBack

この記事であなたが学べる事

JetpackComposeを使って

  • カメラ機能を使えるようになる
  • リアルタイム画像解析方法を勉強できる
  • 背景と前景を分離するプログラムを書ける
  • MVVMでの書き方を学べる

私の環境

  • Dell Inspiron
  • Intel Core i7
  • 実機 Pixel7
  • エミュレータ Pixel7 Pro API 34
  • Android Studio Hedgehog
    • プロジェクト名 MLkit_selfback
    • JetpackCompose
    • Kotlin gradle (build.gradle.kts)
    • compileSDK API34

今回使うMLKitについて

今回はGoogleのMLKitというものを使用します。MLkitとは簡単に言うとGoogleが準備したほぼ完ぺきといえるAIモデルをAndroidに組み込むこんで、画像の推論をさせることができるサービスのことです。
一応 AndroidiOS でもMLKitのモデルは提供されていますがAndroidの方がモデルの豊富さでは軍配が上がります。
MLkit ホームページ

今回はこちらサイトの中の

Android向けMLKitを使用して被験者を分類する

https://developers.google.com/ml-kit/vision/subject-segmentation/android?hl=ja
を利用します。

早速プログラムを作ろう!(前半)

前半ではカメラのプレビュー機能を作成します。

今回はKotlin のJetpacokcomposeで開発を行っていきますが、途中カメラの使用をする際にAndroidViewという結局のところXMLをJetpackComposeで呼び出すという動作をさせます。
本記事では動作検証をする予定はありません。ChatGPTなどを使えばできるかもしれないです。

手順① build.gradle.kts(module:app)に追加

以下のように新しく行を追加します。
(小声)もし、私の記事を使って開発してみる方は「いいね」「ストック」をお願いします。励みになります🥰🫶

build.gradle.kts(module:app)
var mlkitVer = "16.0.0-beta1"
implementation("com.google.android.gms:play-services-mlkit-subject-segmentation:$mlkitVer")
implementation("androidx.camera:camera-core:1.3.1")
implementation("com.google.mlkit:vision-common:17.3.0")

//CameraLibrary
// CameraX core library using camera2 implementation
implementation ("androidx.camera:camera-camera2:1.0.0-rc01")
// CameraX Lifecycle Library
implementation ("androidx.camera:camera-lifecycle:1.0.0-rc01")
// CameraX View class
implementation ("androidx.camera:camera-view:1.0.0-alpha20")

//viewmodel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
implementation("androidx.compose.runtime:runtime-livedata:1.5.4")

手順② Android端末のHW利用権限を取得

Andoid端末ではInternetCamera を利用する際には Manifest.xml に利用することを宣言する必要があります。
その設定を行わないとカメラを利用するプログラムを書いてもAndroid側が権限を解放してくれないので使えないままになってしまいます。
今回はあらかじめ利用許可を申請しておきましょう。

AndroidManifest.xml
<manifest ...>
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    <uses-permission android:name="android.permission.CAMERA"/>
    <application ...>
        <meta-data
          android:name="com.google.mlkit.vision.DEPENDENCIES"
          android:value="subject_segment" />
        ...
    </application>
</manifest>

手順③ 画像処理をするための前準備のプログラム

このプログラムはカメラの1フレームの画像を処理させるプログラムです。
ImageAnalysis.Analyzer というクラスを継承して書いてきます。

DelBackImageAnalyzer.kt
private class DelBackImageAnalyzer(
    private val listener: (ImageProxy) -> Unit
) : ImageAnalysis.Analyzer {

    @SuppressLint("UnsafeOptInUsageError")
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

新しくContext.ktというファイルを作成してください。そして以下を入力

Context.kt
suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation ->
    ProcessCameraProvider.getInstance(this).also { future ->
        future.addListener({
            continuation.resume(future.get())
        }, executor)
    }
}

val Context.executor: Executor
    get() = ContextCompat.getMainExecutor(this)

Contextのクラスに新しいメソッドを追加するようにプログラムを書いていきます。
カメラ用にProviderを設定することでクラスの責任を削減できます。

手順④ Composeでカメラ表示を行うプログラムを開発

CameraPreview.kt
@SuppressLint("UnsafeOptInUsageError")
@Composable
fun CameraPreview(
    cameraExecutorService: ExecutorService = Executors.newSingleThreadExecutor()
){
    val coroutineScope = rememberCoroutineScope()
    val lifecyclerOwner = LocalLifecycleOwner.current

    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = {context ->
            val previewView = PreviewView(context).apply {
                this.scaleType = scaleType
                layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
            }

            val imageAnalyzer = ImageAnalysis.Builder()
                .build()
                .also {
                    it.setAnalyzer(cameraExecutorService, DelBackImageAnalyzer{frameImage ->
                        val mediaImage = frameImage.image
                        if(mediaImage != null){
                            val image = InputImage.fromMediaImage(mediaImage,frameImage.imageInfo.rotationDegrees)
                        }
                    })
                }
            val previewUseCase: Preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }
            coroutineScope.launch{
                val cameraProvider = context.getCameraProvider()
                try {
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecyclerOwner,
                        CameraSelector.DEFAULT_BACK_CAMERA,
                        previewUseCase,
                        imageAnalyzer
                    )
                }catch (ex: Exception){
                    Log.e(TAG,"Use case binding failed",ex)
                }
            }
            previewView
        }

    )

}

このプログラムを説明していきます。

cameraExecutorService

CameraPreviewという名前の関数を定義して、カメラの操作を非同期で実行するようにしていまsう。

imageAlalyzer

この変数はカメラから取得した画像を解析するためのものです。setAnalyzerメソッドには、解析を実行するExecutorServiceと画像解析を行うImageAnalyzerが指定されています。
先ほど作成したDelBackImageAnalyzerのクラスでコールバック関数が含まれていたのでこの画面でも画像処理をすることができるようになっています。
しかし、今回は各クラスの責任を減らしたいためこの画面では書かないようにしています。

previewUseCase

カメラをプレビューするものです。ビルダーを起動させています。

④ 一番最後の行にあるpreviewView

最後に、作成したPreviewViewを返しています。これが、このCompose関数によって表示されるビューになります。このビューは、カメラのプレビューを表示します。カメラから取得した画像は、DelBackImageAnalyzerによって解析されます。このコードはカメラのプレビューと画像解析を行うためのUIを構築します。

カメラが映るかのビルドをしましょう。

この段階でカメラが映るようになりました。
カメラが映るかどうかの検証をしてから次のステップに進むことをお勧めします。ではJetpackComposeの最大の利点一行で表示させるプログラムを書くとしますか~

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MLKit_seflbackTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    CameraPreview()
                }
            }
        }
    }
}

端末にインストールが終わったら、アプリケーションの方でパーミッション許可を行ってください。
最近のAndroidAPIではパーミッション許可をKotlinのコードで尋ねるようにしなくてはならなくなりました。
僕は今回そのコードを割愛するのでアプリ設定の方で手動でお願いします。

早速プログラムを作ろう!(後半)

後半では画像解析のプログラムを書きます

① ViewModelを作成しよう

画像から切り抜いた写真をカメラのした半分のプレビュー画面にて表示できるようにViewModelを作成しましょう。

SelfViewModel.kt
class SelfViewModel: ViewModel() {
    private val _image = MutableLiveData<MutableList<Bitmap>>()
    val image: LiveData<MutableList<Bitmap>> = _image

    fun clearImages(){
        _image.value = mutableListOf()
    }
    
    fun setImages(images: MutableList<Bitmap>){
        _image.value = images
    }
}

②背景を透過させるプログラムを書きましょう

DelBackImageAnalyzer.ktを以下のように編集します。

DelBackImageAnalyzer.kt

class DelBackImageAnalyzer(
    selfViewModel: SelfViewModel,
    private val listener: (ImageProxy) -> Unit
) : ImageAnalysis.Analyzer {
    val ssselfViewModel = selfViewModel

    private val subjectResultOptions = SubjectSegmenterOptions.SubjectResultOptions.Builder()
        .enableSubjectBitmap()
        .build()

    val options = SubjectSegmenterOptions.Builder()
        .enableMultipleSubjects(subjectResultOptions)
        .build()

    val segmenter = SubjectSegmentation.getClient(options)
    @SuppressLint("UnsafeOptInUsageError")
    override fun analyze(imageProxy: ImageProxy) {

        val mediaImage = imageProxy.image

        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            segmenter.process(image)
                .addOnSuccessListener { segmentationMask ->
                    // Task completed successfully
                    // ...
                    val subjects = segmentationMask.subjects
                    val bitmaps = mutableListOf<Bitmap>()
                    for (subject in subjects) {
                        val mask = subject.bitmap
                        if (mask != null) {
                            bitmaps.add(mask)
                        }
                    }
                    ssselfViewModel.clearImages()
                    ssselfViewModel.setImages(bitmaps)
                    listener(imageProxy)
                    imageProxy.close()

                }
                .addOnCanceledListener() {
                    imageProxy.close()
                }
            // Pass image to an ML Kit Vision API
            // ...
        }

    }
}

プログラム解説

selfviewmodel(引数)

MainActivity.ktで先ほど作ったViewModelのデータを取得しています。データが更新されたら自動的にMainActivityの画面で切り抜いた画像が表示されるようにしています。

sssslefViewModel

良い命名をしたかったのですが、思いつかなかったです。このクラスだけでしか使わないし、まっいいかってことで適当に命名しました。
引数のViewmodelをクラス内で利用できるようにしたものです。

subjectResultOptions, options,segmenter

これはML kitで背景切り抜きに使うときのおまじないです。
https://developers.google.com/ml-kit/vision/subject-segmentation/android?hl=ja#foreground_confidence_mask
このあたりの記事を参考にして書きました。
また今回は、複数のオブジェクトの背景を切り抜きたかったので複数モードを選択しました。

if(mediaImage != null)以下

これは画像処理を行う部分のプログラムです。
複数認識したオブジェクトを判別するためにfor文でAIが判別したオブジェクトをぶん回しています。
判別したオブジェクトはBitMap形式で取り出して先ほどのViewModelに保存するようにしています。

imageProxy.close()について
このプログラムを書かないと画像処理がいつ終わるかをCameraPreview.ktに通知できないため、一回しかオブジェクト検知がされなくなってしまいます。

③CameraPreviewを編集

CameraPreview.kt
@SuppressLint("UnsafeOptInUsageError")
@Composable
fun CameraPreview(
    modifier: Modifier,
    selfViewModel: SelfViewModel,
    cameraExecutorService: ExecutorService
){
 ....
             val imageAnalyzer = ImageAnalysis.Builder()
                .build()
                .also {
                    it.setAnalyzer(cameraExecutorService, DelBackImageAnalyzer(selfViewModel){frameImage ->
                        Log.d("カメラ情報","取得中")
                        val mediaImage = frameImage.image
                        if(mediaImage != null){
                            val image = InputImage.fromMediaImage(mediaImage,frameImage.imageInfo.rotationDegrees)
                        }
                    })
                }
...

先ほど、DelBackImageAnalyzer.ktに引数を追加したので、それに対応した値を入れるように修正しました。
またcameraExecutorServiceをMainAcitivtyで準備するようにプログラムを変えたので、MainActivity.kt側の処理はこうなります。

④MainAcitivtyをいかのように編集

MainAcitivy.kt

class MainActivity : ComponentActivity() {
    private lateinit var selfViewModel: SelfViewModel
    private lateinit var cameraExecutor: ExecutorService
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        selfViewModel = ViewModelProvider(this)[SelfViewModel::class.java]
        cameraExecutor = Executors.newSingleThreadExecutor()
        setContent {
            MLKit_seflbackTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val images = selfViewModel.image.observeAsState(initial = mutableListOf())
                    Column(modifier = Modifier.fillMaxSize()) {
                        CameraPreview(modifier = Modifier.fillMaxWidth().height(300.dp),selfViewModel = selfViewModel,
                            cameraExecutorService = cameraExecutor)
                        LazyColumn{
                            items(images.value){ bitmap->
                                    Image(bitmap = bitmap.asImageBitmap(), contentDescription = null,modifier = Modifier.rotate(90f).size(300.dp))
                                

                            }
                        }
                    }
                }
            }
        }
    }
}

終わりに

最近のNow in Android(なうい)は最高に楽しいです。
今後もNow inで面白い機能が紹介されていたら記事にしたいなと思います。

今回は読んでいただきありがとうございました。
記事が面白ければ「いいね」「ストック」をお願いします。
励みになります🥰🥰🥰🥰🥰

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0