Erste Schritte mit CameraX

In diesem Codelab lernen Sie, wie Sie eine Kamera-App erstellen, die mit CameraX einen Sucher darstellt, Fotos aufnimmt und einen Bildstream der Kamera analysiert.

Aus diesem Grund führen wir das Konzept von Anwendungsfällen in CameraX ein, das für verschiedene Kameravorgänge verwendet werden kann, von der Anzeige eines Suchers bis zur Analyse von Frames in Echtzeit.

Was wir lernen

  • Hinzufügen der CameraX-Abhängigkeiten.
  • Kameravorschau in einer Aktivität anzeigen (Anwendungsvorschau)
  • Foto aufnehmen und im Speicher speichern (Anwendungsfall: ImageCapture)
  • Frames in Echtzeit von der Kamera analysieren (Anwendungsfall Bildanalyse)

Hardware, die wir benötigen

  • Ein Android-Gerät, obwohl der Emulator von Android Studio problemlos funktioniert. Das Mindest-API-Level, das unterstützt wird, ist Level 21.

Software, die wir benötigen

  • Android Studio 3.3 oder höher.

Starten Sie ein neues Projekt in Android Studio und wählen Sie Empty Activity aus, wenn Sie dazu aufgefordert werden.

Als Nächstes wählen wir einen beliebigen Namen aus – wir haben die raffinierte CameraX App ausgewählt. Wir sollten darauf achten, dass die Sprache auf Kotlin eingestellt ist, das API-Level mindestens 21 beträgt (für KameraX mindestens erforderlich) und AndroidX-Artefakte verwendet werden.

Zuerst fügen Sie unserer App-Gradle-Datei im Bereich Abhängigkeiten die CameraX-Abhängigkeiten hinzufügen:

// Use the most recent version of CameraX, currently that is alpha04
def camerax_version = "1.0.0-alpha04"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

Wenn Sie dazu aufgefordert werden, klicken Sie auf Jetzt synchronisieren, damit wir CameraX in unserer App verwenden können.

Wir verwenden den SurfaceTexture, um den Kamerasucher darzustellen. In diesem Codelab wird der Sucher im Square-Format im festen Format angezeigt. Ein ausführlicheres Beispiel für einen responsiven Sucher finden Sie in der offiziellen Stichprobe.

So bearbeiten Sie die Layoutdatei activity_main unter res > Layout > activity_main.xml:

<?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" />

</androidx.constraintlayout.widget.ConstraintLayout>

Für das Hinzufügen aller Funktionen in unserem Projekt, die die Kamera nutzen, ist ein wichtiger Schritt erforderlich, um die entsprechenden Berechtigungen für die Kamera anzufordern. Zuerst müssen wir sie im Manifest vor dem Anwendungs-Tag deklarieren:

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

Anschließend müssen Sie in MainActivity Berechtigungen zur Laufzeit anfordern. Die Änderungen werden in der Hauptaktivitätsdatei unter java > com.example.cameraxapp > MainActivity.kt vorgenommen:

Fügen Sie oben in der Datei außerhalb der Definition der MainActivity-Klasse die folgenden Konstanten und Importe hinzu:

// Your IDE likely can auto-import these classes, but there are several
// different implementations so we list them here to disambiguate
import android.Manifest
import android.util.Size
import android.graphics.Matrix
import java.util.concurrent.TimeUnit

// This is an arbitrary number we are using to keep tab of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts
private const val REQUEST_CODE_PERMISSIONS = 10

// This is an array of all the permission specified in the manifest
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

Fügen Sie in die Hauptaktivitätsklasse die folgenden Felder und Hilfsmethoden ein, mit denen Berechtigungen angefordert und der Code ausgelöst wird, sobald uns bekannt ist, dass alle Berechtigungen gewährt wurden:

class MainActivity : AppCompatActivity(), LifecycleOwner {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    // Add this after onCreate

    private lateinit var viewFinder: TextureView

    private fun startCamera() {
        // TODO: Implement CameraX operations
    }

    private fun updateTransform() {
        // TODO: Implement camera viewfinder transformations
    }

    /**
     * Process result from permission request dialog box, has the request
     * been granted? If yes, start Camera. Otherwise display a toast
     */
    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()
            }
        }
    }

    /**
     * Check if all permission specified in the manifest have been granted
     */
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
               baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
}

Schließlich fügen wir alles in „Create“ zusammen, um gegebenenfalls die Berechtigungsanfrage auszulösen:

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Add this at the end of onCreate function

    viewFinder = findViewById(R.id.view_finder)

    // Request camera permissions
    if (allPermissionsGranted()) {
        viewFinder.post { startCamera() }
    } else {
        ActivityCompat.requestPermissions(
            this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
    }

    // Every time the provided texture view changes, recompute layout
    viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
        updateTransform()
    }
}

Wenn die Anwendung jetzt startet, überprüft sie, ob sie die erforderlichen Kameraberechtigungen hat. Wenn dies der Fall ist, wird „startCamera()“ direkt aufgerufen. Andernfalls werden die Berechtigungen angefordert und nach dem Gewähren von „StartCamera()“ aufgerufen.

Bei den meisten Kameraanwendungen ist es sehr wichtig, den Nutzern einen Sucher anzuzeigen. Ansonsten ist es für sie sehr schwierig, die Kamera an die richtige Stelle zu zeigen. Ein Sucher kann mithilfe der KameraX-Klasse „Preview“ implementiert werden.

Wenn Sie die Vorschau verwenden möchten, müssen Sie zuerst eine Konfiguration definieren, mit der eine Instanz des Anwendungsfalls erstellt wird. Die daraus resultierende Instanz müssen wir an den CameraX-Lebenszyklus binden. Führen Sie dazu die Methode „startCamera()“ aus. Tragen Sie den folgenden Code ein:

private fun startCamera() {

    // Create configuration object for the viewfinder use case
    val previewConfig = PreviewConfig.Builder().apply {
        setTargetAspectRatio(Rational(1, 1))
        setTargetResolution(Size(640, 640))
    }.build()

    // Build the viewfinder use case
    val preview = Preview(previewConfig)

    // Every time the viewfinder is updated, recompute layout
    preview.setOnPreviewOutputUpdateListener {

        // To update the SurfaceTexture, we have to remove it and re-add it
        val parent = viewFinder.parent as ViewGroup
        parent.removeView(viewFinder)
        parent.addView(viewFinder, 0)

        viewFinder.surfaceTexture = it.surfaceTexture
        updateTransform()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

Jetzt müssen Sie die mysteriöse Methode „updateTransform()“ implementieren. Das Ziel von „updateTransform()“ ist es, Änderungen der Geräteausrichtung auszugleichen, damit der Sucher in der rechten Rotation angezeigt wird:

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

    // Compute the center of the view finder
    val centerX = viewFinder.width / 2f
    val centerY = viewFinder.height / 2f

    // Correct preview output to account for display rotation
    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)

    // Finally, apply transformations to our TextureView
    viewFinder.setTransform(matrix)
}

Wenn Sie eine produktionsreife App implementieren möchten, sehen Sie sich die offizielle Stichprobe an. Um dieses Codelab möglichst kurz zu halten, verwenden wir einige Kurzbefehle. Beispielsweise erfassen wir einige Konfigurationsänderungen nicht, z. B. 180-Grad-Rotationen von Geräten, die keinen Listener für die Layoutänderung auslösen. Sucher, die nicht quadratisch sind, müssen auch das Seitenverhältnis ausgleichen, wenn das Gerät gedreht wird.

Wenn wir die App erstellen und ausführen, sollten Sie jetzt eine Lebensvorschau sehen. Sehr gut!

Damit Nutzer Bilder aufnehmen können, fügen wir nach der Texturansicht in der Datei res > activity_main.xml eine Schaltfläche als Teil des Layouts zur Verfügung:

<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" />

Andere Anwendungsfälle funktionieren auf ähnliche Weise wie die Vorschau. Zuerst müssen wir ein Konfigurationsobjekt definieren, mit dem das tatsächliche Anwendungsfallobjekt instanziiert wird. Wenn Sie Fotos aufnehmen möchten, müssen Sie die Methode „startCamera()“ aktualisieren und ein paar weitere Codezeilen am Ende des Aufrufs an CameraX.bindToLebenszyklus hinzufügen, wenn die Aufnahmetaste gedrückt wird.

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Create configuration object for the image capture use case
    val imageCaptureConfig = ImageCaptureConfig.Builder()
        .apply {
            setTargetAspectRatio(Rational(1, 1))
            // We don't set a resolution for image capture; instead, we
            // select a capture mode which will infer the appropriate
            // resolution based on aspect ration and requested mode
            setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
    }.build()

    // Build the image capture use case and attach button click listener
    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)
            }
        })
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

Aktualisieren Sie dann den Aufruf von CameraX.bindToLebenszyklus, um den neuen Anwendungsfall einzubeziehen:

CameraX.bindToLifecycle(this, preview, imageCapture)

Aus diesem Grund gibt es jetzt eine funktionale Schaltfläche für Fotos.

Eine sehr interessante Funktion von CameraX ist die ImageAnalysis-Klasse. Damit kann eine benutzerdefinierte Klasse definiert werden, die die Benutzeroberfläche „ImageAnalysis.Analyzer“ implementiert, die mit eingehenden Kameraframes aufgerufen wird. Entsprechend der Kern Vision von CameraX müssen wir uns nicht um den Verwaltungsstatus der Kamera oder die Entsorgung von Bildern kümmern. Die Bindung an den gewünschten Lebenszyklus unserer App reicht wie andere Lebenszyklus-Komponenten aus.

Zuerst wird ein benutzerdefiniertes Bildanalysetool implementiert. Unser Analysator ist ganz einfach: Es wird lediglich der durchschnittliche Luma-Wert (Leuchtkraft) des Bildes protokolliert. In diesem Fall wird verdeutlicht, was zu tun ist, wenn es sich um komplexe Fälle handelt. Wir müssen lediglich die Funktion „analysieren“ in einer Klasse überschreiben, in der die Schnittstelle „ImageAnalysis.Analyzer“ implementiert wird. Wir können unsere Implementierung als innere Klasse in MainActivity definieren:

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

    /**
     * Helper extension function used to extract a byte array from an
     * image plane buffer
     */
    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()    // Rewind the buffer to zero
        val data = ByteArray(remaining())
        get(data)   // Copy the buffer into a byte array
        return data // Return the byte array
    }

    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()
        // Calculate the average luma no more often than every second
        if (currentTimestamp - lastAnalyzedTimestamp >=
            TimeUnit.SECONDS.toMillis(1)) {
            // Since format in ImageAnalysis is YUV, image.planes[0]
            // contains the Y (luminance) plane
            val buffer = image.planes[0].buffer
            // Extract image data from callback object
            val data = buffer.toByteArray()
            // Convert the data into an array of pixel values
            val pixels = data.map { it.toInt() and 0xFF }
            // Compute average luminance for the image
            val luma = pixels.average()
            // Log the new luma value
            Log.d("CameraXApp", "Average luminosity: $luma")
            // Update timestamp of last analyzed frame
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}

Durch die Implementierung von „ImageAnalysis. Analyzer“ müssen wir nur noch „ImageAnalysis“ instanziieren und die Funktion „startCamera()“ noch einmal vor dem Aufruf von CameraX.bindToLebenszyklus aktualisieren.

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Setup image analysis pipeline that computes average pixel luminance
    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
        // Use a worker thread for image analysis to prevent glitches
        val analyzerThread = HandlerThread(
            "LuminosityAnalysis").apply { start() }
        setCallbackHandler(Handler(analyzerThread.looper))
        // In our analysis, we care more about the latest image than
        // analyzing *every* image
        setImageReaderMode(
            ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
    }.build()

    // Build the image analysis use case and instantiate our analyzer
    val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
        analyzer = LuminosityAnalyzer()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview, imageCapture)
}

Außerdem aktualisieren wir den Aufruf von CameraX.bindtoLebenszyklus, um den neuen Anwendungsfall zu binden:

CameraX.bindToLifecycle(
    this, preview, imageCapture, analyzerUseCase)

Beim Ausführen der App wird etwa ungefähr jede Sekunde eine ähnliche Meldung wie im Logcat ausgegeben:

D/CameraXApp: Average luminosity: ...

Wenn Sie die App testen möchten, klicken Sie in Android Studio auf Ausführen. Ihr Projekt wird dann auf dem ausgewählten Gerät oder im ausgewählten Emulator erstellt, bereitgestellt und gestartet. Nach dem Laden der App sollte der Sucher angezeigt werden. Er bleibt auch nach dem Drehen des Geräts dank des Codes für die Ausrichtung erfolgreich, und zwar auch dann, wenn er über die Schaltfläche Fotos aufnehmen kann.

Sie haben das Code-Lab erfolgreich abgeschlossen. Im Rückblick haben Sie Folgendes in einer neuen Android-App implementiert:

  • KameraX-Abhängigkeiten wurden in Ihr Projekt aufgenommen.
  • Kamerasucher angezeigt (mit Anwendungsfall „Vorschau“)
  • Fotoaufnahme implementiert, Bilder mithilfe des ImageCapture-Anwendungsfalls gespeichert
  • Implementierung von Frames der Kamera in Echtzeit (mit Anwendungsfall ImageAnalysis)

Wenn Sie mehr über CameraX und die Funktionen erfahren möchten, lesen Sie die Dokumentation oder klonen Sie das offizielle Beispiel.