何番煎じか分かりませんが、CameraXとML KitでQRコード・バーコードリーダーを作ったので、作り方を説明します。
ソースコードはこちら(MITライセンス)
プレイストアでも公開しています。
公開するアプリとしてきちんと作るにはいろいろ面倒なことをやらないといけないですが、QRコード・バーコードリーダーの機能の最低限のところを実現するところに絞って説明します。
必要なライブラリ
CameraXとML Kitを使う上で追加で必要になるのは以下のライブラリ群です。
implementation("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-lifecycle:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")
implementation("com.google.mlkit:barcode-scanning:17.2.0")
AndroidManifestの記述
当然ながらカメラというハードウェアが必要であり、これにアクセスするにはパーミッションが必要です。
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
パーミッションの獲得をする
カメラパーミッションはランタイムパーミッションなのでその処理が必要です。
ランタイムパーミッションの取得については本題から外れますので割愛。
カメラの映像をViewに映す
カメラの映像を映すにはPreviewViewを使うと簡単にできます。
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
CameraXを使ってCameraを使用するには、ProcessCameraProvider
を使用します。
ProcessCameraProvider.getInstance(this)
の戻り値はListenableFuture
になっており、非同期でインスタンスが使えるようになります。
private fun start() {
val future = ProcessCameraProvider.getInstance(activity)
future.addListener({
setUp(future.get())
}, ContextCompat.getMainExecutor(activity))
}
private fun setUp(provider: ProcessCameraProvider) {
val resolutionSelector = ResolutionSelector.Builder()
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
.build()
val preview = Preview.Builder()
.setResolutionSelector(resolutionSelector)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
runCatching {
provider.unbindAll()
provider.bindToLifecycle(
this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis
)
}
}
PreviewView
はPreview
のインスタンスにsurfaceProviderを渡し、ProcessCameraProvider
にbindします。
これだけで、カメラの映像がPreviewViewに表示されるようになります。
カメラの映像を解析し、QRコード・バーコードを検出する
CameraXではProcessCameraProvider
にPreviewと同様にAnalyzerをbindすることができ、映像の解析処理を実装することができます。ここでQRコード・バーコードの検出処理を実装します。
QRコード・バーコードの検出処理もML Kitを使うと簡単で、BarcodeScanner
にInputImage
を渡すだけです。
Analizerを以下のように実装しました。
class CodeAnalyzer(
private val scanner: BarcodeScanner,
private val callback: (List<Barcode>) -> Unit
) : Analyzer {
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val image = imageProxy.image
if (image == null) {
imageProxy.close()
return
}
val inputImage = InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees)
scanner.process(inputImage)
.addOnSuccessListener { callback(it) }
.addOnCompleteListener { imageProxy.close() }
}
}
Analyzer
のanalyze
メソッドに渡されるのはImageProxy
で、そこからInputImage
への変換がちょっと面倒です。
ImageProxy
にgetImage
メソッドがあるので、これでImageを取得すれば良いのですが、このメソッドは@ExperimentalGetImage
がついているので、@SuppressLint("UnsafeOptInUsageError")
をつけて使います。
また、ImageProxy
はanalyze
の処理が終われば、close
をコールする必要がありますが、BarcodeScanner
のprocess
は非同期に処理されるため、そのままimageProxy.close()
をコールしてしまうと、処理中にInputImageが閉じてしまって解析に失敗します。OnCompleteListener
で処理が完了してからclose
をコールしましょう。
検出できたらコールバックで通知します。
このCodeAnalyzer
をProcessCameraProvider
にbindすれば良いのですが、この辺の処理を別クラスに切り出しておきます。
Analyzerは実行するExecutorも指定する必要があり、そのライフサイクル管理も兼ねています。
class CodeScanner(
private val activity: ComponentActivity,
private val previewView: PreviewView,
callback: (List<Barcode>) -> Unit
) {
private val workerExecutor: ExecutorService = Executors.newSingleThreadExecutor()
private val scanner: BarcodeScanner = BarcodeScanning.getClient()
private val analyzer: CodeAnalyzer = CodeAnalyzer(scanner, callback)
init {
activity.lifecycle.addObserver(
LifecycleEventObserver { _, event ->
if (event == ON_DESTROY) {
workerExecutor.shutdown()
scanner.close()
}
}
)
}
fun start() {
val future = ProcessCameraProvider.getInstance(activity)
future.addListener({
setUp(future.get())
}, ContextCompat.getMainExecutor(activity))
}
private fun setUp(provider: ProcessCameraProvider) {
val resolutionSelector = ResolutionSelector.Builder()
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
.build()
val preview = Preview.Builder()
.setResolutionSelector(resolutionSelector)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val analysis = ImageAnalysis.Builder()
.setResolutionSelector(resolutionSelector)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
analysis.setAnalyzer(workerExecutor, analyzer)
runCatching {
provider.unbindAll()
provider.bindToLifecycle(
activity, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis
)
}
}
}
追加部分はここですね、コードリーダーとしては全フレーム処理をしたりする必要も無いため、BackpressureStrategy
としてImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
を指定しています。まあ、処理が滞ったら途中のは捨てて最新のフレームだけ処理するってことですね。
val analysis = ImageAnalysis.Builder()
.setResolutionSelector(resolutionSelector)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
analysis.setAnalyzer(workerExecutor, analyzer)
最後に、これをActivityから呼び出せば完了です。
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var codeScanner: CodeScanner
private val launcher = registerForActivityResult(
CameraPermission.RequestContract(), ::onPermissionResult
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
codeScanner = CodeScanner(this, binding.previewView, ::onDetectCode)
if (CameraPermission.hasPermission(this)) {
start()
} else {
launcher.launch(Unit)
}
}
private fun onPermissionResult(granted: Boolean) {
if (granted) {
start()
} else {
finish()
}
}
private fun start() {
codeScanner.start()
}
private fun onDetectCode(codes: List<Barcode>) {
codes.forEach {
Toast.makeText(this, it.rawValue, Toast.LENGTH_LONG).show()
}
}
}
検出結果は横着してToastしているだけなので、この部分は適宜カスタマイズしていただければ。
ということで、CameraXとML Kitを使うとかなり簡単にコードリーダーアプリが作れました。
凝ったことをしようとするとまだ面倒ですが・・・
なお、ML Kitはオフラインでも使え、入力データを送信することはないそうですが、データ収集などのためクライアントの情報を送信する可能性があります。公開するアプリで使用する場合は、これらをユーザーに告知する必要があるのでご注意を。
以上です。