Подтвердить что ты не робот

Android - Как использовать камеру getSupportedPreviewSizes() для портретной ориентации

Я пытаюсь встроить предварительный просмотр камеры в действие. И это только в портретной ориентации. Проблема заключается в том, что предварительный просмотр растягивается.

Я попытался выбрать оптимальный размер. Но проблема в том, что все поддерживаемые размеры предварительного просмотра от getSupportedPreviewSizes() возвращают размеры в альбомной ориентации. Так что выбор подходящего размера в соответствии с моим кодом не будет работать.

Мой макет XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_take_attendance"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:orientation="vertical"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.lab.rafael.smartattendance.TakeAttendanceActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/take_attendance_label"
        android:id="@+id/take_attendance_label"
        android:layout_marginBottom="@dimen/activity_vertical_margin"/>

    <!-- camera preview container -->
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@color/red"
        android:id="@+id/take_attendance_scan_qr_frame"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="@string/take_attendance_manual_text"
            />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/take_attendance_manual_button"
            android:id="@+id/take_attendance_manual_button"/>
    </LinearLayout>
</LinearLayout>

Здесь мой класс CameraPreview:

package com.lab.rafael.smartattendance.camera;

import android.content.Context;
import android.hardware.Camera;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.List;

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    private Camera mCamera = null;
    private SurfaceHolder mHolder = null;
    private Camera.Size optimalSize = null;

    public CameraPreview(Context context, Camera camera)
    {
        super(context);
        mCamera = camera;
        mHolder = getHolder();
        mHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            Camera.Parameters params = mCamera.getParameters();
            List<String> focusModes = params.getSupportedFocusModes();
            mCamera.setDisplayOrientation(90);
            mCamera.setPreviewDisplay(holder);

            if(focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
                params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            }

            if(optimalSize != null) {
                params.setPreviewSize(optimalSize.width, optimalSize.height);
            }

            mCamera.setParameters(params);

            mCamera.startPreview();
        } catch (IOException e)
        {
            Log.e("created_error", e.getMessage());
        }

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        if(mHolder.getSurface() == null) {
            return;
        }

        try {
            mCamera.stopPreview();
        } catch (Exception e) {
            Log.e("changed_error", e.getMessage());
        }

        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e){
            Log.e("error", e.getMessage());
        }
    }

    @Override
    public void onMeasure(int measureWidthSpec, int measureHeightSpec) {
        optimalSize = getOptimalSize(MeasureSpec.getSize(measureWidthSpec), MeasureSpec.getSize(measureHeightSpec));
        setMeasuredDimension(optimalSize.width, optimalSize.height);
    }

    protected Camera.Size getOptimalSize(int width, int height) {
        List<Camera.Size> supportedSizes = mCamera.getParameters().getSupportedPreviewSizes();
        double targetRatio = (double) width / height,
                optimalRatio = 0.0,
                acceptableRatioMargin = 0.1,
                minDiff = Double.MAX_VALUE;


        for(Camera.Size size : supportedSizes) {
            optimalRatio = (double) size.width / size.height;
            if(Math.abs(optimalRatio - targetRatio) < acceptableRatioMargin) {
                if(Math.abs(height - size.height) < minDiff) {
                    minDiff = Math.abs(height - size.height);
                    optimalSize = size;
                }
            }
        }

        if(optimalSize == null) {
            for(Camera.Size size : supportedSizes) {
                if(Math.abs(height - size.height) <= minDiff) {
                    minDiff = Math.abs(height - size.height);
                    optimalSize = size;
                }
            }
        }

        return optimalSize;
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}

Ниже изображены следующие значения:

Specified resolution from measureSpecWidth/Height = `984x1335`

Returned from getOptimalSize() = `1600x1200`.

Потому что при условии, что supportedPreviewSizes для пейзажа не портрет.

Здесь результат:

введите описание изображения здесь

4b9b3361

Ответ 1

У меня была такая же проблема, как год назад. Кроме того, мне приходилось иметь дело с фронтальной и задней камерой. Я не очень много помню о коде, но я попробовал его, прежде чем публиковать этот ответ, и он все еще работает как шарм.
Надеюсь, вы сможете копать и сравнивать с вашим кодом. Я могу поделиться больше кода, если вы только что-то работаете;)

/**
 * A simple wrapper around a Camera and a SurfaceView that renders a centered preview of the Camera
 * to the surface. We need to center the SurfaceView because not all devices have cameras that
 * support preview sizes at the same aspect ratio as the device display.
 */
public class Preview extends ViewGroup implements SurfaceHolder.Callback {

    SurfaceView mSurfaceView;
    SurfaceHolder mHolder;
    Camera.Size mPreviewSize;
    List<Camera.Size> mSupportedPreviewSizes;
    Camera mCamera;
    private Context context;
    private int mCameraId;
    public boolean use_front_camera;

    public Preview(Context context, int cameraId) {
        super(context);

        this.context = context;
        mCameraId = cameraId;
        use_front_camera = true;

        mSurfaceView = new SurfaceView(context);
        addView(mSurfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
    }

    public void setCamera(Camera camera) {
        mCamera = camera;
        if (mCamera != null) {
            mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
            requestLayout();
        }
    }

    public void switchCamera(Camera camera) {
        setCamera(camera);
        try {
            camera.setPreviewDisplay(mHolder);
        } catch (IOException exception) {
            android.util.Log.e(IdelityConstants.DEBUG_IDELITY_KEY_LOG, "IOException caused by setPreviewDisplay()", exception);
        }
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
        requestLayout();

        camera.setParameters(parameters);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // We purposely disregard child measurements because act as a
        // wrapper to a SurfaceView that centers the camera preview instead
        // of stretching it.

        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        //MUST CALL THIS
        setMeasuredDimension(width, height);

        if (mSupportedPreviewSizes != null) {
            mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width, height);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed && getChildCount() > 0) {
            final View child = getChildAt(0);

            final int width = r - l;
            final int height = b - t;

            int previewWidth = width;
            int previewHeight = height;
            if (mPreviewSize != null) {
                /**
                 * Como el calculo se hace con la cámara en modo landscape y luego toca
                 * girar la cámara para que se vea bien, se pasan los valores cambiados.
                 */
                previewWidth = mPreviewSize.height;
                previewHeight = mPreviewSize.width;
            }

            // Center the child SurfaceView within the parent.
            if (width * previewHeight < height * previewWidth) {
                final int scaledChildWidth = previewWidth * height / previewHeight;
                child.layout((width - scaledChildWidth) / 2, 0,
                    (width + scaledChildWidth) / 2, height);
            } else {
                final int scaledChildHeight = previewHeight * width / previewWidth;
                child.layout(0, (height - scaledChildHeight) / 2,
                    width, (height + scaledChildHeight) / 2);
            }
        }
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, acquire the camera and tell it where
        // to draw.
        try {
            if (mCamera != null) {
                mCamera.setPreviewDisplay(holder);
            }
        } catch (IOException exception) {
            android.util.Log.e(IdelityConstants.DEBUG_IDELITY_KEY_LOG, "IOException caused by setPreviewDisplay()", exception);
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return, so stop the preview.
    //        if (mCamera != null) {
    //            mCamera.stopPreview();
    //        }
    }


    private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio = (double) w / h;
        if (sizes == null) return null;

        Camera.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;

        int targetHeight = h;

        // Try to find an size match aspect ratio and size
        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }

        // Cannot find the one match the aspect ratio, ignore the requirement
        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Camera.Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }
        return optimalSize;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // Now that the size is known, set up the camera parameters and begin    
        // the preview.

        if (mCamera == null)
            return;

        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        parameters.setJpegQuality(100);
        parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);


        List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
        Camera.Size size = sizes.get(0);
        for(int i=0;i<sizes.size();i++)
        {
            if(sizes.get(i).width > size.width)
                size = sizes.get(i);
        }
        parameters.setPictureSize(size.width, size.height);


        requestLayout();


        mCamera.setParameters(parameters);
            mCamera.setDisplayOrientation(getCameraDisplayOrientation((FragmentActivity)context, mCameraId));
        mCamera.startPreview();
    }


    public static int getCameraDisplayOrientation(FragmentActivity activity, int cameraId) {
        Camera.CameraInfo info = new Camera.CameraInfo();

        Camera.getCameraInfo(cameraId, info);
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();

        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0: degrees = 0; break;
            case Surface.ROTATION_90: degrees = 90; break;
            case Surface.ROTATION_180: degrees = 180; break;
            case Surface.ROTATION_270: degrees = 270; break;
        }


        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        }
        else {  // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }

        return result;
    }


    /** A safe way to get an instance of the Camera object. */
    public static Camera getCameraInstance(int cameraIndex){
        Camera c = null;
        try {
            c = Camera.open(cameraIndex); // attempt to get a Camera instance
        }
        catch (Exception e){
            // Camera is not available (in use or does not exist)
            android.util.Log.e(IdelityConstants.ERROR_IDELITY_KEY_LOG, "Camera is not available: " + e.getMessage());
        }
        return c; // returns null if camera is unavailable
    }
}



вот XML, его простой (вы увидите на скриншоте). Единственное важное значение имеет FrameLayout с id: capture_evidence_camera_preview
  

<RelativeLayout
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:id="@+id/capture_evidence_linearLayout_camera"
    android:layout_weight="3"
    android:layout_gravity="center_horizontal">


    <FrameLayout
        android:id="@+id/capture_evidence_camera_preview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"/>

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/capture_evidence_default_text_number_evidence"
        android:id="@+id/capture_evidence_textView_value_typed"
        android:textSize="50sp"
        android:textColor="@color/idelity_blanco"
        android:gravity="center_horizontal"
        android:paddingLeft="5dp"
        android:paddingRight="5dp"
        android:background="#d2000000"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:paddingTop="8dp"
        android:paddingBottom="8dp" />
</RelativeLayout>


<RelativeLayout
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:layout_weight="1">

    <net.idelity.idelitymobile.ui.helpers.IdelityButton
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:text="@string/button_back"
        android:id="@+id/capture_evidence_button_cancel"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:background="@drawable/button_gray"
        android:textColor="@color/idelity_blanco"
        android:textSize="20sp"
        android:paddingLeft="40dp"
        android:paddingRight="40dp"
        android:textStyle="bold" />

    <net.idelity.idelitymobile.ui.helpers.IdelityButton
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/capture_evidence_button_capture_evidence"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@+id/capture_evidence_button_cancel"
        android:layout_toEndOf="@+id/capture_evidence_button_cancel"
        android:background="@drawable/take_photo_button_camera"
        android:textSize="25sp"
        android:textColor="@color/idelity_blanco"
        android:textStyle="bold"
        android:text="@string/capture_evidence_button_capture_evidence"
        android:paddingBottom="10dp" />
</RelativeLayout>

Предварительный просмотр XML

Используется в FragmentActivity (я могу поделиться им, если вам это нужно)

Ответ 2

tl; dr размеры, используемые как в getSupportedPreviewSizes(), так и в setPreviewSize(int width, int height), находятся в исходной ориентации камеры, которая может (и обычно) отличаться от естественной ориентации телефона и текущего дисплея ориентации.

Из-за этого метод getOptimalSize(int, int) зацикливается на размерах, когда они находятся на их стороне (и используя из них 1/ratio из-за них), не выбирая ни одного из них и не выбирая неправильное соотношение в конце, на основе высота по второму циклу в методе, что приводит к раздавленному изображению.


По-видимому, поддерживаемые размеры всегда относятся к камере в ее естественном угле (хотя в документации это нам не сказано). Естественный угол камеры обычно не совпадает с естественным углом телефона. Вы можете проверить разницу между ними, используя поле CameraInfo.orientation.

Документация, подтверждающая, что это правда (помимо проверки), является той же документацией, которая также решает вашу тайну: Camera.Parameters.setPreviewSize(int width, int height):

Стороны ширины и высоты основаны на ориентации камеры. То есть размер предварительного просмотра - это размер, прежде чем он будет повернут по ориентации дисплея. Поэтому приложения должны учитывать ориентацию дисплея при настройке размера предварительного просмотра. Например, предположим, что камера поддерживает размеры предварительного просмотра 480x320 и 320x480. Приложение хочет получить коэффициент предварительного просмотра 3: 2. Если ориентация дисплея установлена ​​на 0 или 180, размер предварительного просмотра должен быть установлен на 480x320. Если ориентация дисплея установлена ​​на 90 или 270, размер предварительного просмотра должен быть установлен на 320x480. Ориентация дисплея также должна учитываться при настройке размера изображения и размера миниатюр.

(Документация здесь)

Мы можем узнать несколько вещей из этого:

  • Размеры, которые вы получаете, должны быть одинаковыми независимо от ориентации дисплея/телефона, поэтому нет ничего плохого в значениях, которые вы видите там. Вы должны повернуть их на бок, чтобы выбрать лучший для метода onMeasure() для измерения вида в портретной ориентации (в зависимости от экрана и пространства, которое вы хотите, чтобы предварительный просмотр занимал).

    В идеале - включите их после подтверждения угла установки камеры, а текущий угол телефона несовместим (один пейзаж и один портрет).

    //in getOptimalSize(int width, int height)
             //isCameraOnSide() is a new method you should implement
             //return true iff the camera is mounted on the side compared to 
             //the phone natural orientation.
    double targetRatio = (isCameraOnSide()) ?  (double) height / width 
                            : (double) width / height,
            optimalRatio = 0.0,
            acceptableRatioMargin = 0.1,
            minDiff = Double.MAX_VALUE;
    
    
    for(Camera.Size size : supportedSizes) {
        optimalRatio = (double) size.width / size.height;
        if(Math.abs(optimalRatio - targetRatio) < acceptableRatioMargin) {
            if(Math.abs(height - size.height) < minDiff) {
                minDiff = Math.abs(height - size.height);
                optimalSize = size;
            }
        }
    }
    

    В вашем и моем случае isCameraOnSide() возвращает true - как мы видим из вашей строки setPreviewOrientation(90). Для более общей реализации здесь один образец Google Camera2Basic:

    private boolean isCameraOnSide(){
        int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        //Inquire the sensor orientation relative to the natural phone orientation
        android.hardware.Camera.CameraInfo info =
            new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(0, info); //Back-facing camera
        int sensorOrientation = info.orientation;
    
        boolean swappedDimensions = false;
        switch (displayRotation) {
            case Surface.ROTATION_0:
            case Surface.ROTATION_180:
                if (sensorOrientation == 90 || sensorOrientation == 270) {
                    swappedDimensions = true;
                }
                break;
            case Surface.ROTATION_90:
            case Surface.ROTATION_270:
                if (sensorOrientation == 0 || sensorOrientation == 180) {
                    swappedDimensions = true;
                }
                break;
            default:
                Log.e(TAG, "Display rotation is invalid: " + displayRotation);
        }
    
        return swappedDimensions;
    }
    
  • И что еще более важно: если вы используете метод Camera.Parameters.getPreviewSize() как часы или в журнале, я думаю, вы увидите, что он установлен в другом соотношении, чем размер, выбранный setMearuseDimension(int, int).. Это различие в коэффициентах - это начало растяжения/сквоша (он выглядит сжатым по вертикали на вашем снимке. Это также может быть намеком на то, что искажение не происходит из-за смещения пейзажа/портрета, поскольку пейзаж в портретном представлении будет растянут вертикально, а не раздавлен). После выбора правильного размера для представления (в данном случае SurfaceView) вы должны вызвать Camera.Parameters.setPreviewSize(int width, int height) с поддерживаемым размером предварительного просмотра, который имеет такое же соотношение, как размер, который вы использовали для представления (опять же, ширина в зависимости от камеры, а не текущая ориентация телефона/дисплея, что означает, что он может перейти в параметр height).

    Например, вы могли бы сделать это в методах surfaceCreated и surfaceChanged (работал у меня). Убедитесь, что предварительный просмотр не включен, когда вы задаете размер предварительного просмотра камеры и запустите его (или заново запустите) после этого:

        //inside surfaceCreated(SurfaceHolder holder)
        Camera.Parameters params = mCamera.getParameters(); 
        Camera.Size prevSize = getOptimalSize(getWidth(), getHeight()); 
             //prevSize should be still in the camera orientation. In your and my cases - landscape
        params.setPreviewSize(prevSize.width, prevSize.height);
        mCamera.setParameters(params);
    
        mCamera.setPreviewDisplay(holder);
        mCamera.startPreview();
    

Ответ 3

Я работал над разработкой приложений для камеры на некоторое время, и есть много вещей, которые нужно принять во внимание, но пусть это упростит

  • Постарайтесь сделать размер целевого представления таким же, как и один из популярных поддерживаемых размеров предварительного просмотра (3: 2, 16: 9, 4: 3). Если вы не можете выбрать один размер предварительного просмотра, который имеет наименьшую разницу в соотношении сторон
  • После выбора правильного размера вида вы можете центрировать его внутри своей деятельности, переопределив onLayout() на CameraPreview

Ответ 4

public static Camera.Size determineBestPreviewSize(Camera.Parameters parameters) {
    List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
    return determineBestSize(sizes);
}

public static Camera.Size determineBestPictureSize(Camera.Parameters parameters) {
    List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
    return determineBestSize(sizes);
}

protected static Camera.Size determineBestSize(List<Camera.Size> sizes) {
    Camera.Size bestSize = null;
    long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
    long availableMemory = Runtime.getRuntime().maxMemory() - used;
    for (Camera.Size currentSize : sizes) {
        int newArea = currentSize.width * currentSize.height;
        long neededMemory = newArea * 4 * 4; // newArea * 4 Bytes/pixel * 4 needed copies of the bitmap (for safety :) )
        boolean isDesiredRatio = (currentSize.width / 4) == (currentSize.height / 3);
        boolean isBetterSize = (bestSize == null || currentSize.width > bestSize.width);
        boolean isSafe = neededMemory < availableMemory;
        if (isDesiredRatio && isBetterSize && isSafe) {
            bestSize = currentSize;
        }
    }
    if (bestSize == null) {
        return sizes.get(0);
    }
    return