react-native-vision-camera icon indicating copy to clipboard operation
react-native-vision-camera copied to clipboard

🐛 Camera is not aligned properly when using `useSkiaFrameProcessor`

Open karventhan13 opened this issue 1 year ago • 20 comments

What's happening?

🐛 [^4.0.0-beta.16] - Camera is not aligned properly when using useSkiaFrameProcessor but it's work with Screenshot_20240420_133231

Reproduceable Code

return (
    <SafeAreaView style={styles.container}>
      {!hasPermission && <Text>No Camera Permission.</Text>}
      {hasPermission && device != null && (
        <Camera
          style={StyleSheet.absoluteFill}
          device={device}
          isActive={true}
          photo={true}
          video={true}
          format={format}
          onError={(err)=> { console.log("err **",err)}}
          fps={fps}
          enableFpsGraph={true}
          frameProcessor={frameProcessor}  
          videoHdr={format.supportsVideoHdr}
          photoHdr={format.supportsPhotoHdr}
          enableZoomGesture={false}
        />
      )}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

Relevant log output

none

Camera Device

none

Device

vivo y21

VisionCamera Version

[^4.0.0-beta.16]

Can you reproduce this issue in the VisionCamera Example app?

I didn't try (⚠️ your issue might get ignored & closed if you don't try this)

Additional information

karventhan13 avatar Apr 20 '24 09:04 karventhan13

+1

shaozhenged avatar Apr 21 '24 13:04 shaozhenged

I will never understand why people ignore the fact that I need log output. I will write a bot in the future to auto-close issues that don't have valid log output.

mrousavy avatar Apr 21 '24 21:04 mrousavy

Maybe play around with useSkiaFrameProcessor hook, specifically the render() function in there. If you can figure out what's wrong, please submit a PR. I'm on vacation this week

mrousavy avatar Apr 21 '24 22:04 mrousavy

This happened when you using resizeMode={'contain'}

Try to delete this from Camera props

Space6188 avatar Apr 22 '24 13:04 Space6188

@Space6188 does it properly display without the resizeMode?

mrousavy avatar Apr 22 '24 13:04 mrousavy

I can also reproduce this when using resizeMode="cover"...

mrousavy avatar Apr 22 '24 13:04 mrousavy

Try playing around with the code from useSkiaFrameProcessor: https://github.com/mrousavy/react-native-vision-camera/blob/3ff02cc6b4fc34154ec6fb5e650e074e34408e47/package/src/skia/useSkiaFrameProcessor.ts#L165-L175

Maybe we're doing something wrong.

Also, the ImageProxy (which is what the native Frame type holds) contains a getSensorToBufferTransformMatrix() method - maybe we can use this Matrix to transform the Frame safely to view dimensions? cc @rodgomesc if you wanna look into this.

mrousavy avatar Apr 22 '24 14:04 mrousavy

I will never understand why people ignore the fact that I need log output. I will write a bot in the future to auto-close issues that don't have valid log output.

then you will have zero issues in vc 😅

rodgomesc avatar Apr 22 '24 14:04 rodgomesc

i tried all resizeMode possibilities, also all fit possibilities, the problem is not there

rodgomesc avatar Apr 22 '24 14:04 rodgomesc

then it's probably the sensor to buffer transform matrix.

mrousavy avatar Apr 22 '24 14:04 mrousavy

@Space6188правильно ли он отображается без resizeMode?

No,my bad,sorry, it is not working too

Space6188 avatar Apr 22 '24 15:04 Space6188

i think that i found a fix for this, should send a pr some time tomorrow

rodgomesc avatar Apr 29 '24 01:04 rodgomesc

In case it helps - I am able to reproduce this by passing in the format prop with a custom videoResolution to the camera. Removing the format prop seems to fix the issue.

Edit: Actually, removing format only fixes the white bar that appears to the left of the preview, and moves it down to the bottom. Which is less noticeable as it looks like that's just where the camera preview ends. But in my case, the preview should take up the entire screen, but doesn't when using the Skia frame processor.

AndyRC6 avatar Apr 29 '24 17:04 AndyRC6

it seems that canvas.drawImage draws the image with a default clip and matrix that are messed by the canvas rotation somehow? that’s confusing since if we use canvas.drawImageRect with the same src/dst props it draws correctly! in my tests it seem to solve the issue

diff --git a/package/src/skia/useSkiaFrameProcessor.ts b/package/src/skia/useSkiaFrameProcessor.ts
index 3feb336..fd69ca1 100644
--- a/package/src/skia/useSkiaFrameProcessor.ts
+++ b/package/src/skia/useSkiaFrameProcessor.ts
@@ -6,8 +6,34 @@ import { VisionCameraProxy, wrapFrameProcessorWithRefCounting } from '../FramePr
 import type { DrawableFrameProcessor } from '../types/CameraProps'
 import type { ISharedValue, IWorkletNativeApi } from 'react-native-worklets-core'
 import { WorkletsProxy } from '../dependencies/WorkletsProxy'
-import type { SkCanvas, SkPaint, SkImage, SkSurface } from '@shopify/react-native-skia'
+import { type SkCanvas, type SkPaint, type SkImage, type SkSurface, ColorType, AlphaType, Skia } from '@shopify/react-native-skia'
 import { SkiaProxy } from '../dependencies/SkiaProxy'
+import { InteractionManager } from 'react-native'
+
+// Mocked Frame Data for testings
+const mockedImageWidth = 1920
+const mockedImageHeight = 1080
+
+const pixels = new Uint8Array(mockedImageWidth * mockedImageHeight * 4)
+pixels.fill(255)
+let i = 0
+for (let x = 0; x < mockedImageWidth; x++) {
+  for (let y = 0; y < mockedImageHeight; y++) {
+    pixels[i++] = (x * y) % 255
+  }
+}
+
+const data = Skia.Data.fromBytes(pixels)
+const image = Skia.Image.MakeImage(
+  {
+    width: mockedImageWidth,
+    height: mockedImageHeight,
+    alphaType: AlphaType.Opaque,
+    colorType: ColorType.RGBA_8888,
+  },
+  data,
+  mockedImageWidth * 4,
+)!
 
 /**
  * Represents a Camera Frame that can be directly drawn to using Skia.
@@ -155,7 +181,7 @@ export function createSkiaFrameProcessor(
 
     // Convert Frame to SkImage/Texture
     const nativeBuffer = (frame as FrameInternal).getNativeBuffer()
-    const image = Skia.Image.MakeImageFromNativeBuffer(nativeBuffer.pointer)
+    // const image = Skia.Image.MakeImageFromNativeBuffer(nativeBuffer.pointer)
 
     return new Proxy(frame as DrawableFrame, {
       get: (_, property: keyof DrawableFrame) => {
@@ -165,11 +191,36 @@ export function createSkiaFrameProcessor(
             'worklet'
             // rotate canvas to properly account for Frame orientation
             canvas.save()
-            const rotation = getRotationDegrees(frame.orientation)
-            canvas.rotate(rotation, frame.width / 2, frame.height / 2)
-            // render the Camera Frame to the Canvas
-            if (paint != null) canvas.drawImage(image, 0, 0, paint)
-            else canvas.drawImage(image, 0, 0)
+
+            const rotationAngle = 0 //getRotationDegrees('portrait')
+            const { width: frameWidth, height: frameHeight } = getPortraitSize(frame)
+
+            // center of the canvas
+            const centerX = frameWidth / 2
+            const centerY = frameHeight / 2
+
+            const currentPaint = paint ?? Skia.Paint()
+
+            // only draw the rect by 40% of the frame size so we can vizualize the whole thing to debug :)
+            const reductionFactor = 0.4
+            const rectWidth = frameWidth * reductionFactor
+            const rectHeight = frameHeight * reductionFactor
+
+            // apply rotation around the center of the canvas
+            canvas.rotate(rotationAngle, centerX, centerY)
+
+            const rectX = centerX - rectWidth / 2
+            const rectY = centerY - rectHeight / 2
+
+            const srcRect = Skia.XYWHRect(0, 0, image.width(), image.height())
+            // Define the destination rectangle on the canvas
+            const dstRect = Skia.XYWHRect(rectX, rectY, rectWidth, rectHeight)
+            // Draw the image on the canvas
+
+            canvas.drawImageRect(image, srcRect, dstRect, currentPaint)
 
             // restore transforms/rotations again
             canvas.restore()
@@ -185,7 +236,7 @@ export function createSkiaFrameProcessor(
           return () => {
             'worklet'
             // dispose the Frame and the SkImage/Texture
-            image.dispose()
+            // image.dispose() // we do not dispose our mocked image :)
             nativeBuffer.delete()
           }
         }

[!NOTE]
I'm doing some tricks to print the preview at 40% of its original size for better debugging However, at the end of the day, this code just simplifies to:

const srcRect = Skia.XYWHRect(0, 0, frameWidth, frameHeight);
const dstRect = Skia.XYWHRect(0, 0, frameWidth, frameHeight);
canvas.drawImageRect(image, srcRect, dstRect, currentPaint);

portrait 0deg

Alt text for Image 1

landscape-left 90deg

landscape-left 90deg

portrait-upside-down 180deg

>portrait-upside-down

landscape-right 270deg

landscape-right

rodgomesc avatar Apr 30 '24 02:04 rodgomesc

Oh wait, this solves the misalignment of the frame inside the canvas; however, it causes another issue: the image buffer is now rotated 90 degrees clockwise inside a (0 deg portrait) preview for me. I think this is expected since we are lacking orientation support for Android, @mrousavy?

rodgomesc avatar Apr 30 '24 03:04 rodgomesc

I am applying the orientation inside the render(..) function, so actually it is expected that the Preview looks correct.

mrousavy avatar Apr 30 '24 07:04 mrousavy

I am applying the orientation inside the render(..) function, so actually it is expected that the Preview looks correct.

i mean, frame.orientation will always returns landscape-right since orientation support not being complete on android, and in render you are just doing getRotationDegrees(frame.orientation) so i can't visualize the full picture here, of how did you got the image buffers always in portrait without a counter-clockwise rotation of -90deg in the current code that's running in render() {...}

https://github.com/mrousavy/react-native-vision-camera/blob/a53166b01dd08678a354ad81fc04bf9f67bee9f3/package/android/src/main/java/com/mrousavy/camera/frameprocessors/Frame.java#L89-L93

rodgomesc avatar Apr 30 '24 10:04 rodgomesc

i mean, frame.orientation will always returns landscape-right since orientation support not being complete on android,

no, frame.orientation is an exception here - this is the sensor relative orientation of the ImageProxy, so this is correct as it is.

mrousavy avatar Apr 30 '24 13:04 mrousavy

Any news on this? I'm also experiencing this misplacement. I observed that it depends on format and video resolution:

  1. Without passing format props to Camera:

  2. Default format got from useCasmeraFormat():

  const format = useCameraFormat(device, []);
  console.log(format);

Log:

{"autoFocusSystem": "contrast-detection", "fieldOfView": 80.98175244802063, "maxFps": 30, "maxISO": 3200, "minFps": 7, "minISO": 100, "photoHeight": 2160, "photoWidth": 4096, "supportsDepthCapture": false, "supportsPhotoHdr": false, "supportsVideoHdr": false, "videoHeight": 2160, "videoStabilizationModes": ["off", "cinematic"], "videoWidth": 3840}

const format = useCameraFormat(device, [
    { videoResolution: { width: 600, height: 400 } },
  ]);
  console.log(format);

Log:

{"autoFocusSystem": "contrast-detection", "fieldOfView": 80.98175244802063, "maxFps": 30, "maxISO": 3200, "minFps": 7, "minISO": 100, "photoHeight": 2160, "photoWidth": 4096, "supportsDepthCapture": false, "supportsPhotoHdr": false, "supportsVideoHdr": false, "videoHeight": 480, "videoStabilizationModes": ["off", "cinematic"], "videoWidth": 640}
const format = useCameraFormat(device, [
    { videoResolution: { width: 1200, height: 800 } },
  ]);
  console.log(format);

Log:

{"autoFocusSystem": "contrast-detection", "fieldOfView": 80.98175244802063, "maxFps": 30, "maxISO": 3200, "minFps": 7, "minISO": 100, "photoHeight": 2160, "photoWidth": 4096, "supportsDepthCapture": false, "supportsPhotoHdr": false, "supportsVideoHdr": false, "videoHeight": 720, "videoStabilizationModes": ["off", "cinematic"], "videoWidth": 1280}
![2024-05-08 10 01 42]()

So in case of lower resolutions (default or set manually) only bottom part is cut off, while in higher resolutions entire frame is misplacement. Maybe that somehow helps you

pweglik avatar May 08 '24 08:05 pweglik

Є новини з цього приводу? Я також відчуваю це неправильне розташування. Я помітив, що це залежить від формату та роздільної здатності відео:

  1. Без передачі параметрів формату в камеру:
2. Формат за замовчуванням отримано з `useCasmeraFormat()`:
  const format = useCameraFormat(device, []);
  console.log(format);

Журнал:

{"autoFocusSystem": "contrast-detection", "fieldOfView": 80.98175244802063, "maxFps": 30, "maxISO": 3200, "minFps": 7, "minISO": 100, "photoHeight": 2160, "photoWidth": 4096, "supportsDepthCapture": false, "supportsPhotoHdr": false, "supportsVideoHdr": false, "videoHeight": 2160, "videoStabilizationModes": ["off", "cinematic"], "videoWidth": 3840}
const format = useCameraFormat(device, [
    { videoResolution: { width: 600, height: 400 } },
  ]);
  console.log(format);

Журнал:

{"autoFocusSystem": "contrast-detection", "fieldOfView": 80.98175244802063, "maxFps": 30, "maxISO": 3200, "minFps": 7, "minISO": 100, "photoHeight": 2160, "photoWidth": 4096, "supportsDepthCapture": false, "supportsPhotoHdr": false, "supportsVideoHdr": false, "videoHeight": 480, "videoStabilizationModes": ["off", "cinematic"], "videoWidth": 640}
const format = useCameraFormat(device, [
    { videoResolution: { width: 1200, height: 800 } },
  ]);
  console.log(format);

Журнал:

{"autoFocusSystem": "contrast-detection", "fieldOfView": 80.98175244802063, "maxFps": 30, "maxISO": 3200, "minFps": 7, "minISO": 100, "photoHeight": 2160, "photoWidth": 4096, "supportsDepthCapture": false, "supportsPhotoHdr": false, "supportsVideoHdr": false, "videoHeight": 720, "videoStabilizationModes": ["off", "cinematic"], "videoWidth": 1280}

2024-05-08 10 01 42 Таким чином, у разі нижчої роздільної здатності (за замовчуванням або встановленої вручну) обрізається лише нижня частина, тоді як у вищій роздільній здатності зміщується весь кадр. Можливо, це якось тобі допоможе

For some reason, the format parameters don't change anything for me, the behavior is the same as yours in the second example. And the same problem with useSkiaFrameProcessor. On the emulator, the screen is completely black, and on real devices, part of it is. Another strange thing is that the frame itself processes frames rotated 90 degrees, I noticed this when my model started drawing rectangle around object on a black screen turned to the horizontal position. In IOS all ok.

OleksiiZdaly avatar May 14 '24 08:05 OleksiiZdaly

Another strange thing is that the frame itself processes frames rotated 90 degrees

That's because of how Cameras work. Sensors are in 90deg orientation. See frame.orientation.

mrousavy avatar May 21 '24 15:05 mrousavy

I also encountered this when using useSkiaFrameProcessor:

Screenshot_20240529-163510.png

but useFrameProcessor works normally.

tomerh2001 avatar May 29 '24 13:05 tomerh2001

I just changed this line

-              canvas.rotate(rotation, frame.width / 2, frame.height / 2)
+              canvas.rotate(rotation, frame.width / 2, frame.height / 1.5)

on useSkiaFrameProcessor.ts Not it's working as expected Screenshot 2024-06-02 at 15 05 34

EnzoDomingues avatar Jun 02 '24 17:06 EnzoDomingues

Created a fix for this here; https://github.com/mrousavy/react-native-vision-camera/pull/2931

Can you guys test if this works for you? :)

mrousavy avatar Jun 03 '24 13:06 mrousavy

Created a fix for this here; #2931

Can you guys test if this works for you? :)

Screenshot_20240603-121255

@mrousavy Hey, for me, it's even worse I'm using Moto G84

EnzoDomingues avatar Jun 03 '24 15:06 EnzoDomingues

@mrousavy I was using this format before

const format = useCameraFormat(device, [
    {
      videoResolution: {
        width: 480,
        height: Platform.OS === 'ios' ? 640 : 720,
      },
    },
  ]);

Using this format, even the colors are changing

Screenshot_20240603-122303

EnzoDomingues avatar Jun 03 '24 15:06 EnzoDomingues

@EnzoDomingues can you leave the format, aspect ration configurations at the default or use them exactly like in this example app and send a new screenshot?

rodgomesc avatar Jun 03 '24 16:06 rodgomesc

@EnzoDomingues can you leave the format, aspect ration configurations at the default or use them exactly like in this example app and send a new screenshot?

The first screenshot is default, without any format

EnzoDomingues avatar Jun 03 '24 16:06 EnzoDomingues

Hm. Matrixes are really hard to get right.

mrousavy avatar Jun 03 '24 18:06 mrousavy

@EnzoDomingues can you leave the format, aspect ration configurations at the default or use them exactly like in this example app and send a new screenshot?

The first screenshot is default, without any format

yup, i can confirm, it's worse for me as well

image

rodgomesc avatar Jun 04 '24 02:06 rodgomesc