Thursday, March 13, 2014

[coding] Nancy Drew and the Case of the Fucked Up Camera Orientation

That Android devices may be held in different orientations, and that the users of those devices don't want upside down pictures is probably obvious to everyone.  For some reason, the Android SDK developers chose not to include right-side-up rotation code in the SDK.  It is clear that they knew there would be a nearly universal need for it because the put the alleged algorithm for it directly into the comments of the Camera API.  However they apparently couldn't be bothered to write the method to calculate it for us.

Which leaves me with The Problem.  I'm going to skip the intermediate steps because I don't remember them.  Here is what I know now:

1.  my camera rotation code is wrong according to the API documentation
2.  my camera rotation code is most definitely wrong according to common sense
3.  my camera rotation code works perfectly on:  the Nexus 7, the Acer Iconia A, the Acer Iconia B, the Digital2, and the lenovo Yoga 8.
4.  my camera rotation code DOES NOT work on the Galaxy Tab 3
4b.  The preview does work, but the final picture doesn't, and the preview and the final picture are using the same rotation angle.
5. the inputs and outputs of my camera rotation code are identical on the Galaxy Tab 3 and the Acer Iconia A.  That is to say, the camera angle is the same on both devices, and my resulting rotation calculations are the same value on both devices, AND I'm setting the same rotation parameters on the camera, but its working on the Iconia A and not on the Galaxy Tab.

I don't know whats going on.  The fucking code I wrote shouldn't work on any of the devices (I switched the front and back camera algorithms), and the behavior should be consistent, especially between the preview and the final picture.  Even worse, no one on stack overflow appears to be having this problem--that, or everyone asking the right questions are just being drowned out by people asking how to use the camera.

Once thing I'm doing differently is that I am choosing not to handle orientation changes myself--the sample camera code appears to assume that people would do that.  Another thing I can try is to look at the code for the camera app, if its open source.



[Edit]
Quick update:  android orientation handling is fucked up.  I still prefer it over windows phone though.

[Edit]
First of all, to get the source for the camera app apparently now you have to use git:

    git clone https://android.googlesource.com/platform/packages/apps/Camera.git








Fixed my code...the problem is now occurring only on samsung devices.

based on some spot tests, it looks like the culprit is the EXIF orientation, which the android documentation did warn me about...I guess I just assumed the android bitmap decoder knew how to read jpg files correctly.


[Edit]
The windows picture viewer ALSO ignores the EXIF orientation.

GIMP detects the orientation, and prompts you to "change" it if you want to see the image in the correct orientation.

What I'm gathering from this, is that EXIF orientation is bullshit, and also the Samsung camera device driver developers were fucking lazy.


[Edit]
Android provides an EXIF parser for you.

The EXIF parser provided by android cannot read from a Stream.  Only a file.  (the source code of the Camera app uses some 3d library that is not part of the SDK)

The Camera code callback gives you a stream, not a file.

The core design of Java can be accused of being bloated but it has, from the beginning, made a giant fucking deal about file vs stream abstractions.

You don't need to memorize Code Complete to avoid these types of design mistakes.


[Final Edit ?]
Yeah, so cam orientation is still messed up on this piece of shit Digital Pad 2.  I'm going to write that one off, partly because my code is consistent with the official camera app and partly because the manufacturer's website did not come up in web searches.

Ladies and Gentlemen, I give you fuckers the Glorious Arrow of Orientation:


That picture was actually taken on my Samsung Galaxy S 2 with the official Camera app, which means that since I was holding the phone ostensibly right side up, that file actually has the rotation wrong--if you open it in the wrong program, the arrow is pointing to the left.

So.

Conclusions:

There are four ways to get device orientation:

1.  toDegrees(getWindowManager().getDefaultDisplay().getRotation())
2.  toDegrees(getResources().getConfiguration().orientation)
3.  onConfigurationChanged(Configuration)
4.  OrientationEventListener

1 & 2 are equivalent, so lets revise our list for impact:

1.  toDegrees(getWindowManager().getDefaultDisplay().getRotation()) / toDegrees(getResources().getConfiguration().orientation)
2.  onConfigurationChanged(Configuration)
3.  OrientationEventListener

Each of those methods gives a DIFFERENT FUCKING ANSWER.  One of them actually only seems to give the values 90 and 180 regardless of which orientation.

The one that works with the Camera is #3.  Naturally, it is the most inconvenient to use.  You can't pull the value (and in fact, when your activity is getting created, the value might be wrong anyway so you have to update later), you have to receive it in the listener.  The value you get will be in the range [0-359], but the camera only takes {0, 90, 180, 270} so you have to:

lastReadOrientation = (orientation + 45) / 90 * 90 % 360;

Then you set the orientation on the camera.  The API code turned to be right, however you'll note that for the Front Camera, the preview is mirrored while the camera is not, so you need two different algorithms.

Next, some cameras actually give you a messed up picture.  That is to say the camera didn't actually rotate the image;  it just put a "hey this image is rotated" note in the EXIF header.

I don't give a fuck how well the data matches the EXIF standard;  the fact that the  platforms own bitmap decoder can't fix the rotation automatically  and probably half of the image editing programs out there ignore the EXIF orientation means the data is fucked up.

The official Camera app actually doesnt fix the orientation.  The android developers and most of the posters on stack overflow appear to believe it is acceptable to just save the corrupt picture and deal with the rotation every fucking time an image is loaded.  I consider that unacceptable;  so I'm rotating the image in memory.

If you were paying attention, you'll note that I just decided to do something the API devs didn't specifically anticipate.  As always, this means we're in for a big fucking headache.  First, the stupid ass EXIF tools in the API can't read bytes in memory.  So I used a third party lib:

http://javadoc.metadata-extractor.googlecode.com/git/2.6.4/index.html

    public static int getExifOrientation(byte[] jpg) {
       
        // http://stackoverflow.com/questions/12944123/reading-android-jpeg-exif-metadata-from-picture-callback
       
       
        if(jpg == null) return 0;
       
       
       
        try{
           
            //uses metadata-extractor-2.6.4.jar and xmpcore.jar
           
            Metadata metadata = JpegMetadataReader.readMetadata(
                    new BufferedInputStream(new ByteArrayInputStream(jpg)), false);
           
            // Get the EXIF orientation.
            final ExifIFD0Directory exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class);
            if(exifIFD0Directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION))
            {
                return exifIFD0Directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);

               
            }
            else
            {
                return 0;
            }
           
        }catch(Exception e){
            log("cant read exif orientation", e);
            return 0;
        }
    }
   
    public static Matrix getMatrixForExifOrientation(int exifOrientation){
       
        // from http://stackoverflow.com/questions/12944123/reading-android-jpeg-exif-metadata-from-picture-callback
       
        final Matrix bitmapMatrix = new Matrix();
        switch(exifOrientation)
        {
            case 1:                                                                                     break;  // top left
            case 2:                                                 bitmapMatrix.postScale(-1, 1);      break;  // top right
            case 3:         bitmapMatrix.postRotate(180);                                               break;  // bottom right
            case 4:         bitmapMatrix.postRotate(180);           bitmapMatrix.postScale(-1, 1);      break;  // bottom left
            case 5:         bitmapMatrix.postRotate(90);            bitmapMatrix.postScale(-1, 1);      break;  // left top
            case 6:         bitmapMatrix.postRotate(90);                                                break;  // right top
            case 7:         bitmapMatrix.postRotate(270);           bitmapMatrix.postScale(-1, 1);      break;  // right bottom
            case 8:         bitmapMatrix.postRotate(270);                                               break;  // left bottom
            default:                                                                                    break;  // Unknown
        }
       
        return bitmapMatrix;
    }




That code will probably look like shit...I don't care.

Next, we have to deal with the fact that bitmaps can be transformed in place.  You have to create a new bitmap in the process, which invariably causes an Out of Memory Error.

To mitigate this, first put this in the application tag in your manifest:

android:largeHeap="true"


Secondly, ONLY do the transform if the EXIF orientation != 1.  One means no orientation change, and--of the devices I found in Best Buy--samsung appears to be the only manufacturer I found that uses the EXIF orientation, so by limiting the transformation code to only those devices we greatly reduce the chances of Out of Memory Errors.

Fun fact:  The Samsung Galaxy S 2 cannot do reverse portrait.

Also, it is important to be constantly setting the camera orientation when the orientation listener fires.  You can't rely on having your activity recreated, or even on onConfigurationChanged() being fired when doing orientation changes manually;  the value will be wrong at those points in time, especially when you physically rotate the devices more than 135 degrees in one shot.  Also, the Acer Iconia A series seems to have the completely wrong orientation when the activity starts, so you need the listener to fire to fix that.

No comments:

Post a Comment