¡Otro nuevo desafío para el desarrollo de Android! ¿CameraX está a punto de dominar el mundo?

Puede que no hayas pensado que CameraX también es parte de Jetpack.

Nuestras vidas se han vuelto cada vez más inseparables de la cámara, desde los selfies hasta las transmisiones en vivo, el escaneo de códigos a la realidad virtual, etc. Los pros y los contras de las cámaras se han convertido naturalmente en un campo que los fabricantes deben perseguir. Para los desarrolladores de aplicaciones, cómo manejar rápidamente la cámara, proporcionar una excelente experiencia de disparo y optimizar el consumo de energía de la cámara es el objetivo que han estado persiguiendo.

Prefacio

La interfaz de la cámara quedó obsoleta en Android 5.0, por lo que el enfoque general es usar su interfaz Camera2 de reemplazo. Pero con la llegada de CameraX, esta opción ya no es la única.

Primero revisemos el simple requisito de la vista previa de la imagen y cómo se implementa usando la interfaz Camera2.

Cámara2

Aparte del procesamiento adicional, como devoluciones de llamada y excepciones, aún se requieren varios pasos para lograrlo, lo cual es más engorroso. ※ El código se omite por razones de espacio y solo explica los pasos ※

Del mismo modo, si la vista previa de la imagen usa CameraX, la implementación es muy simple.

CameraX

Vista previa de la imagen

Se puede hacer en una docena de líneas. Al igual que Camera2, debe mostrar el control de vista previa PreviewView en el diseño y asegurarse de tener el permiso de la cámara. La diferencia se refleja principalmente en los pasos de configuración de la cámara.

private void setupCamera(PreviewView previewView) {
    ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
            ProcessCameraProvider.getInstance(this);
    cameraProviderFuture.addListener(() -> {
        try {
            mCameraProvider = cameraProviderFuture.get();
            bindPreview(mCameraProvider, previewView);
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
    }, ContextCompat.getMainExecutor(this));
}

private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
                         PreviewView previewView) {
    mPreview = new Preview.Builder().build();
    mCamera = cameraProvider.bindToLifecycle(this,
            CameraSelector.DEFAULT_BACK_CAMERA, mPreview);
    mPreview.setSurfaceProvider(previewView.getSurfaceProvider());
}

Cambio de lente

Si desea cambiar de lente, simplemente vincule el ejemplo de CameraSelector de la lente de destino a CameraProvider. Agregamos botones en la pantalla para cambiar tomas.

public void onChangeGo(View view) {
    if (mCameraProvider != null) {
        isBack = !isBack;
        bindPreview(mCameraProvider, binding.previewView);
    }
}

private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
                         PreviewView previewView) {
    ...
    CameraSelector cameraSelector = isBack ? CameraSelector.DEFAULT_BACK_CAMERA
            : CameraSelector.DEFAULT_FRONT_CAMERA;
    // 绑定前确保解除了所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常
    cameraProvider.unbindAll(); 
    mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, mPreview);
    ...
}

Enfoque de la lente

El disparo que no se puede enfocar está incompleto. Escuchamos el evento Touch de Preview y le decimos a CameraX las coordenadas táctiles para comenzar a enfocar.

protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    binding.previewView.setOnTouchListener((v, event) -> {
        FocusMeteringAction action = new FocusMeteringAction.Builder(
                binding.previewView.getMeteringPointFactory()
                        .createPoint(event.getX(), event.getY())).build();
        try {
            showTapView((int) event.getX(), (int) event.getY());
            mCamera.getCameraControl().startFocusAndMetering(action);
        }...
    });
}

private void showTapView(int x, int y) {
    PopupWindow popupWindow = new PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT);
    ImageView imageView = new ImageView(this);
    imageView.setImageResource(R.drawable.ic_focus_view);
    popupWindow.setContentView(imageView);
    popupWindow.showAsDropDown(binding.previewView, x, y);
    binding.previewView.postDelayed(popupWindow::dismiss, 600);
    binding.previewView.playSoundEffect(SoundEffectConstants.CLICK);
}

Además de la vista previa de imágenes, existen muchos otros escenarios de uso, como captura de imágenes, análisis de imágenes y grabación de video. CameraX abstrae estos escenarios de uso como UseCase.

Tiene cuatro subcategorías, a saber, Vista previa, ImageCapture, ImageAnalysis y VideoCapture. A continuación, presentaré cómo usarlos.

Captura de imagen

La imagen se puede tomar con la ayuda de takePicture () proporcionada por ImageCapture. Admite el ahorro en espacio de almacenamiento externo, por supuesto, necesita obtener permisos de lectura y escritura para el almacenamiento externo.

private void takenPictureInternal(boolean isExternal) {
    final ContentValues contentValues = new ContentValues();
    contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, CAPTURED_FILE_NAME
            + "_" + picCount++);
    contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");

    ImageCapture.OutputFileOptions outputFileOptions = 
            new ImageCapture.OutputFileOptions.Builder(
                    getContentResolver(),
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            .build();
    if (mImageCapture != null) {
        mImageCapture.takePicture(outputFileOptions, CameraXExecutors.mainThreadExecutor(),
                new ImageCapture.OnImageSavedCallback() {
                    @Override
                    public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
                        Toast.makeText(DemoActivityLite.this, "Picture got"
                                + (outputFileResults.getSavedUri() != null
                                ? " @ " + outputFileResults.getSavedUri().getPath()
                                : "") + ".", Toast.LENGTH_SHORT)
                                .show();
                    }
                    ...
                });
    }
}

private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
                         PreviewView previewView) {
    ...
    mImageCapture =  new ImageCapture.Builder()
            .setTargetRotation(previewView.getDisplay().getRotation())
            .build();
    ...
    // 需要将ImageCapture场景一并绑定
    mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, mPreview, mImageCapture);
    ...
}

Análisis de imagen

El análisis de imágenes se refiere al análisis en tiempo real de la imagen de la vista previa para identificar información como el color y el contenido, y aplicarla a escenarios empresariales como el aprendizaje automático y el reconocimiento de códigos QR.

Continúe haciendo algunas modificaciones a la demostración y agregue un botón para escanear el código QR. Haga clic en el botón para ingresar al modo de código de escaneo, y el resultado del análisis aparecerá después de que el código QR se haya analizado correctamente.

public void onAnalyzeGo(View view) {
    if (!isAnalyzing) {
        mImageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), image -> {
           analyzeQRCode(image);
        });
    }
    ...
}

// 从ImageProxy取出图像数据,交由二维码框架zxing解析
private void analyzeQRCode(@NonNull ImageProxy imageProxy) {
    ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
    byte[] data = new byte[byteBuffer.remaining()];
    byteBuffer.get(data);
    ...
    BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
    Result result;
    try {
        result = multiFormatReader.decode(bitmap);
    }
    ...
    showQRCodeResult(result);
    imageProxy.close();
}

private void showQRCodeResult(@Nullable Result result) {
    if (binding != null && binding.qrCodeResult != null) {
        binding.qrCodeResult.post(() ->
                binding.qrCodeResult.setText(result != null ? "Link:\n" + result.getText() : ""));
        binding.qrCodeResult.playSoundEffect(SoundEffectConstants.CLICK);
    }
}

Grabación de vídeo

Basándose en startRecording () de VideoCapture, se puede realizar la grabación de video.

Agregue un botón para cambiar entre el modo de captura de imágenes y el modo de grabación de video en la demostración, y vincule el UseCase de grabación de video al CameraProvider cuando cambie al modo de grabación de video.

public void onVideoGo(View view) {
    bindPreview(mCameraProvider, binding.previewView, isVideoMode);
}

private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
                         PreviewView previewView, boolean isVideo) {
    ...
    mVideoCapture = new VideoCapture.Builder()
            .setTargetRotation(previewView.getDisplay().getRotation())
            .setVideoFrameRate(25)
            .setBitRate(3 * 1024 * 1024)
            .build();
    cameraProvider.unbindAll();
    if (isVideo) {
        mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,
                mPreview, mVideoCapture);
    } else {
        mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,
                mPreview, mImageCapture, mImageAnalysis);
    }
    mPreview.setSurfaceProvider(previewView.getSurfaceProvider());
}

Después de hacer clic en el botón de grabación, primero asegúrese de obtener el almacenamiento externo y los permisos de audio, y luego inicie la grabación de video.

public void onCaptureGo(View view) {
    if (isVideoMode) {
        if (!isRecording) {
            // Check permission first.
            ensureAudioStoragePermission(REQUEST_STORAGE_VIDEO);
        }
    }
    ...
}

private void ensureAudioStoragePermission(int requestId) {
    ...
    if (requestId == REQUEST_STORAGE_VIDEO) {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED
                || ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(...);
            return;
        }
        recordVideo();
    }
}

private void recordVideo() {
   try {
        mVideoCapture.startRecording(
                new VideoCapture.OutputFileOptions.Builder(getContentResolver(),
                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
                        .build(),
                CameraXExecutors.mainThreadExecutor(),
                new VideoCapture.OnVideoSavedCallback() {
                    @Override
                    public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
                        // Notify user...
                    }
                }
        );
    } 
    ...
    toggleRecordingStatus();
}

private void toggleRecordingStatus() {
    // Stop recording when toggle to false.
    if (!isRecording && mVideoCapture != null) {
        mVideoCapture.stopRecording();
    }
}

Episodio

Se encontró un problema al implementar la función de grabación de video.

Cuando se hace clic en el botón de grabación de video, si no se ha obtenido el permiso de audio en ese momento, se solicitará el permiso. Incluso después de obtener permiso para llamar a la interfaz de disparo, se producirá una excepción. El registro muestra que la instancia de AudioRecorder es nula y se activa NPE.

Mirando de cerca la lógica relevante, se encuentra que el procesamiento actual de la demostración es vincular VideoCapture al CameraProvider cuando se cambia al modo de grabación de video. En este momento, si no se ha obtenido el permiso de audio, la grabadora de audio no se inicializará.

De hecho, el mensaje correspondiente aparecerá en el registro: El objeto AudioRecord no se puede inicializar correctamente. Pero, ¿por qué sigue ocurriendo NPE cuando obtengo permiso para llamar a la interfaz de disparo de VideoCapture?

Debido a que el procesamiento interno de la interfaz de disparo startRecording () es que si la instancia de AudioRecorder es nula, la solicitud se terminará directamente. No importa cuántas veces se llame más tarde, no ayudará. De hecho, hay lógica en la última parte de la función para obtener la instancia de AudioRecorder nuevamente, pero no hay posibilidad de ejecutarla debido a la NPE anterior.

// VideoCapture.java
public void startRecording(
        @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
        @NonNull OnVideoSavedCallback callback) {
    ...
    try {
        // mAudioRecorder为null将引发NPE终止录制的请求
        mAudioRecorder.startRecording();
    } catch (IllegalStateException e) {
        postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
        return;
    }

    ...
    mRecordingFuture.addListener(() -> {
        ...
        if (getCamera() != null) {
            // 前面发生了NPE,那么将失去此处再次获得AudioRecorder实例的机会
            setupEncoder(getCameraId(), getAttachedSurfaceResolution());
            notifyReset();
        }
    }, CameraXExecutors.mainThreadExecutor());
    ...
}

No sé si esto es una laguna en la implementación de VideoCapture o el desarrollador deliberadamente. Sin embargo, no parece razonable que la NPE todavía se produzca cuando se llama a la interfaz de grabación cuando se obtiene claramente el permiso de audio.

Solo hay algunos esquemas evasivos que se pueden tomar en este momento, ¿o deberían los desarrolladores hacer esto?

Ahora, la vinculación de VideoCapture se realiza antes de obtener el permiso de audio, lo que puede provocar la repetición de NPE mencionada anteriormente. Así que cámbielo para obtener permiso de audio y luego vincule VideoCapture para evitarlo.

Dicho esto, ¿sería mejor agregar instrucciones para obtener permisos de audio en la documentación de VideoCaptue?

Extensión de efectos de cámara

El uso de las escenas mencionadas por sí solo no puede satisfacer la demanda cada vez mayor de disparos. Los efectos de la cámara como el retrato, el disparo nocturno y la belleza son esenciales. Afortunadamente, CameraX admite efectos extendidos. Pero no todos los dispositivos son compatibles con esta extensión. Puede consultar la lista de compatibilidad de dispositivos en el sitio web oficial para obtener más detalles.

Los efectos que se pueden extender se dividen principalmente en dos categorías.

Uno es PreviewExtender para la extensión del efecto durante la vista previa de la imagen y el otro es ImageCaptureExtender para la extensión del efecto durante la captura de la imagen.

Cada categoría contiene varios efectos típicos.

NightPreviewExtender

BokehPreviewExtender vista previa vertical

BeautyPreviewExtender

Vista previa de HdrPreviewExtender HDR

Vista previa automática de AutoPreviewExtender

La realización de activar estos efectos también es muy simple.

private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
                         PreviewView previewView, boolean isVideo) {
    Preview.Builder previewBuilder = new Preview.Builder();
    ImageCapture.Builder captureBuilder = new ImageCapture.Builder()
            .setTargetRotation(previewView.getDisplay().getRotation());
    ...
    setPreviewExtender(previewBuilder, cameraSelector);
    mPreview = previewBuilder.build();

    setCaptureExtender(captureBuilder, cameraSelector);
    mImageCapture =  captureBuilder.build();
    ...
}

private void setPreviewExtender(Preview.Builder builder, CameraSelector cameraSelector) {
    BeautyPreviewExtender beautyPreviewExtender = BeautyPreviewExtender.create(builder);
    if (beautyPreviewExtender.isExtensionAvailable(cameraSelector)) {
        // Enable the extension if available.
        beautyPreviewExtender.enableExtension(cameraSelector);
    }
}

private void setCaptureExtender(ImageCapture.Builder builder, CameraSelector cameraSelector) {
    NightImageCaptureExtender nightImageCaptureExtender = NightImageCaptureExtender.create(builder);
    if (nightImageCaptureExtender.isExtensionAvailable(cameraSelector)) {
        // Enable the extension if available.
        nightImageCaptureExtender.enableExtension(cameraSelector);
    }
}

Desafortunadamente, el Redmi 6A que tengo en la mano no está en la lista de dispositivos que admiten la expansión de efectos OEM, por lo que no puedo mostrarles una imagen de muestra del efecto de expansión exitoso.

Uso de alto nivel

Además de los escenarios comunes de uso de la cámara mencionados anteriormente, existen otros métodos de configuración opcionales. El límite de espacio ya no se ampliará en detalle, y los interesados ​​pueden consultar el sitio web oficial para probarlo.

Salida de conversión CameraX admite la conversión de datos de imagen y salida, como dibujar un diagrama de bloques de caras después de aplicarlo al reconocimiento de retratos

https://developer.android.google.cn/training/camerax/transform-output?hl=zh-cn

Rotación de casos de uso La pantalla puede girar durante la captura y el análisis de imágenes. Aprenda a configurar para que CameraX pueda obtener la orientación de la pantalla y el ángulo de rotación en tiempo real para capturar la imagen correcta https://developer.android.google.cn/training/ camerax / orientación-rotación? hl = zh-cn

Opciones de configuración Controle la resolución, el enfoque automático, la configuración de la forma del visor y otras pautas de configuración https://developer.android.google.cn/training/camerax/configuration?hl=zh-cn

Preste atención

  1. Recuerde llamar a unbindAll () antes de llamar a bindToLifecycle () de CameraProvider, de lo contrario, puede ocurrir una excepción de enlace repetido.
  2. El analyse () de ImageAnalyzer debe llamar al close () de ImageProxy para liberar la imagen inmediatamente después de analizar la imagen, de modo que las imágenes posteriores puedan continuar transmitiéndose. De lo contrario, se bloqueará la devolución de llamada. Por lo tanto, también debemos prestar atención al problema de analizar imágenes que consume mucho tiempo.
  3. No almacene una referencia a cada instancia de ImageProxy después de que se cierre, porque una vez que se llama a close (), estas imágenes se volverán ilegales.
  4. Una vez finalizado el análisis de la imagen, debe llamar al clearAnalyzer () de ImageAnalysis para indicarle que no transmita la imagen para evitar el desperdicio de rendimiento.
  5. No olvide obtener permiso de audio para escenas de grabación de video.

Interesante manejo de compatibilidad

Al implementar la función de captura de imágenes, encontré una nota tan interesante en el documento takePicture () de ImageCapture.

Antes de activar la canalización de captura de imágenes, si la ubicación para guardar es un archivo o MediaStore, primero se verifica para asegurarse de que sea válida y se pueda escribir.

Un archivo se verifica al intentar abrirle un FileOutputStream, mientras que una ubicación en MediaStore es validada por ContentResolver # insert () creando una nueva fila en la tabla definida por el usuario,

recuperando un Uri apuntando a él, luego intentando abrirle un OutputStream.
La fila recién creada es ContentResolver # delete () eliminado al final de la verificación.

En los dispositivos Huawei, esta eliminación hace que el sistema muestre una notificación que informa al usuario que se ha eliminado una foto.

Para evitar esto, se omite la validación de la ubicación para guardar la captura de imágenes en MediaStore en los dispositivos Huawei.

La idea principal es que si el Uri guardado es un MediaStore, se insertará una línea para verificar si la ruta de guardado es legal y se puede escribir. La línea de prueba se eliminará después de la verificación.

Sin embargo, eliminar el registro de línea en el dispositivo Huawei activará una notificación para eliminar la foto. Por lo tanto, para evitar molestar a los usuarios, CameraX omitirá la verificación de ruta en los dispositivos Huawei.

class ImageSaveLocationValidator {
    // 将判断设备品牌是否为华为或荣耀,是则直接跳过验证
    static boolean isValid(final @NonNull ImageCapture.OutputFileOptions outputFileOptions) {
        ...
        if (isSaveToMediaStore(outputFileOptions)) {
            // Skip verification on Huawei devices
            final HuaweiMediaStoreLocationValidationQuirk huaweiQuirk =
                    DeviceQuirks.get(HuaweiMediaStoreLocationValidationQuirk.class);
            if (huaweiQuirk != null) {
                return huaweiQuirk.canSaveToMediaStore();
            }

            return canSaveToMediaStore(outputFileOptions.getContentResolver(),
                    outputFileOptions.getSaveCollection(), outputFileOptions.getContentValues());
        }
        return true;
    }
    ...
}

public class HuaweiMediaStoreLocationValidationQuirk implements Quirk {
    static boolean load() {
        return "HUAWEI".equals(Build.BRAND.toUpperCase())
                || "HONOR".equals(Build.BRAND.toUpperCase());
    }

    /**
     * Always skip checking if the image capture save destination in
     * {@link android.provider.MediaStore} is valid.
     */
    public boolean canSaveToMediaStore() {
        return true;
    }
}

Ventajas de CameraX

Viene del hecho de que CameraX ha realizado un alto grado de encapsulación sobre la base de Camera2 y ha realizado procesamiento de compatibilidad para una gran cantidad de dispositivos, lo que hace que CameraX tenga muchas ventajas.

  • Facilidad de uso La API encapsulada puede lograr el objetivo de manera eficiente.
  • Consistencia del equipo No se preocupe por la versión, ignore las diferencias de hardware y obtenga una experiencia de desarrollo consistente.
  • Nueva experiencia de cámara La misma belleza y otras funciones de disparo que la cámara original se pueden lograr a través de la extensión de efectos.

Conclusión

CameraX se lanzó el 7 de agosto de 2019. Se actualizó de la versión alfa a la versión beta actual.

A partir del interesante procesamiento de compatibilidad de dispositivos Huawei anterior, podemos ver la determinación de CameraX de dominar los ríos y lagos.

La última es todavía una versión beta y necesita mejorarse, pero no es imposible ponerla en un entorno de producción.

Con un marco tan útil, todos deberían usarlo mucho y dar sugerencias, para que se vuelva cada vez más perfecto y pueda traer buenas noticias a desarrolladores y usuarios.

Supongo que te gusta

Origin blog.csdn.net/A_pyf/article/details/115149091
Recomendado
Clasificación