4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Android Camera X

Posted at

#CameraX
※ まだアルファのライブラリーです。

CameraX: a Jetpack support library for camera app development

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に次のように追加

AndroidManifest
<uses-permission android:name="android.permission.CAMERA" />
}

ビューファインダーを作る(ViewFinder)

ビューファインダーを作る View は TextureView にする

  • TexttureViewを使う理由はカメラからのコンテンツストリームを表現するには適切なビューである。 Androidでのストリームを SurfaceTexture で扱う。
スクリーンショット 2019-05-28 22.31.40.png
activity_main
<?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には次のような定数を追加

MainActivity
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}

MainActivityの内部には次のようなフィルドとメソッドを追加する。カメラの権限が要請され、権限が承認されたら startCamera()を呼び出す。

MainActivity
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
    }
}
onCreate()
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ライブラリーを使って実装する。

onCreate()
    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)
    }

updateTransform()
    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() を実装して、端末の画面回転によってカメラの映像も合わせて出力する。

#写真を撮る処理を実装

activity_main
<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 に写真を撮るためのボタンを追加する。

startCamera()
    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 インタフェースが実装できる。カメラセッションを管理したりイメージを廃棄する心配はいらない。

LuminosityAnalyzer
    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() に設定処理をする

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 との組み合わせも良さそう。

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?