Android 系统原生相机API角度原理与适配

Camera1

虽然Camera作为第一代原生android所提供的相机类一直被开发者甚至Google官方开发人员所诟病,但为了兼容和适配Android版本5.0以下的App应用,我们别无选择。因此,有了本篇文档详细阐述1.0版的Camera 是如何使用的。本篇使用的是SurfaceView与Camera类。

一个方向,四个角度

  • 终端自然方向
  • 相机传感器偏角
  • 屏幕旋转角度
  • 终端自然方向偏角
  • 图像写入偏角

文档下文会在拍照流程中的不同的阶段应用到上述四个角度,而“终端自然方向”贯穿整个流程当中。这一个方向、四个角度非常重要,缺一不可,是支撑相机Camera 系列API的关键。在设计NXDesign的相机项目中,经过对官方文档的研读和各路资料的调研之后发现,我们在网络上查到的博客类相关资料有80%的实现方式是存在问题的,当然,这也可以归咎于该API其本身确实不好用,如果不对源码注释进行仔细研究,很容易对开发者产生误导。

相机拍照的生命周期

Camera生命周期.png

更加准确的说,相机的生命周期是依托于SurfaceView的创建和销毁来完成的。SurfaceView的作用是提供相机内容的实时预览。我们需要在surfaceview创建好之后打开相机使用相机资源,在surfaceview被销毁后释放相机资源。

  • 关联surfaceview

surfaceview 提供了holder机制向调用方通知surfaceview的变化时机,为了在不同的时机对相机资源做不同的事情,需要调用SurfaceHolder.addCallback()方法。

surfaceview.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {

            }

            override fun surfaceDestroyed(holder: SurfaceHolder?) {
                releaseCamera()//释放相机资源
            }

            override fun surfaceCreated(holder: SurfaceHolder?) {
                    //开启相机
                startCamera(Camera.CameraInfo.CAMERA_FACING_BACK)
            }

        })
  • 打开相机,进行预览(终端自然方向、相机传感器偏角)

现在的Android手机一般会有多个摄像头,但根据其方向可以归为两类:CAMERA_FACING_BACK 和 CAMERA_FACING_FRONT。在打开摄像头之前,首先需要获取相机资源,判断相机个数Camera.getNumberOfCameras()。每个相机对应一个CameraInfo,它的定义如下:

public static class CameraInfo {
        /**
         * The facing of the camera is opposite to that of the screen.
         * 前置摄像头标记
         */
        public static final int CAMERA_FACING_BACK = 0;

        /**
         * The facing of the camera is the same as that of the screen.
         * 后置摄像头标记
         */
        public static final int CAMERA_FACING_FRONT = 1;

        /**
         * The direction that the camera faces. It should be
         * CAMERA_FACING_BACK or CAMERA_FACING_FRONT.
         * 摄像头方向(值取CAMERA_FACING_BACK 或 CAMERA_FACING_FRONT)
         */
        public int facing;

        /**
         * <p>The orientation of the camera image. The value is the angle that the
         * camera image needs to be rotated clockwise so it shows correctly on
         * the display in its natural orientation. It should be 0, 90, 180, or 270.</p>
         *
         * <p>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.</p>
         *
         * @see #setDisplayOrientation(int)
         * @see Parameters#setRotation(int)
         * @see Parameters#setPreviewSize(int, int)
         * @see Parameters#setPictureSize(int, int)
         * @see Parameters#setJpegThumbnailSize(int, int)
         * 
         * 相机拍摄出来的图片的旋转角度。拍出的图片需要顺时针旋转这个角度,才能正常展示。
         * 取值只有0,90,180 和270 四种。
         * 比如:一个手机是竖版屏幕,后置摄像头的图像传感器是横向物理摆放,当你面向屏幕时:
         * 如果相机自带的传感器顶部与屏幕自然方向的右边缘一致,则这个值就是90度。
         * 如果前置摄像头传感器的顶部与手机自然方向一致,则这个值就是270度。
         */
        public int orientation;

        /**
         * <p>Whether the shutter sound can be disabled.</p>
         *
         * <p>On some devices, the camera shutter sound cannot be turned off
         * through {@link #enableShutterSound enableShutterSound}. This field
         * can be used to determine whether a call to disable the shutter sound
         * will succeed.</p>
         *
         * <p>If this field is set to true, then a call of
         * {@code enableShutterSound(false)} will be successful. If set to
         * false, then that call will fail, and the shutter sound will be played
         * when {@link Camera#takePicture takePicture} is called.</p>
         */
        public boolean canDisableShutterSound;
    };

这里涉及到一个重要概念:相机图像传感器(camera sensor),想要理解上述注释的含义,就需要先理解下图内容。

传感器坐标与view坐标对比图.png

左图是通常情况下,我们对view的x y方向的认知,以屏幕的左上角为原点向右为x正方向,向下为y正方向;但是,右图描述的是绝大多数情况下,相机图像传感器的起始位置和方向判定。与view不同的是,传感器以手机屏幕在自然方向上的右上角为原点,向下为x正方向,向左为y正方向。因此,我们理解上述注释就不难了。如果相机自带的传感器顶部与终端自然方向(手机屏幕的硬件方向,一般手机都是竖直方向,也就是文档中说的naturally tall screen)的右边缘一致,则这个值就是90度。如果前置摄像头传感器的顶部与手机自然方向一致,则这个值就是270度。

当我们定义startCamera()方法时,要做5件事情,1.遍历摄像头cameraId,找到想要打开的摄像头(前置还是后置);2.获取摄像头信息,主要获取orientation;3. 设置相机DisplayOrientation 4.设置相机参数,主要是宽高比、对焦模式、图片格式、setRotation等。5. 向camera设置surfaceview.viewholder,并且startPreview。主要逻辑如下:

private fun startCamera(cameraFacing: Int) {
    val numbers = Camera.getNumberOfCameras()
    var targetCameraInfo: Camera.CameraInfo? = null
    var targetId: Int? = null
    for (i in 0 until numbers) {//1、遍历摄像头信息,找到需要的摄像头
        val cameraInfo = Camera.CameraInfo()
        Camera.getCameraInfo(i, cameraInfo)
        if (cameraInfo.facing == cameraFacing) {
            targetCameraInfo = cameraInfo//2、获取该摄像头信息
            targetId = i//获取该摄像头id,其id与下标一致
            break
        }
    }
    if (targetCameraInfo != null && targetId != null) {
        try {
            curCameraDetail.cameraId = targetId
            curCameraDetail.camera = Camera.open(targetId)
            curCameraDetail.cameraFacing = cameraFacing
            curCameraDetail.cameradetail = targetCameraInfo
            setCameraDisplayOrientation(curCameraDetail)//3、设置surfaceview预览方向
            setParameters(curCameraDetail)//4、设置参数信息
            startPreview(curCameraDetail.camera, surfaceview.holder)//5、开始预览
        } catch (e: RuntimeException) {
            Toast.makeText(activity, "打开相机失败,请检查相机权限", Toast.LENGTH_SHORT).show()
            pop()
        }
    } else {
        //TODO:不支持
    }
}
  • 接下来,设置预览方向 setCameraDisplayOrientation(curCameraDetail)

拿到cameraInfo.orientation之后,要调用camera.setDisplayOrientation设置进去,保证通过surfaceview预览到的取景跟当前的手机方向保持一致,但是,setDisplayOrientation设置的其实是经过两个角度计算之后的复合角度,而并不单纯是cameraInfo.orientation。正确的做法是这样的:先获取手机屏幕的旋转方向,然后与cameraInfo.orientation加和得到最终角度。通常情况下,如果我们设置相机为portrait,则不用考虑rotation。这也是为什么绝大部分网络资料中都会粗暴的写入一个90度完事儿而并没有解释这么做的道理。

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);
}
  • 设置参数信息 camera.parameters.setRotation(rotation)
    setRotation()的意义是,设置一个需要顺时针旋转的角度,这个角度在拍照前以参数的形式设置给相机,在拍照时会被写入到拍照之后所保存的图像文件中,并且可以通过Exif工具类拿到。之所以这么做是因为,相机驱动往往在生成照片的时候不会按照当前的屏幕方向做出纠正,而是直接生成图片,这就需要我们通过计算当前的手机相对于自然方向(上文已解释)的偏角以及相机传感器偏角(上文已解释)进行计算,得到该角度。只要对原始图片在顺时针方向旋转该角度之后,无论屏幕旋转方向怎样,都会在重力感应的方向进行正向的展示。
    为了获得这个角度,需要使用系统提供的android.view.OrientationEventListener:
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);
    orientation = (orientation + 45) / 90 * 90;
    int rotation = 0;
    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);
}

  • 拍照

调用camera.takePicture(null, null, pictureCallback)

private val pictureCallback = Camera.PictureCallback { data, camera ->
        val filePath = "${getExternalFileDir()}/original_${System.currentTimeMillis()}.jpg"
        with(HandlerThread("background")) {
            this.start()
            Handler(looper)
        }.post {
            val originalFile = File(filePath)
            FileOutputStream(originalFile).apply {
                write(data)
                close()
            }
            val file2 = toolCompressAndRotate(originalFile.absolutePath, getExternalFileDir(), "compress_${System.currentTimeMillis()}.jpg", 800, 600)
                    ?: return@post

        }
    }

这里需要做的仅仅是将callback中返回的data存储为File。需要注意的是,data中会包含setRotation()方法中的角度信息,因此如果直接使用Bitmap工具类生成bitmap,再进行存储或者展示,生成出来的图像其实是缺失了旋转角度的原始方向,这十有八九会发生图像展示角度错误的情况。因此,需要直接保存,再通过Exif工具类读取File中的角度信息(当然Exif工具类就是为了读取File中的各种信息而生的,比如拍照时间、经纬度等等)。

总结

基于Camera API,
surfaceview的预览需要setDisplayOrientation(),入参角度与CameraInfo.orientation(传感器偏角)和WindowManager.default.displayOrientation(屏幕旋转角度)两个角度有关。
相机拍照前需要setRotation(),入参角度与CameraInfo.orientation(传感器偏角)和OrientationEventListener返回的orientation(终端自然角度偏角)有关,二者的换算结果就是图像写入偏角,该偏角意味着图像被顺时针旋转该角度就能够回正展示。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272