Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
24
Help us understand the problem. What is going on with this article?
@emusute1212

【Android】CameraX試してみた

More than 1 year has passed since last update.

はじめに

ついこの間Google I/Oが開催され、その場で紹介された CameraX が気になったので、試してみたという記事です。
(実際にはチュートリアルしかやっていません・・・。:innocent:

CameraXとは

CameraX は、カメラアプリの開発を簡単に行うための Jetpack サポート ライブラリです。ほとんどの Android デバイスで機能する、使いやすく一貫性のある API サーフェスを提供するほか、Android 5.0(API レベル 21)への下位互換性も備えています。

公式より引用

まあつまり、今までのCameraやCamera2を使いやすくして、さらに古いAndroid(5.0)でも使えるようにしたよというものです。

チュートリアル

公式がチュートリアルを出しています。
https://codelabs.developers.google.com/codelabs/camerax-getting-started/#0

今回はこれを進めていきました。
本記事ではプロジェクトの作成などは飛ばします。

対応環境

  • AndroidAPI21以上の端末 or エミュレータ
  • AndroidStudio 3.3以上

Gradleへの記述

以下を記述します。CameraX は Camera2 の機能を使用しているので、Camera2も含めます。

ちなみに、appcompat1.1.0-alpha01じゃないと動かなかったです・・・:thinking:

build.gradle
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
def camerax_version = "1.0.0-alpha02"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

レイアウト作成

自分は画面いっぱいに画面が表示されて欲しかったので、縦横を0dpで指定しています。

activity.xml
<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/camera"
            android:layout_height="0dp"
            android:layout_width="0dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

カメラのパーミッションを制御する

ActivityManifest.xmlに以下を追記して、カメラを使うことを明示します。

ActivityManifest.xml
<uses-permission android:name="android.permission.CAMERA" />

次にMainActivityからCameraの要求を投げます。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        if (allPermissionsGranted()) {
            viewFinder.post { startCamera() }
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }
    }

    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 {
        ContextCompat.checkSelfPermission(
            baseContext, it
        ) == PackageManager.PERMISSION_GRANTED
    }

    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 10
        /**
         * 必要なパーミッションのリスト
         */
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }
}

パーミッションの許可ってこんな書き方があったんですね・・・知りませんでした。

カメラが映るようにする

ついにカメラの映像を画面に反映させます。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var camera: TextureView
    override fun onCreate(savedInstanceState: Bundle?) {
        camera = findViewById(R.id.camera)

        //パーミッション許可処理・・・

        camera.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            updateTransform()
        }
    }
    private fun startCamera() {
        val previewConfig = PreviewConfig.Builder().apply {
            setTargetAspectRatio(Rational(1, 1))
            setTargetResolution(Size(camera.width, camera.height))
        }.build()

        val preview = Preview(previewConfig)
        preview.setOnPreviewOutputUpdateListener {
            parent.removeView(camera)
            parent.addView(camera, 0)
            camera.surfaceTexture = it.surfaceTexture
            updateTransform()
        }

        //ライフサイクルにbindさせる
        CameraX.bindToLifecycle(this, preview)
    }

    private fun updateTransform() {
        val matrix = Matrix()

        // 中心を求める
        val centerX = camera.width / 2f
        val centerY = camera.height / 2f

        val rotationDegrees = when (camera.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へ反映
        camera.setTransform(matrix)
    }
}

これだけでカメラが映るようになるのか・・・(感動)

写真を撮れるようにする

先ほどのはプレビューだけですので、写真を撮れるようにします。

まずは写真を撮るためのボタンを追加します。

activity_main.xml
<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" />

その後に、写真をとるための設定を行い、それを bind させます。
startCamera()メソッドのカメラ設定処理の直下に書きます。

MainActivity.kt
private fun startCamera() {
    //カメラ設定処理・・・

    val imageCaptureConfig = ImageCaptureConfig.Builder()
        .apply {
            setTargetAspectRatio(Rational(1, 1))
            setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
        }.build()

    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 = "Photo capture succeeded: ${file.absolutePath}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d("CameraXApp", msg)
                }
            })
    }

    //引数に imageCapture を追加!
    CameraX.bindToLifecycle(this, preview, imageCapture)
}

カメラボタンを押すと写真が撮られて、ディレクトリへ保存されます。

解析

解析と書きましたが、写っているものが何か、という解析ではなく、チュートリアルではピクセルの平均値を求めるという解析です。

一つMainActivityの下にクラスを作成します。

private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
    private var lastAnalyzedTimestamp = 0L

    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()
        val data = ByteArray(remaining())
        get(data)
        return data
    }

    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("CameraXApp", "Average luminosity: $luma")
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}

その後に、画像解析のための設定を行い、それを bind させます。
startCamera()メソッドのカメラ設定処理の直下に書きます。

MainActivity.kt
private fun startCamera() {
    //写真を撮る設定処理・・・

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

    //引数にanalyzerUseCaseを追加!
    CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
}

これでピクセルがログに出力されます!

以上でチュートリアル終わり

最後は動作テストをして終わりです :thumbsup:

リファクタリングの余地はありそうですが、以上で終了です!

CameraXを試してみて

Cameraライブラリはちょっとかじったことがあるのですが、段違いなぐらい楽になっているかなと思いました。
Configを設定してそれをbindさせていくことが基本的な使い方ですかね:thinking:

あと、チュートリアルが親切で、英語がわからない自分でも理解することができました。

最後に

今回はチュートリアルしか試していませんが、今後、Firebaseなどと組み合わせて画像解析を行うのも楽しそうだなと思いました。

24
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
emusute1212
Androidエンジニア。 メイン言語はJavaからKotlinに移行しました✌️

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
24
Help us understand the problem. What is going on with this article?