Android 相机 (一): 摄像头属性, 预览方向, 预览尺寸等问题探究

7,316 阅读11分钟

参考:
developer.android.google.cn/reference/a…
developer.android.google.cn/reference/a…
blog.csdn.net/daiqiquan/a…
dev.qq.com/topic/583ba…

相机最好的学习资料就是官方文档(还有对应的类和方法的注释),这能帮你避免很多坑.

# 摄像头和屏幕的一些基本属性

1. 相机朝向

CameraInfo#facing
1. CameraInfo#CAMERA_FACING_BACK(值为0):后置摄像头
2. CameraInfo#CAMERA_FACING_FRONT(值为1):前置摄像头

2. 相机角度:

CameraInfo#orientation:可能取值为0,90,180,270
相机图像的角度,这个角度指的是相机图像需要顺时针旋转多少度才能在自然方向上正确显示.
如果屏幕是portrait,那么后置摄像头是90度,前置摄像头是270度,这样算下来,最后的result都是90度。不过不同的手机可能不一定是这样的.
这个值是固定的

3. 设备的自然方向:

每个设备都有一个自然方向,手机和平板的自然方向不同。Android:screenOrientation的默认值unspecified即为自然方向。
关于orientation的两个常见值是这样定义的:
1. landscape(横屏):the display is wider than it is tall,正常拿着设备的时候,宽比高长,这是平板的自然方向。
2. portrait(竖屏):the display is taller than it is wide,正常拿设备的时候,宽比高短,这是手机的自然方向。
3. orientation的值直接影响了getDefaultDisplay().getSize()的返回值:
例如:Activity在Landscape下,w*h=1280*720,那么在portrait下就是w*h=720*1280

4. 屏幕角度

Display#getOrientation()|#getRotation():可能取值为0,1,2,3对应0,90,180,270
为屏幕以自然方向顺时针旋转的角度,等于设备逆时针旋转的角度.
1. 视图转向与设备物理转向刚好相反。
2. 比如设备逆时针旋转了90度,系统计算的时候是考虑设备要顺时针旋转多少度才能恢复到自然方向,当然是再顺时针旋转90度,因此该方法此时返回的是90,即Surface.ROTATION_90 (值为1);
3. 如果顺时针旋转90度,那么设备要顺时针旋转270度才能恢复到自然方向,因此返回值为270。
4. 因此,想快速计算出getRotation的返回值就这么想:设备逆时针旋转过的角度。手机在portrait下,该方法返回的是0度。

5. 屏幕尺寸

Display#getSize(Point)|#getHeight(),getWidth()|#getMetrics(DisplayMetrics):单位为像素
1. 不要用于计算 layouts,因为界面上一般会有各种系统装饰(如状态栏等)会减少这里的这个值.如果要布局可以用 Window.size.
2. 屏幕的尺寸与屏幕角度相关: 这个值返回的值不一定是真实的屏幕物理尺寸大小(原始分辨率),可能会去除一些系统级的可见的装饰元素. 如果要获取真实的尺寸,可以用getRealSize(Point)|getRealMetrics(DisplayMetrics)(这两个方法也不一定返回真实的屏幕物理尺寸,比如 WindowManager 模拟一个小的 Display)
3. 如果当前屏幕是横屏,则返回的尺寸也是横屏的尺寸,如果是竖屏,返回的是竖屏的尺寸. 如某个设备在Landscape下,w*h=1280*720,那么在portrait下就是w*h=720*1280

# 保持摄像头预览界面与实景一直

如果要保持预览界面与实景一致,需要正确设置下面三个属性(一个相机的显示角度,一个合适的预览尺寸,一个正确的预览界面Surface尺寸).

1. 设置显示角度

Camera#setDisplayOrientation(int)
设置预览图像顺时针旋转的角度.
1. 这个值会影响预览的帧数据和快照的图片.但是不会影响传入PreviewCallback#onPreviewFrame()中的,JPEG 图片,视频录制中的的字节数据;
2. 不能在预览的时候调用.但是API>=14时可以
3. 为了保证相机图像和界面的方向一致,可以使用下面的代码(代码来自官方):

public static void setCameraDisplayOrientation(Activity activity,
         int cameraId, android.hardware.Camera camera) {
     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) {
         case Surface.ROTATION_0: degrees = 0; break;
         case Surface.ROTATION_90: degrees = 90; break;
         case Surface.ROTATION_180: degrees = 180; break;
         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);
 }

2. 设置预览尺寸

Camera#setPreviewSize(int width, int height)
设置预览图片的尺寸.
1) 这里的宽高基于相机的方向: orientation. 所以设置这个值的时候同时也要考虑屏幕显示方向.

//例如,相机支持480*320和320*480这两个预览尺寸,如果应用需要一个3:2的预览尺寸,那么当应用是竖屏(0或180)的时候,则设置预览尺寸为480*320,如果屏幕是横屏(90或270),则应该设置预览尺寸为320*480.
parameters.setPreviewSize(320, 480)

2) 如果设置的时候预览已经开始了.那么应用应该先停止预览然后设置
3) 这个值只是影响预览,不影响拍出的图片或者录制的视频的分辨率.

寻找最佳的预览尺寸

1) 获取到所有支持的预览尺寸

final List<Camera.Size> supportedPreviewSizes =Camera.getParameters().getSupportedPreviewSizes();

2) 排除太小的预览分辨率值:如480 * 320
3) 尽可能去取与屏幕分辨率接近的尺寸(移除比例差异太大的,如>0.15),如果有和屏幕分辨率一致的预览尺寸则直接返回.
4) 如果找不到就还是使用默认值.

代码示例(示例来自网上,我做了优化,可以支持横屏和竖屏):

/**
     * 最小预览界面的分辨率
     */
    private static final int MIN_PREVIEW_PIXELS = 480 * 320;
    /**
     * 最大宽高比差
     */
    private static final double MAX_ASPECT_DISTORTION = 0.15;
    /**
     * 找出最适合的预览界面分辨率
     *
     * @return
     */
    public static Point findBestPreviewResolution(Camera.Parameters cameraParameters, Point screenResolution, int screenOrientation, int cameraOrientation) {
        Camera.Size defaultPreviewResolution = cameraParameters.getPreviewSize(); //默认的预览尺寸
        Log.d(TAG, "camera default resolution " + defaultPreviewResolution.width + "x" + defaultPreviewResolution.height);
        List<Camera.Size> rawSupportedSizes = cameraParameters.getSupportedPreviewSizes();
        if (rawSupportedSizes == null) {
            Log.w(TAG, "Device returned no supported preview sizes; using default");
            return new Point(defaultPreviewResolution.width, defaultPreviewResolution.height);
        }
        // 按照分辨率从大到小排序
        List<Camera.Size> supportedPreviewResolutions = new ArrayList<Camera.Size>(rawSupportedSizes);
        Collections.sort(supportedPreviewResolutions, new Comparator<Camera.Size>() {
            @Override
            public int compare(Camera.Size a, Camera.Size b) {
                int aPixels = a.height * a.width;
                int bPixels = b.height * b.width;
                if (bPixels < aPixels) {
                    return -1;
                }
                if (bPixels > aPixels) {
                    return 1;
                }
                return 0;
            }
        });
        printlnSupportedPreviewSize(supportedPreviewResolutions);
        // 在camera分辨率与屏幕分辨率宽高比不相等的情况下,找出差距最小的一组分辨率
        // 由于camera的分辨率是width>height,这里先判断我们的屏幕和相机的角度是不是相同的方向(横屏 or 竖屏),然后决定比较的时候要不要先交换宽高值
        boolean isCandidatePortrait = screenOrientation % 180 != cameraOrientation % 180;
        double screenAspectRatio = (double) screenResolution.x / (double) screenResolution.y;
        // 移除不符合条件的分辨率
        Iterator<Camera.Size> it = supportedPreviewResolutions.iterator();
        while (it.hasNext()) {
            Camera.Size supportedPreviewResolution = it.next();
            int width = supportedPreviewResolution.width;
            int height = supportedPreviewResolution.height;
            // 移除低于下限的分辨率,尽可能取高分辨率
            if (width * height < MIN_PREVIEW_PIXELS) {
                it.remove();
                continue;
            }
            //移除宽高比差异较大的
            int maybeFlippedWidth = isCandidatePortrait ? height : width;
            int maybeFlippedHeight = isCandidatePortrait ? width : height;
            double aspectRatio = (double) maybeFlippedWidth / (double) maybeFlippedHeight;
            double distortion = Math.abs(aspectRatio - screenAspectRatio);
            if (distortion > MAX_ASPECT_DISTORTION) {
                it.remove();
                continue;
            }
            // 找到与屏幕分辨率完全匹配的预览界面分辨率直接返回
            if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
                Point exactPoint = new Point(width, height);
                Log.d(TAG, "found preview resolution exactly matching screen resolutions: " + exactPoint);
                return exactPoint;
            }
        }
        // 如果没有找到合适的,并且还有候选的像素,则设置其中最大比例的,对于配置比较低的机器不太合适
        if (!supportedPreviewResolutions.isEmpty()) {
            Camera.Size largestPreview = supportedPreviewResolutions.get(0);
            Point largestSize = new Point(largestPreview.width, largestPreview.height);
            Log.d(TAG, "using largest suitable preview resolution: " + largestSize);
            return largestSize;
        }
        // 没有找到合适的,就返回默认的
        Point defaultResolution = new Point(defaultPreviewResolution.width, defaultPreviewResolution.height);
        Log.d(TAG, "No suitable preview resolutions, using default: " + defaultResolution);
        return defaultResolution;
    }
    private static void printlnSupportedPreviewSize(List<Camera.Size> supportedPreviewSizes) {
        Log.d(TAG, "--------------------Support Preview Size--------------------");
        for (int i = 0; i < supportedPreviewSizes.size(); i++) {
            Log.d(TAG, String.format("(%s,%s)", supportedPreviewSizes.get(i).width, supportedPreviewSizes.get(i).height));
        }
        Log.d(TAG, "------------------------------------------------------------");
    }

一般相机会支持很多个的预览尺寸,如小米 Max 支持的的预览尺寸如下:

CameraConfigUtils: --------------------Support Preview Size--------------------
CameraConfigUtils: (1920,1080)
CameraConfigUtils: (1440,1080)
CameraConfigUtils: (1280,960)
CameraConfigUtils: (1280,720)
CameraConfigUtils: (800,480)
CameraConfigUtils: (720,480)
CameraConfigUtils: (640,480)
CameraConfigUtils: (640,360)
CameraConfigUtils: (480,320)
CameraConfigUtils: (384,288)
CameraConfigUtils: (352,288)
CameraConfigUtils: (320,240)
CameraConfigUtils: (176,144)
CameraConfigUtils: ------------------------------------------------------------

3. 设置 SurfaceView

Camera#setPreviewDisplay(holder);,示例代码如下:

SurfaceView cameraPreview=findViewById(...);
SurfaceHolder holder=cameraPreview.getHolder();
mCamera.setPreviewDisplay(holder);
  1. 要把预览数据显示在手机上,最后我们还要设置一个显示界面 SurfaceView. 通过调用setPreviewDisplay(SurfaceHolder holder)来设置,这个holder必须包含一个surface.如果用SurfaceView的话,可以通过注册一个SurfaceHolder.Callback,在回调方法#surfaceCreated(SurfaceHolder)中设置setPreviewDisplay().
  2. 这个方法必须在startPreview()之前调用.

设置 SurfaceView 的大小

拿到合适的预览尺寸以后,我们要给 SurfaceView设置一个合适的大小来显示预览视图.这个大小的要求首先:比例要和 PreviewSize一致,并且保证宽度充满,高度按比例缩放(当然,这个要看具体业务的需求了,我这里只是最常规的一种情况就是保证预览窗口尽量全屏).
参考代码如下:

public static Rect getSurfaceViewSize(Point displaySize, int screenOrientation, Point previewSize, int cameraOrientation) {
        Point previewSize2 = new Point();
        //方向不一致则交换
        if (screenOrientation % 180 != cameraOrientation % 180) {
            previewSize2.set(previewSize.y, previewSize.x);
        } else {
            previewSize2 = previewSize;
        }
        int width = displaySize.x;
        int height = previewSize2.y * displaySize.x / previewSize2.x;
        int left = 0;
        int right = width;
        int top = 0;
        int bottom = top + height;
        return new Rect(left, top, right, bottom);
    }

这样,不论默认的Activity 设置是横屏还是竖屏,都可以正确的显示预览.

拿到正确的捕获窗口尺寸

有时候虽然是全屏预览,但是我们只需要其中特定的区域图片,如扫描二维码,扫描身份证等.我们会设置一个捕获窗口,这个窗口一般在预览界面的居中位置(也可以自己制定位置).如果前面的预览窗口尺寸和相机设置的预览尺寸不一致,那说明预览窗口的尺寸是预览尺寸经过缩放了的的,这样,捕获窗口也是经过缩放的.然而我们要拿到捕获窗口区域对应的图像数据,就必须拿到它相对于预览尺寸的坐标. 这个坐标相对好获取一些.因为我们目前有预览尺寸,有预览窗口的尺寸,有捕获窗口的尺寸,那么只要按照预览窗口和相机预览尺寸的比例缩放捕获区域的坐标即可. 这个坐标+预览尺寸+预览数据即可拿到捕获窗口的数据.


        /**
         * 拿到捕获窗口的大小:坐标系相对于预览尺寸
         * 这个才是我们识别条码的时候需要的
         *
         * @param previewSurfaceRect
         * @param previewSize
         * @param captureRect
         * @return 按照预览尺寸拿到的捕获区域坐标
         */
        public static Rect getCaptureRectInPreview(Rect previewSurfaceRect, int screenOrientation, Point previewSize, int cameraOrientation, Rect captureRect) {
            //缩放比例:SurfaceView 的尺寸肯定是根据相机预览尺寸缩放加旋转得到的,所以我们先要旋转到正确的方向,然后缩放即可.
            float scale;
            Rect captureRect2;
            if (screenOrientation % 180 != cameraOrientation % 180) {
                captureRect2 = new Rect(captureRect.top, captureRect.left, captureRect.bottom, captureRect.right);
                scale = previewSurfaceRect.width() / (float) previewSize.y;
            } else {
                captureRect2 = new Rect(captureRect.left, captureRect.top, captureRect.right, captureRect.bottom);
                scale = previewSurfaceRect.width() / (float) previewSize.x; //计算出缩放的比例
            }
            captureRect2.set(((int) (captureRect2.left / scale)), ((int) (captureRect2.top / scale)), ((int) (captureRect2.right / scale)), ((int) (captureRect2.bottom / scale)));
            return captureRect2;
        }

# 获取预览数据

前面设置了预览尺寸(setPreviewSize()),然后获取到了正确的捕获区域坐标(而且是相对于预览尺寸的坐标),这样,只要我们拿到一个预览图像数据,然后根据捕获区域坐标就可以截取我们需要的数据.
我们在成功启动相机以后立即进行一次预览图像捕获,可以通过下面的方法:

mCamera.setOneShotPreviewCallback(previewCallback);

这里的 previewCallback 是Camera.PreviewCallback接口的实例,他需要实现onPreviewFrame()接口方法.

PreviewCallback#onPreviewFrame()

onPreviewFrame()的参数 data 是一个byte[]数据,他就是图像内容,这个数据的格式默认是YCbCr_420_SP(NV21),尺寸当然就是我们setPreviewSize()设置的尺寸了. 你可以通过Camera.Parameters#getPreviewFormat()来设置这里预览数据的数据格式.设置格式的时候千万不要随便设置,你需要首先通过Parameters#getSupportedPreviewFormats()拿到支持的格式,然后选择合适的格式.
如果是默认的格式,则通过下面的方式可以转换为 Bitmap 或者存储为文件.

 @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            //data即预览的图像数据
            testPrintBitmapSize(data, camera);
        }
     private void testPrintBitmapSize(byte[] data, Camera camera) {
            //这里直接用 BitmapFactory.decodeByteArray()是不行的.
            Camera.Parameters parameters = camera.getParameters();
            int imageFormat = parameters.getPreviewFormat(); //拿到默认格式
            int w = parameters.getPreviewSize().width; //拿到预览尺寸
            int h = parameters.getPreviewSize().height;
            Rect rect = new Rect(0, 0, w, h); 
            Log.d(TAG, String.format("imageFormat:%d;size:%d,%d", imageFormat, w, h));
            YuvImage yuvImg = new YuvImage(data, imageFormat, w, h, null);
            try {
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                yuvImg.compressToJpeg(rect, 100, os);
                Bitmap bitmap = BitmapFactory.decodeByteArray(os.toByteArray(), 0, os.size());
                Log.d(TAG, String.format("bitmap size:%d,%d", bitmap.getWidth(), bitmap.getHeight()));
            } catch (Exception e) {
            }
        }

拍照和录像的部分待续


谢绝转载!