はじめに
こんにちは。私はAndroidを趣味として開発しているまっつー🀄です。
最近Googleの勢いが半端ないですよね!Google PaLMがGeminiに進化するなど去年からこんなにも基板モデルがすごく成長しているのを見て僕もワクワクが止まりません!!!
(小声・・・でもちょっと最近AIが突っ走っているのは将来まずいと思いますが...)
そんなワクワクのなか、最近のNow in Android(なうい)で前からあった機能が公式で発表されていることを発見したので今更ながら開発方法をまとめてみました。
Modifier.Nodeを使って開発したかったのですが、わざわざ使ってまで開発するところがありませんでした...
本記事のゴール
この動画のような機能が作れます。
二つの物体をリアルタイムに認識できています。複数認識できます。
また認識した物体の背景を切り抜いて表示することができます。
ちなみに公式ドキュメントを読むとわかるのですが、この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に組み込むこんで、画像の推論をさせることができるサービスのことです。
一応 Android
やiOS
でも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)に追加
以下のように新しく行を追加します。
(小声)もし、私の記事を使って開発してみる方は「いいね」「ストック」をお願いします。励みになります🥰🫶
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端末ではInternet
や Camera
を利用する際には Manifest.xml
に利用することを宣言する必要があります。
その設定を行わないとカメラを利用するプログラムを書いてもAndroid側が権限を解放してくれないので使えないままになってしまいます。
今回はあらかじめ利用許可を申請しておきましょう。
<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
というクラスを継承して書いてきます。
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
というファイルを作成してください。そして以下を入力
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でカメラ表示を行うプログラムを開発
@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
の最大の利点一行で表示させるプログラムを書くとしますか~
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()
}
}
}
}
}
端末にインストールが終わったら、アプリケーションの方でパーミッション許可を行ってください。
最近のAndroid
のAPI
ではパーミッション許可をKotlinのコードで尋ねるようにしなくてはならなくなりました。
僕は今回そのコードを割愛するのでアプリ設定の方で手動でお願いします。
早速プログラムを作ろう!(後半)
後半では画像解析のプログラムを書きます
① ViewModelを作成しよう
画像から切り抜いた写真をカメラのした半分のプレビュー画面にて表示できるようにViewModelを作成しましょう。
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
を以下のように編集します。
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を編集
@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をいかのように編集
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で面白い機能が紹介されていたら記事にしたいなと思います。
今回は読んでいただきありがとうございました。
記事が面白ければ「いいね」「ストック」をお願いします。
励みになります🥰🥰🥰🥰🥰