问题描述
我正在使用来自expo软件包的Camera,但遇到相机预览失真的问题.预览使图像在横向视图中看起来更宽,在纵向视图中看起来更细.我发现的大多数解决方案都不使用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",
},
});
该如何解决?
这很乏味.
问题
基本上,问题是摄像头预览与屏幕的宽高比不同.据我所知,这只是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)获取屏幕宽高比
因此,假设屏幕为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
在一起:
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);
- 设置
<Camera>
组件的样式,使其具有适当的缩放高度,以匹配所应用的相机纵横比并居中或在屏幕中居中显示.
<Camera
style={[styles.cameraPreview, {marginTop: imagePadding, marginBottom: imagePadding}]}
onCameraReady={setCameraReady}
ratio={ratio}
ref={(ref) => {
setCamera(ref);
}}
/>
需要注意的是,在横向模式下,相机的纵横比始终为width:height,但屏幕可能是纵向或横向.
执行
此示例仅支持纵向模式屏幕.要同时支持两种屏幕类型,您必须检查屏幕方向,然后根据设备所处的方向更改计算.
import React, { useEffect, useState } from 'react';
import {StyleSheet, View, Text, Dimensions, Platform } from 'react-native';
import { Camera } from 'expo-camera';
import * as Permissions from 'expo-permissions';
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 Permissions.askAsync(Permissions.CAMERA);
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 / 2);
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,
}
});
结果
最后,使用比例保留的摄像机预览,在顶部和底部使用填充使预览居中:
您还可以在线尝试此代码,或在Android的Expo Snack上尝试该代码. /p>
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:
- Each camera manufacturer supports different aspect ratios
- Each phone manufacturer creates different screen aspect ratios
Theory
The way to solve this is essentially to:
- Figure out the aspect ratio (and orientation) of the screen
const { height, width } = Dimensions.get('window');
const screenRatio = height / width;
- 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>
);
- 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' ]
- 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);
- 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';
import * as Permissions from 'expo-permissions';
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 Permissions.askAsync(Permissions.CAMERA);
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 / 2);
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,
}
});
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.
这篇关于世博会中的相机预览变形的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!