How to crop image captured by CameraX based on a rectangle on Preview display

Viewed 441

Hello I am new to android development, can anyone help me to crop the image captured by CameraX based on a rectangle on PreviewView?

This is the activity_main.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/frameRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.camera.view.PreviewView
            android:id="@+id/viewFinder"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <View
            android:id="@+id/viewOverlay_center"
            android:background="@drawable/card_rectangle"
            android:layout_width="270dp"
            android:layout_height="400dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/ll_bottom"/>

        <LinearLayout
            android:id="@+id/ll_bottom"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="@color/black"
            android:gravity="center"
            android:orientation="horizontal"
            android:padding="16dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent">

            <ImageView
                android:id="@+id/image_capture_button"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:background="?android:selectableItemBackground"
                android:src="@drawable/shutter_camera" />
        </LinearLayout>
        
    </androidx.constraintlayout.widget.ConstraintLayout>

</FrameLayout>

This is my MainActivity.java:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewBinding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(viewBinding.getRoot());

        viewBinding.imageCaptureButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                capturePhoto();
            }
        });
    }

    private void capturePhoto(){
        if (imageCapture == null) {
            return;
        }

        String filename = "Card-" + mDateFormat.format(System.currentTimeMillis());


        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "Card-" + mDateFormat.format(System.currentTimeMillis()));
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");


        Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

        imageCapture.takePicture(ContextCompat.getMainExecutor(this), new ImageCapture.OnImageCapturedCallback() {
            @Override
            public void onCaptureSuccess(@NonNull ImageProxy imageProxy) {
                super.onCaptureSuccess(imageProxy);

                ImageInfo imageInfo = imageProxy.getImageInfo();

                Bitmap image = rotateBitmapIfNeeded(imageProxyToBitmap(imageProxy),imageInfo);
                saveBitmap(cropBitmapToCard(image,viewBinding.frameRoot,viewBinding.viewOverlayCenter),uri);

                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setClass(MainActivity.this,  DecoderActivity.class);
                intent.putExtra("KEY", uri.toString());
                startActivity(intent);

                imageProxy.close();
            }

            @Override
            public void onError(@NonNull ImageCaptureException exception) {
                super.onError(exception);
                Log.e("MainActivity","Error on takePicture",exception);
            }
        });

    }

    private void startCamera() {
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);
        cameraProviderFuture.addListener(new Runnable() {
            @Override
            public void run() {
                try {
                    ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
                    startCameraX(cameraProvider);
                } catch (ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, ContextCompat.getMainExecutor(this));
    }

    void startCameraX(@NonNull ProcessCameraProvider cameraProvider) {
        cameraProvider.unbindAll();

        CameraSelector cameraSelector = new CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build();

        // preview use case
        Preview preview = new Preview.Builder().build();
        preview.setSurfaceProvider(viewBinding.viewFinder.getSurfaceProvider());

        // image capture use case
        imageCapture = new ImageCapture.Builder()
                .setTargetRotation(this.getWindowManager().getDefaultDisplay().getRotation())
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build();

        // bind to lifecycle
        cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, preview, imageCapture);
    }

    void saveBitmap(Bitmap target, Uri uri){
        try {
            OutputStream output = getContentResolver().openOutputStream(uri);
            target.compress(Bitmap.CompressFormat.JPEG, 100, output);
        }
        catch (Exception e) {
            Log.d("onBtnSavePng", e.toString()); // java.io.IOException: Operation not permitted
        }
    }

    Bitmap cropBitmapToCard(Bitmap source,View frame, View cardPlaceHolder){
        float scaleX = source.getWidth() / (float)frame.getWidth();
        float scaleY = source.getHeight() / (float) frame.getHeight();

        int x =(int)((cardPlaceHolder.getLeft()) * scaleX);
        int y = (int)((cardPlaceHolder.getTop()) * scaleY);

        Log.v("MainActivity-Crop","leftPos: " + cardPlaceHolder.getLeft() + " width: " + cardPlaceHolder.getWidth());

        int width = (int)(cardPlaceHolder.getWidth() * scaleX);
        int height = (int)(cardPlaceHolder.getHeight() * scaleY);

        return Bitmap.createBitmap(source,x,y,width,height);
    }

    private Bitmap imageProxyToBitmap(ImageProxy image)
    {
        ImageProxy.PlaneProxy planeProxy = image.getPlanes()[0];
        ByteBuffer buffer = planeProxy.getBuffer();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);

        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    }

    private Bitmap rotateBitmapIfNeeded(Bitmap source, ImageInfo info){
        int angle = info.getRotationDegrees();
        Matrix mat = new Matrix();
        mat.postRotate(angle);
        return Bitmap.createBitmap(source,0,0,source.getWidth(),source.getHeight(),mat,true);
    }
}

This is what my app looks like (the preview mode), i want to extract the ROI of the bitmap inside the rectangle

enter image description here

And I got this instead, as you can see my cropping still incorrect.

enter image description here

Seems there is mapping error in the y axis. Can somebody help me how to crop the image properly? Because I already search and research how to do it but still confused and not working for me

1 Answers

Please take a look at the APIs under androidx.camera.view.transform. Here is some untested sample code:

OutputTransform source = previewView.getOutputTransform()
// imageProxy is the output of the ImageCapture.
OutputTransform target = ImageProxyTransformFactory().getOutputTransform(imageProxy);;

// Build the transform from ImageAnalysis to PreviewView
CoordinateTransform coordinateTransform = new CoordinateTransform(source, target);

// The variable box here is your bounding box in PreviewView
coordinateTransform.mapRect(box);

// At this point, the variable box will contain the value you need.
Related