Architektura CameraX

Na tej stronie omawiamy architekturę systemu Aparat X, w tym jego strukturę, korzystanie z interfejsu API, korzystanie z cykli życia i łączenie przypadków użycia.

Struktura CameraX

Dzięki aplikacji CameraX możesz połączyć się z aparatem urządzenia w postaci abstrakcji nazywanej przypadkiem użycia. Dostępne są te przypadki użycia:

  • Podgląd: akceptuje powierzchnię do wyświetlania podglądu, np. PreviewView.
  • Analiza obrazu: udostępnia bufory dostępne dla procesora na potrzeby analiz, np. na potrzeby systemów uczących się.
  • Robienie zdjęć: robi i zapisuje zdjęcie.
  • Nagrywanie obrazu wideo: nagrywanie obrazu i dźwięku za pomocą VideoCapture.

Przypadki użycia można łączyć i aktywne jednocześnie. Aplikacja może na przykład umożliwić użytkownikowi wyświetlenie obrazu widzianego przez aparat za pomocą przykładu użycia podglądu, zastosować przykład analizy obrazu, który określa, czy osoby na zdjęciu się uśmiechają, oraz dodać przypadek użycia przechwytywania obrazu do jego zrobienia, gdy już się one pojawiają.

Model interfejsu API

Aby korzystać z biblioteki, musisz określić te elementy:

  • Pożądany przypadek użycia z opcjami konfiguracji.
  • Co zrobić z danymi wyjściowymi przez dołączenie detektorów.
  • Zamierzony przepływ, np. kiedy włączyć aparaty i kiedy generować dane, przez powiązanie przypadku użycia z cyklami życia architektury Androida.

Aplikację CameraX można napisać na 2 sposoby: CameraController (świetny sposób, jeśli zależy Ci na najprostszym użyciu AparatuX) lub CameraProvider (przydatny, jeśli potrzebujesz większej elastyczności).

Kontroler aparatu

CameraController zapewnia większość podstawowych funkcji aparatuX w jednej klasie. Wymaga niewiele kodowania, a do tego automatycznie obsługuje inicjowanie aparatu, zarządzanie przypadkiem użycia, obrót celu, dotykanie ostrości, powiększanie przez ściąganie palców i inne działania. Konkretna klasa, która rozszerza zakres CameraController, to LifecycleCameraController.

Kotlin

val previewView: PreviewView = viewBinding.previewView
var cameraController = LifecycleCameraController(baseContext)
cameraController.bindToLifecycle(this)
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
previewView.controller = cameraController

Java

PreviewView previewView = viewBinding.previewView;
LifecycleCameraController cameraController = new LifecycleCameraController(baseContext);
cameraController.bindToLifecycle(this);
cameraController.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA);
previewView.setController(cameraController);

Domyślne UseCase w przypadku CameraController to Preview, ImageCapture i ImageAnalysis. Aby wyłączyć funkcję ImageCapture lub ImageAnalysis albo włączyć VideoCapture, użyj metody setEnabledUseCases().

Więcej informacji o zastosowaniu CameraController znajdziesz w przykładzie ze skanerem kodów QR lub filmie z podstawowymi informacjami o usłudze CameraController.

Dostawca kamery

Interfejs CameraProvider jest nadal łatwy w użyciu, ale ponieważ to deweloper aplikacji zajmuje się większą konfiguracją, ma więcej możliwości dostosowania konfiguracji, np. włączenie rotacji obrazu wyjściowego lub ustawienie formatu obrazu wyjściowego w narzędziu ImageAnalysis. Do podglądu z aparatu możesz też użyć niestandardowego obiektu Surface, który daje większą elastyczność, a w przypadku kontrolera CameraController musisz używać kontrolera PreviewView. Użycie istniejącego kodu Surface może być przydatne, jeśli zawiera on już dane wejściowe do innych części aplikacji.

Możesz skonfigurować przypadki użycia za pomocą metod set() i finalizować je za pomocą metody build(). Każdy obiekt przypadku użycia udostępnia zestaw interfejsów API związanych z konkretnym przypadkiem użycia. Na przykład przypadek użycia przechwytywania obrazu zawiera wywołanie metody takePicture().

Zamiast aplikacji wywołującej określone wywołania metod rozpoczęcia i zatrzymania w onResume() i onPause() aplikacja określa cykl życia, z którym ma być powiązana kamera, za pomocą funkcji cameraProvider.bindToLifecycle(). Ten cykl życia informuje następnie CameraX, kiedy należy skonfigurować sesję nagrywania, i dba o to, aby stan kamery odpowiednio się zmieniał, odpowiednio do zmian cyklu życia.

Instrukcje wdrażania w poszczególnych przypadkach użycia znajdziesz w artykułach Implementowanie podglądu, Analizowanie obrazów, Przechwytywanie obrazów i Przechwytywanie wideo

Przypadek użycia podglądu współdziała z Surface na potrzeby wyświetlania. Aplikacje tworzą przypadek użycia z opcjami konfiguracji przy użyciu tego kodu:

Kotlin

val preview = Preview.Builder().build()
val viewFinder: PreviewView = findViewById(R.id.previewView)

// The use case is bound to an Android Lifecycle with the following code
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// PreviewView creates a surface provider and is the recommended provider
preview.setSurfaceProvider(viewFinder.getSurfaceProvider())

Java

Preview preview = new Preview.Builder().build();
PreviewView viewFinder = findViewById(R.id.view_finder);

// The use case is bound to an Android Lifecycle with the following code
Camera camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview);

// PreviewView creates a surface provider, using a Surface from a different
// kind of view will require you to implement your own surface provider.
preview.previewSurfaceProvider = viewFinder.getSurfaceProvider();

Więcej przykładowego kodu znajdziesz w oficjalnej przykładowej aplikacji CameraX.

Cykle życia aplikacji CameraX

CameraX obserwuje cykl życia, aby określić, kiedy otworzyć aparat, kiedy utworzyć sesję przechwytywania, a kiedy zatrzymać i wyłączyć kamerę. Interfejsy API przypadków użycia udostępniają wywołania metod i wywołania zwrotne do monitorowania postępów.

Jak wyjaśniliśmy w sekcji Łączenie przypadków użycia, możesz powiązać niektóre kombinacje przypadków użycia w jednym cyklu życia. Gdy aplikacja musi obsługiwać przypadki użycia, których nie można łączyć, możesz wykonać jedną z tych czynności:

  • Pogrupuj zgodne przypadki użycia w więcej niż jeden fragment, a następnie przełączaj się między fragmentami
  • Utworzenie niestandardowego komponentu cyklu życia i używanie go do ręcznego sterowania cyklem życia kamery

Jeśli odłączysz właścicieli przypadków użycia widoku i kamery (np. używasz niestandardowego cyklu życia lub fragmentu zachowywania danych), musisz zadbać o to, aby wszystkie przypadki użycia nie były powiązane z Aparatem X, używając opcji ProcessCameraProvider.unbindAll() lub oddzielając każdy przypadek użycia z osobna. Z kolei, gdy powiążesz przypadki użycia z cyklem życia, możesz zezwolić aplikacji CameraX na otwieranie i zamykanie sesji przechwytywania oraz usuwanie powiązań przypadków użycia.

Jeśli wszystkie funkcje kamery odpowiadają cyklowi życia elementu uwzględniającego cykl życia, np. elementu AppCompatActivity lub fragmentu AppCompat, zastosowanie cyklu życia tego komponentu do powiązania wszystkich pożądanych przypadków użycia zagwarantuje, że funkcja kamery będzie gotowa, gdy komponent uwzględniający cykl życia będzie aktywny i bezpiecznie wyrzucony bez zużywania zasobów.

Właściciele niestandardowych cyklów życia

W zaawansowanych przypadkach możesz utworzyć niestandardowy obiekt LifecycleOwner, aby umożliwić aplikacji jawne sterowanie cyklem życia sesji w aplikacji CameraX, zamiast wiązać ją ze standardowym Androidem LifecycleOwner.

Poniższy przykładowy kod pokazuje, jak utworzyć prosty niestandardowy element LifecycleOwner:

Kotlin

class CustomLifecycle : LifecycleOwner {
    private val lifecycleRegistry: LifecycleRegistry

    init {
        lifecycleRegistry = LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }
    ...
    fun doOnResume() {
        lifecycleRegistry.markState(State.RESUMED)
    }
    ...
    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class CustomLifecycle implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;
    public CustomLifecycle() {
        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }
   ...
   public void doOnResume() {
        lifecycleRegistry.markState(State.RESUMED);
    }
   ...
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

Za pomocą tego obiektu LifecycleOwner aplikacja może umieszczać przejścia stanów w wybranych punktach kodu. Więcej informacji o implementowaniu tej funkcji w aplikacji znajdziesz w artykule Implementacja niestandardowego właściciela cyklu życia.

Równoczesne przypadki użycia

Przypadki użycia mogą działać jednocześnie. Przypadki użycia można sekwencyjnie powiązać z cyklem życia, ale lepiej powiązać wszystkie przypadki użycia jednym wywołaniem CameraProcessProvider.bindToLifecycle(). Więcej informacji o sprawdzonych metodach wprowadzania zmian w konfiguracji znajdziesz w artykule Obsługiwanie zmian konfiguracji.

W poniższym przykładzie kodu aplikacja określa 2 przypadki użycia, które należy utworzyć i uruchomić jednocześnie. Określa też cykl życia, który ma być używany w obu przypadkach użycia, aby rozpocząć i zakończyć zgodnie z cyklem życia.

Kotlin

private lateinit var imageCapture: ImageCapture

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener(Runnable {
        // Camera provider is now guaranteed to be available
        val cameraProvider = cameraProviderFuture.get()

        // Set up the preview use case to display camera preview.
        val preview = Preview.Builder().build()

        // Set up the capture use case to allow users to take photos.
        imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

        // Choose the camera by requiring a lens facing
        val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
                .build()

        // Attach use cases to the camera with the same lifecycle owner
        val camera = cameraProvider.bindToLifecycle(
                this as LifecycleOwner, cameraSelector, preview, imageCapture)

        // Connect the preview use case to the previewView
        preview.setSurfaceProvider(
                previewView.getSurfaceProvider())
    }, ContextCompat.getMainExecutor(this))
}

Java

private ImageCapture imageCapture;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    PreviewView previewView = findViewById(R.id.previewView);

    ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
            ProcessCameraProvider.getInstance(this);

    cameraProviderFuture.addListener(() -> {
        try {
            // Camera provider is now guaranteed to be available
            ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

            // Set up the view finder use case to display camera preview
            Preview preview = new Preview.Builder().build();

            // Set up the capture use case to allow users to take photos
            imageCapture = new ImageCapture.Builder()
                    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                    .build();

            // Choose the camera by requiring a lens facing
            CameraSelector cameraSelector = new CameraSelector.Builder()
                    .requireLensFacing(lensFacing)
                    .build();

            // Attach use cases to the camera with the same lifecycle owner
            Camera camera = cameraProvider.bindToLifecycle(
                    ((LifecycleOwner) this),
                    cameraSelector,
                    preview,
                    imageCapture);

            // Connect the preview use case to the previewView
            preview.setSurfaceProvider(
                    previewView.getSurfaceProvider());
        } catch (InterruptedException | ExecutionException e) {
            // Currently no exceptions thrown. cameraProviderFuture.get()
            // shouldn't block since the listener is being called, so no need to
            // handle InterruptedException.
        }
    }, ContextCompat.getMainExecutor(this));
}

Te kombinacje konfiguracji na pewno będą obsługiwane (gdy wymagany jest podgląd lub przechwytywanie wideo, ale nie obie jednocześnie):

Podgląd lub przechwytywanie wideo Robienie zdjęć Analiza opisy;
Udostępnij użytkownikowi podgląd lub nagraj film, zrób zdjęcie i przeanalizuj strumień obrazów.
  Zrób zdjęcie i przeanalizuj strumień obrazów.
  Udostępnij podgląd lub nagraj film i zrób zdjęcie.
  Udostępnij podgląd lub nagraj film i przeanalizuj strumień obrazów.

Jeśli wymagany jest zarówno podgląd, jak i rejestracja wideo, warunkowo obsługiwane są te kombinacje przypadków użycia:

Podgląd Nagrywanie wideo Robienie zdjęć Analiza Wymagania specjalne
    Gwarantowane dla wszystkich aparatów
  OGRANICZONA (lub lepsza) kamera.
  Aparat na poziomie LEVEL_3 (lub lepszym).

Reklamy

  • Każdy przypadek użycia może działać samodzielnie. Aplikacja może na przykład nagrać film bez korzystania z podglądu.
  • Po włączeniu rozszerzeń działa tylko kombinacja ImageCapture i Preview. W zależności od implementacji OEM może nie być możliwe dodanie atrybutu ImageAnalysis. Nie można włączyć rozszerzeń w przypadku użycia typu VideoCapture. Szczegółowe informacje znajdziesz w dokumentacji z informacjami o rozszerzeniach.
  • W zależności od możliwości aparatu niektóre kamery mogą obsługiwać kombinację z trybami niższej rozdzielczości, ale nie obsługują tej samej kombinacji w przypadku niektórych wyższych rozdzielczości.

Obsługiwany poziom sprzętu można pobrać z: Camera2CameraInfo. Na przykład ten kod sprawdza, czy domyślny tylny aparat to urządzenie z systemem LEVEL_3:

Kotlin

@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
fun isBackCameraLevel3Device(cameraProvider: ProcessCameraProvider) : Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return CameraSelector.DEFAULT_BACK_CAMERA
            .filter(cameraProvider.availableCameraInfos)
            .firstOrNull()
            ?.let { Camera2CameraInfo.from(it) }
            ?.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
    }
    return false
}

Java

@androidx.annotation.OptIn(markerClass = ExperimentalCamera2Interop.class)
Boolean isBackCameraLevel3Device(ProcessCameraProvider cameraProvider) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        List\ filteredCameraInfos = CameraSelector.DEFAULT_BACK_CAMERA
                .filter(cameraProvider.getAvailableCameraInfos());
        if (!filteredCameraInfos.isEmpty()) {
            return Objects.equals(
                Camera2CameraInfo.from(filteredCameraInfos.get(0)).getCameraCharacteristic(
                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3);
        }
    }
    return false;
}

Uprawnienia

Twoja aplikacja będzie potrzebować uprawnienia CAMERA. Aby zapisywać obrazy w plikach, musisz też mieć uprawnienie WRITE_EXTERNAL_STORAGE. Nie dotyczy to urządzeń z Androidem 10 lub nowszym.

Więcej informacji o konfigurowaniu uprawnień aplikacji znajdziesz w artykule Prośba o uprawnienia aplikacji.

Wymagania

Kamera X ma następujące minimalne wymagania dotyczące wersji:

  • Interfejs API Androida, poziom 21
  • Komponenty architektury Androida 1.1.1

W przypadku działań związanych z cyklem życia używaj wartości FragmentActivity lub AppCompatActivity.

Deklarowanie zależności

Aby dodać zależność w aplikacji CameraX, musisz dodać do projektu repozytorium Google Maven.

Otwórz plik settings.gradle swojego projektu i dodaj repozytorium google() w następujący sposób:

Odlotowe

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Kotlin

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Na końcu bloku kodu Android dodaj następujący ciąg:

Odlotowe

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    // For Kotlin projects
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    // For Kotlin projects
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Do pliku build.gradle każdego modułu dodaj te elementy:

Odlotowy

dependencies {
  // CameraX core library using the camera2 implementation
  def camerax_version = "1.4.0-beta02"
  // The following line is optional, as the core library is included indirectly by camera-camera2
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  // If you want to additionally use the CameraX Lifecycle library
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  // If you want to additionally use the CameraX VideoCapture library
  implementation "androidx.camera:camera-video:${camerax_version}"
  // If you want to additionally use the CameraX View class
  implementation "androidx.camera:camera-view:${camerax_version}"
  // If you want to additionally add CameraX ML Kit Vision Integration
  implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
  // If you want to additionally use the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}

Kotlin

dependencies {
    // CameraX core library using the camera2 implementation
    val camerax_version = "1.4.0-beta02"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation("androidx.camera:camera-core:${camerax_version}")
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    // If you want to additionally use the CameraX Lifecycle library
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    // If you want to additionally use the CameraX VideoCapture library
    implementation("androidx.camera:camera-video:${camerax_version}")
    // If you want to additionally use the CameraX View class
    implementation("androidx.camera:camera-view:${camerax_version}")
    // If you want to additionally add CameraX ML Kit Vision Integration
    implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
    // If you want to additionally use the CameraX Extensions library
    implementation("androidx.camera:camera-extensions:${camerax_version}")
}

Więcej informacji o konfigurowaniu aplikacji pod kątem zgodności z tymi wymaganiami znajdziesz w sekcji Deklarowanie zależności.

Współdziałanie aparatu CameraX z Aparatem 2

Aparat X działa na platformie Aparat 2, a w jego implementacji – umożliwia odczytywanie, a nawet zapisywanie właściwości. Szczegółowe informacje znajdziesz w artykule na temat pakietu Interrop.

Aby dowiedzieć się więcej o tym, jak w aplikacji AparatX skonfigurowano właściwości Aparatu2, użyj polecenia Camera2CameraInfo, aby odczytać bazowe informacje CameraCharacteristics. Możesz też zapisać dane o podstawowych właściwościach Aparatu2, używając jednej z tych dwóch ścieżek:

Poniższa próbka kodu wykorzystuje przypadki użycia strumienia do optymalizacji rozmowy wideo. Użyj parametru Camera2CameraInfo, aby sprawdzić, czy przypadek użycia strumienia rozmowy wideo jest dostępny. Następnie użyj właściwości Camera2Interop.Extender, aby określić podstawowy przypadek użycia strumienia.

Kotlin

// Set underlying Camera2 stream use case to optimize for video calls.

val videoCallStreamId =
    CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong()

// Check available CameraInfos to find the first one that supports
// the video call stream use case.
val frontCameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val isVideoCallStreamingSupported = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES
            )?.contains(videoCallStreamId)
        val isFrontFacing = (cameraInfo.getLensFacing() == 
                             CameraSelector.LENS_FACING_FRONT)
        (isVideoCallStreamingSupported == true) && isFrontFacing
    }

val cameraSelector = frontCameraInfo.cameraSelector

// Start with a Preview Builder.
val previewBuilder = Preview.Builder()
    .setTargetAspectRatio(screenAspectRatio)
    .setTargetRotation(rotation)

// Use Camera2Interop.Extender to set the video call stream use case.
Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId)

// Bind the Preview UseCase and the corresponding CameraSelector.
val preview = previewBuilder.build()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)

Java

// Set underlying Camera2 stream use case to optimize for video calls.

Long videoCallStreamId =
    CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong();

// Check available CameraInfos to find the first one that supports
// the video call stream use case.
List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
CameraInfo frontCameraInfo = null;
for (cameraInfo in cameraInfos) {
    Long[] availableStreamUseCases = Camera2CameraInfo.from(cameraInfo)
        .getCameraCharacteristic(
            CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES
        );
    boolean isVideoCallStreamingSupported = Arrays.List(availableStreamUseCases)
                .contains(videoCallStreamId);
    boolean isFrontFacing = (cameraInfo.getLensFacing() ==
                             CameraSelector.LENS_FACING_FRONT);

    if (isVideoCallStreamingSupported && isFrontFacing) {
        frontCameraInfo = cameraInfo;
    }
}

if (frontCameraInfo == null) {
    // Handle case where video call streaming is not supported.
}

CameraSelector cameraSelector = frontCameraInfo.getCameraSelector();

// Start with a Preview Builder.
Preview.Builder previewBuilder = Preview.Builder()
    .setTargetAspectRatio(screenAspectRatio)
    .setTargetRotation(rotation);

// Use Camera2Interop.Extender to set the video call stream use case.
Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId);

// Bind the Preview UseCase and the corresponding CameraSelector.
Preview preview = previewBuilder.build()
Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)

Dodatkowe materiały

Więcej informacji o aparacie AparatX znajdziesz w tych dodatkowych materiałach.

Ćwiczenia z programowania

  • Pierwsze kroki z AparatemX
  • Przykładowy kod

  • Przykładowe aplikacje AparatuX