2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jetpack Compose + CameraX + yolo v8 で物体検知アプリを作る ~カメラ実装まで~

Last updated at Posted at 2025-01-14

環境

Jetpack Compose
CameraX 1.4.0
Android SDK 35

概要

Androidで物体検知アプリを作成します。
完成するアプリのイメージです。

モデルはyolo v8を使用します。
事前準備編としてまずはJetpack Compose上でCameraXを実装します。

手順

プロジェクト新規作成→Empty Activityを選択しました。
(Empty Activityを選択するとJetpack Composeでプロジェクトが作成されます。)
image.png

プロジェクト名をobject_detection_sampleとしました。
Build configuration languageはデフォルトのKotlin DSLを選択しました。
ss.png

とりあえず実行してみたところエラーが発生します。
image.png

エラーの指示どおりにSDKバージョンを35に変更します。

build.gradle.kts
android {
    namespace = "com.android.example.object_detection_sample"
//    compileSdk = 34
    compileSdk = 35

    defaultConfig {
        applicationId = "com.android.example.object_detection_sample"
        minSdk = 30
//        targetSdk = 34
        targetSdk = 35

起動確認

次にカメラを利用するためパッケージと権限設定を追加します。

build.gradle.kts
dependencies {
    // 追加
    // camerax
    val camerax_version = "1.4.0"
    implementation("androidx.camera:camera-camera2:$camerax_version")
    implementation("androidx.camera:camera-lifecycle:$camerax_version")
    implementation("androidx.camera:camera-view:$camerax_version")
    implementation("androidx.camera:camera-video:$camerax_version")
    implementation("com.google.accompanist:accompanist-permissions:0.30.1")
    // 以下略
AndroidManifest.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-feature
        android:name="android.hardware.camera"
        android:required="false" />
    <uses-permission android:name="android.permission.CAMERA" />
    
    <!--以下略-->

</manifest>

MainActivityから既存のGreeting関連のコードを削除し、カメラの権限確認画面を追加します。
※importは必要に応じて追加してください。

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize()
                ){
                    CameraPreviewScreen()
                }
            }
        }
    }

    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun CameraPreviewScreen() {
        val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
        if (cameraPermissionState.status.isGranted) {
            // プレビュー画面を表示
        } else {
            PermissionRequest(cameraPermissionState)
        }
    }

    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun PermissionRequest(cameraPermissionState: PermissionState) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Button(onClick = {
                cameraPermissionState.launchPermissionRequest()
            }) {
                Text("Grant Camera Permission")
            }
        }
    }
}

起動して権限取得の動作を確認します。

カメラのプレビュー画面のコードを追加します。
CameraXはJetpack Composeに対応していないため実装の際はAndroidViewを使います。

MainActivity.kt
    
    // 中略
    
    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun CameraPreviewScreen() {
        val cameraPermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA)
        if (cameraPermissionState.status.isGranted) {
            // プレビュー画面を表示
            CameraPreview(modifier = Modifier.aspectRatio(3f/4f)) // 追加
        } else {
            PermissionRequest(cameraPermissionState)
        }
    }
    
    // 中略

    // 追加
    @Composable
    fun CameraPreview(modifier: Modifier = Modifier) {
        val lifecycleOwner = LocalLifecycleOwner.current
        val previewView = remember { PreviewView(this) }

        val rotation = LocalConfiguration.current.orientation

        val cameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()

        val preview = androidx.camera.core.Preview.Builder()
            .setTargetRotation(rotation)
            .setResolutionSelector(
                ResolutionSelector.Builder()
                    .setAspectRatioStrategy(
                        AspectRatioStrategy(
                            AspectRatio.RATIO_4_3,
                            AspectRatioStrategy.FALLBACK_RULE_AUTO
                        )
                    )
                    .build()
            )
            .build()
            .also {
                it.surfaceProvider = previewView.surfaceProvider
            }

        LaunchedEffect(Unit) {
            val cameraProvider = getCameraProvider()
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                preview,
            )
        }

        AndroidView({ previewView }, modifier = modifier)
    }

    private suspend fun Context.getCameraProvider(): ProcessCameraProvider =
        suspendCoroutine { continuation ->
            ProcessCameraProvider.getInstance(this).also { cameraProvider ->
                cameraProvider.addListener({
                    continuation.resume(cameraProvider.get())
                }, ContextCompat.getMainExecutor(this))
            }
        }

無事プレビュー画面が表示されました。

最終的なMainActivity.ktのコードは以下の通りになります。

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize()
                ){
                    CameraPreviewScreen()
                }
            }
        }
    }

    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun CameraPreviewScreen() {
        val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
        if (cameraPermissionState.status.isGranted) {
            CameraPreview(modifier = Modifier.aspectRatio(3f/4f))
        } else {
            PermissionRequest(cameraPermissionState)
        }
    }

    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun PermissionRequest(cameraPermissionState: PermissionState) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Button(onClick = {
                cameraPermissionState.launchPermissionRequest()
            }) {
                Text("Grant Camera Permission")
            }
        }
    }

    @Composable
    fun CameraPreview(modifier: Modifier = Modifier) {
        val lifecycleOwner = LocalLifecycleOwner.current
        val previewView = remember { PreviewView(this) }

        val rotation = LocalConfiguration.current.orientation

        val cameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()

        val preview = androidx.camera.core.Preview.Builder()
            .setTargetRotation(rotation)
            .setResolutionSelector(
                ResolutionSelector.Builder()
                    .setAspectRatioStrategy(
                        AspectRatioStrategy(
                            AspectRatio.RATIO_4_3,
                            AspectRatioStrategy.FALLBACK_RULE_AUTO
                        )
                    )
                    .build()
            )
            .build()
            .also {
                it.surfaceProvider = previewView.surfaceProvider
            }

        LaunchedEffect(Unit) {
            val cameraProvider = getCameraProvider()
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                preview,
            )
        }

        AndroidView({ previewView }, modifier = modifier)
    }

    private suspend fun Context.getCameraProvider(): ProcessCameraProvider =
        suspendCoroutine { continuation ->
            ProcessCameraProvider.getInstance(this).also { cameraProvider ->
                cameraProvider.addListener({
                    continuation.resume(cameraProvider.get())
                }, ContextCompat.getMainExecutor(this))
            }
        }
}

お疲れさまでした。
物体検知実装編に続きます。

補足

実際の端末のウェブカメラを使いたい場合はエミュレーターの設定を変更しましょう。

デバイスマネージャーからエミュレーターのEditを選択します。
image.png

Show Advanced Settingsを押下します。
image.png

CameraでWebcam0を選択します。
image.png

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?