Camera Xとは
CameraX は、カメラアプリの開発を簡単に行うための Jetpack サポート ライブラリです。ほとんどの Android デバイスで機能する、使いやすく一貫性のある API サーフェスを提供するほか、Android 5.0(API レベル 21)への下位互換性も備えています。
これまでのcamera2よりもシンプルに実装でき、Android 5.0でも使用することができるので嬉しいですね。
また、拡張機能を利用することでポートレート、HDR、夜景などの効果も利用することができるようです
https://developer.android.com/training/camerax
公式でCodelabが用意されているので、今回はそれを進めていきたいと思います。
https://codelabs.developers.google.com/codelabs/camerax-getting-started
環境
- Kotlin
- minSdkVersion 21
- Android Studio 3.3(以上)
1. Gradleへ依存関係を追加
def camerax_version = '1.0.0-alpha06'
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
現時点ではimplementation 'androidx.appcompat:appcompat:1.1.0'
が必要でした。
また、Camera XのいくつかのメソッドはJava 8が必要なので、コンパイルオプションを設定する必要があります。
androidブロックの最下部に以下を追加します。
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
2. レイアウト作成
プレビュー画面表示用のTextureViewを設定します。
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextureView
android:id="@+id/view_finder"
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>
3. カメラのパーミッションをリクエスト
AndroidManifest.xml
のapplicationタグの前にCameraパーミッションを追加します。
<uses-permission android:name="android.permission.CAMERA" />
次に、カメラへのアクセス許可の要求を実装していきます。
class MainActivity : AppCompatActivity() {
companion object {
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
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
)
}
}
// パーミッションダイアログの結果を取得
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
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
}
}
4. ViewFinderの実装
Preview
クラスを使用します。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
...
// TextureViewが変更される度にレイアウトを再計算
viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateTransform()
}
}
private fun startCamera() {
// PreviewConfigオブジェクトを生成
val previewConfig = PreviewConfig.Builder().apply {
setTargetResolution(Size(640, 480))
}.build()
// PreviewConfigからPreviewインスタンスを生成
val preview = Preview(previewConfig)
preview.setOnPreviewOutputUpdateListener {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
updateTransform()
}
// Camera Xのライフサイクルにバインド
CameraX.bindToLifecycle(this, preview)
}
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)
// TextureViewに変換をを適用
viewFinder.setTransform(matrix)
}
}
ここまでで、カメラが表示されるようになったとおもいます
5. 画像キャプチャ
撮影用のボタンを設置します。
<ImageButton
android:id="@+id/capture_button"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_margin="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@android:drawable/ic_menu_camera" />
カメラボタンを押した時の保存処理を追加します。
private fun startCamera() {
・・・
・・・
// ImageCaptureConfigオブジェクトを生成
val imageCaptureConfig = ImageCaptureConfig.Builder().apply {
setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
}.build()
// ImageCaptureConfigからImageCaptureインスタンスを生成
val imageCapture = ImageCapture(imageCaptureConfig)
findViewById<ImageButton>(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,
cause: Throwable?
) {
val msg = "Photo capture failed: $message"
Log.e("Camera X App", msg, cause)
viewFinder.post {
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
.show()
}
}
override fun onImageSaved(file: File) {
val msg = "Photo capture succeeded: ${file.absolutePath}"
Log.d("Camera X App", msg)
viewFinder.post {
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
.show()
}
}
})
}
// 引数に"imageCapture"を追加
CameraX.bindToLifecycle(this, preview, imageCapture)
}
ボタンを押すと撮影した画像が保存されているはずです
6. 画像解析
最後にImageAnalysisクラスでちょっとした解析を行ってみましょう。
ここでは画像の平均輝度を求めてみます。
private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
private fun ByteBuffer.toByteArray(): ByteArray {
rewind()
val data = ByteArray(remaining())
get(data)
return data
}
// ImageAnalysis.Analyzerインターフェースにある"analyze"メソッドをオーバーライド
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
val currentTimestamp = System.currentTimeMillis()
// 輝度計算を毎秒以上の間隔で行う
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
Log.d("Camera X App", "Average luminosity: $luma")
lastAnalyzedTimestamp = currentTimestamp
}
}
}
startCamera()メソッド内でインスタンスの生成を行い、それをバインドします。
private fun startCamera() {
・・・
・・・
// ImageAnalysisConfigオブジェクトを生成
val analyzerConfig = ImageAnalysisConfig.Builder().apply {
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
}.build()
// ImageAnalysisConfigからImageAnalysisインスタンスを生成
val analyzerUsecase = ImageAnalysis(analyzerConfig).apply {
setAnalyzer(executor, LuminosityAnalyzer())
}
// 引数に"analyzerUsecase"を追加
CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUsecase)
}
これで完了です。
以下のようなログが出力されるようになりましたね
D/Camera X App: Average luminosity: 86.584970703125
D/Camera X App: Average luminosity: 86.58956380208333
D/Camera X App: Average luminosity: 91.70888997395834
D/Camera X App: Average luminosity: 91.86529947916667
D/Camera X App: Average luminosity: 92.1453125
最後に
カメラ機能の実装がとても楽になり、開発工数の削減にも繋がるかもしれませんね!
より詳細な解析や、ポートレート効果なども実装してみたいと思いました