You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android CameraX拍照部分设备图像旋转问题排查与修正

CameraX拍照图像旋转不一致问题解决

问题说明

在Android平台使用CameraX实现点击拍照并转换为指定尺寸正方形的功能时,不同设备拍照结果存在差异:部分设备捕获图像与预览画面一致,部分设备的捕获图像出现旋转,需要让捕获图像与预览画面完全一致(无旋转)。

核心原因

  1. 硬编码设置setTargetRotation(ROTATION_0),未适配设备的屏幕旋转和相机传感器方向
  2. 转换ImageProxy为Bitmap时,忽略了CameraX提供的图像旋转信息,导致图像方向与预览不匹配

修复步骤及代码修改

1. 修正CameraX用例的旋转设置

移除Preview和ImageCapture构建时硬编码的旋转设置,改用预览View的实际旋转值,让CameraX自动适配设备方向:

void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
    Log.d(TAG, "bindPreview: width " + width);
    Log.d(TAG, "bindPreview: height " + height);
    Preview preview = new Preview.Builder()
            .setTargetResolution(new Size(width, height))
            // 适配预览View的实际旋转
            .setTargetRotation(previewView.getDisplay().getRotation())
            .build();

    previewView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
        @Override
        public void onLayoutChange(
                View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
            Size newViewFinderDimens = new Size(right - left, bottom - top);
            // 传入实际屏幕旋转角度
            updateTransform(previewView.getDisplay().getRotation(), MainActivity.bufferDimens, newViewFinderDimens);
        }
    });

     imageCapture =
            new ImageCapture.Builder()
                    .setTargetResolution(new Size(width, height))
                    // 适配预览View的实际旋转
                    .setTargetRotation(previewView.getDisplay().getRotation())
                    .build();

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

    preview.setSurfaceProvider(previewView.getSurfaceProvider());

    UseCaseGroup useCaseGroup = new UseCaseGroup.Builder()
            .addUseCase(preview)
            .addUseCase(imageCapture)
            .build();

    cameraProvider.bindToLifecycle((LifecycleOwner)this, cameraSelector, useCaseGroup);

    previewHeight = previewView.getHeight();
    previewWidth = previewView.getWidth();
}

2. 拍照成功时修正图像旋转

在onCaptureSuccess中,获取CameraX计算好的旋转角度,转换Bitmap后进行方向修正,确保与预览画面一致:

bGetImage.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        final Bitmap[] bitmap = {null};
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            imageCapture.takePicture(getMainExecutor(),new ImageCapture.OnImageCapturedCallback() {
                @Override
                public void onCaptureSuccess(@NonNull ImageProxy image) {
                    super.onCaptureSuccess(image);
                    Log.d(TAG, "onCaptureSuccess: testview.innerRectangle " + testview.innerRectangle.width());
                    Log.d(TAG, "onCaptureSuccess: testview.innerRectangle " + testview.innerRectangle.height());
                    OutputTransform source = previewView.getOutputTransform();
                    OutputTransform target = new ImageProxyTransformFactory().getOutputTransform(image);
                    Log.d(TAG, "onCaptureSuccess: target " + target.getMatrix().toString());
                    CoordinateTransform coordinateTransform = new CoordinateTransform(source, target);
                    coordinateTransform.mapRect(testview.innerRectangle);

                    Log.d(TAG, "onCaptureSuccess: testview.innerRectangle " + testview.innerRectangle.width());
                    Log.d(TAG, "onCaptureSuccess: testview.innerRectangle " + testview.innerRectangle.height());
                    
                    // 获取CameraX计算的旋转角度
                    int rotationDegrees = image.getImageInfo().getRotationDegrees();
                    bitmap[0] = imageProxytoBitmap(image);
                    // 根据旋转角度修正Bitmap方向
                    if (rotationDegrees != 0) {
                        bitmap[0] = rotateImage(bitmap[0], rotationDegrees);
                    }

                    Log.d(TAG, "onCaptureSuccess: bitmap[0] " + bitmap[0].getWidth());
                    Log.d(TAG, "onCaptureSuccess: bitmap[0] " + bitmap[0].getHeight());

                    // 裁剪为指定正方形
                    bitmap[0] = Bitmap.createBitmap(bitmap[0],(int)testview.innerRectangle.left,(int)testview.innerRectangle.top,(int)testview.innerRectangle.width(),(int)testview.innerRectangle.height());

                    try {
                        Uri uri = getUri(bitmap[0], getContentResolver());
                        Log.d(TAG, "onCaptureSuccess: bitmap[0] " + bitmap[0].getWidth());
                        Log.d(TAG, "onCaptureSuccess: bitmap[0] " + bitmap[0].getHeight());
                        String path = uri.toString();
                        moveToScanActivity(path);
                    } catch (IOException e) {
                        Log.e(TAG, "onCaptureSuccess: ",e);
                    } finally {
                        // 关闭ImageProxy避免资源泄漏
                        image.close();
                    }
                }
            });
        }
    }
});

3. 优化ImageProxy转Bitmap的方法

确保正确处理图像格式,避免潜在的格式兼容问题:

private Bitmap imageProxytoBitmap(ImageProxy image) {
    Image.Plane[] planes = image.getPlanes();
    ByteBuffer buffer = planes[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
}

4. 修复预览变换逻辑

确保预览的变换矩阵与实际旋转匹配:

public void updateTransform(int rotation, Size newBufferDimens, Size newViewFinderDimens) {
    if (rotation == viewFinderRotation &&
            newBufferDimens.equals(bufferDimens) &&
            newViewFinderDimens.equals(viewFinderDimens)) {
        return;
    }

    if (newBufferDimens.getWidth() == 0 || newBufferDimens.getHeight() == 0) {
        return;
    } else {
        bufferDimens = newBufferDimens;
    }

    if (newViewFinderDimens.getWidth() == 0 || newViewFinderDimens.getHeight() == 0) {
        return;
    } else {
        viewFinderDimens = newViewFinderDimens;
    }

    Matrix matrix = new Matrix();
    Log.d(TAG, "Applying output transformation.\n" +
            "View finder size: " + viewFinderDimens + "\n" +
            "Preview output size: " + bufferDimens + "\n" +
            "View finder rotation: " + rotation + "\n" +
            "Preview output rotation: " + 0);
    float centerX = previewView.getWidth() / 2f;
    float centerY = previewView.getHeight() / 2f;

    // 根据传入的旋转角度修正预览
    float rotationDegrees;
    switch (rotation) {
        case Surface.ROTATION_0:
            rotationDegrees = 0f;
            break;
        case Surface.ROTATION_90:
            rotationDegrees = 90f;
            break;
        case Surface.ROTATION_180:
            rotationDegrees = 180f;
            break;
        case Surface.ROTATION_270:
            rotationDegrees = 270f;
            break;
        default:
            return;
    }
    matrix.postRotate(-rotationDegrees, centerX, centerY);

    float bufferRatio = (float) bufferDimens.getWidth() / bufferDimens.getHeight();
    int scaledWidth;
    int scaledHeight;

    if (previewView.getWidth() > previewView.getHeight()) {
        scaledWidth = viewFinderDimens.getWidth();
        scaledHeight = Math.round(viewFinderDimens.getWidth() / bufferRatio);
    } else {
        scaledHeight = viewFinderDimens.getHeight();
        scaledWidth = Math.round(viewFinderDimens.getHeight() * bufferRatio);
    }

    float xScale = (float) scaledWidth / viewFinderDimens.getWidth();
    float yScale = (float) scaledHeight / viewFinderDimens.getHeight();
    matrix.preScale(xScale, yScale, centerX, centerY);

    // 将变换矩阵应用到预览View
    previewView.setTransform(matrix);
    viewFinderRotation = rotation;
}

关键注意事项

  • 必须在onCaptureSuccess的最后调用image.close(),避免CameraX资源泄漏
  • 禁止硬编码旋转角度,始终使用CameraX提供的getRotationDegrees()或预览View的实际旋转值
  • 裁剪正方形时,需确保旋转后的Bitmap尺寸正确,避免裁剪区域偏移

内容的提问来源于stack exchange,提问作者Lal Bihari Pandey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 18:05:23