YOLOv5 on Android – Coding

Android

Androidのカメラの映像をYOLOv5を使用し、物体検出する方法を2回に分けて解説します。前回の記事では設定について解説しました。この記事ではコードについて解説します。

MainActivity.kt

MainActivity.ktを開き以下のように修正します。

package com.example.android_ncnn

import android.Manifest
import android.content.pm.PackageManager
import android.graphics.*
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.android_ncnn.databinding.ActivityMainBinding
import com.tencent.yolov5ncnn.YoloV5Ncnn
import java.nio.ByteBuffer
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

typealias Yolov5NcnnListener = (objects: Array<YoloV5Ncnn.Obj>?, bitmap: Bitmap?) -> Unit

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private lateinit var cameraExecutor: ExecutorService
    private var yolov5ncnn = YoloV5Ncnn()

    private class Yolov5NcnnAnalyzer(
        private val yolov5ncnn: YoloV5Ncnn,
        private val listener: Yolov5NcnnListener
    ) : ImageAnalysis.Analyzer {
        override fun analyze(image: ImageProxy) {
            val planes = image.planes
            val buffer: ByteBuffer = planes[0].buffer
            val mat = Matrix().apply {
                postRotate(image.imageInfo.rotationDegrees.toFloat())
            }
            val bitmap = Bitmap.createBitmap(
                image.width, image.height, Bitmap.Config.ARGB_8888
            ).apply {
                copyPixelsFromBuffer(buffer)
            }.let {
                Bitmap.createBitmap(it, 0, 0, it.width, it.height, mat, false)
            }
            val objects: Array<YoloV5Ncnn.Obj>? =
                yolov5ncnn.Detect(bitmap, true) ?: yolov5ncnn.Detect(bitmap, false)

            listener(objects, bitmap)

            image.close()
        }
    }

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

        yolov5ncnn.Init(assets)
        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }
        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    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()
            val imageAnalyzer = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
                .build()
                .apply {
                    setAnalyzer(
                        cameraExecutor,
                        Yolov5NcnnAnalyzer(yolov5ncnn) { objects: Array<Obj>?, bitmap: Bitmap? ->
                            showObjects(objects, bitmap)
                        },
                    )
                }
            // Select back camera as a default
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

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

    private fun showObjects(objects: Array<YoloV5Ncnn.Obj>?, bitmap: Bitmap?) {
        if (objects == null || bitmap == null) {
            //viewBinding.imageView.setImageBitmap(bitmap)
            return
        }
        // draw objects on bitmap
        val rgba: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
        val colors = intArrayOf(
            Color.rgb(54, 67, 244),
            Color.rgb(99, 30, 233),
            Color.rgb(176, 39, 156),
            Color.rgb(183, 58, 103),
            Color.rgb(181, 81, 63),
            Color.rgb(243, 150, 33),
            Color.rgb(244, 169, 3),
            Color.rgb(212, 188, 0),
            Color.rgb(136, 150, 0),
            Color.rgb(80, 175, 76),
            Color.rgb(74, 195, 139),
            Color.rgb(57, 220, 205),
            Color.rgb(59, 235, 255),
            Color.rgb(7, 193, 255),
            Color.rgb(0, 152, 255),
            Color.rgb(34, 87, 255),
            Color.rgb(72, 85, 121),
            Color.rgb(158, 158, 158),
            Color.rgb(139, 125, 96)
        )
        val canvas = Canvas(rgba)
        val paint = Paint().apply {
            style = Paint.Style.STROKE
            strokeWidth = 4f
        }
        val textbgpaint = Paint().apply {
            color = Color.WHITE
            style = Paint.Style.FILL
        }
        val textpaint = Paint().apply {
            color = Color.BLACK
            textSize = 26f
            textAlign = Paint.Align.LEFT
        }
        objects.indices.forEach { i ->
            paint.color = colors[i % 19]
            canvas.drawRect(
                objects[i].x,
                objects[i].y,
                objects[i].x + objects[i].w,
                objects[i].y + objects[i].h,
                paint
            )
            // draw filled text inside image
            run {
                val text = objects[i].label + " = " + String.format(
                    "%.1f",
                    objects[i].prob * 100
                ) + "%"
                val text_width = textpaint.measureText(text)
                val text_height = -textpaint.ascent() + textpaint.descent()
                var x = objects[i].x
                var y = objects[i].y - text_height
                if (y < 0) y = 0f
                if (x + text_width > rgba.width) x = rgba.width - text_width
                canvas.drawRect(x, y, x + text_width, y + text_height, textbgpaint)
                canvas.drawText(text, x, y - textpaint.ascent(), textpaint)
            }
        }
        runOnUiThread { viewBinding.imageView.setImageBitmap(rgba) }
    }

    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 = "android_ncnn"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS =
            mutableListOf(
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO
            ).apply {
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                    add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            }.toTypedArray()
    }
}Code language: Kotlin (kotlin)

主なMainActivityクラスのブロックとして、

  • onCreate関数
  • startCamera関数
  • インナークラスYolov5NcnnAnalyzeranalyze関数
  • showObjects関数

について解説します。

onCreate

onCreate関数は、Activityクラスで最初に呼び出されるコールバック関数です。

yolov5ncnn.Init(assets)を呼び出しyolov5の初期化をします。引数で与えているassets前回コピーしたassetsディレクトリに対応していてassets/yolov5s.paramassets/yolov5s.binを読み出します。

カメラの起動に必要な権限の取得ができたらstartCamera()を呼び出します。

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

        yolov5ncnn.Init(assets)
        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }
        cameraExecutor = Executors.newSingleThreadExecutor()
    }Code language: Kotlin (kotlin)

startCamera

startCameraでは、CameraXライブラリの画像解析の機能を使用します。この機能を使うことで画像をファイルに保存することなく解析することができます。

ImageAnalysis.Builder()の設定として、setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)を指定することで、analyze()関数で使用する画像の扱いが簡単になります。

setAnalyzer関数の引数に、Yolov5NcnnAnalyzerインスタンスを渡します。Kotlinのコードは関数の最終引数が関数オブジェクトの場合、丸括弧(=())の外に記述することが推奨されていてYolov5NcnnAnalyzerの呼び出しは、以下の記述と同等です。

Yolov5NcnnAnalyzer(yolov5ncnn, { objects: Array<Obj>?, bitmap: Bitmap? ->
    showObjects(objects, bitmap)
}),Code language: CSS (css)

analyze

インナークラスYolov5NcnnAnalyzerImageAnalysis.Analyzerを継承したクラスで、作成したインスタンスをstartCamera関数内のsetAnalyzer関数で登録しています。これによりカメラの画像が準備できるたびに、analyze関数が呼び出されます。analyze関数はImageProxy型の変数を引数に取りますが、画像フォーマットとしてImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888を設定しているためBitmapを簡単に作成することが可能です。

image.imageInfo.rotationDegreesで回転させる必要がある度数を得ることができます。この値を使ってBitmapを作成する際に画像を回転させます。

yolov5ncnn.Detect関数ですが、第2引数にgpuを使うかどうかのフラグを指定します。まずこのフラグをtrueにして呼び出しgpuが使えないデバイスの場合この関数はfalseを返します。falseが返った場合はフラグをfalseにしてgpuを使わない設定で再度呼び出します。

            val objects: Array<YoloV5Ncnn.Obj>? =
                yolov5ncnn.Detect(bitmap, true) ?: yolov5ncnn.Detect(bitmap, false)Code language: JavaScript (javascript)

showObjects

このshowObjectshttps://github.com/nihui/ncnn-android-yolov5 からのコピーです。ただしこの関数は、analyzeのコールバックから呼び出されるため、runOnUiThreadを使って画面の更新を行います。

すべてのコードは https://github.com/otamajakusi/ncnn-mobile/tree/main/android にあります。

以上です!

参照

GitHub - nihui/ncnn-android-yolov5: The YOLOv5 object detection android example
The YOLOv5 object detection android example . Contribute to nihui/ncnn-android-yolov5 development by creating an account on GitHub.
CameraX の概要  |  Android デベロッパー  |  Android Developers
Getting Started with CameraX  |  Android Developers
This codelab introduces how to create a camera app that uses CameraX to show a viewfinder, take photos and analyze an image stream from the camera.
https://github.com/otamajakusi/ncnn-mobile/tree/main/android