I am using MediaProjectionManager for taking screenshots from the ForegroundService. I discovered that the behavior of capturing surface works differently in Android 10 and Android 11.
When I take a screenshot
fun captureBitmap(frame: CropFrames, response: (bitmap: Bitmap) -> Unit){
delayed(50) {
this.frames = frame
projection = mgr!!.getMediaProjection(resultCode, resultData!!)
val cb: MediaProjection.Callback = object : MediaProjection.Callback() {
override fun onStop() {
vdisplay!!.release() //?
response.invoke(latestBitmap!!)
}
}
vdisplay = projection?.createVirtualDisplay(
NAME,
width,
height,
App.densityDpi,
FLAGS,
imageReader.surface,
null,
null
)
projection?.registerCallback(cb, null)
}
}
onImageAvailable triggered
override fun onImageAvailable(reader: ImageReader) {
try {
val image = imageReader.acquireNextImage()
if (image != null) {
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * width
val bitmapWidth = width + rowPadding / pixelStride
if (latestBitmap == null || latestBitmap!!.width != bitmapWidth || latestBitmap!!.height != height) {
if (latestBitmap != null) {
latestBitmap!!.recycle()
}
latestBitmap = Bitmap.createBitmap(
bitmapWidth,
height, Bitmap.Config.ARGB_8888
)
}
latestBitmap!!.copyPixelsFromBuffer(buffer)
image.close()
handler.parseFrame(frames!!, latestBitmap!!) {
stopCapture()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
Then I release it
fun stopCapture() {
if (projection != null) {
projection!!.stop()
vdisplay!!.release()
projection = null
}
}
This flow can be triggered a lot of times per lifecycle, but each call has increased completion time of execution and it looks like short twitches of UI while taking ascreenshot (no main thread calculations). Probably I don't clear something properly? Any suggestions are appreciated. Thanks!
Related
I am trying to capture photos in my app using standard camera app intent (I am NOT interested in using JetpackX or other library to have a viewfinder in my app).
When I had the code in my Fragment like so:
// This is in response to user clicking a button
fun startPhotoTaking() {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
resultLauncher.launch(takePictureIntent)
}
private var resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val photo = result.data?.extras?.get("data") as? Bitmap
photo?.let {
// ... do whatever
}
}
}
Then the photo Bitmap came back tiny as apparently Android caps intents at 1 Mb , but the orientation was correct.
Since I actually need the original large image, I have modified the code like so:
// This is in response to user clicking a button
fun startPhotoTaking() {
lastUri = getTmpFileUri()
if (lastUri != null) {
resultLauncher.launch(lastUri)
}
}
private fun getTmpFileUri(): Uri {
requireContext().cacheDir.listFiles()?.forEach { it.delete() }
val tmpFile = File
.createTempFile("tmp_image_file", ".jpg", requireContext().cacheDir)
.apply {
createNewFile()
}
return FileProvider.getUriForFile(
MyApplication.instance.applicationContext,
"${BuildConfig.APPLICATION_ID}.provider",
tmpFile
)
}
var lastUri: Uri? = null
private var resultLauncher =
registerForActivityResult(ActivityResultContracts.TakePicture()) { result ->
if (result) {
val photoUri = lastUri
if (photoUri != null) {
val stream = MyApplication.instance.applicationContext.contentResolver
.openInputStream(photoUri)
val photo = BitmapFactory.decodeStream(stream)
stream?.close()
// ... do whatever
// If i try ExifInterface(photoUri.path!!)
}
}
}
Now I do receive the actual large photo, but it is always landscape :(
I tried creating an instance of ExifInterface(photoUri.path) but that throws an exception for me (which I don't quite understand as I am only writing/reading to my own app's cache directory?):
java.io.FileNotFoundException: /cache/tmp_image_file333131952730914647.jpg: open failed: EACCES (Permission denied)
How can I get my photo to retain orientation when saved to file and/or get access to read EXIF parameters so I can rotate it myself?
Update
As a workaround, this did the trick but it's just... ungodly. So am very keen to find better solutions.
val stream = MyApplication.instance.applicationContext.contentResolver
.openInputStream(photoUri)
if (stream != null) {
val tempFile = File.createTempFile("tmp", ".jpg", requireContext().cacheDir)
.apply { createNewFile() }
val fos = FileOutputStream(tempFile)
val buf = ByteArray(8192)
var length: Int
while (stream.read(buf).also { length = it } > 0) {
fos.write(buf, 0, length)
}
fos.close()
val exif = ExifInterface(tempFile.path)
val orientation =
exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
val matrix = Matrix()
if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
matrix.postRotate(90f)
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
matrix.postRotate(180f)
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
matrix.postRotate(270f)
var photo = BitmapFactory.decodeFile(tempFile.path)
photo = Bitmap.createBitmap(
photo,
0,
0,
photo.width,
photo.height,
matrix,
true
)
}
ExifInterface(photoUri.path)
That isnot an existing path as you have seen.
Better use:
ExifInterface( tmpFile.getAbsolutePath())
You can rotate image to the particular angle like this,
// To rotate image
private fun rotateImage(source: Bitmap, angle: Float): Bitmap? {
val matrix = Matrix()
matrix.postRotate(angle)
return Bitmap.createBitmap(
source, 0, 0, source.width, source.height,
matrix, true
)
}
In your case set angle to 90f to get image in portrait orientation.
I'm working on an QR scanning application that is based on CameraX. The scanning works as expected on most devices except a few random devices. After debugging this issue for a long time, I found out that the cropped image is kind of shattered on the device in which scanning wasn't working.
Expected output (works on Pixel Device):
Current output (on the devices in which scanning is not working):
The analyze method that receives each frame (part of the CustomImageAnalyzer class)
override fun analyze(image: ImageProxy) {
val byteBuffer = image.planes[0].buffer
if (imageData.size != byteBuffer.capacity()) {
imageData = ByteArray(byteBuffer.capacity())
}
byteBuffer[imageData]
val iFact = if (mActivity.getOverlayView().width <= mActivity.getOverlayView().height) {
image.width / mActivity.getOverlayView().width.toDouble()
} else {
image.height / mActivity.getOverlayView().height.toDouble()
}
Log.i(TAG, "")
Log.i(TAG, "image.height" + image.height)
Log.i(TAG, "image.width" + image.width)
Log.i(TAG, "overlay.height" + mActivity.getOverlayView().height)
Log.i(TAG, "overlay.width" + mActivity.getOverlayView().width)
val size = mActivity.getOverlayView().size * iFact
Log.i(TAG, "Obtained size 1: " + mActivity.getOverlayView().size)
Log.i(TAG, "iFact: $iFact")
Log.i(TAG, "calculated size: $size")
val left = (image.width - size) / 2
val top = (image.height - size) / 2
Log.i(TAG, "left: $left")
Log.i(TAG, "top: $top")
val source = PlanarYUVLuminanceSource(
imageData,
image.width, image.height,
left.toInt(), top.toInt(),
size.toInt(), size.toInt(),
false
)
Log.i(TAG, "source.thumbnailHeight" + source.thumbnailHeight.toString())
Log.i(TAG, "source.thumbnailWidth" + source.thumbnailWidth.toString())
mActivity.runOnUiThread {
mActivity.showIntArray(source.renderThumbnail(), source.thumbnailHeight)
}
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
try {
val result = reader.decodeWithState(binaryBitmap)
listener.invoke(result.text)
} catch (e: ReaderException) {
} finally {
reader.reset()
}
// Compute the FPS of the entire pipeline
val frameCount = 10
if (++frameCounter % frameCount == 0) {
frameCounter = 0
val now = System.currentTimeMillis()
val delta = now - lastFpsTimestamp
val fps = 1000 * frameCount.toFloat() / delta
Log.d(TAG, "Analysis FPS: ${"%.02f".format(fps)}")
lastFpsTimestamp = now
}
image.close()
}
Code to start camera:
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(contentFrame.surfaceProvider)
}
overlayView = findViewById(R.id.overlay)
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(960, 960))
.build()
imageAnalysis.setAnalyzer(
executor,
QRCodeImageAnalyzer (this) { response ->
if (response != null) {
handleResult(response)
}
}
)
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis)
Also, I get the image from PlanarYUVLuminanceSource after the cropping is done.
Can someone please help me out with this issue?
some device have rotation special. you can use imageProxy.getImageInfo().getRotationDegrees() to get correct rotation of image. references with (ImageInfo) https://developer.android.com/reference/androidx/camera/core/ImageInfo#getRotationDegrees()
I am pretty new to Android so I hope I can get some directions here.
I have a 360 camera running on Android 7.0. It includes a SDK to get access to the live stitched images. In this SDK there is a function to set a Surface where the output from the stitched images will be directed to.
This is the function provided by the SDK:
public static void SDK.setSurface(Surface inputSurface)
I want to grab an image from that surface every second.
How do I create the right kind of Surface? And how do I grab images from this Surface?
Any help is highly appreciated!
Since I found the answer I might share it here too.
I have create a class which works like a charm. This is what I used:
class Capture: ImageReader.OnImageAvailableListener{
private var mImageReader: ImageReader? = null
private var mThreadHandler: HandlerThread? = null
fun start() {
if (!init) {
if (mImageReader != null) {
mImageReader?.close()
mImageReader = null
}
if (mThreadHandler != null) {
mThreadHandler?.quitSafely()
mThreadHandler = null
}
mThreadHandler = HandlerThread("prev")
mThreadHandler?.start()
mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 5)
mImageReader?.setOnImageAvailableListener(this, Handler(mThreadHandler?.getLooper()))
SDK.setSurface(if (mImageReader == null) null else mImageReader?.getSurface())
init = true
}
}
override fun onImageAvailable(reader: ImageReader) {
val image = reader.acquireLatestImage()
val height = image.height
val stride = image.planes[0].rowStride / image.planes[0].pixelStride
captureCallback(
stride,
height,
image.timestamp,
image.planes[0].buffer
)
image.close()
}
fun stop() {
if (init) {
if (mImageReader != null) {
mImageReader!!.close()
mImageReader = null
}
if (mThreadHandler != null) {
mThreadHandler!!.quitSafely()
mThreadHandler = null
}
SDK.setSurface(null)
}
init = false
}
fun captureCallback(width: Int, Height: Int, timestamp: Long, data: ByteBuffer) {
// do something with data
}
}
`
I'm using CameraX and then FirebaseVision to read some text from the image. when I'm analyzing the Image I want to select a portion of the image, not the entire Image, something like when you use a barcode scanner.
class Analyzer : ImageAnalysis.Analyzer {
override fun analyze(imageProxy: ImageProxy?, rotationDegrees: Int) {
// how to crop the image in here?
val image = imageProxy.image
val imageRotation = degreesToFirebaseRotation(degrees)
if (image != null) {
val visionImage = FirebaseVisionImage.fromMediaImage(image, imageRotation)
val textRecognizer = FirebaseVision.getInstance().onDeviceTextRecognizer
textRecognizer.processImage(visionImage)
}
}
I want to know, is there any way to crop the image?
Your problem is exactly what I have tackled 2 months ago...
object YuvNV21Util {
fun yuv420toNV21(image: Image): ByteArray {
val crop = image.cropRect
val format = image.format
val width = crop.width()
val height = crop.height()
val planes = image.planes
val data =
ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8)
val rowData = ByteArray(planes[0].rowStride)
var channelOffset = 0
var outputStride = 1
for (i in planes.indices) {
when (i) {
0 -> {
channelOffset = 0
outputStride = 1
}
1 -> {
channelOffset = width * height + 1
outputStride = 2
}
2 -> {
channelOffset = width * height
outputStride = 2
}
}
val buffer = planes[i].buffer
val rowStride = planes[i].rowStride
val pixelStride = planes[i].pixelStride
val shift = if (i == 0) 0 else 1
val w = width shr shift
val h = height shr shift
buffer.position(rowStride * (crop.top shr shift) + pixelStride * (crop.left shr shift))
for (row in 0 until h) {
var length: Int
if (pixelStride == 1 && outputStride == 1) {
length = w
buffer[data, channelOffset, length]
channelOffset += length
} else {
length = (w - 1) * pixelStride + 1
buffer[rowData, 0, length]
for (col in 0 until w) {
data[channelOffset] = rowData[col * pixelStride]
channelOffset += outputStride
}
}
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length)
}
}
}
return data
}
}
then convert bytearray into bitmap
object BitmapUtil {
fun getBitmap(data: ByteArray, metadata: FrameMetadata): Bitmap {
val image = YuvImage(
data, ImageFormat.NV21, metadata.width, metadata.height, null
)
val stream = ByteArrayOutputStream()
image.compressToJpeg(
Rect(0, 0, metadata.width, metadata.height),
80,
stream
)
val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size())
stream.close()
return rotateBitmap(bmp, metadata.rotation, false, false)
}
private fun rotateBitmap(
bitmap: Bitmap, rotationDegrees: Int, flipX: Boolean, flipY: Boolean
): Bitmap {
val matrix = Matrix()
// Rotate the image back to straight.
matrix.postRotate(rotationDegrees.toFloat())
// Mirror the image along the X or Y axis.
matrix.postScale(if (flipX) -1.0f else 1.0f, if (flipY) -1.0f else 1.0f)
val rotatedBitmap =
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
// Recycle the old bitmap if it has changed.
if (rotatedBitmap != bitmap) {
bitmap.recycle()
}
return rotatedBitmap
}
}
Please have a look at my open source project https://github.com/minkiapps/Firebase-ML-Kit-Scanner-Demo, I build a demo app where portion of the image proxy is cropped before it is processed by ml kit.
Okay, I went through different posts and find out that depending on mobile manufacturers there can be a complications such as capture images get rotated, so you have to be aware of that. What I did was:
fun rotateBitmap(bitmap: Bitmap): Bitmap? {
val matrix = Matrix()
when (getImageOrientation(bitmap)) {
ExifInterface.ORIENTATION_NORMAL -> return bitmap
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
matrix.setRotate(180f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_TRANSPOSE -> {
matrix.setRotate(90f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_TRANSVERSE -> {
matrix.setRotate(-90f)
matrix.postScale(-1f, 1f)
}
else -> return bitmap
}
This worked. But then I noticed something really weird and that might be related with how I configured Camera X configuration.
With the same device I get differently rotated Bitmaps (well, this should not happen. If devices rotates image weirdly, it should rotate images in both modes - in ImageAnalysesUseCase and ImageCaptureUseCase).
So, why is this happening and how can I fix it?
Code implementation:
Binding camera X to life-cycle:
CameraX.bindToLifecycle(
this,
buildPreviewUseCase(),
buildImageAnalysisUseCase(),
buildImageCaptureUseCase()
)
Preview use case:
private fun buildPreviewUseCase(): Preview {
val previewConfig = PreviewConfig.Builder()
.setTargetAspectRatio(config.aspectRatio)
.setTargetResolution(config.resolution)
.setTargetRotation(Surface.ROTATION_0)
.setLensFacing(config.lensFacing)
.build()
return AutoFitPreviewBuilder.build(previewConfig, cameraTextureView)
}
Capture use case:
private fun buildImageCaptureUseCase(): ImageCapture {
val captureConfig = ImageCaptureConfig.Builder()
.setTargetAspectRatio(config.aspectRatio)
.setTargetRotation(Surface.ROTATION_0)
.setTargetResolution(config.resolution)
.setCaptureMode(config.captureMode)
.build()
val capture = ImageCapture(captureConfig)
manualModeTakePhotoButton.setOnClickListener {
capture.takePicture(object : ImageCapture.OnImageCapturedListener() {
override fun onCaptureSuccess(imageProxy: ImageProxy, rotationDegrees: Int) {
viewModel.onManualCameraModeAnalysis(imageProxy, rotationDegrees)
}
override fun onError(useCaseError: ImageCapture.UseCaseError?, message: String?, cause: Throwable?) {
//
}
})
}
return capture
}
Analysis use case:
private fun buildImageAnalysisUseCase(): ImageAnalysis {
val analysisConfig = ImageAnalysisConfig.Builder().apply {
val analyzerThread = HandlerThread("xAnalyzer").apply { start() }
analyzerHandler = Handler(analyzerThread.looper)
setCallbackHandler(analyzerHandler!!)
setTargetAspectRatio(config.aspectRatio)
setTargetRotation(Surface.ROTATION_0)
setTargetResolution(config.resolution)
setImageReaderMode(config.readerMode)
setImageQueueDepth(config.queueDepth)
}.build()
val analysis = ImageAnalysis(analysisConfig)
analysis.analyzer = ImageRecognitionAnalyzer(viewModel)
return analysis
}
AutoFitPreviewBuilder:
class AutoFitPreviewBuilder private constructor(config: PreviewConfig,
viewFinderRef: WeakReference<TextureView>) {
/** Public instance of preview use-case which can be used by consumers of this adapter */
val useCase: Preview
/** Internal variable used to keep track of the use-case's output rotation */
private var bufferRotation: Int = 0
/** Internal variable used to keep track of the view's rotation */
private var viewFinderRotation: Int? = null
/** Internal variable used to keep track of the use-case's output dimension */
private var bufferDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's dimension */
private var viewFinderDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's display */
private var viewFinderDisplay: Int = -1
/** Internal reference of the [DisplayManager] */
private lateinit var displayManager: DisplayManager
/**
* We need a display listener for orientation changes that do not trigger a configuration
* change, for example if we choose to override config change in manifest or for 180-degree
* orientation changes.
*/
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) {
val viewFinder = viewFinderRef.get() ?: return
if (displayId == viewFinderDisplay) {
val display = displayManager.getDisplay(displayId)
val rotation = getDisplaySurfaceRotation(display)
updateTransform(viewFinder, rotation, bufferDimens, viewFinderDimens)
}
}
}
init {
// Make sure that the view finder reference is valid
val viewFinder = viewFinderRef.get() ?:
throw IllegalArgumentException("Invalid reference to view finder used")
// Initialize the display and rotation from texture view information
viewFinderDisplay = viewFinder.display.displayId
viewFinderRotation = getDisplaySurfaceRotation(viewFinder.display) ?: 0
// Initialize public use-case with the given config
useCase = Preview(config)
// Every time the view finder is updated, recompute layout
useCase.onPreviewOutputUpdateListener = Preview.OnPreviewOutputUpdateListener {
val viewFinder =
viewFinderRef.get() ?: return#OnPreviewOutputUpdateListener
// To update the SurfaceTexture, we have to remove it and re-add it
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
bufferRotation = it.rotationDegrees
val rotation = getDisplaySurfaceRotation(viewFinder.display)
updateTransform(viewFinder, rotation, it.textureSize, viewFinderDimens)
}
// Every time the provided texture view changes, recompute layout
viewFinder.addOnLayoutChangeListener { view, left, top, right, bottom, _, _, _, _ ->
val viewFinder = view as TextureView
val newViewFinderDimens = Size(right - left, bottom - top)
val rotation = getDisplaySurfaceRotation(viewFinder.display)
updateTransform(viewFinder, rotation, bufferDimens, newViewFinderDimens)
}
// Every time the orientation of device changes, recompute layout
displayManager = viewFinder.context
.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)
// Remove the display listeners when the view is detached to avoid
// holding a reference to the View outside of a Fragment.
// NOTE: Even though using a weak reference should take care of this,
// we still try to avoid unnecessary calls to the listener this way.
viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View?) {
displayManager.registerDisplayListener(displayListener, null)
}
override fun onViewDetachedFromWindow(view: View?) {
displayManager.unregisterDisplayListener(displayListener)
}
})
}
/** Helper function that fits a camera preview into the given [TextureView] */
private fun updateTransform(textureView: TextureView?, rotation: Int?, newBufferDimens: Size,
newViewFinderDimens: Size) {
// This should not happen anyway, but now the linter knows
val textureView = textureView ?: return
if (rotation == viewFinderRotation &&
Objects.equals(newBufferDimens, bufferDimens) &&
Objects.equals(newViewFinderDimens, viewFinderDimens)) {
// Nothing has changed, no need to transform output again
return
}
if (rotation == null) {
// Invalid rotation - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderRotation = rotation
}
if (newBufferDimens.width == 0 || newBufferDimens.height == 0) {
// Invalid buffer dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
bufferDimens = newBufferDimens
}
if (newViewFinderDimens.width == 0 || newViewFinderDimens.height == 0) {
// Invalid view finder dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderDimens = newViewFinderDimens
}
val matrix = Matrix()
// Compute the center of the view finder
val centerX = viewFinderDimens.width / 2f
val centerY = viewFinderDimens.height / 2f
// Correct preview output to account for display rotation
matrix.postRotate(-viewFinderRotation!!.toFloat(), centerX, centerY)
// Buffers are rotated relative to the device's 'natural' orientation: swap width and height
val bufferRatio = bufferDimens.height / bufferDimens.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
// Match longest sides together -- i.e. apply center-crop transformation
if (viewFinderDimens.width > viewFinderDimens.height) {
scaledHeight = viewFinderDimens.width
scaledWidth = Math.round(viewFinderDimens.width * bufferRatio)
} else {
scaledHeight = viewFinderDimens.height
scaledWidth = Math.round(viewFinderDimens.height * bufferRatio)
}
// Compute the relative scale value
val xScale = scaledWidth / viewFinderDimens.width.toFloat()
val yScale = scaledHeight / viewFinderDimens.height.toFloat()
// Scale input buffers to fill the view finder
matrix.preScale(xScale, yScale, centerX, centerY)
// Finally, apply transformations to our TextureView
textureView.setTransform(matrix)
}
companion object {
/** Helper function that gets the rotation of a [Display] in degrees */
fun getDisplaySurfaceRotation(display: Display?) = when(display?.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> null
}
/**
* Main entrypoint for users of this class: instantiates the adapter and returns an instance
* of [Preview] which automatically adjusts in size and rotation to compensate for
* config changes.
*/
fun build(config: PreviewConfig, viewFinder: TextureView) =
AutoFitPreviewBuilder(config, WeakReference(viewFinder)).useCase
}
}
If configuration is correct (it looks okay to me), then next idea was that maybe converting captured images objects to bitmap might be faulty. Below you can see implementation.
Capture mode uses this function:
fun imageProxyToBitmap(image: ImageProxy): Bitmap {
val buffer: ByteBuffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
Analysis mode uses this function:
fun toBitmapFromImage(image: Image?): Bitmap? {
try {
if (image == null || image.planes[0] == null || image.planes[1] == null || image.planes[2] == null) {
return null
}
val yBuffer = image.planes[0].buffer
val uBuffer = image.planes[1].buffer
val vBuffer = image.planes[2].buffer
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val nv21 = ByteArray(ySize + uSize + vSize)
/* U and V are swapped */
yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
} catch (e: IllegalStateException) {
Log.e("IllegalStateException", "#ImageUtils.toBitmapFromImage(): Can't read the image file.")
return null
}
}
So, weirdly, on few devices toBitmapFromImage() sometimes comes up upwards, but at the same time (same device) imageProxyToBitmap() returns image in correct rotation - it has to be the image to bitmap functions fault, right?Why is this happening (because capture mode returns image normally) and how to fix this?
Inside onImageCaptureSuccess, get the rotationDegrees and rotate your bitmap by that degree to get the correct orientation.
override fun onImageCaptureSuccess(image: ImageProxy) {
val capturedImageBitmap = image.image?.toBitmap()?.rotate(image.imageInfo.rotationDegrees.toFloat())
mBinding.previewImage.setImageBitmap(capturedImageBitmap)
showPostClickViews()
mCurrentFlow = FLOW_CAMERA
}
toBitmap() and rotate() are extension functions.
fun Image.toBitmap(): Bitmap {
val buffer = planes[0].buffer
buffer.rewind()
val bytes = ByteArray(buffer.capacity())
buffer.get(bytes)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
fun Bitmap.rotate(degrees: Float): Bitmap =
Bitmap.createBitmap(this, 0, 0, width, height, Matrix().apply { postRotate(degrees) }, true)
CameraX returns the captured image with a rotation value in the callback, which can be used to rotate the image.
https://developer.android.com/reference/androidx/camera/core/ImageCapture.OnImageCapturedListener.html#onCaptureSuccess(androidx.camera.core.ImageProxy,%20int)
For Analyzer UseCases, you have to get rotationDegree coming through analyze method of ImageAnalysis.Analyzer and work accordingly.
Hope it helps!