web-dev-qa-db-fra.com

Comment recadrer le rectangle de l'image dans l'aperçu de l'appareil photo sur CameraX

J'ai une application de caméra personnalisée qui a une vue rectangulaire centrée, comme vous pouvez le voir ci-dessous:

enter image description here

Lorsque je prends une photo, je veux ignorer tout ce qui se trouve en dehors du rectangle. Et voici ma mise en page 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"
    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>

Et voici mon code kotlin pour l'aperçu de cameraX:

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

}

Et lorsque je prends la photo et l'envoie dans une nouvelle page (GalleryPage), tout l'écran s'affiche à partir de l'aperçu de la caméra comme vous pouvez le voir ci-dessous:

enter image description here

Et voici le code kotlin pour obtenir l'image à partir de l'aperçu de cameraX et l'afficher dans 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
        )
    }

}

Quelqu'un peut-il m'aider à recadrer l'image correctement? Parce que je cherche déjà et recherche comment le faire mais toujours confus et ne fonctionne pas pour moi.

4

J'ai une solution, j'utilise simplement cette fonction pour recadrer l'image après avoir capturé l'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()
    }

Recadrer une image en prenant une référence un parent de vue comme un cadre et un enfant de vue comme référence finale

  • param bitmap image à recadrer
  • param frame où l'image est définie
  • param reference frame pour prendre référence pour un recadrage de l'image
  • return image déjà recadrée
0

Voici un exemple de la façon dont je recadre une image prise par cameraX comme vous l'avez mentionné. Je ne sais pas si c'est la meilleure façon de le faire et je suis intéressé de connaître d'autres solutions.

camerax_version = "1.0.0-alpha07"

CameraFragment.Java

Initialisez la caméraX:

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

Lorsqu'une photo a été prise, ouvrez le fragment de galerie en passant le chemin de la 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();
        }
    }
};

Pour le moment, la photo n'a pas été recadrée, je ne sais pas s'il est possible de le faire directement avec cameraX.

GalleryFragment.Java

Chargez l'argument passé au fragment.

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

Chargez l'URI avec glide dans un ImageView, puis recadrez-le.

@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>
1
LaurentP22