こんにちは、matsunariです。
今回はGoogleCodeLabsでCameraXの実装を行いました。以前の記事でJavaでPreviewを実装しましたが今回はkotlinでイメージキャプチャまで行います。
GoogleCodeLabsでのチュートリアルがありますが、英語で説明も少ないので今回少し詳しく書いてみます。
今回実装を行ったMainActivityのソースコードを先に載せておきます。
package com.example.mycameraxapp //CameraXに必要となるインポートです import android.Manifest import android.content.pm.PackageManager import android.graphics.Matrix import android.os.Bundle import android.util.Log import android.util.Size import android.view.Surface import android.view.TextureView import android.view.ViewGroup import android.widget.ImageButton import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.camera.core.* import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import java.io.File import java.util.concurrent.Executors private const val REQUEST_CODE_PERMISSIONS = 10 private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA,Manifest.permission.WRITE_EXTERNAL_STORAGE) class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewFinder = findViewById(R.id.view_finder) if (allPermissionsGranted()) { viewFinder.post { startCamera() } } else { ActivityCompat.requestPermissions( this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS) } } //カメラ撮影時に利用する単一のスレッドを取得し再利用できるようにexecutorを作成します。 private val executor = Executors.newSingleThreadExecutor() //後に利用するTextureViewをここで定義します。 private lateinit var viewFinder: TextureView private fun startCamera() { val previewConfig = PreviewConfig.Builder().apply { setTargetResolution(Size(640, 480)) }.build() val preview = Preview(previewConfig) preview.setOnPreviewOutputUpdateListener { val parent = viewFinder.parent as ViewGroup parent.removeView(viewFinder) parent.addView(viewFinder, 0) viewFinder.surfaceTexture = it.surfaceTexture updateTransform() } val imageCaptureConfig = ImageCaptureConfig.Builder() .apply { setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY) }.build() val imageCapture = ImageCapture(imageCaptureConfig) findViewById(R.id.capture_button).setOnClickListener { val file = File(externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg") imageCapture.takePicture(file, executor, object : ImageCapture.OnImageSavedListener { override fun onError( imageCaptureError: ImageCapture.ImageCaptureError, message: String, exc: Throwable? ) { val msg = "Photo capture failed: $message" Log.e("CameraXApp", msg, exc) viewFinder.post { Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() } } override fun onImageSaved(file: File) { val msg = "Photo capture succeeded: ${file.absolutePath}" Log.d("CameraXApp", msg) viewFinder.post { Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() } } }) } CameraX.bindToLifecycle(this, preview, imageCapture) } private fun updateTransform() { val matrix = Matrix() val centerX = viewFinder.width / 2f val centerY = viewFinder.height / 2f val rotationDegrees = when(viewFinder.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) viewFinder.setTransform(matrix) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray) { if (requestCode == REQUEST_CODE_PERMISSIONS) { if (allPermissionsGranted()) { viewFinder.post { startCamera() } } else { Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show() finish() } } } //マニフェストで指定されたすべての権限が付与されているかどうかを確認します private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { ContextCompat.checkSelfPermission( baseContext, it) == PackageManager.PERMISSION_GRANTED } }
CameraXを使用する為に依存関係の設定と権限の設定が必要です。
build.gradleの依存関係(dependenciesの中)にCameraXのライブラリを追加します。
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' //ここに追加しましょう! def camerax_version = '1.0.0-alpha06' implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" }
画面のレイアウトは以下のような実装になっています。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextureView android:id="@+id/view_finder" android:layout_width="640px" android:layout_height="640px" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <ImageButton android:id="@+id/capture_button" android:layout_width="72dp" android:layout_height="72dp" android:layout_margin="24dp" app:srcCompat="@android:drawable/ic_menu_camera" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
画面はこのようになります。
AndroidManifest.xmlに以下の記述を追加します。
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Android10以降の端末であればWRITE_EXTERNAL_STORAGEの記述は必要ないので、書かなくても良いです。
今回はAndroid9端末で確認した為、記述しています。
CameraXのPreviewクラスとImageCaptureクラスを使ってカメラ撮影の機能を実装します。
CameraXのUseCaseクラスが複数ありますが、使用する流れは概ね以下のような感じです。
1.実装したい機能のオプションを設定する
2.リスナーを設定する
3.作成したUseCaseクラスをライフサイクルに紐づける
今回はPreviewクラスとImageCaptureクラスを使います。
1.SurfaceTextureをつくる
viewFinder = findViewById(R.id.view_finder)
CameraXの実装にはこのSurfaceTextureを使います。
プレビュー画面用に画像を描画する為に作成します。
2.プレビュー時のオプションの設定をする
val previewConfig = PreviewConfig.Builder().apply { setTargetResolution(Size(640, 480)) }.build()
ここでは、レンズの解像度や画像のアスペクト比などが設定できます。
今回使用している、setTargetResolutionは解像度を設定しています。
Builderクラスにパラメータを設定します。
3.Previewクラスを作成してリスナーを付与する
val preview = Preview(previewConfig) preview.setOnPreviewOutputUpdateListener { val parent = viewFinder.parent as ViewGroup parent.removeView(viewFinder) parent.addView(viewFinder, 0) viewFinder.surfaceTexture = it.surfaceTexture }
出力結果をもとにしてプレビューを更新するにはremoveView()で以前に取得したSurfaceTextureを解放し、addView()で再度追加し直す必要があります。
※ こちらでsetOnPreviewOutoutUpdateListenerの詳細な説明があります。
取得できる画像データは画面の回転やアスペクト比が考慮されていないので、画面の回転に対してビューファインダー (TextureView)の回転がずれたり、潰れた状態で表示されることがあります。その為回転に対応した実装を行う必要があります。
private fun updateTransform() { val matrix = Matrix() //viewFinderの中心を計算します。 val centerX = viewFinder.width / 2f val centerY = viewFinder.height / 2f //カメラの回転を考慮してプレビュー出力を修正します val rotationDegrees = when(viewFinder.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) //最後にviewFinderに変換を適用します viewFinder.setTransform(matrix) }
流れを順番に書くと
1.回転をTextureViewに適用する為のMatrixと回転の中心点を用意する。
2.画面の向きから回転させたい角度を取得してMatrixを更新する。
3.viewFinderにMatrixを適用する。
という感じです。
・撮影時のオプションを設定する
カメラの向きを指定する等のカスタマイズをしたい場合は、ImageCaptureConfigを作るときのBuilderクラスにパラメータを設定します。
val imageCaptureConfig = ImageCaptureConfig.Builder() .apply { setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY) }.build()
今回の場合だと、撮影時低遅延モードに設定しています。
・ImageCaptureクラスを作成し、撮影メソッドを呼ぶ
val imageCapture = ImageCapture(imageCaptureConfig) findViewById(R.id.capture_button).setOnClickListener { val file = File(externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg") imageCapture.takePicture(file, executor, object : ImageCapture.OnImageSavedListener { override fun onError( imageCaptureError: ImageCapture.ImageCaptureError, message: String, exc: Throwable? ) { val msg = "Photo capture failed: $message" Log.e("CameraXApp", msg, exc) viewFinder.post { Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() } }
撮影ボタン押下時にjpgファイルが出力されます。
takePictureメソッドの引数に指定している場所の保存されます。
画像保存が成功した場合Toastを出します。逆に失敗した時はログを出力します。
CameraX.bindToLifecycle(this, preview, imageCapture)
最後にImageCaptureクラスをライフサイクルと紐づけます。
CameraXはCameraAPI2と比べると、とても簡単に実装できました。Camera2APIのように凝った設定はできないですが、シンプルなカメラ機能の実装には向いているのではないかと思います。またImageAnalysisクラスも使って画像の解析もできたら楽しそうですね。
コメントを見る
viewFinder.surfaceTexture=it.surfaceTexture
において、val cannot reassigned とエラーがでてしまいます。
viewFinder.setSurfaceTexture(it.surfaceTexture)
とすることで解決できました。
参考としたのは下記ページです。
https://stackoverflow.com/questions/63184908/val-cannot-be-reassigned-in-android-buildtool-30-0-1