【Android】mediapipe Image segmentation を使用して、衣服の色を見分ける。

## はじめに
ケアプラン作成は、いまだにエクセル使用している施設ケアマネジャーです。前回（[MediaPipe ObjectDetection](https://qiita.com/naohotal/items/c20096956d537d233828) で一人歩きを発見する）の続きです。

## [課題](https://qiita.com/naohotal/items/c20096956d537d233828#%E4%BB%8A%E5%BE%8C%E3%81%AE%E8%AA%B2%E9%A1%8C)
一人歩きを発見しても利用者であるか職員であるかがわからないので、バウンディングボックス内の人物の属性を識別したい。

## 方法
利用者は黄緑色の服を着ている。職員は職種により色が違うが利用者とは違う色の系統なので、服の色により利用者と職員を判別できる可能性がある。

:::note info
### 職種により服の色が違う
+ 利用者は、上衣下衣とも黄緑色
+ 看護師は、上衣はえんじ色の服、下衣は紺色のズボン
+ 介護職は、上衣は黄色系、下衣はベージュ
+ リハビリ職とケアマネジャーは、上衣は青色系、下衣は紺色のズボン
:::

今回は、[mediapipe Image segmentation](https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter/android?hl=ja) を使い画像をセグメントの、[マルチクラス自撮り写真のセグメンテーションモデル](https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter?hl=ja#multiclass-model) を利用して、バウンディングボックス内の人物の衣服の色を取得する。取得した衣服の色をもとにして、その人物の属性を判別する。

### 衣服の色で利用者を見分ける
バウンディングボックス内の人物の衣服が黄緑色なら利用者とし、何らかのシグナルを出す。
 
## 結果
衣服の色を取得でき、利用者であるか職員であるか見分けることができた。一人歩きの利用者を発見しアラームできる可能性がある。

## 今後の課題
カメラからの映像で一人歩きの利用者を発見すること。



## 環境
Windos11HOME
Android Studio Ladybug | 2024.2.1 Patch 2
Sony SO-41B
Amazon Fire HD8

## コード等

### 手順
- [mediapipe Image segmentation](https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter/android?hl=ja) を使い画像をセグメントする 
- 推論結果の clothes から衣服の領域を取得する
- 衣服の色を判別する

### 参考
https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/image_segmentation/android/category_mask

### モデル
app直下に assets フォルダを作ります。
下記リンクより、マルチクラス自撮り写真のセグメンテーション モデル
selfie_multiclass_256x256.tflite
を、ダウンロードして assets にセットします。

https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter/index?hl=ja#models

### build.gradle.kts(.app)
build.gradle.kts(.app) に、追加
```
dependencies {
    implementation ("com.google.mediapipe:tasks-vision:latest.release")
    //implementation (libs.tasks.vision)
}
```
### 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="150dp"
        android:contentDescription="@null"
        android:background="#E0E0E0"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="ImageContrastCheck" />

    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:layout_marginTop="10dp"
        android:contentDescription="@null"
        android:background="#E0E0E0"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView"
        tools:ignore="ImageContrastCheck" />

    <ImageView
        android:id="@+id/imageView3"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:layout_marginTop="10dp"
        android:contentDescription="@null"
        android:background="#E0E0E0"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView2"
        tools:ignore="ImageContrastCheck" />

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

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="30dp"
        android:layout_marginTop="10dp"
        android:text="推論時間"
        android:textSize="18sp"
        app:layout_constraintStart_toEndOf="@+id/button"
        app:layout_constraintTop_toBottomOf="@+id/imageView3" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="30dp"
        android:layout_marginTop="10dp"
        android:text="判定"
        android:textSize="18sp"
        app:layout_constraintStart_toEndOf="@+id/button"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>


```

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

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Color.rgb
import android.graphics.ImageDecoder
import android.graphics.Paint
import android.net.Uri
import android.os.Bundle
import android.os.SystemClock
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.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.mediapipe.framework.image.BitmapImageBuilder
import com.google.mediapipe.framework.image.ByteBufferExtractor
import com.google.mediapipe.framework.image.MPImage
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.imagesegmenter.ImageSegmenter
import com.google.mediapipe.tasks.vision.imagesegmenter.ImageSegmenterResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.ByteBuffer
import kotlin.math.floor

class MainActivity : AppCompatActivity() {

    private var imagesegmenter: ImageSegmenter? = null
    private var backgroundScope: CoroutineScope? = null
    private val getContent =
        registerForActivityResult(ActivityResultContracts.OpenDocument()) {uri: Uri? ->
            when {
                uri != null -> {
                    runSegmentationOnImage(uri)
                }
            }
        }

    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
        }

        setupImageSegmenter()

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

    private fun setupImageSegmenter() {
        val baseOptionsBuilder = BaseOptions.builder()
        baseOptionsBuilder.setDelegate(Delegate.CPU)
        baseOptionsBuilder.setModelAssetPath("selfie_multiclass_256x256.tflite")

        val baseOptions = baseOptionsBuilder.build()
        val optionsBuilder = ImageSegmenter.ImageSegmenterOptions.builder()
            .setRunningMode(RunningMode.IMAGE)
            .setBaseOptions(baseOptions)
            .setOutputCategoryMask(true)
            .setOutputConfidenceMasks(false)

        val options = optionsBuilder.build()
        imagesegmenter = ImageSegmenter.createFromOptions(this, options)
    }

    private fun runSegmentationOnImage(uri: Uri) {
        val source = ImageDecoder.createSource(
            contentResolver,
            uri
        )
        val bitmap = ImageDecoder.decodeBitmap(source)
        var inputImage = bitmap.copy(Bitmap.Config.ARGB_8888, true)
        val imageView = findViewById<ImageView>(R.id.imageView)
        imageView.setImageBitmap(inputImage)

        inputImage = inputImage.scaleDown(INPUT_IMAGE_MAX_WIDTH)

        backgroundScope = CoroutineScope(Dispatchers.IO)
        backgroundScope?.launch {
            val mpImage = BitmapImageBuilder(inputImage).build()
            val startTime = SystemClock.uptimeMillis()
            val result = segmentImageFile(mpImage)
            val inferenceTimeMs = SystemClock.uptimeMillis() - startTime
            val inferenceTime = "推論時間 : " + inferenceTimeMs.toString() + "ms\n"

            withContext(Dispatchers.Main) {
                val textView = findViewById<TextView>(R.id.textView)
                textView.text = inferenceTime

                val resultMpImage = result!!.categoryMask().get()
                val maskBitmap = getMaskImage(
                    ByteBufferExtractor.extract(resultMpImage),
                    resultMpImage.width,
                    resultMpImage.height
                )
                overlayMaskImage(inputImage, maskBitmap, mpImage.width, mpImage.height)
            }
        }
    }

    private fun segmentImageFile(mpImage: MPImage): ImageSegmenterResult? {
        return imagesegmenter?.segment(mpImage)
    }

    private fun getMaskImage(byteBuffer: ByteBuffer, outputWidth: Int, outputHeight: Int) :Bitmap {
        val pixels = IntArray(byteBuffer.capacity())
        for (i in pixels.indices) {
            val index = byteBuffer.get(i).toUInt() % 20U
            //着衣以外は黒をセット
            when {
                index.toInt() != 4 -> {
                    pixels[i] = Color.BLACK
                }
            }
        }

        val maskImage =
            Bitmap.createBitmap(pixels, outputWidth, outputHeight, Bitmap.Config.ARGB_8888)
        return  maskImage
    }

    private fun overlayMaskImage(image: Bitmap, maskImage: Bitmap, outputWidth: Int, outputHeight: Int) {
        val options = BitmapFactory.Options()
        options.inMutable = true

        val maskedImage =
            Bitmap.createScaledBitmap(image, outputWidth, outputHeight, true)

        val canvas = Canvas(maskedImage)
        canvas.drawBitmap(maskImage, 0f, 0f, null)

        val imageView2 = findViewById<ImageView>(R.id.imageView2)
        imageView2.setImageBitmap(maskedImage)

        val hsv = FloatArray(3)
        val averageColor = getAverageColor(maskedImage)
        Color.colorToHSV(averageColor, hsv)

        val textView2 = findViewById<TextView>(R.id.textView2)
        val setText: String = when {
            (60 <= hsv[0]) && (hsv[0] <= 189) -> {
                "緑です " + "色相(Hue):" + hsv[0]
            }
            else -> {
                "緑ではないです " + "色相(Hue):" + hsv[0]
            }
        }
        textView2.text = setText
    }

    //着衣の色の平均値を計算
    private fun getAverageColor(bitmap: Bitmap): Int {
        val pixels = IntArray(bitmap.width * bitmap.height)
        bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)

        var sumRed = 0
        var sumBlue = 0
        var sumGreen = 0
        var notBlackPixelCount = 0

        for (y in 0 until bitmap.height) {
            for (x in 0 until bitmap.width) {
                val color = pixels[x + y * bitmap.width]
                when {color != Color.BLACK -> {
                        notBlackPixelCount += 1
                        sumRed += color.red
                        sumBlue += color.blue
                        sumGreen += color.green
                    }
                }
            }
        }

        val averageRed = floor((sumRed / notBlackPixelCount).toDouble()).toInt()
        val averageBlue = floor((sumBlue / notBlackPixelCount).toDouble()).toInt()
        val averageGreen = floor((sumGreen / notBlackPixelCount).toDouble()).toInt()

        val averageColorBitmap =
            Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(averageColorBitmap)
        val averageColor = rgb(averageRed,averageGreen,averageBlue)
        val paint = Paint()
        paint.color = averageColor

        canvas.drawRect(
            0f, 0f,
            averageColorBitmap.height.toFloat(), averageColorBitmap.width.toFloat(),
            paint)

        val imageView3 = findViewById<ImageView>(R.id.imageView3)
        imageView3.setImageBitmap(averageColorBitmap)

        return averageColor
    }

    private fun Bitmap.scaleDown(targetWidth: Float): Bitmap {
        when {
            targetWidth >= width -> return this
            else -> {
                val scaleFactor = targetWidth / width
                return Bitmap.createScaledBitmap(
                    this,
                    (width * scaleFactor).toInt(),
                    (height * scaleFactor).toInt(),
                    false
                )
            }
        }
    }

    companion object {
        private const val INPUT_IMAGE_MAX_WIDTH = 512F
    }
}
```

## 参考リンク
https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/image_segmentation/android/category_mask

https://artteknika.hatenablog.com/entry/2019/09/17/151412

https://qiita.com/tamappe/items/1ab3c6536c2365ae1260

https://www.sevensix.co.jp/youtube/vol-91/

https://www.frontier.maxell.co.jp/blog/posts/43.html

https://qiita.com/Yuzu2yan/items/056778f2707d6931519e#%EF%BC%92rgb%E7%A9%BA%E9%96%93%E3%81%8B%E3%82%89hsv%E7%A9%BA%E9%96%93%E3%81%AB%E7%A7%BB%E8%A1%8C%E3%81%95%E3%81%9B%E3%82%8B

https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter?hl=ja

https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter/android?hl=ja