#CameraX
※ まだアルファのライブラリーです。
CameraXの公式ドキュメント
最新のCameraXを確認をするならこちらのリンクを参考
特徴
・ Camera2を利用するので Android5.0(API level21) からサポート
・ ユースケース基盤の設計で Preview、Image analysis、 Image Captureのユースケースを同時サポート
・ ライフサイクル対応
・ デバイスの互換性問題を解決して、機器別の分岐コードを簡略化
・ 特定デバイスの従属される Bokeh、HDR などと同じエフェクトをサポート
最低要求事項
CameraXライブラリーを利用するためには次の条件が必要。
・ Android5.0(API Level21)
・ Android Architecture Components v1.1.1
・ lifecycle-awareアクティビティでは FragmentActivity または AppCompatActivity 利用
Cameraサンプルでやってみる
CameraXを利用して、Preview、Image Capture、Image analysis 3つの機能があるサンプルアプリを作る
依存関係追加
CameraXを利用するためには Google Maven レポジトリをプロジェクトに追加する
プロジェクトレベルの build.gradle に google() を次のように追加する
allprojects {
repositories {
// 追加
google()
jcenter()
}
}
モジュールレベルの build.gradle 次のように追加する
dependencies {
// coreライブラリー
implementation "androidx.camera:camera-core:1.0.0-alpha01"
// Camera2 extensionsを利用するならこちらも追加
implementation "androidx.camera:camera-camera2:1.0.0-alpha01"
}
Permission追加
カメラを使うためにはPermissionの追加が必要です。AndroidManifestに次のように追加
<uses-permission android:name="android.permission.CAMERA" />
}
ビューファインダーを作る(ViewFinder)
ビューファインダーを作る View は TextureView にする
- TexttureViewを使う理由はカメラからのコンテンツストリームを表現するには適切なビューである。 Androidでのストリームを SurfaceTexture で扱う。
<?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/textureView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity実装
MainActivityには次のような定数を追加
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
MainActivityの内部には次のようなフィルドとメソッドを追加する。カメラの権限が要請され、権限が承認されたら startCamera()を呼び出す。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
}
private lateinit var viewFinder: TextureView
private fun startCamera() {
...
}
private fun updateTransform() {
...
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<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 {
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(
this, permission) != PackageManager.PERMISSION_GRANTED) {
return false
}
}
return true
}
}
override fun onCreate(savedInstanceState: Bundle?) {
textureView = findViewById(R.id.textureView)
// パーミッションチェック
if (allPermissionsGranted()) {
textureView.post { startCamera() }
} else {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
}
// [TextureView]が変更されるたびにレイアウトを再読込する
textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateTransform()
}
}
これでアプリを起動するとカメラのパーミッションをチェックする。 カメラのパーミッションが既に許可されていれば startCamera() を呼び出す。パーミッションが許可されてなければパーミッションをチェックし、許可された時に startCamera() を呼び出す。
ビューファインダー実装
starCamera() メソッドに CameraXライブラリーを使って実装する。
private fun startCamera() {
// プレビューを確認するための設定のオブジェクトを生成
val previewConfig = PreviewConfig.Builder().apply {
setTargetAspectRatio(Rational(1, 1))
setTargetResolution(Size(640, 640))
}.build()
// プレビューのオブジェクトを生成する
val preview = Preview(previewConfig)
// ビューファインダーが更新されるたびにレイアウトを再設定する
preview.setOnPreviewOutputUpdateListener {
textureView.surfaceTexture = it.surfaceTexture
updateTransform()
}
// ライフサイクルにカメラをバインディングする
CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
}
private fun updateTransform() {
val matrix = Matrix()
// ビューファインダーの中心を計算
val centerX = textureView.width / 2f
val centerY = textureView.height / 2f
// 画面回転を考慮してプレビューを出力
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)
textureView.setTransform(matrix)
}
updateTransform() を実装して、端末の画面回転によってカメラの映像も合わせて出力する。
#写真を撮る処理を実装
<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" />
activity_main.xml に写真を撮るためのボタンを追加する。
private fun startCamera() {
val previewConfig = ...
val preview = ...
preview.setOnPreviewOutputUpdateListener {
....
}
// 写真を撮る設定のために ImageCaptureConfig を生成
val imageCaptureConfig = ImageCaptureConfig.Builder()
.apply {
setTargetAspectRatio(Rational(1, 1))
// 写真の解像度を設定しない代わりに画面の比率とキャプチャーモードで
// CameraXライブラリーが解像度を決める
setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
}.build()
// ImageCapture を利用してボタンがクリックされた時に写真を撮る
val imageCapture = ImageCapture(imageCaptureConfig)
findViewById<ImageButton>(R.id.capture_button).setOnClickListener {
val file = File(externalMediaDirs.first(),
"${System.currentTimeMillis()}.jpg")
imageCapture.takePicture(file,
object : ImageCapture.OnImageSavedListener {
override fun onError(error: ImageCapture.UseCaseError,
message: String, exc: Throwable?) {
val msg = "Photo capture failed: $message"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.e("CameraXApp", msg)
exc?.printStackTrace()
}
override fun onImageSaved(file: File) {
val msg = "PATH : ${file.absolutePath}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d("CameraXApp", msg)
}
})
}
CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
}
イメージプロセシングを実装
CameraX の面白い機能は ImageAnalysis クラスである。カメラフレームと一緒に ImageAnalysis.Analyzer インタフェースが実装できる。カメラセッションを管理したりイメージを廃棄する心配はいらない。
private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
/**
* Image Buffer を Byte 配列に抽出する
*/
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Buffer の Position を0に戻す
val data = ByteArray(remaining())
get(data) // Byte Buffer を Byte 配列にコピーする
return data // Byte 配列に返す
}
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
val currentTimestamp = System.currentTimeMillis()
// 1秒ごとに計算
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
// ピクセルの平均値を求めてログ出力する
val buffer = image.planes[0].buffer
val data:ByteArray = buffer.toByteArray()
val pixels:List<Int> = data.map { it.toInt() and 0xFF }
val luma:Double = pixels.average()
Log.d("CameraXApp", "Average luminosity: $luma")
lastAnalyzedTimestamp = currentTimestamp
}
}
}
LuminosityAnalyzer をインスタンス化して、 startCamera() に設定処理をする
private fun startCamera() {
val previewConfig = ...
// 写真を撮る設定処理
val analyzerConfig = ImageAnalysisConfig.Builder().apply {
val analyzerThread = HandlerThread("LuminosityAnalysis").apply { start() }
setCallbackHandler(Handler(analyzerThread.looper))
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
}.build()
// イメージプロセシングのオブジェクトを生成
val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
analyzer = LuminosityAnalyzer()
}
// ライフサイクルにカメラをバインディングする
CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
}
これでログが1秒ごとに出力されることが確認できる。
#最後に
Firebase の ML Kit との組み合わせも良さそう。