【问题标题】:How to crop image rectangle in camera preview on CameraX如何在 CameraX 上的相机预览中裁剪图像矩形
【发布时间】:2020-04-02 04:03:34
【问题描述】:

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

当我拍照时,我想忽略矩形之外的所有内容。这是我的 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
        )
    }

}

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

【问题讨论】:

    标签: kotlin crop android-camerax


    【解决方案1】:

    我发现了一种使用 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)
    

    使用此链接CropRect 了解更多信息

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

    编辑

    Link to Source Code Sample

    【讨论】:

    • 如果可能的话,您能否分享一个指向源代码的链接。一个android文档有点不清楚
    • @Ssenyonjo 我已经用源代码链接编辑了我的答案
    • createSurfaceProvider() 不再在 PreviewView 类中。有没有其他方法可以在不使用 createSurfaceProvider() 的情况下实现类似的功能?
    • Java 可以使用 viewFinder.surfaceProvider 或 viewFinder.getSurfaceProvider
    【解决方案2】:

    这是一个示例,说明我如何裁剪您提到的 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>
    

    【讨论】:

    • 那么,在先显示之前,不能裁剪图像吧?如果我想在拍摄图像后先裁剪,然后显示裁剪结果
    • 嗯,这可能是可能的,但我不知道该怎么做。如果您找到更好的解决方案,我很感兴趣!
    【解决方案3】:

    我有一个解决方案,我只是在捕获图像后使用此功能裁剪图像:

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

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

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

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

    【讨论】:

    • 这个解决方案不起作用,它给出了一个裁剪的图像,但是当我将它保存在一个文件中时,它是完整的图像,而不是裁剪的。
    • 你只需要保存cropping之后的图片,而不是taking a picture之后的图片
    • 是的,这就是我所做的,但不幸的是没有成功。
    • 我不明白参数,你能详细说明吗?
    • 您在代码中的哪里裁剪了图像? GalleryFragment 还是 CameraFragment?你能完成上面的代码吗?
    【解决方案4】:

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

    val useCaseGroup = UseCaseGroup.Builder()
            .addUseCase(preview!!)
            .addUseCase(imageCapture!!)
            .setViewPort(previewView.viewPort!!)
            .build()
    
    camera = cameraProvider.bindToLifecycle(
                this, cameraSelector, useCaseGroup)
    

    【讨论】:

    • 输出是什么?是否允许从相机预览中裁剪图像?
    猜你喜欢
    • 2018-12-23
    • 1970-01-01
    • 2012-07-22
    • 1970-01-01
    • 1970-01-01
    • 2017-05-18
    • 2023-03-31
    • 2013-06-05
    • 1970-01-01
    相关资源
    最近更新 更多