MediaPipe ObjectDetection で一人歩きを発見する

## はじめに
はじめまして。ケアプラン作成は、いまだにエクセル使用している施設ケアマネジャーです。

## 状況
私の勤める介護保険施設では、転倒リスクのある方が一人歩きをして転んでしまうことがあります。踏むとアラーム音がでるセンサーマットを使用していますが、複数個所で鳴動していたり、ケア中であったりと、人力での見守りでは限りがあります。

## 目的
機械の力を借りて早期発見し、見守りにつなげ転倒を防ぎたい

## 課題
施設内で一人歩き（独歩）の利用者を発見したい

## 方法
MediaPipe オブジェクト検出タスクを用いて画像内の人（person）をカウントする。一人であれば何らかのシグナルを出す。

## 結果
画像内の人をカウントして、一人の時にバウンディングボックスを描くことができた。

## 今後の課題
一人であることがわかっても、その人が看護師であるか、介護士であるか、利用者であるかがわからないので、バウンディングボックスの人物を識別する必要がある。

## 参考
https://ai.google.dev/edge/mediapipe/solutions/vision/object_detector/android?hl=ja

https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/object_detection/android

## 環境
Windos11HOME
Android Studio Jellyfish | 2023.3.1
Sony SO-41B
Amazon Fire HD8

# コード等
人物のカウントをします。一人の時はバウンディングボックスを描きます。

## assets
app直下に assets フォルダを作り、下記リンクよりモデルをダウンロードしてセットします。
### モデル
https://ai.google.dev/edge/mediapipe/solutions/vision/object_detector/index?hl=ja#models

:::note warn
上記のリンク内でのモデルの表記は、ハイフン入りですが
EfficientDet-Lite0 (int8)
となっていますが、ダウンロードしたものはアンダースコア入り
efficientdet_lite0.tflite
になっています。
本コード内では、ハイフン入り
EfficientDet-Lite0 (int8)
です。
:::

## build.gradle.kts(.app) 
build.gradle.kts(.app) に、追加
```build.gradle.kts(.app) 
dependencies {
    implementation ("com.google.mediapipe:tasks-vision:latest.release")
}
```
# コード

### activity_main.xml
```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">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="@color/cardview_dark_background"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="200dp"
        android:text="TextView"
        android:textSize="18sp"
        android:scrollbars="vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="Button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

### MainActivity.kt
```MainActivity.kt
package yourpackageName

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ImageDecoder
import android.graphics.Paint
import android.graphics.RectF
import android.net.Uri
import android.os.Bundle
import android.os.SystemClock
import android.text.method.ScrollingMovementMethod
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.mediapipe.framework.image.BitmapImageBuilder
import com.google.mediapipe.tasks.core.BaseOptions
import com.google.mediapipe.tasks.core.Delegate
import com.google.mediapipe.tasks.vision.core.RunningMode
import com.google.mediapipe.tasks.vision.objectdetector.ObjectDetector
import com.google.mediapipe.tasks.vision.objectdetector.ObjectDetectorResult

class MainActivity : AppCompatActivity() {

    private var objectDetector: ObjectDetector? = null
    private val getContent =
        registerForActivityResult(ActivityResultContracts.OpenDocument()) {
            if (it != null) {
                runDetectionOnImage(it)
            }
        }
    private var image: Bitmap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            getContent.launch(arrayOf("image/*"))
        }

        val textView = findViewById<TextView>(R.id.textView)
        textView.movementMethod = ScrollingMovementMethod()
    }

    private fun runDetectionOnImage(uri: Uri) {
        //setup
        val source = ImageDecoder.createSource(
            contentResolver,
            uri
        )

        val bitmap = ImageDecoder.decodeBitmap(source)
        image = bitmap.copy(Bitmap.Config.ARGB_8888, true)
        val imageView = findViewById<ImageView>(R.id.imageView)
        imageView.setImageBitmap(image)

        val baseOptionsBuilder = BaseOptions.builder()
        baseOptionsBuilder.setDelegate(Delegate.CPU)

        val modelName = "efficientdet-lite0.tflite"
        baseOptionsBuilder.setModelAssetPath(modelName)

        val runningMode: RunningMode = RunningMode.IMAGE
        val threshold: Float = THRESHOLD_DEFAULT
        val maxResults: Int = MAX_RESULTS_DEFAULT

        val optionsBuilder =
            ObjectDetector.ObjectDetectorOptions.builder()
                .setBaseOptions(baseOptionsBuilder.build())
                .setScoreThreshold(threshold)
                .setRunningMode(runningMode)
                .setMaxResults(maxResults)

        optionsBuilder.setRunningMode(runningMode)
        val options = optionsBuilder.build()
        objectDetector = ObjectDetector.createFromOptions(applicationContext, options)

        // detect
        val mpImage = BitmapImageBuilder(image).build()
        val startTime = SystemClock.uptimeMillis()
        val detectionResult = objectDetector?.detect(mpImage)
        val inferenceTimeMs = SystemClock.uptimeMillis() - startTime
        val headCount = personCount(detectionResult)
        val textView = findViewById<TextView>(R.id.textView)
        textView.append("person count : " + headCount + "人\n")
        textView.append("推論時間 : " + inferenceTimeMs.toString() + "ms\n")

        //draw boundingBox
        if (headCount == 1){
            textView.append("一人です")
            val boxRect = detectionResult!!.detections()[0].boundingBox()

            if (image != null){
                drawBoundingBox(boxRect)
            }
        }

        //clear objectDetector
        objectDetector?.close()
        objectDetector = null
    }

    private fun drawBoundingBox(boundingBox : RectF)
    {
        val canvas = Canvas(image!!)
        val boxPaint = Paint()
        boxPaint.setStrokeWidth(2f)
        boxPaint.setStyle(Paint.Style.STROKE)
        boxPaint.color = Color.GREEN

        val top = boundingBox.top
        val bottom = boundingBox.bottom
        val left = boundingBox.left
        val right = boundingBox.right

        val drawableRect = RectF(left, top, right, bottom)
        canvas.drawRect(drawableRect, boxPaint)
    }

    private fun personCount(detectionResults: ObjectDetectorResult?) : Int
    {
        val results: ObjectDetectorResult? = detectionResults
        var personCount = 0
        val textView = findViewById<TextView>(R.id.textView)
        textView.text = ""

        results?.detections()?.map {
        }?.forEachIndexed { index, _ ->
            val category = results.detections()!![index].categories()[0]
            val drawableText =
                category.categoryName() + " score:" + category.score()
            if (category.categoryName() == "person") {
                personCount++
            }
            textView.append(  drawableText + "\n")
        }
        return personCount
    }

    companion object {
        const val MAX_RESULTS_DEFAULT = 3
        const val THRESHOLD_DEFAULT = 0.5F
    }
}
```