×
广告

Android Camera 踩坑

96
koguma
2017.06.23 18:42* 字数 2970

标签(空格分隔):Android Camera 相机 图像方向 图像大小

【注】本文所提到的 Camera 均为 android.hardware 这个包下的 Camera


在开发自定义 Camera 时,经常会遇见各种在方向和大小上的问题。例如:预览的方向颠倒了、预览的图像被拉升了、拍摄出来的照片与预览时的图像不一致等等。这些问题的主要原因是对于Camera 中有关方向和大小的几个概念没有理解。本文系统的梳理这些知识,同时有误之处欢迎大家留言以提醒其它的读者。

1. 方向

1.1 相机传感器方向

这里的相机传感器有的博客中也叫图像传感器(Image Sensor)这里以官方文档中给出名词(Camera Sensor)为准。
在 Camera#setRotation 方法的注释中有这样一段话:

Suppose a back-facing camera sensor is mounted inlandscape and the top side of the camera sensor is aligned with the right edge of the display in natural orientation
假设后置相机的传感器是被水平安装的,并且相机传感器的顶部与自然状态下手机的右边缘对齐。

一图胜千言:


图1 相机传感器的位置
图1 相机传感器的位置

其实,这段话中还包含着一个隐藏信息。即,相机传感器获得的图像是水平的且宽大于高。举例来说,一个4:3的800万像素的传感器在水平方向上有3264个像素,在竖直方向上有2448个像素。即,800w ≈ 3264*2448。这样的一张图片就是我们通过相机传感器获得的最原始的图片。

当我们让相机传感器分别位于手机的左上角、右上角、右下角、左下角时我们采集到的原始图像分别如下图所示:


图2 相机传感器方向与所采集到的图像方向
图2 相机传感器方向与所采集到的图像方向

1.2 相机预览方向

1.2.1 什么是相机的预览方向:

【注】这里的图像指的是我们通过 SurfaceView 或者 TextureView 所看到的,在手机屏幕上呈现出来的图像。

1.2.2 如何设置预览方向:

当我们拿到相机传感器所采集到的图像后,我们需要对图像进行变换以符合我们直观的视觉感受。由于采集到的图像信息是通过 SurfaceView 或者 TextureView 显示的,所以我们只需要控制好这两个 View 的方向即可。

对于 SurfaceView,由于其不在 View hierachy 中,因此我们不能简单的对其进行选择变换操作。而是应该在调用完 Camera#setPreviewDisplay 方法绑定好 SurfaceHolder 之后。再通过 Camera#setDisplayOrientation 来改变其方向。

对于 TextureView 由于其作为 View hierachy 中的一个普通 View。因此我们可以通过操作 matrix ,最后再调用 TextureView#setTransform 来改变其方向;

1.2.3 如何确定预览方向:

我们直接分析一下,Camera#setDisplayOrientation 的注释中所给出的方案。

//If you want to make the camera image show in the same orientation as the display, you can use the following code.
 public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {
        //通过相机ID获得相机信息      
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);

        //获得当前屏幕方向   
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            //若屏幕方向与水平轴负方向的夹角为0度       
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            //若屏幕方向与水平轴负方向的夹角为90度       
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            //若屏幕方向与水平轴负方向的夹角为180度       
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            //若屏幕方向与水平轴负方向的夹角为270度   
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }
        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            //前置摄像头作镜像翻转      
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror   
        } else {  // back-facing       
            result = (info.orientation - degrees + 360) % 360;
        }
        camera.setDisplayOrientation(result);
    }

从上述代码可以看出,相机的预览方向与以下3个因素有关:
1.屏幕方向:orientation
2.相机图像方向: (orientation of the camera image):在 Camera 中对应的是CameraInfo.orientation ,在 Camera2 中对应的是 CameraCharacteristics.SENSOR_ORIENTATION。
3.相机是前置还是后置:CAMERA_FACING_FRONT/CAMERA_FACING_BACK

对于屏幕方向:
在开启屏幕自动旋转的状态下:自然握持状态下为0度,逆时针旋状依次为:90度、180度(有的手机没有这个角度)、270度;
在锁定屏幕方向的状态下:均为0度。

对于相机图像方向:
先来看下官方文档

The orientation of the camera image. The value is the angle that the camera image needs to be rotated clockwise so it shows correctly onthe display in its natural orientation. It should be 0, 90, 180, or 270.
For example, suppose a device has a naturally tall screen. The back-facing camera sensor is mounted in landscape. You are looking at the screen. If the top side of the camera sensor is aligned with the right edge of the screen in natural orientation, the value should be 90. If the top side of a front-facing camera sensor is aligned with the right of the screen, the value should be 270.

简单的总结下:
1.相机的图像方向即是相机图像需要顺时针旋转的角度,以便让图像以正确的角度呈现。这个角度应该是0、90、180、270度。
2.举例来说,当使用后置摄像头时,这个角度是90度。当使用前置摄像头时,这个角度时270度。在我手上的测试机器中没有发现0度和180度的情况(猜想:这两种角度可能在那些摄像头非固定的手机上会出现)。

这里我暂时没有想到为何这样去计算相机的预览方向,但是既然原理我们已经理解了,那么我们可以从结果入手分析。


图3 不同屏幕方向及相机图像方向下对应的 display orientation 的值
图3 不同屏幕方向及相机图像方向下对应的 display orientation 的值

在屏幕方向锁定的情况下,由于 rotation 的值始终为0,因此上面给出的计算相机预览方向的方法就不适用了。下面我们来分析这种相对来说较为简单的情况:
1.在锁定竖屏的情况下:此时 SurfaceView/TextureView 的宽小于高,而相机传感器采集到的图像宽大于高,故需要旋转90即可。
2.在锁定横屏的情况下:此时 SurfaceView/TextureView 的宽大于高,与相机传感器采集到的图像的宽高大小关系相同,故设置为0度即可。

1.3 图片预览方向

1.3.1什么是图片的预览方向:

这里的图像指的是我们通过 ImageView 或类似的用来显示 Bitmap 的控件所显示的,并最终能以 jpg 之类的格式保存起来的图像。

如何设置图片的预览方向:
当使用 Camera 类时,我门可以通过 Camera.Parameters#setRotation(jpegOrientation) 方法来设置图片的预览方向。当使用 Camera2 类时,我们则可以通过 CaptureRequest.Builder#set(CaptureRequest.JPEG_ORIENTATION, jpegOrientation)方法来设置图片的预览方向。

1.3.2 如何确定图片的预览方向:

我们不妨还是先来看一下 Camera.Parameters#setRotation 方法的注释中所给出的方案。

For example, suppose the natural orientation of the device isportrait. The device is rotated 270 degrees clockwise, so the deviceorientation is 270. Suppose a back-facing camera sensor is mounted inlandscape and the top side of the camera sensor is aligned with theright edge of the display in natural orientation. So the cameraorientation is 90. The rotation should be set to 0 (270 + 90).

 public void onOrientationChanged(int orientation) {
        if (orientation == ORIENTATION_UNKNOWN) return;
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);
        // Round device orientation to a multiple of 90  
        orientation = (orientation + 45) / 90 * 90;   
        int rotation = 0;
        // Reverse device orientation for front-facing cameras   
        if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
            rotation = (info.orientation - orientation + 360) % 360;
        } else {  // back-facing camera        
            rotation = (info.orientation + orientation) % 360;
        }
        mParameters.setRotation(rotation);
    }

从上述代码可以看出,相机图片的方向与设备的方向和相机的前后置有关。

对于设备的方向:
1.在不锁屏的状态下:我们可以通过 WindowManager.getDefaultDisplay().getRotation() 获得。但是需要注意的是这里的 orientation 与屏幕的 rotation 不同。根据文档的描述可以看出设备在自然竖直状态下顺时针旋转270度所得 orientation 为270度,而此时的屏幕 rotation 为90度。可见两者是一个简单的互补关系。
2.在锁屏状态下:我们依然可以通过 SensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),SensorManager.SENSOR_DELAY_NORMAL) 来获得。

对于相机的前后置:
1.当相机为前置相机时:需要额外对采集到的图像做镜像翻转,这是由于底层相机在传递前置摄像头预览数据时做了水平翻转变换,因为这里需要再做一次水平翻转。
**2.当相机为后置相机时: **则不需要做额外的处理。

依旧举例来说明:
当我们以图1中的①的姿势握持手机并使用前置相机拍摄时:
orientation = (270 + 45) / 90 * 90 = 270
rotation = (270 + 90) % 360 = 0
此时我们不需要对相机传感器采集到的图像进行任何旋转,直接交给对应的 View 去显示即可。
当我们以图1中的③的姿势握持手机并使用后置相机拍摄时:
orientation = (90 + 45) / 90 * 90 = 90
rotation = (90 + 90) % 360 = 180
此时我们需要对相机采集到的图像顺时针旋转180度,即将图1 中的③顺时针旋转180度再交给对应的 View 去显示,这样就符合我们的直观感受了。

Camera 方向总结:
对于相机传感器方向、相机预览方向以及图片预览方向这三个方向来说。除相机传感器方向是我们不能够通过代码控制的之外,后两个方向都是都是我们可以去设置的。另外,相机的预览方向和图片的预览方向二者无关,它们都只和传感器的方向有关。

方向说完了我们再来看看大小(Size)。

2. 大小

2.1 SurfaceView/TextView 的大小

即用来展示图像的 View 的大小,一般以其一边为基准设置其高宽比(aspect ratio),可以通过重写 onMeasure 方法实现。

2.2 PreViewSize

2.2.1 什么是 PreViewSize:

相机硬件提供的预览帧数据尺寸。预览帧数据传递给 view,实现预览图像的显示。其宽高宽比要尽可能的与 View 的高宽比相同,以免造成显示的比例失调。

2.2.2 如何设置 PreViewSize:

在 Camera 中我们可以通过Camera.Parameters#setPreviewSize 方法设置 PreviewSize。在 Camera2 中如果我们使用 TextureView ,设置预览 PreViewSize 的过程相当于把预览尺寸投射到 View 上。 同样可以通过 TextureView#setTransform(matrix)来实现,具体实现方法可以参照文末给出的开源项目 material-camera

2.2.3 如何确定 PreViewSize:

目前所见过的一共2种方法思路:
1.挑选 PreViewSize 中宽高比与所给 Size 相同,且 PreViewSize 的长宽大于等于所给的 Size 长宽的一项。
2.挑选 PreViewSize 中宽高比与所给 Size 比列差值最小的一项。

public CameraController.Size getOptimalPreviewSize(List<CameraController.Size> sizes) {
        if( MyDebug.LOG )
            Log.d(TAG, "getOptimalPreviewSize()");
        final double ASPECT_TOLERANCE = 0.05;
        if( sizes == null )
            return null;
        CameraController.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;
        Point display_size = new Point();
        Activity activity = (Activity)this.getContext();
        {
            Display display = activity.getWindowManager().getDefaultDisplay();
            display.getSize(display_size);
            if( MyDebug.LOG )
                Log.d(TAG, "display_size: " + display_size.x + " x " + display_size.y);
        }
        double targetRatio = calculateTargetRatioForPreview(display_size);
        int targetHeight = Math.min(display_size.y, display_size.x);
        if( targetHeight <= 0 ) {
            targetHeight = display_size.y;
        }
        // Try to find the size which matches the aspect ratio, and is closest match to display height
        for(CameraController.Size size : sizes) {
            if( MyDebug.LOG )
                Log.d(TAG, "    supported preview size: " + size.width + ", " + size.height);
            double ratio = (double)size.width / size.height;
            if( Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE )
                continue;
            if( Math.abs(size.height - targetHeight) < minDiff ) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }
        if( optimalSize == null ) {
            // can't find match for aspect ratio, so find closest one
            if( MyDebug.LOG )
                Log.d(TAG, "no preview size matches the aspect ratio");
            optimalSize = getClosestSize(sizes, targetRatio);
        }
        if( MyDebug.LOG ) {
            Log.d(TAG, "chose optimalSize: " + optimalSize.width + " x " + optimalSize.height);
            Log.d(TAG, "optimalSize ratio: " + ((double)optimalSize.width / optimalSize.height));
        }
        return optimalSize;
    }

    private CameraController.Size getClosestSize(List<CameraController.Size> sizes, double targetRatio) {
        if( MyDebug.LOG )
            Log.d(TAG, "getClosestSize()");
        CameraController.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;
        for(CameraController.Size size : sizes) {
            double ratio = (double)size.width / size.height;
            if( Math.abs(ratio - targetRatio) < minDiff ) {
                optimalSize = size;
                minDiff = Math.abs(ratio - targetRatio);
            }
        }
        return optimalSize;
    }

2.3 PictureSize

2.3.1 什么是 PictureSize:

相机硬件提供的拍摄帧数据尺寸。拍摄帧数据可以生成位图文件,最终保存成.jpg或者.png等格式的图片。

2.3.2 如何设置 PictureSize:

在 Camera 中可以先通过 Camera.Parameters#getSupportedPictureSizes() 获得设备所支持的 PictureSize,再通过 Camera.parameters#setPictureSize 方法设置最终输出的 PictureSize。
在 Camera2 中可以先通过 StreamConfigurationMap.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) 方法获得设备所支持的 PictureSize,再通过 ImageReader#newInstance(int width, int height, int format, int maxImages) 方法设置最终输出的 PictureSize 以及图像格式。

2.3.3 如何确定 PictureSize:

PictureSize 的高宽比也应尽量与对应的 View 的高宽比相同,以免造成预览时的图像与输出后的图像在比例上的不一致,给人很明显的视觉上的不一致。另外 PictureSize 也决定了最终输出的图片的像素大小(即所占物理空间),这就需要结合实际的需求考虑了。

总结:SurfaceView/TextureView 与 PreViewSize 以及 PictureSize 这三者的宽高比应尽量相同,以免造成视觉上的不一致。前两者决定预览时我们所看见的效果,而 PictureSize 只最终决定生成的位图文件的大小。

参考的开源项目:
opencamera-code
material-camera
CameraFragment

参考的博客
http://ticktick.blog.51cto.com/823160/1592267
https://juejin.im/entry/56aa36fad342d300542e7510
https://www.zybuluo.com/kmfish/note/269589

Android
Web note ad 1