Камера Android искажает изображение
У меня большая проблема с API камеры в Android. Я хочу запустить предварительный просмотр в камере в SurfaceView. Это работает. Но изображение предварительного просмотра искажено. Почему это происходит и каково решение?
У меня есть приложение, как Ретро-камера в App Store. Так что я хочу иметь предварительный просмотр камеры в прямоугольнике или квадрате, например. Но предварительный просмотр не имеет правильного решения.
Мой код:
public class CameraActivity extends Activity implements SurfaceHolder.Callback {
Camera camera;
SurfaceView surfaceView;
SurfaceHolder surfaceHolder;
boolean previewing = false;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ImageView buttonStartCameraPreview = (ImageView)findViewById(R.id.scanning);
ImageView buttonStopCameraPreview = (ImageView)findViewById(R.id.buttonBack);
getWindow().setFormat(PixelFormat.UNKNOWN);
surfaceView = (SurfaceView)findViewById(R.id.surfaceView);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
buttonStartCameraPreview.setOnClickListener(
new Button.OnClickListener() {
@Override
public void onClick(View v) {
if(!previewing) {
camera = Camera.open();
if(camera != null) {
try {
camera.setDisplayOrientation(90);
camera.setPreviewDisplay(surfaceHolder);
camera.startPreview();
previewing = true;
}
catch(IOException e) {
e.printStackTrace();
}
}
}
}
}
);
buttonStopCameraPreview.setOnClickListener(
new Button.OnClickListener() {
@Override
public void onClick(View v) {
if(camera != null && previewing) {
camera.stopPreview();
camera.release();
camera = null;
previewing = false;
}
}
}
);
}
@Override
public void surfaceChanged(
SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
Результат:
У меня есть квадрат, но предварительный просмотр - это прямоугольник.
1 ответ
Это требует небольшой математики ...
Проблема
По сути, проблема в том, что предварительный просмотр камеры отличается от экрана по ширине / высоте. Насколько я могу судить, это проблема только на 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' ]
- Найдите наиболее близкое соотношение сторон камеры к экрану, высота которого не превышает соотношения сторон экрана (при условии, что вам нужен горизонтальный буфер, а не вертикальный).
По сути, вы пытаетесь здесь прокрутить поддерживаемые соотношения сторон камеры и определить, какие из них наиболее близки к экрану. Все, что слишком велико, мы отбрасываем, поскольку в этом примере мы хотим, чтобы предварительный просмотр занимал всю ширину экрана, и мы не возражаем, если предварительный просмотр будет короче, чем экран в портретном режиме.
а) Получить соотношение сторон экрана
Допустим, экран имеет размер 480 x 800 пикселей, тогда соотношение сторон высота / ширина равно
1.666...
Если бы мы были в ландшафтном режиме, мы бы сделали ширину / высоту.
б) Получите поддерживаемые соотношения сторон камеры
Затем мы смотрим на соотношение сторон каждой камеры и вычисляем ширину / высоту. Причина, по которой мы вычисляем это, а не высоту / ширину, как экран, заключается в том, что соотношение сторон камеры всегда в альбомном режиме.
Так:
- Аспект => расчет
-
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...
(слишком широкий)
г) ближайшее кратчайшее соотношение сторон камеры, соответствующее соотношению сторон экрана
Итак, мы выбираем
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);
}}
/>
Следует отметить, что соотношение сторон камеры всегда ширина: высота в альбомном режиме, но ваш экран может быть как в портретном, так и в ландшафтном.
Исполнение
В этом примере поддерживается только портретный режим экрана. Чтобы поддерживать оба типа экрана, вам нужно будет проверить ориентацию экрана и изменить вычисления в зависимости от ориентации устройства.
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,
}
});
Вы можете поиграть с Expo Snack здесь
Полученные результаты
И, наконец, предварительный просмотр камеры с сохраненными пропорциями, в котором используются отступы сверху и снизу для центрирования предварительного просмотра:
Вы также можете попробовать этот код онлайн или на своем Android-устройстве на Expo Snack.