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
関数- インナークラス
Yolov5NcnnAnalyzer
のanalyze
関数 showObjects
関数
について解説します。
onCreate
onCreate
関数は、Activityクラスで最初に呼び出されるコールバック関数です。
yolov5ncnn.Init(assets)
を呼び出しyolov5の初期化をします。引数で与えているassets
は前回コピーしたassets
ディレクトリに対応していてassets/yolov5s.param
とassets/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
インナークラスYolov5NcnnAnalyzer
はImageAnalysis.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
このshowObjects
は https://github.com/nihui/ncnn-android-yolov5 からのコピーです。ただしこの関数は、analyze
のコールバックから呼び出されるため、runOnUiThread
を使って画面の更新を行います。
すべてのコードは https://github.com/otamajakusi/ncnn-mobile/tree/main/android にあります。
以上です!