web-dev-qa-db-fra.com

Android - Comment utiliser l'appareil photo getSupportedPreviewSizes () pour l'orientation portrait

J'essaie d'intégrer un aperçu de la caméra dans une activité. Et c'est seulement en orientation portrait. Le problème est que l'aperçu est étiré.

J'ai essayé de choisir la taille optimale. Mais le problème vient de toutes les tailles de prévisualisation prises en charge à partir de getSupportedPreviewSizes() renvoie les tailles en orientation paysage. Donc, choisir la bonne taille selon mon code ne fonctionnera pas, je suppose. 

Ma mise en page 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>

Voici ma classe 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) {
    }
}

Les images ci-dessous sont issues des valeurs: 

Specified resolution from measureSpecWidth/Height = `984x1335`

Returned from getOptimalSize() = `1600x1200`.

Parce que supportedPreviewSizes sont fournis pour le paysage et non pour le portrait.

Voici le résultat:

 enter image description here

17
Rafael Adel

J'ai eu le même problème comme il y a 1 an. De plus, j'ai dû faire face à la caméra frontale et arrière. Je ne me souviens pas beaucoup du code mais je l'ai essayé avant de poster cette réponse et cela fonctionne toujours comme un charme.
J'espère que vous pourrez creuser et comparer avec votre code. Je peux partager plus de code si vous voulez que quelque chose fonctionne;)

/**
 * 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's 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
    }
}



Voici le XML, c'est simple (vous verrez dans la capture d'écran). Le seul élément important est le FrameLayout avec l'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 preview

Son utilisé sous FragmentActivity (je peux le partager si vous en avez aussi besoin)

8
MiguelHincapieC

tl; dr les tailles utilisées à la fois dans getSupportedPreviewSizes() et dans setPreviewSize(int width, int height) sont dans l'orientation d'origine de l'appareil photo, ce qui peut (et est généralement) différent de l'orientation du téléphone naturel et de l'orientation actuelle de l'affichage. 

De ce fait, la méthode getOptimalSize(int, int) parcourt les tailles lorsqu'elles sont de leur côté (et en utilisant 1/ratio pour cette raison), ne sélectionnant aucune d'entre elles et choisissant un mauvais rapport à la fin, basé sur la hauteur en fonction de la seconde boucle. dans la méthode, résultant en une image écrasée.


Apparemment, les tailles prises en charge font toujours référence à la caméra dans son angle naturel (bien que la documentation ne nous l'indique pas). L'angle naturel de l'appareil photo n'est normalement pas le même que l'angle naturel du téléphone. Vous pouvez vérifier leur différence en utilisant le champ CameraInfo.orientation.

La documentation qui suggère que cela est vrai (en plus de l'essayer) est la même documentation qui résout également votre mystère: Camera.Parameters.setPreviewSize(int width, int height):

Les côtés de la largeur et de la hauteur sont basés sur l'orientation de la caméra. C'est-à-dire que la taille de l'aperçu est la taille avant sa rotation en fonction de l'orientation de l'affichage. Les applications doivent donc tenir compte de l'orientation de l'affichage lors du réglage de la taille de l'aperçu. Par exemple, supposons que l'appareil photo prenne en charge les formats de prévisualisation 480x320 et 320x480. L'application souhaite un taux de prévisualisation de 3: 2. Si l'orientation de l'affichage est définie sur 0 ou 180, la taille de l'aperçu doit être définie sur 480x320. Si l'orientation de l'affichage est définie sur 90 ou 270, la taille de l'aperçu doit être définie sur 320x480. L'orientation de l'affichage doit également être prise en compte lors du réglage de la taille de l'image et de la taille de la vignette.

( Documentation ici )

Nous pouvons apprendre quelques choses de cela:

  1. Les tailles que vous obtenez sont supposées être les mêmes quelle que soit l'orientation de l'affichage/du téléphone, il n'y a donc aucun problème avec les valeurs que vous voyez ici. Vous devriez les tourner de leur côté afin de choisir la meilleure méthode pour la méthode onMeasure () afin de mesurer la vue en orientation portrait (en fonction de l'écran et de l'espace que vous souhaitez que l'aperçu occupe).

    Idéalement, tournez-les après avoir vérifié l'angle de montage de l'appareil photo et l'angle actuel du téléphone ne sont pas compatibles (un paysage et un portrait).

    //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's 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;
            }
        }
    }
    

    Dans votre cas et dans mes cas, isCameraOnSide() renvoie true - comme nous pouvons le constater à partir de votre ligne de setPreviewOrientation(90). Pour une implémentation plus générale, voici un exemple Camera2Basic de Google:

    private boolean isCameraOnSide(){
        int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        //Inquire the sensor's orientation relative to the natural phone's 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;
    }
    
  2. Et plus important encore: si vous utilisez la méthode Camera.Parameters.getPreviewSize() comme montre ou dans un journal, vous verrez que il est défini sur un rapport différent de celui de la taille choisie par la méthode setMearuseDimension(int, int). Cette différence de rapport est l’origine du tronçon/de la courge (elle semble écrasée verticalement dans votre image. Cela peut également indiquer que la distorsion ne provient pas d’une confusion paysage/portrait, mais l'image de paysage en mode portrait serait étirée verticalement plutôt qu'écrasée). Après avoir choisi la bonne taille pour la vue (dans ce cas SurfaceView), vous devez appeler Camera.Parameters.setPreviewSize(int width, int height) avec une taille de prévisualisation prise en charge ayant le même rapport que la taille utilisée pour la vue (à nouveau, largeur en fonction de la caméra, pas du téléphone actuel). Cela signifie que cela peut aller dans le paramètre height).

    Par exemple, vous pouvez le faire avec les méthodes surfaceCreated et surfaceChanged (travaillé pour moi). Assurez-vous que l'aperçu n'est pas activé lorsque vous définissez la taille de l'aperçu de l'appareil photo et démarrez-le (ou redémarrez-le) après avoir effectué les opérations suivantes:

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

Je travaille sur le développement d'applications de caméras depuis un moment et il y a beaucoup de choses à prendre en considération, mais restons simples.

  1. Essayez de faire en sorte que la taille de la vue cible soit au même format que l’une des tailles de prévisualisation couramment prises en charge (3: 2, 16: 9, 4: 3). Si vous ne pouvez pas essayer de choisir une taille d'aperçu présentant la plus petite différence de format d'image
  2. Après avoir choisi une taille de vue appropriée, vous pouvez la centrer dans votre activité en remplaçant onLayout() par votre CameraPreview
0
Mohammad Abbas
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
0
MIkka Marmik