Expo中的相机预览出现扭曲

29

我正在使用来自expo包的相机,并且在处理相机预览失真方面遇到了问题。预览会使图像在横向视图中变宽,在纵向视图中变窄。我发现的大多数解决方案都没有使用expo-camera。

相关代码:

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",
    },
});

我该怎么做才能修复这个问题?


这里的 this.cam 的值在哪里获取? - hong developer
2个回答

72
这个有点繁琐。
问题:
基本上问题是相机预览的宽高比与您的屏幕不同。据我所知,这只是在Android上的问题,其中:
每个相机制造商支持不同的宽高比 每个手机制造商创建不同的屏幕宽高比
理论:
解决这个问题的方法基本上是:
弄清屏幕的宽高比(和方向)
const { height, width } = Dimensions.get('window');
const screenRatio = height / width;

等相机准备好
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>
);

弄清相机支持的画面比例
const ratios = await camera.getSupportedRatiosAsync();

这将返回一个字符串数组,格式为['w:h'],所以你可能会看到类似这样的内容:
[ '4:3', '1:1', '16:9' ]

找到相机与屏幕最接近的纵横比,其中高度不超过屏幕纵横比(假设您想要一个水平缓冲区,而不是垂直缓冲区)。
基本上,您在这里要做的是循环遍历支持的相机纵横比,并确定哪些纵横比与屏幕最接近。任何太高的纵横比都会被舍弃,因为在这个例子中,我们希望预览占据整个屏幕的宽度,而在纵向模式下,预览比屏幕短并不重要。
a)获取屏幕纵横比
假设屏幕尺寸为480宽x800高,则高度/宽度的纵横比为1.666... 如果我们处于横向模式,则计算宽度/高度。
b)获取支持的相机纵横比
然后我们查看每个相机的纵横比,并计算宽度/高度。我们计算宽度/高度而不是高度/宽度的原因是相机的纵横比始终处于横向模式。
所以:
  • Aspect => calculation
  • 4:3 => 1.3333
  • 1:1 => 1
  • 16:9 => 1.77777

c) 计算支持的相机宽高比

对于每个宽高比,我们从屏幕的宽高比中减去,以找到差异。任何超过屏幕宽高比的长边的宽高比都会被丢弃:

  • Aspect => calculation => difference from screen
  • 4:3 => 1.333... => 0.333...最接近但不超过!
  • 1:1 => 1 => 0.666...(最差匹配)
  • 16:9 => 1.777... => -0.111...(太宽了)

d) 最接近且最短的相机宽高比与屏幕宽高比匹配

因此,我们选择在此屏幕上使用4:3的宽高比。

计算相机的宽高比与屏幕的宽高比之间的差异,以确定填充和定位的方式。
为了将预览图像居中显示在屏幕上,我们可以计算屏幕高度与相机预览图像缩放后的高度之间的差值的一半。
verticalPadding = (screenHeight - bestRatio * screenWidth) / 2

一起来:

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] = distance;
  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);

<Camera>组件的样式设置为适当的缩放高度,以匹配应用的相机纵横比,并在屏幕中居中显示或其他位置。
<Camera
  style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
  onCameraReady={setCameraReady}
  ratio={ratio}
  ref={(ref) => {
    setCamera(ref);
  }}
/>

需要注意的是,相机的宽高比始终是横向模式下的宽度:高度,但您的屏幕可能是纵向或横向。
执行
此示例仅支持纵向模式的屏幕。要支持两种屏幕类型,您需要检查屏幕方向并根据设备的方向更改计算。
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] = distance;
        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,
  }
});

你可以在这里玩Expo Snack

结果

最后,一个保持比例的相机预览,使用顶部和底部的填充来居中预览:

Android screenshot

你还可以在Expo Snack上在线尝试这段代码,或者在你的Android设备上试试看。

1
这太完美了! - solarnz
1
这是一个很好的答案,功能良好,但似乎 Snack 和真实应用程序之间有一些差异。我发现在我的 Expo 项目中使用代码时,摄像头视图略微变形(圆形变成了椭圆形)。如果您的 app.json 中的 translucent 键设置为 true(从 SDK38 开始默认为真),则可能需要将 StatusBar.currentHeight 添加到相机预览样式的 marginTop 中。 - Conor Watson
1
我认为你应该将 setImagePadding(remainder / 2); 替换为 setImagePadding(remainder); - Arnaud
1
建议编辑队列已满。在循环中进行距离检查时存在问题,它将距离与比率进行比较。这几乎总是会返回支持的比率中的最后一个比率,这会给人留下代码正常工作的假象,而实际上并非如此。解决方法是用distances[ratio] = distance;替换distances[ratio] = realRatio; - Robin Goupil

3
一个在竖屏模式下简单的解决方案:

import * as React from "react";
import { Camera } from "expo-camera";
import { useWindowDimensions } from "react-native";

const CameraComponent = () => {
  const {width} = useWindowDimensions();
  const height = Math.round((width * 16) / 9);
  return (
    <Camera
      ratio="16:9"
      style={{
        height: height,
        width: "100%",
      }}
    ></Camera>
  );
};

export default CameraComponent;

此解决方案用于计算屏幕比例,不等同于相机预览纹理比例。 - Nick Chan Abdullah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接