1

我对 Android 中的相机 API 有一个大问题。我想在 SurfaceView 的相机中开始预览。这样可行。但预览图像失真。为什么会发生,解决方案是什么?

我在 App Store 中有一个类似 Retro Camera 的应用。所以我想在矩形或正方形中预览相机。但是预览没有正确的解决方案。

我的代码:

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) {
}
 }

结果:
我有一个正方形,但预览是一个矩形。

4

1 回答 1

0

这需要一点数学...

问题

基本上问题在于相机预览与屏幕的宽度/高度比不同。据我所知,这只是 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. 在高度不超过屏幕比例的情况下找到相机最接近屏幕的纵横比(假设您想要水平缓冲区,而不是垂直缓冲区)

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

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

你可以在这里玩世博小吃

结果

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

安卓截图

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

于 2021-10-05T10:29:32.473 回答