【Android】CameraXから一定間隔でビットマップを取得する【kotlin】

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

　前回[【Android】CameraX習作（ビットマップを取得する）](https://qiita.com/naohotal/items/be35b9ca4744a2b9e382 "")ではカメラ映像からビットマップを取得できました。今回は、ImageAnalysis ユースケースからビットマップを取得しています。アナライザーにやってくるイメージを間引きながら、一定間隔でビットマップ画像を取得しています。
　ボタンを押すとビットマップ画像を取得する間隔が切り替わります。取得間隔は１０フレーム毎と１０００ミリ秒毎に設定しています。


## 課題（前回と同じ）
　一定間隔で、カメラ映像からビットマップ画像を取得したい。ただし、保存はしない。

## まとめ
　カメラでプレビューを表示しながら、イメージビューにビットマップを表示しています。
　ビットマップを取得できたので、[ObjectDetection](https://qiita.com/naohotal/items/c20096956d537d233828#%E7%8A%B6%E6%B3%81) で一人歩きを発見し、一人の時に[Image segmentation](https://qiita.com/naohotal/items/927d925cd7768b925b5f)で利用者であるか特定する、という順序で当初の目的（機械の力を借りて早期発見し、見守りにつなげ転倒を防ぎたい）を達成できそうです。

## コード
### build.gradle.kts(.app) 

```build.gradle.kts(.app) 

android {
...
    buildFeatures {
        viewBinding = true
    }
}

dependencies {

    val camerax_version = "1.5.0-alpha03"
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    implementation("androidx.camera:camera-view:$camerax_version")
...

```

### AndroidManifest.xml
```AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
    ...

```

### 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">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_marginTop="4dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/viewFinder"
        tools:srcCompat="@tools:sample/avatars" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

### MainActivity.kt
```MainActivity.kt
package yourpackagename

import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Matrix
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import android.widget.Toast
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.core.Preview
import androidx.camera.core.CameraSelector
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
//
import yourpackagename.databinding.ActivityMainBinding

typealias TakeBitmapListener = (mode: String, bitmap: Bitmap) -> Unit

class MainActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityMainBinding
    private lateinit var cameraExecutor: ExecutorService
    private var everyFrame = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }

        cameraExecutor = Executors.newSingleThreadExecutor()

        viewBinding.button.setOnClickListener {
            everyFrame = !everyFrame
            startCamera()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)//

        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // Preview
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
                }

            val imageAnalyzer = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor, EveryTakeBitmapAnalyzer(everyFrame){ mode, bitmap ->
                        setBitmap(bitmap)
                        setModeButtonText(mode)
                    })
                }

            // Select front camera フロントカメラにしてあります
            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()
                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageAnalyzer)
            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "CameraXApp"
        private const val FRAME_INTERVAL = 10//10frames
        private const val MILLISECOND_INTERVAL = 1000//1000ms
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }

    private fun setBitmap(bitmap: Bitmap?) {
        bitmap ?: return
        runOnUiThread {viewBinding.imageView.setImageBitmap(bitmap)}
    }

    private fun setModeButtonText(mode: String) {
        runOnUiThread{
            viewBinding.button.text = mode
        }
    }

    private inner class EveryTakeBitmapAnalyzer(analyzerFlag: Boolean, private val listener: TakeBitmapListener) : ImageAnalysis.Analyzer {
        //一定時間ごとにイメージを取得する場合（ここでは１０００ｍｓごと）
        var nowTime = System.currentTimeMillis()
        var beforeSystemTime = 0L

        //一定フレームごとにイメージを取得する場合（ここでは１０フレームごと）
        var frameCounter = 0
        val analyzeFlag = analyzerFlag

        override fun analyze(image: ImageProxy) {
            if (analyzeFlag){
                /* 指定したフレーム間隔でイメージを取得する場合 */
                frameCounter += 1
                if (frameCounter < FRAME_INTERVAL){
                    image.close()
                }else {
                    frameCounter = 0
                    listener("every frame", imageProxyToBitmap(image))
                    image.close()
                }
            }else {
                /* 指定した時間間隔でイメージを取得する場合 */
                nowTime = System.currentTimeMillis()
                if ((nowTime - beforeSystemTime) < MILLISECOND_INTERVAL) {
                    image.close()
                } else {
                    beforeSystemTime = nowTime
                    listener("every millisecond", imageProxyToBitmap(image))
                    image.close()
                }
            }
        }

        private fun imageProxyToBitmap(image: ImageProxy?): Bitmap {
            val planes: Array<ImageProxy.PlaneProxy> = image!!.planes
            val buffer = planes[0].buffer
            val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
            bitmap.copyPixelsFromBuffer(buffer)

            val rotation = image.imageInfo.rotationDegrees
            val rotateBitmap = rotateImage(bitmap, rotation)//上下を整える
            val mirroredBitmap = mirrorImageBitmap(rotateBitmap)//鏡像にする

            return mirroredBitmap
        }
        private fun rotateImage(bitmap: Bitmap, degree: Int): Bitmap {
            val matrix = Matrix()
            matrix.postRotate(degree.toFloat())
            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        }

        private fun mirrorImageBitmap(bitmap: Bitmap) : Bitmap{
            val matrix = Matrix()
            matrix.preScale(-1f, 1f)
            return  Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        }
    }
}
```