4

I'm trying to make a custom camera app using hardware.camera.

I've implemented a PictureCallback which will write into a file with a certain path when the picture is taken. The data written into the file is the ByteArray returned by takePicture in camera API.

So after writing into the file, I've noticed the picture taken vertically is saved horizontally. The problem wasn't because of the Exif tag cause the byteArray had ORIENTATION_NORMAL both before and after writing into the file.

The data written into the file is the ByteArray returned by takePicture in camera API.

Here's what takePicture looks like in Camera.Java :

    public final void takePicture(ShutterCallback shutter, PictureCallback raw,
            PictureCallback jpeg) {
        takePicture(shutter, raw, null, jpeg);
    }

Here's part of the CameraPreview which will capture the photo :

Code for Camera Preview

    val imageProcessor = ImageProcessor()
    private val fileSaver = FileSaver(context)
    fun capture() {
        val callback = PictureCallback { data, _ ->
            imageProcessor.process(data)?.apply {
                val file = fileSaver.saveBitmap(this, outputFileName ?: DEFAULT_FILE_NAME)
                onCaptureTaken?.invoke(file)
            }
        }
        camera?.takePicture(null, null, callback)
    }

Code for ImageProcessor.kt

class ImageProcessor {

    fun process(data: ByteArray): Bitmap? {
        val options = BitmapFactory.Options().apply {
            inMutable = true
        }

        val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, options)
        return fixImageRotation(data, bitmap)
    }
    private fun fixImageRotation(picture: ByteArray, bitmap: Bitmap): Bitmap? {
        return when (exifPostProcessor(picture)) {
            ExifInterface.ORIENTATION_ROTATE_90 ->
                rotateImage(bitmap, 90F)
            ExifInterface.ORIENTATION_ROTATE_180 ->
                rotateImage(bitmap, 180F)
            ExifInterface.ORIENTATION_ROTATE_270 ->
                rotateImage(
                    bitmap, 270F
                )
            ExifInterface.ORIENTATION_NORMAL -> bitmap
            else -> bitmap
        }
    }

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

    private fun exifPostProcessor(picture: ByteArray?): Int {
        try {
            return getExifOrientation(ByteArrayInputStream(picture))
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return -1
    }

    @Throws(IOException::class)
    private fun getExifOrientation(inputStream: InputStream): Int {
        val exif = ExifInterface(inputStream)
        return exif.getAttributeInt(
            ExifInterface.TAG_ORIENTATION,
            ExifInterface.ORIENTATION_NORMAL
        )
    }
}

Code for FileSaver.kt

internal class FileSaver(context: Context) {

    private val context: Context = context.applicationContext
    fun saveBitmap(bitmap: Bitmap, fileName: String): File {
        val file = File(mkdirsCacheFolder(), fileName)
        try {
            FileOutputStream(file).use { out ->
                bitmap.compress(Bitmap.CompressFormat.JPEG, ORIGINAL_QUALITY, out)
            }
            bitmap.recycle()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return file
    }


    private fun mkdirsCacheFolder(): File {
        return File(context.externalCacheDir, CACHE_DIRECTORY).apply {
            if (!exists()) {
                mkdirs()
            }
        }
    }

    companion object {
        private const val ORIGINAL_QUALITY = 100
        private const val CACHE_DIRECTORY = "/Lens"
    }
}

Any suggestions?

EDIT: I printed the Exif tag and it turns out to be ORIENTATION_NORMAL so I don't really know if it is rotated at all.

Edit 2 : Sample pictures were taken in portrait mode and opened from file manager[! Not that, these results are tested on both emulator and real android phone and they are the same. Preview: Preview

Captured image from file manager: Captured image from file manager

Maryam Mirzaie
  • 536
  • 6
  • 24
  • I think most of it is included in the code. The file name is input and then a file is created with that name and then written on it. @blackapps – Maryam Mirzaie Aug 12 '20 at 10:58
  • @blackapps I'm sorry :( the input file name is a hardcoded string which I passed to this function. Should I include it in the description? – Maryam Mirzaie Aug 12 '20 at 12:37
  • @blackapps I added an edit. Thanks for the feedback – Maryam Mirzaie Aug 12 '20 at 12:44
  • @blackapps I added what I thought I could include. I used `camera?.takePicture` so the data returned from this function is being written in the file. I don't exactly know how the api works in detail. I just know it returns the picture as a `byteArray`. – Maryam Mirzaie Aug 12 '20 at 12:55
  • @blackapps Here's what `takePicture` do in `Camera.Java`. I think it's jpg file as you mentioned. `public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) { takePicture(shutter, raw, null, jpeg); }` – Maryam Mirzaie Aug 12 '20 at 12:59
  • 1
    He he... finally there is the information i asked for. The data byte array contains a jpg file and you are saving the bytes to file. Please put in your post that you obtained a jpg image which you are saving to file. And remoive comments like i did please. – blackapps Aug 12 '20 at 13:01
  • @blackapps Thank you so much. I really appreciate it. – Maryam Mirzaie Aug 12 '20 at 13:01
  • 1
    *when writing the file the tags are being ignored* – this is very strange. Your code writes all jpeg bytes, including the EXIF header. Maybe you mean that your *viewer* ignores these flags? I would suggest to try some good viewer (i.e. not Windows default) to check this. If you find that the EXIF header is broken, you can fix it (see [ExifInterface](https://developer.android.com/reference/androidx/exifinterface/media/ExifInterface) library). If you want the image to be compatible with viewers that ignore the header, you have no choice but to perform rotation yourself. – Alex Cohn Aug 12 '20 at 17:32
  • So the photo saved in the jpeg file is actually horizontal with the EXIF tag? @AlexCohn – Maryam Mirzaie Aug 13 '20 at 07:42
  • I did try this. The tags are ignored when writing into a file @AlexCohn . I checked if the file has exif tag afterwards. it didn't. – Maryam Mirzaie Aug 13 '20 at 11:57
  • You can use [ExifInterface](https://developer.android.com/reference/androidx/exifinterface/media/ExifInterface) to add the orientation info to your JPEG. – Alex Cohn Aug 15 '20 at 17:22
  • @AlexCohn I don't know if the picture is rotated, how would I know what tag to add? – Maryam Mirzaie Aug 15 '20 at 18:01
  • You can use the device orientation sensor for that. Sure, this is not cheat-proof: the user could click the button in portrait mode, quickly rotate the device to landscape, take the shot, quickly rotate back to portrait mode, and your orientation sensor will say "it was portrait before and after, so let's apply the orientation flag". But in most cases, the two readings should be enough. – Alex Cohn Aug 15 '20 at 18:58
  • Just in case, could you post a sample picture taken from your app, in portrait mode? – Alex Cohn Aug 15 '20 at 19:00
  • Hmmm, but the thing is I want to know why this happens exactly. I've been dealing with this problem for almost a week now @AlexCohn I'll add the pictures in a new edit. – Maryam Mirzaie Aug 15 '20 at 19:54
  • @AlexCohn I added the pictures in edit 2 :) – Maryam Mirzaie Aug 15 '20 at 20:09
  • @MaryJane you should have mentioned that you tested this on emulator! The emulator camera support is very special (even when you use the PC camera), and EXIF info is only one facet that cannot be relied upon. Please try your logic on a real device, and if it's still not working, we'll continue this discussion. – Alex Cohn Aug 16 '20 at 07:26
  • @AlexCohn Actually I did, the results are the same. I tried this on a Xiami mi phone. – Maryam Mirzaie Aug 16 '20 at 07:37
  • See https://stackoverflow.com/questions/59700685/wrong-exif-orientation-tag-in-xiaomi. Device-specific bug. – Alex Cohn Aug 16 '20 at 08:29
  • @AlexCohn Thanks but is it ok when the orientation code is 1 representing `normal` orientation? in the question you mentioned, he knows how it is rotated. I couldn't know if it has rotated 90 degrees because the code is `1`. – Maryam Mirzaie Aug 16 '20 at 08:44
  • No, the linked question says that this device gives a wrong flag. Let's try out something: if you take a photo in landscape mode, what orientation tag do you see? Per *[Sajjad Z](https://stackoverflow.com/users/12160947/sajjad-z)*, it should be *rotate 270*, while the picture does actually need no rotation. I don't have a Xiaomi Mi device to test this. – Alex Cohn Aug 16 '20 at 08:51
  • @AlexCohn Yes, still the same. Taking photo in landscape mode also gives me `1`. And I tried with a Samsung galaxy too now. no difference. – Maryam Mirzaie Aug 16 '20 at 08:58
  • Oh sorry. Now I understand what went wrong here. Please wait until I write the answer. – Alex Cohn Aug 16 '20 at 09:01
  • @AlexCohn Ok Thanks ^_^ – Maryam Mirzaie Aug 16 '20 at 11:13

1 Answers1

1

Few issues with this situation got overlapped in this question, therefore it took me so long to understand what really was going on.

What you did, you received a valid Jpeg ByteArray from the camera, and this stream contained some EXIF information, but it was missing the orientation tag. This happens on many devices, also on Xiaomi Mi.

So, you could not rotate the bitmap correctly. But you know exactly the orientation of your Activity: preview.display.rotation. This should tell you how the bitmap should be rotated in this case, but if your activity is locked into portrait, you don't even need to check. Display rotation may be in range 0…3 and these represent Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, or Surface.ROTATION_270.

To choose the correct rotation, you must know the way the hardware is assembled, i.e. how the camera sensor is aligned with the device. This orientation of the camera can be 0, 90, 180, or 270.

You might have seen this piece of code in different sources:

var degrees = 0
when (preview.display.rotation) {
    Surface.ROTATION_0 -> degrees = 0
    Surface.ROTATION_90 -> degrees = 90
    Surface.ROTATION_180 -> degrees = 180
    Surface.ROTATION_270 -> degrees = 270
}
val ci = Camera.CameraInfo()
Camera.getCameraInfo(cameraId, ci)
if (ci.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
    degrees += ci.orientation
    degrees %= 360
    degrees = 360 - degrees
} else {
    degrees = 360 - degrees
    degrees += ci.orientation
}
camera!!.setDisplayOrientation(degrees % 360)

This code allows the camera preview to be correctly aligned with your screen; you probably have this somewhere in your app, too. Same code can be used to choose the correct bitmap rotation in your fixImageRotation() if getExifOrientation() returns ExifInterface.ORIENTATION_UNKNOWN.

In some cases, you need more detailed info about the device orientation, as explained here.

Anyways, I would recommend you to switch to the modern CameraX API, which provides better support for most devices. It allows me to call ImageCapture.setTargetRotation() and the resulting Jpeg is is rotated for me by the library.

Alex Cohn
  • 56,089
  • 9
  • 113
  • 307
  • So this function is actually correcting the Exif tag and the output is correct after this? or should I rotate the output picture with `degrees`? – Maryam Mirzaie Aug 17 '20 at 08:04
  • btw my min sdk is 17 so I can't use cameraX as it's available in API 21 and higher. – Maryam Mirzaie Aug 17 '20 at 08:05
  • It's your choice: you can keep your current code and have this function find the correct rotation, or you can use ExifInterface library to set the orientation tag. In the latter case, you don't need all this code that decodes `data` to bitmap, writing Jpeg with modified Exif header is much faster and saves memory. – Alex Cohn Aug 17 '20 at 08:28
  • Thanks. I'll write in in my code and let you know about the result :) – Maryam Mirzaie Aug 17 '20 at 08:53
  • The question is whether your activity is locked in portrait, and if not, is it tagged with `android:configChanges="orientation"`. – Alex Cohn Aug 17 '20 at 11:24
  • 1
    I locked my activity to PORTRAIT and manually wrote `ORIENTATION_ROTATE_90` Exif. This solves my problem I think. Is this the correct way to do so? – Maryam Mirzaie Aug 18 '20 at 09:53
  • 1
    @MaryJane, as long as it serves your use case, it's fine. Don't forget that `ci.orientation` may not be `90` on some devices (the famous example is/was [Nexus 5X](https://www.reddit.com/r/Android/comments/3rjbo8/nexus5x_marshmallow_camera_problem/cwqzqgh/)), and you should make sure you handle such phones correctly. – Alex Cohn Aug 18 '20 at 10:18
  • Oh, so how could I check if the orientation of my activity is portrait in all devices?? – Maryam Mirzaie Aug 18 '20 at 11:48
  • In the formula above you see how `CameraInfo.orientation` is used to calculate the correct rotation `degrees`. – Alex Cohn Aug 18 '20 at 19:33