Trouble with JPEG Orientation in Camera2 API — Upside Down on Landscape Right

62 views
Skip to first unread message

Manali Patel

unread,
Nov 4, 2025, 7:12:30 AMNov 4
to Android CameraX Discussion Group

Hi everyone,

I’m working with the Camera2 API to capture still images and handle device rotation properly for JPEG orientation. I’m using an OrientationEventListener to track device rotation (in degrees: 0, 90, 180, 270) and mapping these to Surface.ROTATION_* constants for orientation correction.

My current approach for JPEG orientation is:
val rotationConstant =currentDeviceOrientation
val jpegOrientation = (sensorOrientation + ORIENTATIONS.get(rotationConstant) + 270) % 360
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, jpegOrientation)


where currentDeviceOrientation is 

orientationEventListener = object : OrientationEventListener(requireContext()) {
    override fun onOrientationChanged(orientation: Int) {
        if (orientation != ORIENTATION_UNKNOWN) {
            currentDeviceOrientation = (orientation + 45) / 90 * 90/*when (orientation) {
                in 45..134 -> 90   // Landscape left
                in 135..224 -> 180  // Upside down
                in 225..314 -> 270   // Landscape right
                else -> 0           // Portrait
            }*/
        }
        Log.e(TAG, "onOrientationChanged:$orientation---- $currentDeviceOrientation")
    }
}
orientationEventListener?.enable()

and my ORIENTATIONS mapping is:

ORIENTATIONS.append(Surface.ROTATION_0, 90)
ORIENTATIONS.append(Surface.ROTATION_90, 0)
ORIENTATIONS.append(Surface.ROTATION_180, 270)
ORIENTATIONS.append(Surface.ROTATION_270, 180)


Problem:

  • Works fine in portrait and landscape left.

  • When the device is rotated to landscape right (clockwise 90°), the captured image is upside down.

  • Tried variants adding/subtracting 90°, 270°, or 360°, but can’t get consistent results across all orientations.

  • Sensor orientation is consistently 90°.

Wenhung Teng

unread,
Nov 6, 2025, 12:45:19 AMNov 6
to Android CameraX Discussion Group, Manali Patel

Hello,

You have hit a very common point of confusion in the Camera2 API: mixing up Device Rotation (in degrees: 0, 90, 180, 270) with Display Rotation surface constants (0, 1, 2, 3).

Your OrientationEventListener correctly calculates rotation in degrees (e.g., 270 for landscape right). However, you are using this degree value as a key to your ORIENTATIONS (SparseIntArray?). Your map uses Surface.ROTATION_* constants as keys, which are just integers 0, 1, 2, 3. 

When your device is at 270°, you call ORIENTATIONS.get(270). Since 270 is not a key (only 0, 1, 2, 3 are), it returns standard default 0, messing up your calculation.

Since you are already using OrientationEventListener to get the physical device rotation in degrees, you don't need the ORIENTATIONS map or Surface constants for JPEG orientation. You can use the standard formula directly.

Here is the standard robust way to calculate JPEG_ORIENTATION that handles both front and back cameras:

```
private fun getJpegOrientation(deviceOrientationDegrees: Int): Int {
    var rotation = deviceOrientationDegrees
    if (rotation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) {
        // Fallback to display orientation if unknown physical orientation
        val displayRotation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             context.display?.rotation ?: Surface.ROTATION_0
        } else {
             @Suppress("DEPRECATION")
             (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation
        }
        rotation = when (displayRotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> 0
        }
    }

    // Round to nearest 90 degrees
    rotation = (rotation + 45) / 90 * 90

    val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
    val facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)

    // Standard formula for JPEG orientation
    return if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
        (sensorOrientation + rotation) % 360
    } else {
        // Back-facing camera
        (sensorOrientation - rotation + 360) % 360
    }
}
```

Scenario: Landscape Right (Device rotated 270° clockwise), Back Camera (Sensor = 90°).

Your Old Code: ORIENTATIONS.get(270) returned 0. Result: (90 + 0 + 270) % 360 = 0. (Image is upside down compared to needed 180).
New Code: (sensorOrientation - rotation + 360) % 360 -> (90 - 270 + 360) % 360 = 180. (Correct orientation).

For official reference, you can check the documentation for CaptureRequest.JPEG_ORIENTATION, which provides similar formulas.

Manali Patel

unread,
Nov 7, 2025, 12:14:23 AMNov 7
to Android CameraX Discussion Group, Wenhung Teng, Manali Patel

I did as you said, but while the 270° (left, anti-clockwise) rotation works correctly, the issue occurs with the clockwise rotation — it rotates only 90 °. As your calculation for 90, the final orientation  becomes 0.Please correct me if I'm wrong.

@RequiresApi(Build.VERSION_CODES.P)
private fun captureStillPicture() {
try {
val activity: Activity? = activity
if (null == activity || null == mCameraDevice) {
return
}

mCameraDevice?.let { _ ->
for (i in 0..3) {
val captureBuilder =
mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
mImageReader?.surface?.let { surface -> captureBuilder.addTarget(surface) }

// Enhanced image quality settings for capture
captureBuilder.apply {
// Set JPEG quality to maximum
set(CaptureRequest.JPEG_QUALITY, 100.toByte())

// Enable lens distortion correction if available
if (characteristics?.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES)
?.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) == true
) {
set(
CaptureRequest.DISTORTION_CORRECTION_MODE,
CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
)
}

// Set noise reduction to high quality
set(
CaptureRequest.NOISE_REDUCTION_MODE,
CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY
)

// Enhanced edge processing
set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_HIGH_QUALITY)

// Set color correction to high quality
set(
CaptureRequest.COLOR_CORRECTION_MODE,
CaptureRequest.COLOR_CORRECTION_MODE_HIGH_QUALITY
)

// Set tonemap mode to high quality
set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)

// Set hot pixel correction
set(
CaptureRequest.HOT_PIXEL_MODE,
CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY
)

// Optimize lens shading
set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY)

// Set exposure bracketing
set(CaptureRequest.CONTROL_AE_LOCK, false)
set(
CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION,
CameraConstants.EXPOSURE_BRACKET[i]
)
}

// Important: Set zoom ratio BEFORE setting crop region
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (isRearUltraCamera && ultraWideAngleValue != null) {
captureBuilder.set(
CaptureRequest.CONTROL_ZOOM_RATIO, ultraWideAngleValue!!
)
} else if (pinchZoomRect != null) {
captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, pinchZoomRect)
}
}

// Set exposure bracketing
/*captureBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false)
captureBuilder.set(
CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION,
CameraConstants.EXPOSURE_BRACKET[i]
)*/

// Important: Set zoom ratio BEFORE setting crop region
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (isRearUltraCamera && ultraWideAngleValue != null) {
// For ultra-wide camera, use CONTROL_ZOOM_RATIO
captureBuilder.set(
CaptureRequest.CONTROL_ZOOM_RATIO, ultraWideAngleValue!!
)
} else if (pinchZoomRect != null) {
// For regular camera, use SCALER_CROP_REGION
captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, pinchZoomRect)
}
}

// Use device orientation from OrientationEventListener
val sensorOrientation =
characteristics!!.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0

var rotation = currentDeviceOrientation
if (rotation == OrientationEventListener.ORIENTATION_UNKNOWN) {

// Fallback to display orientation if unknown physical orientation
val displayRotation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
requireContext().display.rotation
} else {
@Suppress("DEPRECATION") (requireContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation
}
rotation = when (displayRotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> 0
}
}


val jpegOrientation: Int = (sensorOrientation - rotation + 360) % 360
Log.e(TAG, "captureStillPicture: $currentDeviceOrientation--$jpegOrientation")

captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, jpegOrientation)

// Ensure we wait for each capture to complete
val captureCallback = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
// Log capture result
JLog.e("HK", "Capture completed for exposure: $i")
}
}

// Capture with callback
mCaptureSession?.capture(captureBuilder.build(), captureCallback, null)
}

// Only unlock focus after all exposures are captured
unlockFocus()
}
} catch (e: CameraAccessException) {
JLog.e("HK", "Error during still picture capture: ${e.message}")
e.printStackTrace()
}
}
orientationEventListener = object : OrientationEventListener(requireContext()) {
override fun onOrientationChanged(orientation: Int) {
if (orientation != ORIENTATION_UNKNOWN) {
val rotation = when (orientation) {

in 45..134 -> 90 // Landscape left
in 135..224 -> 180 // Upside down
in 225..314 -> 270 // Landscape right
else -> 0 // Portrait
}
currentDeviceOrientation = (orientation + 45) / 90 * 90
}
Log.e(TAG, "onOrientationChanged:$orientation---- $currentDeviceOrientation")
}
}

Wenhung Teng

unread,
Nov 11, 2025, 2:51:54 AMNov 11
to Android CameraX Discussion Group, manalip...@gmail.com, Wenhung Teng

Hello,

The calculation (90 - 90 + 360) % 360 = 0 is mathematically correct for the formula you are using. If JPEG_ORIENTATION is 0, it tells the JPEG encoder "Do not apply any rotation."

If the resulting image is incorrect (e.g., it looks sideways or upside down) when the result is 0, it usually means one of two things:

  1. You are using the Front Camera: The formula you are using is only for the Back Camera. The front camera requires a different calculation (sensor + rotation).

  2. Confusion about Landscape Left vs. Right: Your comments in the OrientationEventListener are swapped.

Here is the breakdown of the issue and the corrected solution.

1. The Landscape Left/Right Confusion

In your code comments, you have:

```
in 45..134 -> 90 // Landscape left <-- INCORRECT LABEL
in 225..314 -> 270 // Landscape right <-- INCORRECT LABEL
```
In Android standard coordinates:
  • 90° is Landscape Right (The top of the phone points to the Right).

  • 270° is Landscape Left (The top of the phone points to the Left).

You mentioned: "270° (left) works correctly". This is likely because (90 - 270 + 360) % 360 = 180. The image is flipped 180 degrees. You mentioned: "90° fails... becomes 0". (90 - 90) = 0. If you are holding the phone Landscape Right, the sensor is physically aligned with the horizon. 0 should be the correct rotation for a Back Camera.

2. If you are using the Front Camera (Selfie) but using the Back Camera formula (sensor - rotation), the math will fail for specific angles.
    Back Camera Formula: (sensor - deviceRotation + 360) % 360
    Front Camera Formula: (sensor + deviceRotation) % 360

If JPEG_ORIENTATION is 0, the image file is saved exactly as the sensor read it.

  • If you view the photo on a PC or Gallery that respects EXIF: It should look correct.

  • If you view the photo in a generic image viewer without EXIF support: It might look "rotated" because the raw pixel data of a phone sensor is usually 4:3 (Portrait) even when held in Landscape. The JPEG_ORIENTATION tag is what tells the viewer to rotate it to be Wide.

Please check if you are using the Front Camera. If so, the result 0 for the Back Camera formula is effectively "Upside Down" or "Mirrored" behavior depending on the angle. Using the combined function above will solve it.


Reply all
Reply to author
Forward
0 new messages