Камера 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, где:

  1. Каждый производитель камеры поддерживает разные форматы изображения.
  2. Каждый производитель телефонов создает разные соотношения сторон экрана.

Теория

Решить эту проблему можно следующим образом:

  1. Определите соотношение сторон (и ориентацию) экрана
      const { height, width } = Dimensions.get('window');
const screenRatio = height / width;
  1. Подождите, пока камера будет готова
      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. Выясните поддерживаемые соотношения сторон камеры
      const ratios = await camera.getSupportedRatiosAsync();

Это вернет массив строк в формате ['w:h'], поэтому вы можете увидеть что-то вроде этого:

      [ '4:3', '1:1', '16:9' ]
  1. Найдите наиболее близкое соотношение сторон камеры к экрану, высота которого не превышает соотношения сторон экрана (при условии, что вам нужен горизонтальный буфер, а не вертикальный).

По сути, вы пытаетесь здесь прокрутить поддерживаемые соотношения сторон камеры и определить, какие из них наиболее близки к экрану. Все, что слишком велико, мы отбрасываем, поскольку в этом примере мы хотим, чтобы предварительный просмотр занимал всю ширину экрана, и мы не возражаем, если предварительный просмотр будет короче, чем экран в портретном режиме.

а) Получить соотношение сторон экрана

Допустим, экран имеет размер 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);
  1. Стиль <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.

Другие вопросы по тегам