【问题标题】:Android CameraX image rotatedAndroid CameraX 图像旋转
【发布时间】:2021-01-03 08:33:54
【问题描述】:

我已关注 Google CameraX code lab 实现自定义摄像头。相机预览很好,但是当我在图像捕获图像旋转后拍摄图像时。我正在以纵向模式拍摄图像,但保存的图像是横向的。这是配置相机的方法

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

使用EXIF拍摄后是否需要自己旋转图像,或者我可以在配置相机时修复它?

【问题讨论】:

  • 澄清问题:您使用的是三星设备吗?他们中的许多人都有一个已知的错误,即在保存时以错误的方向记录图像。
  • @JohnLord 你有这个声明的来源吗?是否有任何已知的解决方法?
  • 唯一已知的解决方法是保存图像,然后读取 exif 数据。这是一个众所周知的问题,并且在 stackoverflow 上有各种关于它的帖子,例如这个。 stackoverflow.com/questions/47261434/…我们公司有数百个三星平板电脑,我们必须在上面的链接中包含一个类似的修复程序,尽管我们的更简单,因为我们的平板电脑被锁定为纵向。在上面的链接中,他们将读取的 exif 数据与当前设备方向进行比较。

标签: android android-camera2 android-orientation image-rotation android-camerax


【解决方案1】:

默认情况下,ImageCapture 将捕获的方向设置为显示旋转。如果图像保存到磁盘,旋转将在 EXIF 中。

您的设备是否处于锁定纵向模式?在这种情况下,显示旋转与设备的方向不匹配,您需要自己设置目标旋转。示例。

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

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

【讨论】:

  • LifecycleCameraController 的一个问题是缺少样板代码; API 描述页面中的内容不足以开始使用,并且在 Android 应用程序中通过反复试验进行编程非常耗时……我非常感谢对此的 codelab 扩展。我会检查官方的 cameraXsample 看看是否有帮助。
  • 我知道代码实验室有点过时了。有兴趣的可以看一下CameraX的测试app,了解控制器应该如何使用:android.googlesource.com/platform/frameworks/support/+/refs/…
  • 感谢您的链接;但据我了解,CameraX 不喜欢干预图像数据,所以它唯一要做的就是设置 EXIF 元数据。最后我还是使用了ProcessCameraProvider类,在调用takePicture()之前设置了目标旋转,然后手动从保存的JPEG中读取EXIF数据,并相应地旋转图片以获得直上图像。即使我希望 CameraX 负责旋转图像并且不依赖 EXIF 元数据,我也会将此作为单独的答案添加。
  • 我尝试使用 LifecycleCameraController。它没有帮助。照片仍旋转 -90 度。顺便说一句,这门课非常原始。无法设置照片/分析的分辨率,并且其他基本功能不可用。
  • 天哪,我爱你提到 LifecycleCameraController。我删除了所有代码并用控制器替换了它。
【解决方案2】:

我也遇到了同样的情况。我用hacky方式解决了这个问题。

我的解决办法是:

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

【讨论】:

  • 是的,我自己也在轮换。请参阅我对轮换助手类的回答
【解决方案3】:

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

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
}

}

【讨论】:

    【解决方案4】:

    我也遇到了同样的问题;据我了解,从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 直接处理图像旋转,而不依赖元数据(据他们自己承认,很少有库和工具会被读取并实际处理)。

    【讨论】:

      【解决方案5】:

      这个简单的代码对我有用:

      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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-09-29
        • 2020-09-26
        • 2018-08-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-24
        相关资源
        最近更新 更多