Expo 中的相机预览失真 [英] Camera Preview in Expo is Distorted

查看:21
本文介绍了Expo 中的相机预览失真的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用来自 expo 包的相机,但我遇到了相机预览失真的问题.预览使图像在横向视图中显得更宽,在纵向视图中显得更薄.我发现的大多数解决方案都没有使用 expo-camera.

相关代码:

camera.page.js:

从'react'导入反应;从'react-native'导入{视图,文本};从'expo-camera'导入{相机};从'expo-permissions'导入*作为权限从'react-native'导入{平台};从 './styles' 导入样式;从'./toolbar.component'导入工具栏;常量 DESIRED_RATIO = "18:9";导出默认类 CameraPage 扩展 React.Component {相机=空;状态 = {hasCameraPermission:空,};异步组件DidMount() {常量相机 = 等待 Permissions.askAsync(Permissions.CAMERA);常量音频 = 等待 Permissions.askAsync(Permissions.AUDIO_RECORDING);const hasCameraPermission = (camera.status === 'granted' && audio.status === 'granted');this.setState({ hasCameraPermission });};使成为() {常量 { hasCameraPermission } = this.state;if (hasCameraPermission === null) {返回<查看/>;} else if (hasCameraPermission === false) {return <Text>访问相机已被拒绝.</Text>;}返回 (<React.Fragment><查看><相机参考={相机=>this.camera = 相机}风格={styles.preview}/></查看><工具栏/></React.Fragment>);};};

styles.js:

import { StyleSheet, Dimensions } from 'react-native';const { 宽度:winWidth,高度:winHeight } = Dimensions.get('window');导出默认 StyleSheet.create({预览: {身高:winHeight,宽度:winWidth,位置:'绝对',左:0,顶部:0,对:0,底部:0,paddingBottom: 1000,},居中对齐: {弹性:1,alignItems:'中心',justifyContent: '中心',},底部工具栏:{宽度:winWidth,位置:'绝对',身高:100,底部:0,},捕获Btn:{宽度:60,身高:60,边框宽度:2,边界半径:60,边框颜色:#FFFFFF",},捕获BtnActive:{宽度:80,身高:80,},捕获Btn内部:{宽度:76,身高:76,边框宽度:2,边界半径:76,背景颜色:红色",边框颜色:透明",},});

我能做些什么来解决这个问题?

解决方案

这个有点乏味.

问题

基本上,问题在于相机预览与屏幕的宽/高比不同.据我所知,这只是 Android 上的一个问题:

  1. 每个相机制造商都支持不同的纵横比
  2. 每个手机制造商都会创建不同的屏幕纵横比

理论

解决这个问题的方法基本上是:

  1. 确定屏幕的纵横比(和方向)

const { height, width } = Dimensions.get('window');常量 screenRatio = 高度/宽度;

  1. 等待相机准备好

const [isRatioSet, setIsRatioSet] = useState(false);//必须加载相机才能//访问支持的比率常量 setCameraReady = async() =>{如果(!isRatioSet){等待准备比率();}};返回 (<相机onCameraReady={setCameraReady}参考={(参考)=>{设置相机(参考);}}></相机>);

  1. 找出相机支持的纵横比

const ratios = await camera.getSupportedRatiosAsync();

这将返回格式为 ['w:h'] 的字符串数组,因此您可能会看到如下内容:

[ '4:3', '1:1', '16:9' ]

  1. 在高度不超过屏幕比例的情况下找到最接近屏幕的相机纵横比(假设您需要水平缓冲区,而不是垂直缓冲区)

基本上,您在这里尝试做的是循环浏览受支持的相机比例,并确定它们中的哪一个与屏幕的比例最接近.任何太高的我们都会扔掉,因为在此示例中,我们希望预览占据整个屏幕宽度,并且我们不介意预览是否比纵向模式下的屏幕短.

a) 获取屏幕纵横比

假设屏幕是480w x 800h,那么高/宽的纵横比是1.666...如果我们在横向模式下,我们会做宽/高.

b) 获取支持的相机纵横比

然后我们查看每个相机的纵横比并计算宽度/高度.我们计算这个而不是像我们计算屏幕那样计算高度/宽度的原因是相机纵横比在横向模式下始终.

所以:

  • 方面=>计算
  • 4:3 =>1.3333
  • 1:1 =>1
  • 16:9 =>1.77777

c) 计算支持的相机纵横比

对于每一个,我们从屏幕的纵横比中减去以找出差异.任何超过长边屏幕纵横比的都将被丢弃:

  • 方面=>计算=>与屏幕的区别
  • 4:3 =>1.333... =>0.333...(最接近但不超过!)
  • 1:1 =>1 =>0.666...(最差匹配)
  • 16:9 =>1.777... =>-0.111...(太宽)

d) 与屏幕纵横比匹配的最接近的最短摄像头纵横比

所以我们在这个屏幕上为这个相机选择 4:3 纵横比.

e) 计算相机纵横比和屏幕纵横比之间的差异,用于填充和定位.

要将预览定位在屏幕中央,我们可以计算屏幕高度与相机预览缩放高度之间的差值的一半.

verticalPadding = (screenHeight - bestRatio * screenWidth)/2

一起来:

让距离 = {};让 realRatios = {};让 minDistance = null;for (const ratio of ratios) {常量部分 = ratio.split(':');const realRatio = parseInt(parts[0])/parseInt(parts[1]);真实比率[比率] = 真实比率;//ratio 不能高于 screen,所以我们不需要 abs()常量距离 = screenRatio - realRatio;距离[比率] = realRatio;if (minDistance == null) {minDistance = 比率;} 别的 {if (距离 >= 0 && 距离 < 距离[minDistance]) {minDistance = 比率;}}}//设置最佳匹配期望比率 = minDistance;//计算相机宽度和屏幕高度之间的差异常量余数 = Math.floor((高度 - realRatios[desiredRatio] * 宽度)/2);//设置预览内边距和预览比例setImagePadding(余数/2);

  1. 设置 <Camera> 组件的样式,使其具有适当的缩放高度,以匹配应用的相机纵横比并居中或在屏幕中的任何位置.

<相机style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}onCameraReady={setCameraReady}比率={比率}参考={(参考)=>{设置相机(参考);}}/>

需要注意的是,在横向模式下,相机的纵横比始终是宽度:高度,但您的屏幕可能是纵向或横向的.

执行

此示例仅支持纵向模式屏幕.要支持这两种屏幕类型,您必须检查

您还可以在线试用此代码或在您的 Android 上的 Expo Snack.

I'm using Camera that comes from expo package and I'm having trouble with camera preview distortion. The preview makes images appear wider in landscape view and thinner in portrait view. Most of the solutions I have found are not using expo-camera.

Relevant Code:

camera.page.js:

import React from 'react';
import { View, Text } from 'react-native';
import { Camera } from 'expo-camera';
import * as Permissions from 'expo-permissions'
import { Platform } from 'react-native';

import styles from './styles';
import Toolbar from './toolbar.component';

const DESIRED_RATIO = "18:9";

export default class CameraPage extends React.Component {
    camera = null;

    state = {
        hasCameraPermission: null,
    };

    async componentDidMount() {
        const camera = await Permissions.askAsync(Permissions.CAMERA);
        const audio = await Permissions.askAsync(Permissions.AUDIO_RECORDING);
        const hasCameraPermission = (camera.status === 'granted' && audio.status === 'granted');

        this.setState({ hasCameraPermission });
    };


    render() {
        const { hasCameraPermission } = this.state;

        if (hasCameraPermission === null) {
            return <View />;
        } else if (hasCameraPermission === false) {
            return <Text>Access to camera has been denied.</Text>;
        }

        return (
          <React.Fragment>
            <View>
              <Camera
                ref={camera => this.camera = camera}
                style={styles.preview}
                />
            </View>
            <Toolbar/>
          </React.Fragment>

        );
    };
};

styles.js:

import { StyleSheet, Dimensions } from 'react-native';

const { width: winWidth, height: winHeight } = Dimensions.get('window');
export default StyleSheet.create({
    preview: {
        height: winHeight,
        width: winWidth,
        position: 'absolute',
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
        paddingBottom: 1000,
    },
    alignCenter: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
    },
    bottomToolbar: {
        width: winWidth,
        position: 'absolute',
        height: 100,
        bottom: 0,
    },
    captureBtn: {
        width: 60,
        height: 60,
        borderWidth: 2,
        borderRadius: 60,
        borderColor: "#FFFFFF",
    },
    captureBtnActive: {
        width: 80,
        height: 80,
    },
    captureBtnInternal: {
        width: 76,
        height: 76,
        borderWidth: 2,
        borderRadius: 76,
        backgroundColor: "red",
        borderColor: "transparent",
    },
});

What can I do to fix this?

解决方案

This one is kind of tedious.

Problem

Basically the problem is that the camera preview is a different width/height ratio from your screen. As far as I can tell, this is only a problem on Android where:

  1. Each camera manufacturer supports different aspect ratios
  2. Each phone manufacturer creates different screen aspect ratios

Theory

The way to solve this is essentially to:

  1. Figure out the aspect ratio (and orientation) of the screen

const { height, width } = Dimensions.get('window');
const screenRatio = height / width;

  1. Wait for camera to be ready

const [isRatioSet, setIsRatioSet] = useState(false);

// the camera must be loaded in order to 
// access the supported ratios
const setCameraReady = async() => {
  if (!isRatioSet) {
    await prepareRatio();
  }
};

return (
  <Camera
    onCameraReady={setCameraReady}
    ref={(ref) => {
      setCamera(ref);
    }}>
  </Camera>
);

  1. Figure out the supported aspect ratios of the camera

const ratios = await camera.getSupportedRatiosAsync();

This will return an array of strings with the format ['w:h'], so you might see something like this:

[ '4:3', '1:1', '16:9' ]

  1. Find the camera's closest aspect ratio to the screen where the height does not exceed the screen ratio (assuming you want a horizontal buffer, not a vertical buffer)

Essentially what you are trying to do here is to loop through the supported camera ratios and determine which of them are the closest in proportion to the screen. Any that are too tall we toss out since in this example we want to the preview to take up the entire width of the screen and we don't mind if the preview is shorter than the screen in portrait mode.

a) Get screen aspect ratio

So let's say that the screen is 480w x 800h, then the aspect ratio of the height / width is 1.666... If we were in landscape mode, we would do width / height.

b) Get supported camera aspect ratios

Then we look at each camera aspect ratio and calculate the width / height. The reason we calculate this and not the height / width like we do the screen is that the camera aspect ratios are always in landscape mode.

So:

  • Aspect => calculation
  • 4:3 => 1.3333
  • 1:1 => 1
  • 16:9 => 1.77777

c) Calculate supported camera aspect ratios

For each one, we subtract from the aspect ratio of the screen to find the difference. Any that exceed the aspect ratio of the screen on the long side are discarded:

  • Aspect => calculation => difference from screen
  • 4:3 => 1.333... => 0.333... (closest without going over!)
  • 1:1 => 1 => 0.666... (worst match)
  • 16:9 => 1.777... => -0.111... (too wide)

d) closest shortest camera aspect ratio matching screen aspect ratio

So we pick the 4:3 aspect ratio for this camera on this screen.

e) Calculate difference between camera aspect ratio and screen aspect ratio for padding and positioning.

To position the preview in the center of the screen, we can calculate half the difference between the screen height and the scaled height of the camera preview.

verticalPadding = (screenHeight - bestRatio * screenWidth) / 2

All together:

let distances = {};
let realRatios = {};
let minDistance = null;
for (const ratio of ratios) {
  const parts = ratio.split(':');
  const realRatio = parseInt(parts[0]) / parseInt(parts[1]);
  realRatios[ratio] = realRatio;
  // ratio can't be taller than screen, so we don't want an abs()
  const distance = screenRatio - realRatio; 
  distances[ratio] = realRatio;
  if (minDistance == null) {
    minDistance = ratio;
  } else {
    if (distance >= 0 && distance < distances[minDistance]) {
      minDistance = ratio;
    }
  }
}
// set the best match
desiredRatio = minDistance;
//  calculate the difference between the camera width and the screen height
const remainder = Math.floor(
  (height - realRatios[desiredRatio] * width) / 2
);
// set the preview padding and preview ratio
setImagePadding(remainder / 2);

  1. Style the <Camera> component to have the appropriate scaled height to match the applied camera aspect ratio and to be centered or whatever in the screen.

<Camera
  style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
  onCameraReady={setCameraReady}
  ratio={ratio}
  ref={(ref) => {
    setCamera(ref);
  }}
/>

Something to note is that the camera aspect ratios are always width:height in landscape mode, but your screen might be in either portrait or landscape.

Execution

This example only supports a portrait-mode screen. To support both screen types, you'll have to check the screen orientation and change the calculations based on which orientation the device is in.

import React, { useEffect, useState } from 'react';
import {StyleSheet, View, Text, Dimensions, Platform } from 'react-native';
import { Camera } from 'expo-camera';

export default function App() {
  //  camera permissions
  const [hasCameraPermission, setHasCameraPermission] = useState(null);
  const [camera, setCamera] = useState(null);

  // Screen Ratio and image padding
  const [imagePadding, setImagePadding] = useState(0);
  const [ratio, setRatio] = useState('4:3');  // default is 4:3
  const { height, width } = Dimensions.get('window');
  const screenRatio = height / width;
  const [isRatioSet, setIsRatioSet] =  useState(false);

  // on screen  load, ask for permission to use the camera
  useEffect(() => {
    async function getCameraStatus() {
      const { status } = await Camera.requestPermissionsAsync();
      setHasCameraPermission(status == 'granted');
    }
    getCameraStatus();
  }, []);

  // set the camera ratio and padding.
  // this code assumes a portrait mode screen
  const prepareRatio = async () => {
    let desiredRatio = '4:3';  // Start with the system default
    // This issue only affects Android
    if (Platform.OS === 'android') {
      const ratios = await camera.getSupportedRatiosAsync();

      // Calculate the width/height of each of the supported camera ratios
      // These width/height are measured in landscape mode
      // find the ratio that is closest to the screen ratio without going over
      let distances = {};
      let realRatios = {};
      let minDistance = null;
      for (const ratio of ratios) {
        const parts = ratio.split(':');
        const realRatio = parseInt(parts[0]) / parseInt(parts[1]);
        realRatios[ratio] = realRatio;
        // ratio can't be taller than screen, so we don't want an abs()
        const distance = screenRatio - realRatio; 
        distances[ratio] = realRatio;
        if (minDistance == null) {
          minDistance = ratio;
        } else {
          if (distance >= 0 && distance < distances[minDistance]) {
            minDistance = ratio;
          }
        }
      }
      // set the best match
      desiredRatio = minDistance;
      //  calculate the difference between the camera width and the screen height
      const remainder = Math.floor(
        (height - realRatios[desiredRatio] * width) / 2
      );
      // set the preview padding and preview ratio
      setImagePadding(remainder);
      setRatio(desiredRatio);
      // Set a flag so we don't do this 
      // calculation each time the screen refreshes
      setIsRatioSet(true);
    }
  };

  // the camera must be loaded in order to access the supported ratios
  const setCameraReady = async() => {
    if (!isRatioSet) {
      await prepareRatio();
    }
  };

  if (hasCameraPermission === null) {
    return (
      <View style={styles.information}>
        <Text>Waiting for camera permissions</Text>
      </View>
    );
  } else if (hasCameraPermission === false) {
    return (
      <View style={styles.information}>
        <Text>No access to camera</Text>
      </View>
    );
  } else {
    return (
      <View style={styles.container}>
        {/* 
        We created a Camera height by adding margins to the top and bottom, 
        but we could set the width/height instead 
        since we know the screen dimensions
        */}
        <Camera
          style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
          onCameraReady={setCameraReady}
          ratio={ratio}
          ref={(ref) => {
            setCamera(ref);
          }}>
        </Camera>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  information: { 
    flex: 1,
    justifyContent: 'center',
    alignContent: 'center',
    alignItems: 'center',
  },
  container: {
    flex: 1,
    backgroundColor: '#000',
    justifyContent: 'center'
  },
  cameraPreview: {
    flex: 1,
  }
});

You can play with the Expo Snack here

Results

And finally, a camera preview with preserved proportions, which uses padding on the top and bottom to center the preview:

You can also try this code out online or in your Android on Expo Snack.

这篇关于Expo 中的相机预览失真的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆