はじめに
ケアプラン作成は、いまだにエクセル使用している施設ケアマネジャーです。前回(MediaPipe ObjectDetection で一人歩きを発見する)の続きです。
課題
一人歩きを発見しても利用者であるか職員であるかがわからないので、バウンディングボックス内の人物の属性を識別したい。
方法
利用者は黄緑色の服を着ている。職員は職種により色が違うが利用者とは違う色の系統なので、服の色により利用者と職員を判別できる可能性がある。
今回は、mediapipe Image segmentation を使い画像をセグメントの、マルチクラス自撮り写真のセグメンテーションモデル を利用して、バウンディングボックス内の人物の衣服の色を取得する。取得した衣服の色をもとにして、その人物の属性を判別する。
衣服の色で利用者を見分ける
バウンディングボックス内の人物の衣服が黄緑色なら利用者とし、何らかのシグナルを出す。
結果
衣服の色を取得でき、利用者であるか職員であるか見分けることができた。一人歩きの利用者を発見しアラームできる可能性がある。
今後の課題
カメラからの映像で一人歩きの利用者を発見すること。
環境
Windos11HOME
Android Studio Ladybug | 2024.2.1 Patch 2
Sony SO-41B
Amazon Fire HD8
コード等
手順
- mediapipe Image segmentation を使い画像をセグメントする
- 推論結果の clothes から衣服の領域を取得する
- 衣服の色を判別する
参考
モデル
app直下に assets フォルダを作ります。
下記リンクより、マルチクラス自撮り写真のセグメンテーション モデル
selfie_multiclass_256x256.tflite
を、ダウンロードして assets にセットします。
build.gradle.kts(.app)
build.gradle.kts(.app) に、追加
dependencies {
implementation ("com.google.mediapipe:tasks-vision:latest.release")
//implementation (libs.tasks.vision)
}
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
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
}
}
参考リンク