如何在 CameraX 的相机预览中裁剪图像矩形

How to crop image rectangle in camera preview on CameraX

我有一个自定义相机应用程序,它有一个居中的矩形视图,如下所示:

当我拍照时,我想忽略矩形以外的一切。这是我的 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>

这是我的 cameraX 预览的 kotlin 代码:

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

}

当我拍照并将其发送到新页面 (GalleryPage) 时,它会显示相机预览中的所有屏幕,如下所示:

这是从 cameraX 预览中获取图片并将其显示到 ImageView 中的 kotlin 代码:

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

}

有人可以帮我如何正确裁剪图像吗?因为我已经在搜索和研究如何去做,但仍然感到困惑并且不适合我。

这是我如何裁剪您提到的 cameraX 拍摄的图像的示例。我不知道这是否是最好的方法,我很想知道其他解决方案。

camerax_version = "1.0.0-alpha07"

CameraFragment.java

初始化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);
    });
}

拍完照片后,打开经过照片路径的图库片段:

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

目前照片还没有裁剪,不知道能不能直接用cameraX裁剪。

GalleryFragment.java

加载传递给片段的参数。

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

在 ImageView 中使用 glide 加载 Uri,然后裁剪它。

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

我有一个解决办法,我就是用这个功能在截取图片后裁剪图片:

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

裁剪图像以参考父视图(如框架)和子视图(如最终参考)

  • param bitmap 要裁剪的图像
  • param frame图片设置的地方
  • param reference 帧作为裁剪图像的参考
  • return 图片已裁剪

你可以看到这个例子:https://github.com/rrifafauzikomara/CustomCamera/tree/custom_camerax

我找到了一种使用 camerax 配置执行此操作的简单直接的方法。

从相机预览中获取您需要的预览区域矩形的高度和宽度。

例如

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

我的宽度是350dp,高度是100dp

然后用ViewPort得到你需要的区域

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

然后绑定CameraProvider的LifeCycle

cameraProvider.bindToLifecycle(this, cameraSelector, useCaseGroup)

使用此 link CropRect 了解更多信息

如果您需要任何帮助,请在下方评论,我可以为您提供工作源代码。

编辑

Link to Source Code Sample

如果您希望将图像裁剪为 PreviewView 显示的图像,只需执行以下操作:

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

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