CameraXはまだベータですが、その精度の高さには驚かされるばかりです。
本記事はAndroid Advent Calendar 2019の2019/12/04分です。
とてもQRコードリーダーを作れちゃうので、その方法を紹介します。
今回は、Firebase MLKitと組み合わせて「QRコードリーダー」を作ってみます。
※Firebaseプロジェクトの準備は別途ご用意ください
下準備
Gradleに必要なものを追加
CameraXとMLKitを追加しましょう
implementation "androidx.camera:camera-core:1.0.0-alpha06"
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
implementation "com.google.firebase:firebase-ml-vision:24.0.1"
描画する領域を追加
それではレウアウトのXMLに、描画するための領域(TextureView)を追加します
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
MLKitを組み込んだAnalyzerを作成する
カメラが起動された際に「QRコードを読み取って解析する」ためのAnalyzerを作成します。
クラス名は QrCodeAnalyzer
とでもしましょう。
private val TAG = QrCodeAnalyzer::class.java.simpleName
class QrCodeAnalyzer : ImageAnalysis.Analyzer {
var barcodeStr = ObservableField<String>()
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
val img = image.image ?: return
runBarcodeScanner(img, degreesToFirebaseRotation(rotationDegrees))
}
private fun runBarcodeScanner(src: Image, rotationDegrees: Int) {
try {
val image = FirebaseVisionImage.fromMediaImage(src, rotationDegrees)
// 読み取るバーコードのタイプを指定する
val barcodeFormatQr = FirebaseVisionBarcode.FORMAT_QR_CODE
val options = FirebaseVisionBarcodeDetectorOptions.Builder()
.setBarcodeFormats(barcodeFormatQr)
.build()
// 画像の解析
FirebaseVision.getInstance().getVisionBarcodeDetector(options).detectInImage(image)
.addOnSuccessListener { firebaseVisionBarcodeList ->
if (firebaseVisionBarcodeList.isNotEmpty()) {
// QRを複数読み込んだ場合に1つだけを対象にする
val barcode = firebaseVisionBarcodeList[0]
when (barcode.valueType) {
FirebaseVisionBarcode.TYPE_URL -> {
barcode.displayValue?.let {
if (barcodeStr.get() != it) {
barcodeStr.set(it)
}
return@addOnSuccessListener
}
}
}
}
}
.addOnFailureListener {
Log.e(TAG, "something wrong...")
}
} catch (e: Exception) {
Log.e(TAG, e.message)
}
}
/**
* 画像の回転を ML Kit の ROTATION_ 定数のいずれかに変換する
*/
private fun degreesToFirebaseRotation(degrees: Int): Int = when (degrees) {
0 -> FirebaseVisionImageMetadata.ROTATION_0
90 -> FirebaseVisionImageMetadata.ROTATION_90
180 -> FirebaseVisionImageMetadata.ROTATION_180
270 -> FirebaseVisionImageMetadata.ROTATION_270
else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
}
}
「QRコードを読み取るんだ!」という目的のためにやることは、さほど多くはありません。
画像を解析する際にオプションをセットできますが、その際に FirebaseVisionBarcode.FORMAT_QR_CODE
を指定してあげるだけで終わりです。
もちろんこのタイプの指定はひとつではなく、複数セットが可能です。
ここではObservableFieldを使って読み取った(ObservableField内のデータが書き換わった)ことを通知するように工夫していますが、おそらくCoroutinesを使ったらもっとキレイに書けます。
(一度Coroutinesを使ったんですが、ワケあってやめたのです)
カメラをスタートして画像を読み取る
それでは、実際にカメラを起動し、上で作成したAnalyzerを使って画像を解析する部分を作っていきます。
少しQiitaに書くコードとしては大きくなってしまいますが、これが全てです。
これだけでQRコードリーダーがほぼ完成です。
(※細かなTODOはまだまだありますが)
private val TAG = MainActivity::class.java.simpleName
@RuntimePermissions
class MainActivity : AppCompatActivity() {
companion object {
val analyzerThread = HandlerThread(
"CameraXAnalysis"
).apply { start() }
}
val textureView by lazy {
findViewById<TextureView>(R.id.textureView)
}
private var fitPreview: FitPreview? = null
private var analyzerUseCase: ImageAnalysis? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launchCameraWithPermissionCheck()
textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateTransform()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Permission has not granted", Toast.LENGTH_LONG).show()
}
onRequestPermissionsResult(requestCode, grantResults)
}
@NeedsPermission(Manifest.permission.CAMERA)
fun launchCamera() {
textureView.post { startCamera() }
}
private fun startCamera() {
try {
val textureViewWidth = textureView.width
val textureViewHeight = textureView.height
val targetResolution = Size(textureViewWidth, textureViewHeight)
val previewConfig = PreviewConfig.Builder()
.setTargetResolution(targetResolution)
.setTargetAspectRatio(Rational(textureViewWidth, textureViewHeight))
.setLensFacing(CameraX.LensFacing.BACK)
.build()
fitPreview = FitPreview(textureView, previewConfig)
val analyzerConfig = ImageAnalysisConfig.Builder().apply {
setCallbackHandler(Handler(analyzerThread.looper))
setTargetResolution(targetResolution)
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
}.build()
analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
val qrCodeAnalyzer = QrCodeAnalyzer()
qrCodeAnalyzer.barcodeStr.addOnPropertyChangedCallback(object :
Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
val observableField =
sender as ObservableField<String>
observableField.get()?.let { barcodeStr ->
Log.d(TAG, "barcodeStr = $barcodeStr")
}
}
})
analyzer = qrCodeAnalyzer
}
CameraX.bindToLifecycle(this, fitPreview, analyzerUseCase)
} catch (e: Exception) {
Log.e(TAG, e.message)
}
}
private fun updateTransform() {
val matrix = Matrix()
// Compute the center of the view finder
val centerX = textureView.width / 2f
val centerY = textureView.height / 2f
// Correct preview output to account for display rotation
val rotationDegrees = when (textureView.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> return
}
matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
// Finally, apply transformations to our TextureView
textureView.setTransform(matrix)
}
}
メインは startCameraメソッド
です。
startCameraメソッド以外は、カメラ起動のためのPermissionなど必要な手続きばかりです。
ここでは触れてはいませんが、注意が必要なことがいくつかあります。
- CameraXでは、カメラをズームする場合は
CameraX.bindToLifecycle()の後
でやる - 富士通の端末の場合、電池残量が10%未満の場合は
RuntimeException(camera low battery)
となる - カメラを止めたり再開したり(エラーダイアログを出してる最中は画像解析を止めるとか)する場合は、CameraXのライフサイクルに気をつけないと簡単にクラッシュする
他にも色々とありますが、挙げ出したらキリがないかもしれません…
まとめ
ここまで「CameraXはええで!MLKitと組み合わせたらめっちゃええで!」とばかり述べてきましたが、やはりまだ過去のAndroidのカメラが通ってきた闇の歴史を乗り越えきれてはいないように感じます。
Xperiaでは非常に画像解析の精度が低いですし、カメラのズームをすると画像がガビガビになったりする端末もありました。
個人的にCameraXには非常に注目しており好んではいますが、Xzingなどといった歴史のあるライブラリのほうが、今はプロダクトとしては選択肢として強い気がします。
P.S.
ここで述べてきたことは、CameraXのCodelabsでかなり触れられているので、そちらを試してみたらよいかと思います。
https://codelabs.developers.google.com/codelabs/camerax-getting-started/