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