vision-camera-resize-plugin icon indicating copy to clipboard operation
vision-camera-resize-plugin copied to clipboard

Inconsistencies in the return values (android)

Open c-goettert opened this issue 1 year ago • 13 comments

To verify / understand the content of the frames, I created a simple demo app that stores the content returned from the resize plugin and just draws it right below the original image (via react-native-skia).

The resize call within my frame processor looks like follows:

const data = resize(frame, {
  scale: {
    width: 320,
    height: 320
  },
  pixelFormat: 'rgb',
  dataType: 'uint8'
})
  
// Note: As I somehow failed to use the arraybuffer-values directly within my component.
// I found that storing the values in a separate array worked for me. So I reshape the frame data like this
// before I use it to draw my skia image:

const arrayData = new Array(width * height * 4).fill(255)
for (let i = 0, j = 0; i < data.length; i += 3,  j += 4) {
  arrayData[j] = data[i] // R
  arrayData[j+1] = data[i + 1] // G
  arrayData[j+2] = data[i + 2] // B
  arrayData[j+3] = 255 // A
}

My camera settings look like this:

  const format = useCameraFormat(device, [
    { videoResolution: { width: 640, height: 480 } },
    { fps: 30 },
    { videoStabilizationMode: 'auto' }
  ])

I can could verify my approach is generally working on iOS, where my test-frame is rendered correctly:

iPhone X: ios

However, on my Android phone (Pixel 7A, Android 14), the frame seems to be rotated by -90°. Also, as you can see in the image below, the format is returned in bgr instead rgb..

Pixel 7A: android1

Furthermore, I don't actually want to use the default centre-crop, but the upper square of the image. I have therefore adapted the resize call as follows:

const data = resize(frame, {
  scale: {
    width,
    height
  },
  crop: {
    y: 0,
    x: 0,
    width: frame.width,
    height: frame.width
  },
  pixelFormat: 'rgb',
  dataType: 'uint8'
})

This setting strangely cuts the result into 3 image-areas: android2

To summarize the issues:

  • Everything runs as expected on iOS
  • On Android (Pixel 7A, Android 14) there are the following problems
    • The image is rotated
    • The image is bgr despite rgb was requested
    • The crop mechanism seems to have a glitch (possibly related to the rotation?)

I use the following configuration:

  • react-native: 0.73.4
  • react-native-vision-camera: 3.9.1
  • react-native-worklets-core: 0.3.0
  • vision-camera-resize-plugin: 2.0.1

If it helps to trace the issue I can gladly add my complete test component.

c-goettert avatar Mar 05 '24 15:03 c-goettert

Hey - impressive work that you can draw it to a skia canvas! Is that running smooth? What FPS?

The VisionCamera Skia integration will use GPU Hardware Buffers, so it's gonna run really smooth, but I was wondering how smooth your approach is.

Regarding your issue; hm, well to be honest I don't have a lot of time to investigate this right now, but maybe it's due to the buffers just always being in device native orientation on Android?

I'd gladly accept PRs!

mrousavy avatar Mar 05 '24 17:03 mrousavy

Hi, thanks for you answer! Regarding performance: My current approach is only suitable for debugging purposes. The app only runs with approx. 2-3 frames and crashes after a few seconds (probably a memory leak somewhere). However, it is sufficient for checking the frame content.

https://github.com/mrousavy/vision-camera-resize-plugin/assets/7591624/10e1e758-aae9-4180-a79e-224369f96143

Regarding the issue:

maybe it's due to the buffers just always being in device native orientation on Android?

That would also be my guess, is there an easy way to check the native orientation of the device?

c-goettert avatar Mar 06 '24 08:03 c-goettert

device.sensorOrientation - but this is not guaranteed to be correct by Android. Some vendors choose to not implement it properly, specifically on Samsungs it's 180 deg rotated.

CameraX has workarounds for all such quirks, in VisionCamera V4 that should be fixed.

mrousavy avatar Mar 06 '24 16:03 mrousavy

We now have rotation support in vision-camera-resize-plugin - thanks @rodgomesc! ❤️

So you can now use rotation: '90deg' to fix the rotation issue.

mrousavy avatar Mar 14 '24 09:03 mrousavy

I'm not sure why it's blueish though. This might not be a problem of the resize plugin, but rather of the way you display it maybe? Because when I inspect the UIImage of the ARGB 8888 array on the native side it looks correct

mrousavy avatar Mar 14 '24 09:03 mrousavy

I'm not sure why it's blueish though. This might not be a problem of the resize plugin, but rather of the way you display it maybe? Because when I inspect the UIImage of the ARGB 8888 array on the native side it looks correct

it's blueish for me as well if i use rgba on ios, since i don't have time to investigate now my workaround is

pixelFormat: IS_ANDROID ? 'rgba' : 'argb'

rodgomesc avatar Mar 14 '24 09:03 rodgomesc

Strange for me it seems the other way around.. My request format for the resize plugin is pixelFormat: 'rgb'. I interpret it as described in my first post:

const data = resize(frame, {
  scale: {
    width: 320,
    height: 320
  },
  pixelFormat: 'rgb',
  dataType: 'uint8'
})

const arrayData = new Array(width * height * 4).fill(255)
for (let i = 0, j = 0; i < data.length; i += 3,  j += 4) {
  arrayData[j] = data[i] // R
  arrayData[j+1] = data[i + 1] // G
  arrayData[j+2] = data[i + 2] // B
  arrayData[j+3] = 255 // A
}

If I use the same code on iOS and Android (same display method), it looks fine on iOS (tested on iPhone X) but blueish on Android (tested on Pixel 7A). I can fix it for android by switching the blue and red channel (= interpret values as BGR):

const arrayData = new Array(width * height * 4).fill(255)
for (let i = 0, j = 0; i < data.length; i += 3,  j += 4) {
  arrayData[j] = data[i + 2] // R
  arrayData[j+1] = data[i + 1] // G
  arrayData[j+2] = data[i] // B
  arrayData[j+3] = 255 // A
}

c-goettert avatar Mar 14 '24 11:03 c-goettert

Maybe it is a bug then in this method: https://github.com/mrousavy/vision-camera-resize-plugin/blob/8a190a320cad637b917ab90d24f964ae3ece540e/ios/ResizePlugin.mm#L183-L250

You could try playing around with those values or hitting breakpoints to see if all values are correct, it seems like some channels are swapped wrong?

mrousavy avatar Mar 14 '24 12:03 mrousavy

If I use the same code on iOS and Android (same display method), it looks fine on iOS (tested on iPhone X) but blueish on Android (tested on Pixel 7A). I can fix it for android by switching the blue and red channel (= interpret values as BGR):

you are right! the rgb buffer is incorrect, right now it's returning bgr we can validate that creating the bitmap right before the return of the SharedValue in ResizePlugin.kt

diff --git a/node_modules/vision-camera-resize-plugin/android/src/main/java/com/visioncameraresizeplugin/ResizePlugin.kt b/node_modules/vision-camera-resize-plugin/android/src/main/java/com/visioncameraresizeplugin/ResizePlugin.kt
index 8c8c0c5..b018bf4 100644
--- a/node_modules/vision-camera-resize-plugin/android/src/main/java/com/visioncameraresizeplugin/ResizePlugin.kt
+++ b/node_modules/vision-camera-resize-plugin/android/src/main/java/com/visioncameraresizeplugin/ResizePlugin.kt
@@ -1,5 +1,6 @@
 package com.visioncameraresizeplugin
 
+import android.graphics.Bitmap
 import android.graphics.ImageFormat
 import android.media.Image
 import android.util.Log
@@ -156,7 +157,25 @@ class ResizePlugin(private val proxy: VisionCameraProxy) : FrameProcessorPlugin(
       targetType.ordinal
     )
 
-    return SharedArray(proxy, resized)
+    val result = SharedArray(proxy, resized)
+
+
+    val bitmap = Bitmap.createBitmap(scaleWidth, scaleHeight, Bitmap.Config.ARGB_8888)
+    val intArray = IntArray(scaleWidth * scaleHeight)
+
+    result.byteBuffer.rewind()
+
+    for (i in 0 until intArray.size) {
+      // note: use bgr instead of rgb  
+      val b = result.byteBuffer.get().toInt() and 0xFF
+      val g = result.byteBuffer.get().toInt() and 0xFF
+      val r = result.byteBuffer.get().toInt() and 0xFF
+      intArray[i] = (0xFF shl 24) or (r shl 16) or (g shl 8) or b
+    }
+
+   
+    bitmap.setPixels(intArray, 0, scaleWidth, 0, 0, scaleWidth, scaleHeight)
+    // breakpoint the line above to visualize the correct preview
+    return result
   }
 
   private enum class PixelFormat {

rodgomesc avatar Mar 28 '24 14:03 rodgomesc

Hi @rodgomesc @mrousavy I think I was able to track the bug down to libyuv library and fix it - check out PR: https://github.com/mrousavy/vision-camera-resize-plugin/pull/53

pweglik avatar Apr 17 '24 15:04 pweglik

device.sensorOrientation - but this is not guaranteed to be correct by Android. Some vendors choose to not implement it properly, specifically on Samsungs it's 180 deg rotated.

CameraX has workarounds for all such quirks, in VisionCamera V4 that should be fixed.

Hi, any ideas when VisionCamera V4 will be published ? :)

tarasovladislav avatar Jun 30 '24 16:06 tarasovladislav

Hi, any ideas when VisionCamera V4 will be published ? :)

VisionCamera V4 was released 3 months ago

mrousavy avatar Jul 01 '24 08:07 mrousavy

Hi, any ideas when VisionCamera V4 will be published ? :)

VisionCamera V4 was released 3 months ago

Hi, sorry, looked at wrong npm package.

The problem with Samsung devices frames 180 deg rotated still persists, right? Any idea if this is something that can be fixed?

tarasovladislav avatar Jul 01 '24 08:07 tarasovladislav