【问题标题】:Android CameraX (Java) Wrong preview orientationAndroid CameraX (Java) 预览方向错误
【发布时间】:2020-02-01 08:59:51
【问题描述】:

我将 CameraX 从 Kotlin 移植到 Java,但我的预览 (TextureView) 仅在以横向打开时正确。当我尝试旋转(纵向)时,预览会失真(可能是 90 度)。 我从清单中删除了 android:screenOrientation="sensorPortrait" ,否则第一次预览也出现错误。 对 updateTransform() 的任何其他调用都不起作用,更改旋转值也会使我的 TextureView 更小,并且不能解决失真问题。 我问是因为大多数代码都在 Kotlin 中,而我在 Java 中找不到有效的解决方案。 代码如下:

package com.sweetieapps.librarianpro;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureConfig;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.core.PreviewConfig;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.Image;
import android.os.Bundle;
import android.util.Log;
import android.util.Rational;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;


public class CameraXActivity extends AppCompatActivity {

    private final int REQUEST_CODE_PERMISSIONS = 10; //arbitrary number, can be changed accordingly
    private final String[] REQUIRED_PERMISSIONS = new String[]{"android.permission.CAMERA","android.permission.WRITE_EXTERNAL_STORAGE"};//array w/ permissions from manifest
    private TextureView txView;
    ImageView imgRotation, imgCapture;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camerax);

        txView = findViewById(R.id.txvCameraXViewFinder);
        imgRotation = findViewById(R.id.imgCameraXRotation);
        imgCapture = findViewById(R.id.imgCameraXCapture);

        ImageView imgMask = findViewById(R.id.imgCameraXMask);

        if(Variables.PictureMode == Variables.PictureModes.User){
            imgMask.setVisibility(View.VISIBLE);
        }
        else{
            imgMask.setVisibility(View.INVISIBLE);
        }

        if(allPermissionsGranted()){
            startCamera(); //start camera if permission has been granted by user
        } else{
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
        }

        imgRotation.setOnClickListener(v ->
                updateTransform());
    }

    private void startCamera(){
        //make sure there isn't another camera instance running before starting
        CameraX.unbindAll();

        /* start preview */
        int aspRatioW = txView.getWidth(); //get width of screen
        int aspRatioH = txView.getHeight(); //get height
        Rational asp = new Rational(aspRatioW,aspRatioH); //aspect ratio
        Size screen = new Size(aspRatioW,aspRatioH); //size of the screen

        //config obj for preview/viewfinder thingy.
        PreviewConfig pConfig = new PreviewConfig.Builder().setTargetAspectRatio(asp).setTargetResolution(screen).build();
        Preview preview = new Preview(pConfig); //lets build it

        //to update the surface texture we have to destroy it first, then re-add it
        preview.setOnPreviewOutputUpdateListener(
                output -> {
                    ViewGroup parent=(ViewGroup)txView.getParent();
                    parent.removeView(txView);
                    parent.addView(txView,0);
                    txView.setSurfaceTexture(output.getSurfaceTexture());
                    updateTransform();
                });

        /* image capture */
        //config obj, selected capture mode
        ImageCaptureConfig imgCapConfig = new ImageCaptureConfig.Builder().setCaptureMode(ImageCapture.CaptureMode.MAX_QUALITY)
                .setTargetRotation(getWindowManager().getDefaultDisplay().getRotation()).build();


        final ImageCapture imgCap = new ImageCapture(imgCapConfig);

        imgCapture.setOnClickListener(v -> {
            imgCap.takePicture(new ImageCapture.OnImageCapturedListener() {
                @Override
                public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
                    //ByteBuffer bb = image.getPlanes()[0].getBuffer();
                    //byte[] buf = new byte[bb.remaining()];
                    //bb.get(buf);
                    Intent returnIntent = new Intent();
                    if(image.getImage() != null){
                        //img.setImageBitmap(imageToBitmap(image.getImage()));
                        Variables.bitmap = bitmapRotate(imageToBitmap(image.getImage()));
                        image.close();
                        setResult(Activity.RESULT_OK,returnIntent);
                        finish();
                    }
                    else{
                        setResult(Activity.RESULT_CANCELED, returnIntent);
                        finish();
                    }
                }
                /*
                @Override
                public void onError(
                        ImageCapture.UseCaseError error, String message, @Nullable Throwable cause) {

                    // silently ingore error
                }

                 */
            });
            /*
            //img.setImageBitmap(txView.getBitmap());

             */
            /*
            File file = new File(Environment.getExternalStorageDirectory() + "/" + System.currentTimeMillis() + ".jpg");
            imgCap.takePicture(file, new ImageCapture.OnImageSavedListener() {
                @Override
                public void onImageSaved(@NonNull File file) {
                    String msg = "Photo capture succeeded: " + file.getAbsolutePath();
                    Toast.makeText(getBaseContext(), msg,Toast.LENGTH_LONG).show();
                }

                @Override
                public void onError(@NonNull ImageCapture.UseCaseError useCaseError, @NonNull String message, @Nullable Throwable cause) {
                    String msg = "Photo capture failed: " + message;
                    Toast.makeText(getBaseContext(), msg,Toast.LENGTH_LONG).show();
                    if(cause != null){
                        cause.printStackTrace();
                    }
                }
            });
            */
        });
        /* image analyser

        ImageAnalysisConfig imgAConfig = new ImageAnalysisConfig.Builder().setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE).build();
        ImageAnalysis analysis = new ImageAnalysis(imgAConfig);

        analysis.setAnalyzer(
            new ImageAnalysis.Analyzer(){
                @Override
                public void analyze(ImageProxy image, int rotationDegrees){
                    //y'all can add code to analyse stuff here idek go wild.
                }
            });

        //bind to lifecycle:
        CameraX.bindToLifecycle(this, analysis, imgCap, preview);
        */
        CameraX.bindToLifecycle(this, imgCap, preview);
    }


    private void updateTransform(){
         //compensates the changes in orientation for the viewfinder, bc the rest of the layout stays in portrait mode.
         //methinks :thonk:
         //imgCap does this already, this class can be commented out or be used to optimise the preview
        Matrix mx = new Matrix();
        float w = txView.getMeasuredWidth();
        float h = txView.getMeasuredHeight();

        float centreX = w / 2f; //calc centre of the viewfinder
        float centreY = h / 2f;

        int rotationDgr;
        int rotation = (int)txView.getRotation(); //cast to int bc switches don't like floats

        switch(rotation){ //correct output to account for display rotation
            case Surface.ROTATION_0:
                rotationDgr = 0;
                break;
            case Surface.ROTATION_90:
                rotationDgr = 90;
                break;
            case Surface.ROTATION_180:
                rotationDgr = 180;
                break;
            case Surface.ROTATION_270:
                rotationDgr = 270;
                break;
            default:
                return;
        }
        mx.postRotate((float)rotationDgr, centreX, centreY);
        txView.setTransform(mx); //apply transformations to textureview
    }


    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        //start camera when permissions have been granted otherwise exit app
        if(requestCode == REQUEST_CODE_PERMISSIONS){
            if(allPermissionsGranted()){
                startCamera();
            } else{
                Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show();
                finish();
            }
        }
    }

    private boolean allPermissionsGranted(){
        //check if req permissions have been granted
        for(String permission : REQUIRED_PERMISSIONS){
            if(ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED){
                return false;
            }
        }
        return true;
    }

    private Bitmap imageToBitmap(Image image){
        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[buffer.capacity()];
        buffer.get(bytes);
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
    }

    private Bitmap bitmapCrop(Bitmap bitmap){
        return bitmapCompress(Bitmap.createBitmap(bitmap, 0,0,bitmap.getWidth(), bitmap.getWidth()));
    }

    private Bitmap bitmapRotate(Bitmap bitmap){
        Matrix matrix = new Matrix();
        matrix.postRotate(90);
        if(bitmap.getHeight() < bitmap.getWidth()){
            if(Variables.PictureMode == Variables.PictureModes.User){
                return bitmapCrop(Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true));
            }
            else{
                return bitmapCompress(Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true));
            }

        }
        else{
            if(Variables.PictureMode == Variables.PictureModes.User){
                return bitmapCrop(bitmap);
            }
            else{
                return bitmapCompress(bitmap);
            }
        }
    }

    private Bitmap bitmapCompress(Bitmap bitmap){
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
        Bitmap compressedBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(stream.toByteArray()));
        int width, height;
        width = compressedBitmap.getWidth() / 5;
        height = width * 8;
        Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth(), height, true);
        return bitmapScale(scaledBitmap);
    }

    private Bitmap bitmapScale(Bitmap bitmap){
        //Matrix
        int maxHeight = 500;
        int maxWidth = 500;
        float scale = Math.min(((float)maxHeight / bitmap.getWidth()), ((float)maxWidth / bitmap.getHeight()));
        Matrix matrix = new Matrix();
        matrix.postScale(scale, scale);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig)
    {
        Log.d("tag", "config changed");
        super.onConfigurationChanged(newConfig);

        updateTransform();
        int orientation = newConfig.orientation;
        if (orientation == Configuration.ORIENTATION_PORTRAIT)
            //Log.d("tag", "Portrait");
            imgCapture.setVisibility(View.VISIBLE);
        else if (orientation == Configuration.ORIENTATION_LANDSCAPE)
            imgCapture.setVisibility(View.INVISIBLE);
            //Log.d("tag", "Landscape");
        //else
           // Log.w("tag", "other: " + orientation);
    }
}

这里是 Xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextureView
        android:id="@+id/txvCameraXViewFinder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/imgCameraXMask"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guidelineCameraX"
        app:layout_constraintVertical_bias="0.0"
        app:srcCompat="@android:color/black"
        tools:visibility="invisible" />

    <ImageButton
        android:id="@+id/imgCameraXCapture"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="24dp"
        android:background="@android:color/transparent"
        android:scaleType="fitCenter"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guidelineCameraXCapture"
        app:srcCompat="@drawable/shutter" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guidelineCameraX"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.6" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guidelineCameraXPortraitMode"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.8" />

    <ImageView
        android:id="@+id/imgCameraXRotation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:adjustViewBounds="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@+id/imgCameraXMask"
        app:layout_constraintStart_toStartOf="@+id/guidelineCameraXPortraitMode"
        app:srcCompat="@drawable/take_in_portrait_mode" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guidelineCameraXCapture"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.85" />

</androidx.constraintlayout.widget.ConstraintLayout>

【问题讨论】:

    标签: java android android-camerax


    【解决方案1】:

    您的updateTransform() 方法似乎只是通过旋转来更正预览,使其与 UI 方向匹配。当相机预览输出的大小与预览表面的大小不同时,通常会发生预览失真,要解决此问题,必须适当缩放 TextureView 以适应预览在其范围内,或者您可以通过应用更新 TextureView 的 SurfaceTexture使用 Matrix 对其进行转换(就像您对旋转所做的那样)。

    鉴于不同的相机硬件级别、众多的 Android 制造商以及各种可能出错的方式,正确预览并非易事。 camerax 库附带一个包含 PreviewView 类的视图工件 (androidx.camera.view),建议使用它而不是 TextureView,因为它试图在后台解决这些问题。使用它看起来像这样。

    在您的布局文件中,包含一个 PreviewView。

    <androidx.constraintlayout.widget.ConstraintLayout>
        <androidx.camera.view.PreviewView 
            android:id="@+id/previewView"
            ... />
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    然后,在您创建一个 Preview 用例后,将其与 PreviewView 的 PreviewSurfaceProvider 挂钩。

    final Preview preview = new Preview.Builder().build();
    preview.setPreviewSurfaceProvider(previewView.getPreviewSurfaceProvider());
    

    请记住,视图工件仍处于 alpha 阶段且仍在开发中,因此它可能仍存在一些问题。但总的来说,它解决了开发人员面临的许多预览问题(包括预览失真)。

    【讨论】:

    • 人们可能会期望 - 但目前情况并非如此。无缘无故地否决我并不能使这个宗教声明成为一个高质量的答案,可以复制。你试过吗?
    猜你喜欢
    • 2021-09-28
    • 1970-01-01
    • 2012-12-17
    • 1970-01-01
    • 2020-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多