9

I have a custom camera app which has a centered rectangle view, as you can see below:

enter image description here

When I take a picture I want to ignore everything outside the rectangle. And this is my XML layout:

<?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"
    android:background="@color/black_50">

    <TextureView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:layout_margin="16dp"
        android:background="@drawable/rectangle"
        app:layout_constraintBottom_toTopOf="@+id/cameraBottomView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/cameraBottomView"
        android:layout_width="match_parent"
        android:layout_height="130dp"
        android:background="@color/black_50"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <ImageButton
        android:id="@+id/cameraCaptureImageButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        android:src="@drawable/ic_capture_image"
        app:layout_constraintBottom_toBottomOf="@id/cameraBottomView"
        app:layout_constraintEnd_toEndOf="@id/cameraBottomView"
        app:layout_constraintStart_toStartOf="@id/cameraBottomView"
        app:layout_constraintTop_toTopOf="@id/cameraBottomView"
        tools:ignore="ContentDescription" />

</androidx.constraintlayout.widget.ConstraintLayout>

And this is my kotlin code for the cameraX preview:

class CameraFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_camera, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewFinder.post { setupCamera() }
    }

    private fun setupCamera() {
        CameraX.unbindAll()
        CameraX.bindToLifecycle(
            this,
            buildPreviewUseCase(),
            buildImageCaptureUseCase(),
            buildImageAnalysisUseCase()
        )
    }

    private fun buildPreviewUseCase(): Preview {
        val preview = Preview(
            UseCaseConfigBuilder.buildPreviewConfig(
                viewFinder.display
            )
        )
        preview.setOnPreviewOutputUpdateListener { previewOutput ->
            updateViewFinderWithPreview(previewOutput)
            correctPreviewOutputForDisplay(previewOutput.textureSize)
        }
        return preview
    }

    private fun updateViewFinderWithPreview(previewOutput: Preview.PreviewOutput) {
        val parent = viewFinder.parent as ViewGroup
        parent.removeView(viewFinder)
        parent.addView(viewFinder, 0)
        viewFinder.surfaceTexture = previewOutput.surfaceTexture
    }

    /**
     * Corrects the camera/preview's output to the display, by scaling
     * up/down and/or rotating the camera/preview's output.
     */
    private fun correctPreviewOutputForDisplay(textureSize: Size) {
        val matrix = Matrix()

        val centerX = viewFinder.width / 2f
        val centerY = viewFinder.height / 2f

        val displayRotation = getDisplayRotation()
        val (dx, dy) = getDisplayScalingFactors(textureSize)

        matrix.postRotate(displayRotation, centerX, centerY)
        matrix.preScale(dx, dy, centerX, centerY)

        // Correct preview output to account for display rotation and scaling
        viewFinder.setTransform(matrix)
    }

    private fun getDisplayRotation(): Float {
        val rotationDegrees = when (viewFinder.display.rotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> throw IllegalStateException("Unknown display rotation ${viewFinder.display.rotation}")
        }
        return -rotationDegrees.toFloat()
    }

    private fun getDisplayScalingFactors(textureSize: Size): Pair<Float, Float> {
        val cameraPreviewRation = textureSize.height / textureSize.width.toFloat()
        val scaledWidth: Int
        val scaledHeight: Int
        if (viewFinder.width > viewFinder.height) {
            scaledHeight = viewFinder.width
            scaledWidth = (viewFinder.width * cameraPreviewRation).toInt()
        } else {
            scaledHeight = viewFinder.height
            scaledWidth = (viewFinder.height * cameraPreviewRation).toInt()
        }
        val dx = scaledWidth / viewFinder.width.toFloat()
        val dy = scaledHeight / viewFinder.height.toFloat()
        return Pair(dx, dy)
    }

    private fun buildImageCaptureUseCase(): ImageCapture {
        val capture = ImageCapture(
            UseCaseConfigBuilder.buildImageCaptureConfig(
                viewFinder.display
            )
        )
        cameraCaptureImageButton.setOnClickListener {
            capture.takePicture(
                FileCreator.createTempFile(JPEG_FORMAT),
                Executors.newSingleThreadExecutor(),
                object : ImageCapture.OnImageSavedListener {
                    override fun onImageSaved(file: File) {
                        requireActivity().runOnUiThread {
                            launchGalleryFragment(file.absolutePath)
                        }
                    }

                    override fun onError(
                        imageCaptureError: ImageCapture.ImageCaptureError,
                        message: String,
                        cause: Throwable?
                    ) {
                        Toast.makeText(requireContext(), "Error: $message", Toast.LENGTH_LONG)
                            .show()
                        Log.e("CameraFragment", "Capture error $imageCaptureError: $message", cause)
                    }
                })
        }
        return capture
    }

    private fun buildImageAnalysisUseCase(): ImageAnalysis {
        val analysis = ImageAnalysis(
            UseCaseConfigBuilder.buildImageAnalysisConfig(
                viewFinder.display
            )
        )
        analysis.setAnalyzer(
            Executors.newSingleThreadExecutor(),
            ImageAnalysis.Analyzer { image, rotationDegrees ->
                Log.d(
                    "CameraFragment",
                    "Image analysis: $image - Rotation degrees: $rotationDegrees"
                )
            })
        return analysis
    }

    private fun launchGalleryFragment(path: String) {
        val action = CameraFragmentDirections.actionLaunchGalleryFragment(path)
        findNavController().navigate(action)
    }

}

And when I take the picture and send it into new page (GalleryPage), it's show all screen from the camera preview as you can see below:

enter image description here

And this is the kotlin code to get the picture from cameraX preview and display it into ImageView:

class GalleryFragment : Fragment() {

    private lateinit var imageView: ImageView


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_gallery, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        imageView = view.findViewById(R.id.img)

        val imageFilePath = GalleryFragmentArgs.fromBundle(arguments!!).data
        val bitmap = BitmapFactory.decodeFile(imageFilePath)
        val rotatedBitmap = bitmap.rotate(90)

        if (imageFilePath.isBlank()) {
            Log.i(
                "GalleryFragment",
                "Image is Null or Empty"
            )
        } else {
            Glide.with(activity!!)
                .load(rotatedBitmap)
                .into(imageView)
        }

    }

    private fun Bitmap.rotate(degree:Int):Bitmap{
        // Initialize a new matrix
        val matrix = Matrix()

        // Rotate the bitmap
        matrix.postRotate(degree.toFloat())

        // Resize the bitmap
        val scaledBitmap = Bitmap.createScaledBitmap(
            this,
            width,
            height,
            true
        )

        // Create and return the rotated bitmap
        return Bitmap.createBitmap(
            scaledBitmap,
            0,
            0,
            scaledBitmap.width,
            scaledBitmap.height,
            matrix,
            true
        )
    }

}

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.

R Rifa Fauzi Komara
  • 1,101
  • 2
  • 13
  • 28

4 Answers4

5

Here is an example of how I'm cropping an image taken by cameraX as you mentioned. I don't know if it the best way to do it and I'm interested to know other solutions.

camerax_version = "1.0.0-alpha07"

CameraFragment.java

Initialize cameraX :

// Views
private PreviewView previewView;
// CameraX
private ProcessCameraProvider cameraProvider;
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
private CameraSelector cameraSelector;
private Executor executor;
private ImageCapture imageCapture;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
    executor = ContextCompat.getMainExecutor(getContext());
    cameraSelector = new CameraSelector.Builder().requireLensFacing(LensFacing.BACK).build();
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    previewView = view.findViewById(R.id.preview);
    ImageButton btnCapture = view.findViewById(R.id.btn_capture);
    // Wait for the view to be properly laid out
    previewView.post(() ->{
        //Initialize CameraX
        cameraProviderFuture.addListener(() -> {
            if(cameraProvider != null) cameraProvider.unbindAll();
            try {
                cameraProvider = cameraProviderFuture.get();
                // Set up the preview use case to display camera preview
                Preview preview = new Preview.Builder()
                        .setTargetAspectRatio(AspectRatio.RATIO_4_3)
                        .setTargetRotation(previewView.getDisplay().getRotation())
                        .build();

                preview.setPreviewSurfaceProvider(previewView.getPreviewSurfaceProvider());

                // Set up the capture use case to allow users to take photos
                imageCapture = new ImageCapture.Builder()
                        .setCaptureMode(ImageCapture.CaptureMode.MINIMIZE_LATENCY)
                        .setTargetRotation(previewView.getDisplay().getRotation())
                        .setTargetAspectRatio(AspectRatio.RATIO_4_3)
                        .build();

                // Apply declared configs to CameraX using the same lifecycle owner
                cameraProvider.bindToLifecycle(this, cameraSelector, preview,imageCapture);
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }, ContextCompat.getMainExecutor(getContext()));
    });

    btnCapture.setOnClickListener(v -> {
        String format = "yyyy-MM-dd-HH-mm-ss-SSS";
        SimpleDateFormat fmt = new SimpleDateFormat(format, Locale.US);
        String date = fmt.format(System.currentTimeMillis());

        File file = new File(getContext().getCacheDir(), date+".jpg");
        imageCapture.takePicture(file, executor, imageSavedListener);
    });
}

When a photo has been taken, open the gallery fragment passing the path of the photo :

private ImageCapture.OnImageSavedCallback imageSavedListener = new ImageCapture.OnImageSavedCallback() {
    @Override
    public void onImageSaved(@NonNull File photoFile) {
        // Create new fragment and transaction
        Fragment newFragment = new GalleryFragment();
        FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
        // Set arguments
        Bundle args = new Bundle();
        args.putString("KEY_PATH", Uri.fromFile(photoFile).toString());
        newFragment.setArguments(args);
        // Replace whatever is in the fragment_container view with this fragment,
        transaction.replace(R.id.fragment_container, newFragment,null);
        transaction.addToBackStack(null);
        // Commit the transaction
        transaction.commit();
    }

    @Override
    public void onError(int imageCaptureError, @NonNull String message, @Nullable Throwable cause) {
        if (cause != null) {
            cause.printStackTrace();
        }
    }
};

At this moment, the photo has not been cropped, I don't know if it possible to do it directly with cameraX.

GalleryFragment.java

Load the argument passed to the fragment.

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    String path = getArguments().getString("KEY_PATH");
    sourceUri = Uri.parse(path);
}

Load the Uri with glide in an ImageView and then crop it.

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    // Initialize the views
    ImageView imageView = view.findViewById(R.id.image_view);
    View cropArea = view.findViewById(R.id.crop_area);
    // Display the image
    Glide.with(this).load(sourceUri).listener(new RequestListener<Drawable>() {
        @Override
        public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
            return false;
        }

        @Override
        public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
            // Get original bitmap
            sourceBitmap = ((BitmapDrawable)resource).getBitmap();

            // Create a new bitmap corresponding to the crop area
            int[] cropAreaXY = new int[2];
            int[] placeHolderXY = new int[2];
            Rect rect = new Rect();
            imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
                @Override
                public boolean onPreDraw() {
                    try {
                        imageView.getLocationOnScreen(placeHolderXY);

                        cropArea.getLocationOnScreen(cropAreaXY);
                        cropArea.getGlobalVisibleRect(rect);

                        croppedBitmap = Bitmap.createBitmap(sourceBitmap, cropAreaXY[0], cropAreaXY[1] - placeHolderXY[1], rect.width(), rect.height());
                        // Save the croppedBitmap if you wish

                        getActivity().runOnUiThread(() -> imageView.setImageBitmap(croppedBitmap));
                        return true;
                    }finally {
                        imageView.getViewTreeObserver().removeOnPreDrawListener(this);
                    }
                }
            });
            return false;
        }
    }).into(imageView);
}

fragment_camera.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black">

    <androidx.camera.view.PreviewView
        android:id="@+id/preview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="3:4"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/crop_area"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="8dp"
        android:background="@drawable/rectangle_round_corners"
        app:layout_constraintBottom_toBottomOf="@+id/preview"
        app:layout_constraintDimensionRatio="4.5:3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/cameraBottomView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="@android:color/black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/preview" />


    <ImageButton
        android:id="@+id/btn_capture"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:background="@drawable/ic_shutter"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/preview" />
</androidx.constraintlayout.widget.ConstraintLayout>

fragment_gallery.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"
    android:id="@+id/layout_main"
    android:background="@android:color/black"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/crop_area"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="4.5:3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
LaurentP22
  • 426
  • 4
  • 11
  • So, before display first, it cannot be cropping the image right? How if I want to crop first after taking the image and then display it with result of cropping – R Rifa Fauzi Komara Jan 08 '20 at 03:37
  • Well it might be possible but I don't know how to do it. If you find a better solution I'm interested ! – LaurentP22 Jan 08 '20 at 04:10
4

I found a simple and straight forward way of doing this using camerax configuration.

Get the height and width of your rectangle shape of the preview area that you need from the camera preview.

For example

<View
            android:background="@drawable/background_drawable"
            android:id="@+id/border_view"
            android:layout_gravity="center"
            android:layout_width="350dp"
            android:layout_height="100dp"/>

The width of mine is 350dp and a height of 100dp

Then use ViewPort to get the area you need

     val viewPort =  ViewPort.Builder(Rational(width, height), rotation).build()
//width = 350, height = 100, rotation = Surface.ROTATION_0 
    val useCaseGroup = UseCaseGroup.Builder()
        .addUseCase(preview) //your preview
        .addUseCase(imageAnalysis) //if you are using imageAnalysis
        .addUseCase(imageCapture)
        .setViewPort(viewPort)
        .build()

Then bind to LifeCycle of CameraProvider

cameraProvider.bindToLifecycle(this, cameraSelector, useCaseGroup)

Use this link CropRect for more information

If you need any help comment below, I can provide you with the working source code.

Edit

Link to Source Code Sample

Wojuola Ayotola
  • 427
  • 6
  • 11
3

I have a solution, I just use this function to cropping the Image after capturing the Image:

private fun cropImage(bitmap: Bitmap, frame: View, reference: View): ByteArray {
        val heightOriginal = frame.height
        val widthOriginal = frame.width
        val heightFrame = reference.height
        val widthFrame = reference.width
        val leftFrame = reference.left
        val topFrame = reference.top
        val heightReal = bitmap.height
        val widthReal = bitmap.width
        val widthFinal = widthFrame * widthReal / widthOriginal
        val heightFinal = heightFrame * heightReal / heightOriginal
        val leftFinal = leftFrame * widthReal / widthOriginal
        val topFinal = topFrame * heightReal / heightOriginal
        val bitmapFinal = Bitmap.createBitmap(
            bitmap,
            leftFinal, topFinal, widthFinal, heightFinal
        )
        val stream = ByteArrayOutputStream()
        bitmapFinal.compress(
            Bitmap.CompressFormat.JPEG,
            100,
            stream
        ) //100 is the best quality possibe
        return stream.toByteArray()
    }

Crop an image taking a reference a view parent like a frame and a view child like final reference

  • param bitmap image to crop
  • param frame where the image is set it
  • param reference frame to take reference for a crop the image
  • return image already cropped

You can see this example: https://github.com/rrifafauzikomara/CustomCamera/tree/custom_camerax

R Rifa Fauzi Komara
  • 1,101
  • 2
  • 13
  • 28
0

If you want the image to be cropped to whichever your PreviewView is showing, just do:

val useCaseGroup = UseCaseGroup.Builder()
        .addUseCase(preview!!)
        .addUseCase(imageCapture!!)
        .setViewPort(previewView.viewPort!!)
        .build()

camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCaseGroup)