【2025年最新】YOLO画像認識をAndroidアプリに実装する方法【Kotlin + Jetpack Compose完全ガイド】

リアルタイムで物体検出ができる画像認識技術「YOLO」をご存知でしょうか。近年、機械学習の民主化が進み、AndroidアプリにもYOLOを簡単に組み込めるようになりました。

この記事では、KotlinとJetpack Composeを使って、YOLOによる画像認識機能をAndroidアプリに実装する方法を初心者にもわかりやすく解説します。

YOLOとは?なぜAndroidアプリに最適なのか

YOLO(You Only Look Once)は、画像内の複数の物体をリアルタイムで検出できる画像認識技術です。従来の物体検出アルゴリズムと比較して、以下の特徴があります。

YOLOの主な特徴

  • 高速な処理速度:リアルタイム検出が可能で、動画やカメラ映像にも対応
  • 一度の処理で複数物体を検出:画像全体を一度に解析するため効率的
  • モバイル最適化版が存在:YOLOv5やYOLOv8にはモバイル向けの軽量版がある

Androidアプリでよく使われるのは、TensorFlow LiteまたはONNX Runtimeで動作するYOLOv5、YOLOv8です。これらはスマートフォンでも十分な速度で動作します。

必要な環境とライブラリの準備

まず、Android StudioでKotlinプロジェクトを作成します。以下の依存関係をbuild.gradle.kts(Module)に追加しましょう。

dependencies {
    // Jetpack Compose
    implementation("androidx.compose.ui:ui:1.6.0")
    implementation("androidx.compose.material3:material3:1.2.0")
    implementation("androidx.activity:activity-compose:1.8.2")
    
    // CameraX
    val cameraxVersion = "1.3.1"
    implementation("androidx.camera:camera-core:$cameraxVersion")
    implementation("androidx.camera:camera-camera2:$cameraxVersion")
    implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
    implementation("androidx.camera:camera-view:$cameraxVersion")
    
    // TensorFlow Lite
    implementation("org.tensorflow:tensorflow-lite:2.14.0")
    implementation("org.tensorflow:tensorflow-lite-support:0.4.4")
    implementation("org.tensorflow:tensorflow-lite-gpu:2.14.0") // GPU高速化用
}

次に、AndroidManifest.xmlにカメラの権限を追加します。

<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />

YOLOモデルの準備

YOLOv5やYOLOv8の事前学習済みモデルをTensorFlow Lite形式に変換する必要があります。公式リポジトリでは変換ツールが提供されています。

変換したモデルファイル(.tflite)をAndroidプロジェクトのapp/src/main/assets/フォルダに配置します。例えば、yolov5s.tfliteのような名前で保存しましょう。

ラベルファイル(labels.txt)も同じフォルダに配置します。COCOデータセットで学習したモデルの場合、以下のような80クラスのラベルが含まれます。

person
bicycle
car
motorcycle
...

Kotlin実装:YOLOラッパークラスの作成

TensorFlow Liteを使ってYOLOを動かすラッパークラスを作成します。

import android.content.Context
import android.graphics.Bitmap
import android.graphics.RectF
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.support.common.FileUtil
import org.tensorflow.lite.support.image.TensorImage
import org.tensorflow.lite.support.image.ops.ResizeOp
import java.nio.ByteBuffer

data class Detection(
    val label: String,
    val score: Float,
    val boundingBox: RectF
)

class YoloDetector(context: Context, modelPath: String) {
    private val interpreter: Interpreter
    private val labels: List<String>
    private val inputSize = 640 // YOLOv5/v8の標準入力サイズ
    
    init {
        // モデルの読み込み
        val model = FileUtil.loadMappedFile(context, modelPath)
        val options = Interpreter.Options().apply {
            setNumThreads(4) // マルチスレッド処理
            // GPU使用する場合
            // addDelegate(GpuDelegate())
        }
        interpreter = Interpreter(model, options)
        
        // ラベルの読み込み
        labels = FileUtil.loadLabels(context, "labels.txt")
    }
    
    fun detect(bitmap: Bitmap, confidenceThreshold: Float = 0.5f): List<Detection> {
        // 画像の前処理
        val tensorImage = TensorImage.fromBitmap(bitmap)
        val resizedImage = ResizeOp(inputSize, inputSize, ResizeOp.ResizeMethod.BILINEAR)
            .apply(tensorImage)
        
        // 推論実行
        val outputBuffer = ByteBuffer.allocateDirect(4 * 25200 * 85) // YOLOv5の出力形状
        interpreter.run(resizedImage.buffer, outputBuffer)
        
        // 結果の後処理(NMS適用など)
        return parseOutput(outputBuffer, confidenceThreshold, bitmap.width, bitmap.height)
    }
    
    private fun parseOutput(
        output: ByteBuffer,
        threshold: Float,
        originalWidth: Int,
        originalHeight: Int
    ): List<Detection> {
        val detections = mutableListOf<Detection>()
        
        // YOLOの出力を解析してBounding Boxを抽出
        // ここでは簡略化していますが、実際にはNMS(Non-Maximum Suppression)を実装
        output.rewind()
        
        // ... 出力解析処理 ...
        
        return detections
    }
    
    fun close() {
        interpreter.close()
    }
}

Jetpack Composeでカメラプレビュー実装

次に、Jetpack Composeでカメラプレビューと検出結果を表示するUIを作成します。

import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors

@Composable
fun YoloDetectionScreen() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    
    var detections by remember { mutableStateOf<List<Detection>>(emptyList()) }
    val yoloDetector = remember { YoloDetector(context, "yolov5s.tflite") }
    
    DisposableEffect(Unit) {
        onDispose {
            yoloDetector.close()
        }
    }
    
    Box(modifier = Modifier.fillMaxSize()) {
        // カメラプレビュー
        CameraPreview(
            onImageCaptured = { bitmap ->
                // YOLOで物体検出
                detections = yoloDetector.detect(bitmap)
            }
        )
        
        // 検出結果のBounding Box描画
        DetectionOverlay(detections = detections)
    }
}

@Composable
fun CameraPreview(onImageCaptured: (Bitmap) -> Unit) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
    
    AndroidView(
        factory = { ctx ->
            val previewView = PreviewView(ctx)
            val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
            
            cameraProviderFuture.addListener({
                val cameraProvider = cameraProviderFuture.get()
                
                val preview = Preview.Builder().build().also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }
                
                val imageAnalysis = ImageAnalysis.Builder()
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                    .build()
                    .also {
                        it.setAnalyzer(cameraExecutor) { imageProxy ->
                            val bitmap = imageProxy.toBitmap()
                            onImageCaptured(bitmap)
                            imageProxy.close()
                        }
                    }
                
                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
                
                try {
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        cameraSelector,
                        preview,
                        imageAnalysis
                    )
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }, ContextCompat.getMainExecutor(ctx))
            
            previewView
        },
        modifier = Modifier.fillMaxSize()
    )
}

@Composable
fun DetectionOverlay(detections: List<Detection>) {
    Canvas(modifier = Modifier.fillMaxSize()) {
        detections.forEach { detection ->
            val rect = detection.boundingBox
            
            // Bounding Boxの描画
            drawRect(
                color = Color.Green,
                topLeft = Offset(rect.left, rect.top),
                size = Size(rect.width(), rect.height()),
                style = Stroke(width = 4f)
            )
            
            // ラベルとスコアの描画(簡略化)
            drawContext.canvas.nativeCanvas.apply {
                drawText(
                    "${detection.label} ${(detection.score * 100).toInt()}%",
                    rect.left,
                    rect.top - 10f,
                    android.graphics.Paint().apply {
                        color = android.graphics.Color.GREEN
                        textSize = 40f
                    }
                )
            }
        }
    }
}

パフォーマンス最適化のポイント

実際のアプリでYOLOを動かす際は、以下のポイントに注意してパフォーマンスを最適化しましょう。

1. GPU高速化の活用 TensorFlow LiteのGPU Delegateを使うことで、推論速度を大幅に向上できます。

val options = Interpreter.Options().apply {
    addDelegate(GpuDelegate())
}

2. 軽量モデルの選択 YOLOv5nやYOLOv8nといった軽量版モデルを使うことで、精度をある程度保ちながら高速化できます。

3. フレームレートの調整 すべてのフレームで推論を実行する必要はありません。3〜5フレームに1回程度の検出でも十分な場合が多いです。

var frameCount = 0
imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
    frameCount++
    if (frameCount % 3 == 0) {
        // 3フレームごとに検出
        val bitmap = imageProxy.toBitmap()
        onImageCaptured(bitmap)
    }
    imageProxy.close()
}

実際のアプリ例:何が作れる?

YOLOをAndroidアプリに実装することで、以下のようなアプリケーションが作成できます。

  • 交通量カウンター:道路を通過する車両を自動でカウント
  • 在庫管理アプリ:商品を自動認識して在庫を記録
  • ペット見守りカメラ:ペットの行動を検出して通知
  • 安全監視システム:工事現場でヘルメット着用を確認

まとめ

YOLOをKotlinとJetpack Composeを使ってAndroidアプリに実装する方法を解説しました。TensorFlow Liteを使えば、高度な機械学習モデルもモバイルアプリで簡単に動かせます。

重要なポイントをおさらいしましょう。YOLOはリアルタイム物体検出に適しており、モバイル向けの軽量版も存在します。TensorFlow Liteを使うことでAndroidアプリに組み込むことができ、Jetpack ComposeとCameraXを組み合わせることで、モダンなUIでカメラ連携が実現できます。GPU高速化やフレームレート調整によって、実用的な速度で動作させることも可能です。

画像認識技術は今後さらに身近になっていきます。ぜひあなたも独自の画像認識アプリを作ってみてください。

参考リンク

  • TensorFlow Lite公式ドキュメント
  • YOLOv5 GitHub リポジトリ
  • Ultralytics YOLOv8 公式サイト
  • Android Developers – CameraX

この記事がお役に立ちましたら、ぜひシェアしてください。質問やコメントもお待ちしています!