Camera Size and 4:3 aspect ratio is not matching

2019-07-09 04:01发布

问题:

I am using the Surface View of Camera to show camera and take a photo i need the camera preview to be of specific ration 4:3, instagram is a square and mine is a rectangle.

If you look at the instagram app the camera preview is not stretching or compressed, but in mine its compressed.

This is my Camera Preview Class :

class CustomCam extends SurfaceView implements SurfaceHolder.Callback {

    private final String TAG = "PIC-FRAME";
    private static final double ASPECT_RATIO = 4.0 / 3.0;
    private static final int PICTURE_SIZE_MAX_WIDTH = 1280;
    private static final int PREVIEW_SIZE_MAX_WIDTH = 640;

    private SurfaceHolder mHolder;
    private Camera mCamera;
    private Display display;
    public List<Camera.Size> mSupportedPreviewSizes;
    private Camera.Size mPreviewSize;

    public CustomCam(Activity context, Camera camera) {
        super(context);
        mCamera = camera;
        display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

        mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
        for (Camera.Size str : mSupportedPreviewSizes)
            Log.e(TAG, str.width + "/" + str.height);
        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = getHolder();
        mHolder.addCallback(this);
        // deprecated setting, but required on Android versions prior to 3.0
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        setKeepScreenOn(true);
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // empty. Take care of releasing the Camera preview in your activity.
        this.getHolder().removeCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    private Camera.Size getBestPreviewSize(int width, int height) {
        Camera.Size result = null;
        Camera.Parameters p = mCamera.getParameters();
        for (Camera.Size size : p.getSupportedPreviewSizes()) {
            if (size.width <= width && size.height <= height) {
                if (result == null) {
                    result = size;
                } else {
                    int resultArea = result.width * result.height;
                    int newArea = size.width * size.height;

                    if (newArea > resultArea) {
                        result = size;
                    }
                }
            }
        }
        return result;

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        //This line helped me set the preview Display Orientation to Portrait
        //Only works API Level 8 and higher unfortunately.

        try {
            Camera.Parameters parameters = mCamera.getParameters();
//        Camera.Size size = getBestPreviewSize(width, height);
//            Camera.Size size = getOptimalPreviewSize(mSupportedPreviewSizes, width, height);
//            parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
//            initialCameraPictureSize(parameters);
//            parameters.setPreviewSize(size.width, size.height);

            Camera.Size bestPreviewSize = determineBestPreviewSize(parameters);
            Camera.Size bestPictureSize = determineBestPictureSize(parameters);

            parameters.setPreviewSize(bestPreviewSize.width, bestPreviewSize.height);
            parameters.setPictureSize(bestPictureSize.width, bestPictureSize.height);

            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            mCamera.setDisplayOrientation(90);
            mCamera.getParameters().setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
            mCamera.setPreviewDisplay(mHolder);
            mCamera.setParameters(parameters);
            mCamera.startPreview();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void initialCameraPictureSize(Camera.Parameters parameters) {

        List list = parameters.getSupportedPictureSizes();
        if (list != null) {
            Camera.Size size = null;
            Iterator iterator = list.iterator();
            do {
                if (!iterator.hasNext())
                    break;
                Camera.Size size1 = (Camera.Size) iterator.next();
                if (Math.abs(3F * ((float) size1.width / 4F) - (float) size1.height) < 0.1F * (float) size1.width && (size == null || size1.height > size.height && size1.width < 3000))
                    size = size1;
            } while (true);
            if (size != null)
                parameters.setPictureSize(size.width, size.height);
            else
                Log.e("CameraSettings", "No supported picture size found");
        }
    }

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

        if (sizes == null) return null;

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

        int targetHeight = h;

        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);
            }
        }

        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;
    }

    /**
     * Measure the view and its content to determine the measured width and the
     * measured height.
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);

        if (width > height * ASPECT_RATIO) {
            width = (int) (height * ASPECT_RATIO + 0.5);
        } else {
            height = (int) (width / ASPECT_RATIO + 0.5);
        }

        setMeasuredDimension(width, height);
    }

    protected Camera.Size determineBestSize(List<Camera.Size> sizes, int widthThreshold) {
        Camera.Size bestSize = null;

        for (Camera.Size currentSize : sizes) {
            boolean isDesiredRatio = (currentSize.width / 4) == (currentSize.height / 3);
            boolean isBetterSize = (bestSize == null || currentSize.width > bestSize.width);
            boolean isInBounds = currentSize.width <= PICTURE_SIZE_MAX_WIDTH;

            if (isDesiredRatio && isInBounds && isBetterSize) {
                bestSize = currentSize;
            }
        }

        if (bestSize == null) {
            return sizes.get(0);
        }

        return bestSize;
    }

    private Camera.Size determineBestPreviewSize(Camera.Parameters parameters) {
        List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();

        return determineBestSize(sizes, PREVIEW_SIZE_MAX_WIDTH);
    }

    private Camera.Size determineBestPictureSize(Camera.Parameters parameters) {
        List<Camera.Size> sizes = parameters.getSupportedPictureSizes();

        return determineBestSize(sizes, PICTURE_SIZE_MAX_WIDTH);
    }
}

My Custom Frame layout :

CustomFrameLayout extends FrameLayout {

    private static final float RATIO = 4f / 3f;

    public CustomFrameLayout(Context context) {
        super(context);
    }

    public CustomFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CustomFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,
                            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
        int heigthWithoutPadding = height - getPaddingTop() - getPaddingBottom();

        int maxWidth = (int) (heigthWithoutPadding * RATIO);
        int maxHeight = (int) (widthWithoutPadding / RATIO);

        if (widthWithoutPadding > maxWidth) {
            width = maxWidth + getPaddingLeft() + getPaddingRight();
        } else {
            height = maxHeight + getPaddingTop() + getPaddingBottom();
        }

        setMeasuredDimension(width, height);
    }

But the cam preview is compressed inside the frame layout how can i solve this ?issue ?

Update

Ok after some research got to know that its because of onMeasure ASPECT_RATIO = 4:3

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);

        if (width > height * ASPECT_RATIO) {
            width = (int) (height * ASPECT_RATIO + 0.5);
        } else {
            height = (int) (width / ASPECT_RATIO + 0.5);
        }

        setMeasuredDimension(width, height);
    }

Solution

So i was thinking of a solution, might be (just like Instagram does) to make your camera at full size and then hide some areas of the layout just to make it look like its 4:3 ratio.Then by using some crop mechanism have to cut the image to make the image look like 4:3.

Say i always show preview from top with 4:3 ratio and rest of the below part is hidden, so now as soon as i take photo i want to crop the image from top to 4:3 ratio and save it.

How can i achieve this, is this a feasible solution ?

回答1:

As far as I understand, your current problem is how to crop the image you receive and show it. Here is a small example:

@OnClick(R.id.btn_record_start)
    public void takePhoto() {
        if (null != actions) {
            EasyCamera.PictureCallback callback = new EasyCamera.PictureCallback() {
                public void onPictureTaken(byte[] data, EasyCamera.CameraActions actions) {
                    // store picture
                    Bitmap bitmap = ImageUtils.getExifOrientedBitmap(data);
                    if ((portrait && bitmap.getHeight() < bitmap.getWidth()) ||
                        (!portrait && bitmap.getHeight() > bitmap.getWidth())) {
                        Matrix matrix = new Matrix();
                        matrix.postRotate(90);
                        bitmap =
                            Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
                                matrix, true);
                    }

                    Camera.CameraInfo info = new Camera.CameraInfo();
                    Camera.getCameraInfo(cameraId, info);
                    if (Camera.CameraInfo.CAMERA_FACING_FRONT == info.facing) {
                        Matrix matrix = new Matrix();
                        matrix.postRotate(180);
                        bitmap =
                            Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
                                matrix, true);
                    }
                    showPhoto(bitmap);
                }
            };
            actions.takePicture(EasyCamera.Callbacks.create()
                .withJpegCallback(callback));
        }
    }

This is a method I'm using to handle image orientation after the photo is taken. It can easily be modified to handle cropping too. To achieve this, you have to specify the target width and height of the image (currently I'm sending the whole bitmap's size). A possible solution is to take the image's height and delete the excessive width - so the params you send to the createBitmap method would be bitmap.getHeight() * 4.0 / 3.0 and bitmap.getHeight(). Here is the modified example:

@OnClick(R.id.btn_record_start)
public void takePhoto() {
    if (null != actions) {
        EasyCamera.PictureCallback callback = new EasyCamera.PictureCallback() {
            public void onPictureTaken(byte[] data, EasyCamera.CameraActions actions) {
                // store picture
                Bitmap bitmap = ImageUtils.getExifOrientedBitmap(data);
                if ((portrait && bitmap.getHeight() < bitmap.getWidth()) ||
                    (!portrait && bitmap.getHeight() > bitmap.getWidth())) {
                    Matrix matrix = new Matrix();
                    matrix.postRotate(90);
                    bitmap =
                        Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
                            matrix, true);
                }

                Camera.CameraInfo info = new Camera.CameraInfo();
                Camera.getCameraInfo(cameraId, info);
                if (Camera.CameraInfo.CAMERA_FACING_FRONT == info.facing) {
                    Matrix matrix = new Matrix();
                    matrix.postRotate(180);
                    bitmap =
                        Bitmap.createBitmap(bitmap, 0, 0, (int) (bitmap.getHeight() * 4.0 / 3.0), bitmap.getHeight(),
                            matrix, true);
                }
                showPhoto(bitmap);
            }
        };
        actions.takePicture(EasyCamera.Callbacks.create()
            .withJpegCallback(callback));
    }
}

A few things to note:

  • you can substitute the 4.0 / 3.0 part with the ASPECT_RATIO variable
  • my example is doing image rotation so it looks as it was during the preview, in your case the required UI might be different.
  • I'm using the EasyCamera library to simplify the camera management

Here are the other ImageUtils methods I'm using:

getExifOrientedBitmap

public static Bitmap getExifOrientedBitmap(byte[] data) {
    File newPhotoFile = writeToFile(data);
    if (newPhotoFile == null) {
        return null;
    }

    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
    bitmap = fixOrientationIfNeeded(newPhotoFile, bitmap);
    newPhotoFile.delete();
    return bitmap;
}

writeToFile

@Nullable
public static File writeToFile(byte[] data) {
    File dir = PhotoMessageComposer.getPhotoDir();
    if (!dir.exists()) {
        dir.mkdir();
    }
    File newPhotoFile = new File(dir, ImageUtils.getRandomFilename());
    FileOutputStream fos = null;
    try
    {
        fos = new FileOutputStream(newPhotoFile);
        fos.write(data);
        fos.close();
    } catch (Exception error) {
        return null;
    } finally {
        try {
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return newPhotoFile;
}

getPhotoDir

@NonNull
public static File getPhotoDir() {
    return new File(
        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) +
            PICTURES_DIR);
}

getRandomFileName

public static String getRandomFilename() {
    return UUID.randomUUID().toString() + IMAGE_EXTENSION;
}

fixOrientationIfNeeded

public static Bitmap fixOrientationIfNeeded(File sourceFile, Bitmap source) {
    ExifInterface exif;
    try {
        exif = new ExifInterface(sourceFile.getAbsolutePath());
        int exifOrientation = exif.getAttributeInt(
                ExifInterface.TAG_ORIENTATION,
                ExifInterface.ORIENTATION_NORMAL);

        if (exifOrientation != ExifInterface.ORIENTATION_NORMAL) {
            Matrix matrix = new Matrix();
            int angle = findRotationAngle(exifOrientation);
            matrix.postRotate(angle);
            source = Bitmap.createBitmap(source, 0, 0, source.getWidth(),
                    source.getHeight(), matrix, true);
            return source;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return source;
}

findRotationAngle

protected static int findRotationAngle(int exifOrientation) {
    switch (exifOrientation) {
    case ExifInterface.ORIENTATION_ROTATE_270:
        return 270;
    case ExifInterface.ORIENTATION_ROTATE_180:
        return 180;
    case ExifInterface.ORIENTATION_ROTATE_90:
        return 90;
    default:
        return 0;
    }
}

P.S. It has been a few years since this ImageUtils class was implemented, so probably there are better ways to handle some of these operations. They should be good enough for a starting point though.