Android CameraX 图像旋转

16

我已经按照 Google CameraX 的代码实验室来实现自定义相机。 相机预览正常,但是当我拍照后,捕获的图像会旋转。 我在纵向模式下拍照,但保存的图像是横向的。 这是配置相机的方法:

private fun startCamera() {

    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener(Runnable {
        // Used to bind the lifecycle of cameras to the lifecycle owner
        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

        // Preview
        val preview = Preview.Builder()
            .setTargetRotation(this.windowManager.defaultDisplay.rotation)
            .build()
            .also {
                it.setSurfaceProvider(viewFinder.createSurfaceProvider())
            }

        imageCapture = ImageCapture.Builder()
            .setTargetRotation(this.windowManager.defaultDisplay.rotation)
            .build()

        val imageAnalyzer = ImageAnalysis.Builder()
            .build()
            .also {
                it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                    Log.d(TAG, "Average luminosity: $luma")
                })
            }

        // Select back camera as a default
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            // Unbind use cases before rebinding
            cameraProvider.unbindAll()

            // Bind use cases to camera
            cameraProvider.bindToLifecycle(
                this, cameraSelector, preview, imageCapture, imageAnalyzer)

        } catch(exc: Exception) {
            Log.e(TAG, "Use case binding failed", exc)
        }

    }, ContextCompat.getMainExecutor(this))
}

这里是捕获图像的方法:

private fun takePhoto() {
    val imageCapture = imageCapture ?: return

    // Create time-stamped output file to hold the image
    val photoFile = File(
        outputDirectory,
        SimpleDateFormat(FILENAME_FORMAT, Locale.US
        ).format(System.currentTimeMillis()) + ".jpg")

    // Create output options object which contains file + metadata
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

    // Set up image capture listener, which is triggered after photo has
    // been taken
    imageCapture.takePicture(
        outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
            }

            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                val savedUri = Uri.fromFile(photoFile)
                val msg = "Photo capture succeeded: $savedUri"
                val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, savedUri)
                ivCapturedImage.setImageBitmap(bitmap)
                setCaptureUI(false)
                Log.d(TAG, msg)
            }
        })
}

拍摄完照片后,我需要手动旋转图片吗?还是在配置相机时就可以解决?


澄清问题:您是否使用三星设备?其中许多设备存在已知的错误,即在保存时将图像记录为错误方向。 - John Lord
@JohnLord,你有这个声明的任何来源吗?是否有任何已知的解决方法? - Alex Mazzariol
唯一已知的解决方法是保存图像,然后读取exif数据。这是一个众所周知的问题,在stackoverflow上有各种帖子,比如这个。 https://dev59.com/nlYN5IYBdhLWcg3wjIuZ我们公司有数百台三星平板电脑,我们不得不包含与上面链接类似的修复程序,尽管我们的平板电脑被锁定为纵向。在上面的链接中,他们将读取的exif数据与当前设备方向进行比较。 - John Lord
我以为使用CameraX现在就不必再处理exif数据了?@JohnLord - Astha Garg
我坦率地讲,我不知道。我的同事将我们的相机应用转换为使用camerax。我甚至不确定问题是否是某种错误。现在,您可以从相机传感器中提取属性,这告诉您其中一些已经安装了90度或270度,因此不识别这一点的软件很可能会显示图像错误。 - John Lord
6个回答

7

默认情况下,ImageCapture会将拍摄图像的方向设置为显示旋转。如果将图像保存到磁盘中,则其旋转信息将储存在EXIF中。

如果您的设备处于锁定竖屏模式,那么显示旋转方向将与设备的方向不匹配,您需要自己设置目标旋转方向。下面是一个示例:

// The value is whatever the display rotation should be, if the device orientation is not locked.
imageCapture.setTargetRotation(...) 

或者,您可以简单地使用LifecycleCameraController API。它会为您处理旋转并以所见即所得的方式使所有用例一致。


2
我对LifecycleCameraController的问题是缺少样板代码;API描述页面中的代码不足以开始编写,而在Android应用程序中进行试错编程非常耗时... 我真的很希望有一个关于这个的codelab扩展。我会检查官方的cameraXsample,看看它是否有帮助。 - Alex Mazzariol
1
我知道这个代码实验室有点过时了。如果你感兴趣,可以看一下CameraX的测试应用程序,了解控制器的使用方法:https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java - Xi 张熹
2
谢谢提供的链接;但据我所知,CameraX不喜欢干涉图像数据,因此它唯一能做的就是设置EXIF元数据。最终,我仍然使用了ProcessCameraProvider类,在调用takePicture()之前设置目标旋转角度,然后手动从保存的JPEG中读取EXIF数据,并相应地旋转图片以获得直立的图像。尽管我希望CameraX能够处理图像旋转而不依赖于EXIF元数据,但我将其作为一个单独的答案添加到问题中。 - Alex Mazzariol
1
我尝试使用LifecycleCameraController,但没有帮助。照片仍然旋转了-90度。顺便说一句,这个类非常原始。无法设置照片/分析的分辨率,其他基本功能也不可用。 - Victor Cold
哦我的天啊,感谢你提到LifecycleCameraController。我删除了所有的代码,并用控制器替换了它们。我爱你! - iSWORD

4

我已经使用这个类来旋转图片

object CaptureImageHelper {

/**
 * This method is responsible for solving the rotation issue if exist. Also scale the images to
 * 1024x1024 resolution
 *
 * @param context       The current context
 * @param selectedImage The Image URI
 * @return Bitmap image results
 * @throws IOException
 */
@Throws(IOException::class)
fun handleSamplingAndRotationBitmap(context: Context, selectedImage: Uri?): Bitmap? {
    val MAX_HEIGHT = 1024
    val MAX_WIDTH = 1024

    // First decode with inJustDecodeBounds=true to check dimensions
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    var imageStream: InputStream = context.getContentResolver().openInputStream(selectedImage!!)!!
    BitmapFactory.decodeStream(imageStream, null, options)
    imageStream.close()

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT)

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false
    imageStream = context.getContentResolver().openInputStream(selectedImage!!)!!
    var img = BitmapFactory.decodeStream(imageStream, null, options)
    img = rotateImageIfRequired(img!!, selectedImage)
    return img
}

/**
 * Calculate an inSampleSize for use in a [BitmapFactory.Options] object when decoding
 * bitmaps using the decode* methods from [BitmapFactory]. This implementation calculates
 * the closest inSampleSize that will result in the final decoded bitmap having a width and
 * height equal to or larger than the requested width and height. This implementation does not
 * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
 * results in a larger bitmap which isn't as useful for caching purposes.
 *
 * @param options   An options object with out* params already populated (run through a decode*
 * method with inJustDecodeBounds==true
 * @param reqWidth  The requested width of the resulting bitmap
 * @param reqHeight The requested height of the resulting bitmap
 * @return The value to be used for inSampleSize
 */
private fun calculateInSampleSize(
    options: BitmapFactory.Options,
    reqWidth: Int, reqHeight: Int
): Int {
    // Raw height and width of image
    val height = options.outHeight
    val width = options.outWidth
    var inSampleSize = 1
    if (height > reqHeight || width > reqWidth) {

        // Calculate ratios of height and width to requested height and width
        val heightRatio =
            Math.round(height.toFloat() / reqHeight.toFloat())
        val widthRatio =
            Math.round(width.toFloat() / reqWidth.toFloat())

        // Choose the smallest ratio as inSampleSize value, this will guarantee a final image
        // with both dimensions larger than or equal to the requested height and width.
        inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio

        // This offers some additional logic in case the image has a strange
        // aspect ratio. For example, a panorama may have a much larger
        // width than height. In these cases the total pixels might still
        // end up being too large to fit comfortably in memory, so we should
        // be more aggressive with sample down the image (=larger inSampleSize).
        val totalPixels = width * height.toFloat()

        // Anything more than 2x the requested pixels we'll sample down further
        val totalReqPixelsCap = reqWidth * reqHeight * 2.toFloat()
        while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
            inSampleSize++
        }
    }
    return inSampleSize
}

/**
 * Rotate an image if required.
 *
 * @param img           The image bitmap
 * @param selectedImage Image URI
 * @return The resulted Bitmap after manipulation
 */
@Throws(IOException::class)
private fun rotateImageIfRequired(img: Bitmap, selectedImage: Uri): Bitmap? {
    val ei = ExifInterface(selectedImage.path)
    val orientation: Int =
        ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
    return when (orientation) {
        ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(img, 90)
        ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(img, 180)
        ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(img, 270)
        else -> img
    }
}

private fun rotateImage(img: Bitmap, degree: Int): Bitmap? {
    val matrix = Matrix()
    matrix.postRotate(degree.toFloat())
    val rotatedImg =
        Bitmap.createBitmap(img, 0, 0, img.width, img.height, matrix, true)
    img.recycle()
    return rotatedImg
}

}

3

我遇到了同样的问题,但我用了一个不太正规的方式解决了它。

我的解决办法是:

    fun Bitmap.rotate(degrees: Float): Bitmap {
    val matrix = Matrix().apply { postRotate(degrees) }
    return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
    }

用法:

imageViewCapturedImage.setImageBitmap(bitmap?.rotate(90F))

是的,我也在自己旋转它。请查看我的答案以获取旋转助手类。 - Nouman Bhatti

2

对我来说最简单的解决方案。

从imageProxy中获取rotationDegrees,并将位图旋转相应的角度。

Matrix matrix = new Matrix();                   
matrix.postRotate((float)imageProxy.getImageInfo().getRotationDegrees());
Bitmap bitmap2 =  Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
binding.imgPreview.setImageBitmap(bitmap2);

1
这段简单的代码对我很有用:
Java版本:
Context context = ... //The current Context
Camera camera = cameraProvider.bindToLifecycle(...); //The one you get after initializing the camera
ImageProxy image = ... //The one that takePicture or Analyze give you
int currentLensOrientation = ... //CameraSelector.LENS_FACING_BACK or CameraSelector.LENS_FACING_FRONT

int rotationDirection = currentLensOrientation == CameraSelector.LENS_FACING_BACK ? 1 : -1;
int constantRotation = image.getImageInfo().getRotationDegrees() - camera.getCameraInfo().getSensorRotationDegrees();
int rotationDegrees = camera.getCameraInfo().getSensorRotationDegrees() - context.getDisplay().getRotation() * 90 + constantRotation * rotationDirection;

Kotlin 版本:

val context: Context = ... //The current Context
val camera: Camera? = cameraProvider.bindToLifecycle(...) //The one you get after initializing the camera
val image: ImageProxy = ... //The one that takePicture or Analyze give you
val currentLensOrientation: Int = ... //CameraSelector.LENS_FACING_BACK or CameraSelector.LENS_FACING_FRONT

val rotationDirection = if (currentLensOrientation == CameraSelector.LENS_FACING_BACK) 1 else -1
val constantRotation = image.imageInfo.rotationDegrees - camera!!.cameraInfo.sensorRotationDegrees
val rotationDegrees = camera!!.cameraInfo.sensorRotationDegrees - context.display!!.rotation * 90 + constantRotation * rotationDirection

然后我使用rotationDegrees来旋转CameraX在takePicture和analyze回调中传递给您的ImageProxy。

如果需要,您可以在此处找到完整的Java代码:https://github.com/CristianDavideConte/SistemiDigitali/blob/7b40e50d8b2fbdf4e4a61edba7443da92b96c58d/app/src/main/java/com/example/sistemidigitali/views/CameraProviderView.java#L207


-1

我一直遇到同样的问题;据我所了解,从像thisthis这样的回复中得知,CameraX背后的团队不喜欢干预从硬件返回的原始图像数据,并且非常希望仅限于设置EXIF元数据。

因此,我绕过了这个问题,从与你的代码相似的代码开始(受到codelab中的代码启发),我添加了以下内容:

Display d = getDisplay();
if (d != null) {
    iCapture.setTargetRotation(d.getRotation());
}

在调用iCapture.takePicture()(其中iCapture是我的ImageCapture用例实例)之前,确保EXIF文件元数据中的目标旋转与拍摄照片时实际显示旋转一致。

然后,在接收到数据后(在我的情况下,在onImageSaved()处理程序上),我检查EXIF元数据的旋转,并在这种情况下手动旋转图像以获得所需的度数,并保存不同的文件以确保没有保留不一致值的EXIF标签。

try {
    ExifInterface ei = new ExifInterface(tempFile.getAbsolutePath());
    if (ei.getRotationDegrees() != 0) {
        actualPicture = ImageUtil.rotateDegrees(tempFile, ei.getRotationDegrees());
    }
} catch (IOException exc) {
    Log.e(TAG, "Tried to fix image rotation but could not continue: " + exc,getMessage());
}

其中ImageUtil是一个自定义的图像工具类,rotateDegrees实现了这个功能,使用自定义矩阵进行初始化,如下所示:

//inside rotateDegrees(), degrees is the parameter to the function
Matrix m = new Matrix();
m.postRotate(degrees);

从原始文件导入的位图开始,创建一个新的位图:

Bitmap b = Bitmap.createBitmap(sourceFile, 0, 0, sourceFile.getWidth(), sourceFile.getHeight(), m, true);
b.compress(Bitmap.CompressFormat.JPEG, 85, /* a suitably-created output stream */);

不过,我希望CameraX能够直接处理图像旋转,而不是依赖元数据(据他们自己承认,很少有库和工具去读取和处理这些元数据)。


仅适用于 >= Build.VERSION_CODES.R 的工作。 - Beemo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接