Android相机开发和遇到的坑

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://xuexuan.blog.csdn.net/article/details/53350551

转载请标明出处:http://blog.csdn.net/xx326664162/article/details/53350551 文章出自:薛瑄的博客

在Android相机开发实际开发过程中遇到了不少问题,在网上找了这些资料,但是感觉如果没有经历过Android相机开发开发,直接看这些还是有点太抽象,建议参考一些代码来学习下面的内容

由于之前没有接触过Android相机开发,所以在整个开发过程中踩了不少坑,费了不少时间和精力。这篇文章总结了Android相机开发的相关知识、流程,以及容易遇到的坑,希望能帮助今后可能会接触Android相机开发的朋友快速上手,节省时间,少走弯路。

一.Android中开发相机应用的两种方式

Android系统提供了两种使用手机相机资源实现拍摄功能的方法,

  • 一种是直接通过Intent调用系统相机组件,这种方法快速方便,适用于直接获得照片的场景,如上传相册,微博、朋友圈发照片等。
  • 另一种是使用相机API来定制自定义相机,这种方法适用于需要定制相机界面或者开发特殊相机功能的场景,如需要对照片做裁剪、滤镜处理,添加贴纸,表情,地点标签等。

这篇文章主要是从如何使用相机API来定制自定义相机这个方向展开的

二.相机API中关键类解析

通过相机API实现拍摄功能涉及以下几个关键类和接口:

2.1、SurfaceView:用于绘制相机预览图像的类,展现实时的预览图像。

  • 普通的view以及派生类都是共享同一个surface的,所有的绘制都必须在UI线程中进行。 surface是指向屏幕窗口原始图像缓冲区(raw buffer)的一个句柄,通过它可以获得这块屏幕上对应的canvas,进而完成在屏幕上绘制View的工作。

  • surfaceview是一种比较特殊的view,它并不与其他普通view共享surface,而是在内部持有了一个独立的surface,surfaceview负责管理这个surface的格式、尺寸以及显示位置。由于UI线程还要同时处理其他交互逻辑,因此对view的更新速度和帧率无法保证,而surfaceview由于持有一个独立的surface,因而可以在独立的线程中进行绘制,因此可以提供更高的帧率。自定义相机的预览图像由于对更新速度和帧率要求比较高,所以比较适合用surfaceview来显示。

2.2、SurfaceHolder:控制surface的一个抽象接口

它能够控制surface的尺寸和格式,修改surface的像素,监视surface的变化等等,surfaceholder的典型应用就是用于surfaceview中。surfaceview通过getHolder()方法获得surfaceholder 实例,通过后者管理监听surface 的状态。

2.3、SurfaceHolder.Callback接口:负责监听surface状态变化的接口,

有三个方法:

  • surfaceCreated(SurfaceHolder holder):在surface创建后立即被调用。在开发自定义相机时,可以通过重载这个函数调用camera.open()、camera.setPreviewDisplay(),来实现获取相机资源、连接camera和surface等操作。

  • surfaceChanged(SurfaceHolder holder, int format, int width, int height):在surface发生format或size变化时调用。在开发自定义相机时,可以通过重载这个函数调用camera.startPreview来开启相机预览,使得camera预览帧数据可以传递给surface,从而实时显示相机预览图像。

  • surfaceDestroyed(SurfaceHolder holder):在surface销毁之前被调用。在开发自定义相机时,可以通过重载这个函数调用camera.stopPreview(),camera.release()来实现停止相机预览及释放相机资源等操作。

2.4、Camera:最主要的类,用于管理和操作camera资源。

它提供了完整的相机底层接口,支持相机资源切换,设置预览/拍摄尺寸,设定光圈、曝光、聚焦等相关参数,获取预览/拍摄帧数据等功能,主要方法有以下这些:

  • open():获取camera实例。

  • setPreviewDisplay(SurfaceHolder):通过surfaceHolder可以将Camera和surface连接起来,当camera和surface连接后,camera获得的预览帧数据就可以通过surface显示在屏幕上了。

  • setPrameters设置相机参数,包括前后摄像头,闪光灯模式、聚焦模式、预览和拍照尺寸等。

  • startPreview():开始预览,将camera底层硬件传来的预览帧数据显示在绑定的surface上。

  • stopPreview():停止预览,关闭camra底层的帧数据传递以及surface上的绘制。

  • release():释放Camera实例

  • takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):这个是实现相机拍照的主要方法,包含了三个回调参数。shutter是快门按下时的回调,raw是获取拍照原始数据的回调,jpeg是获取经过压缩成jpg格式的图像数据的回调。

三.自定义相机的开发过程

定制一个自定义相机应用,通常需要完成以下步骤,其流程图如图1所示:

  • 检测并访问相机资源 检查手机是否存在相机资源,如果存在,请求访问相机资源。
  • 创建预览类 创建继承自SurfaceView并实现SurfaceHolder接口的拍摄预览类。此类能够显示相机的实时预览图像。
  • 建立预览布局 有了拍摄预览类,即可创建一个布局文件,将预览画面与设计好的用户界面控件融合在一起。
  • 设置拍照监听器 给用户界面控件绑定监听器,使其能响应用户操作(如按下按钮), 开始拍照过程。
  • 拍照并保存文件 将拍摄获得的图像转换成位图文件,最终输出保存成各种常用格式的图片。
  • 释放相机资源 相机是一个共享资源,必须对其生命周期进行细心的管理。当相机使用完毕后,应用程序必须正确地将其释放,以免其它程序访问使用时,发生冲突。
    这里写图片描述

四. 开发过程遇到的一些坑

下面再讲讲我在开发自定义相机时踩过的一些坑:

1.相机预览方向适配问题的产生

1.1、相机的安装方向如何获取?

Android官方提供orientation这个属性:表示相机采集的图像顺时针旋转orientation度后才能与到设备自然方向一致。

假设设备是竖屏显示。后置相机传感器是横屏安装的。

这里写图片描述

你面向屏幕时,如果后置相机传感器所采集的图像的上边是在设备自然方向的右边,则后置相机的orientation是90。

如果前置相机传感器所采集的图像的上边是在设备自然方向的右边,则前置相机的orientation是270。

这个值,不同的设备有所差异,但大多数都是这样的值。

1.2、Activity设为竖屏时,SurfaceView预览图像为什么是逆时针旋转90度?

说明这个问题之前,先介绍下Android手机上几个方向的概念:

屏幕坐标:在Android系统中,屏幕的左上角是坐标系统的原点(0,0)坐标。原点向右延伸是X轴正方向,原点向下延伸是Y轴正方向。

自然方向(natrual orientation)

每个设备都有一个自然方向,手机和平板的自然方向不同。Android:screenOrientation的默认值unspecified即为自然方向。
关于orientation的两个常见值是这样定义的:
landscape(横屏):the display is wider than it is tall,正常拿着设备的时候,宽比高长,这是平板的自然方向。
portrait(竖屏):the display is taller than it is wide,正常拿设备的时候,宽比高短,这是手机的自然方向。

图像传感器(Image Sensor)方向:手机相机的图像数据都是来自于摄像头硬件的图像传感器,这个传感器在被固定到手机上后有一个默认的取景方向,如下图2所示,坐标原点位于手机横放时的左上角,即与横屏应用的屏幕X方向一致。换句话说,与竖屏应用的屏幕X方向呈90度角。

这里写图片描述

相机的预览方向:将图像传感器捕获的图像,显示在屏幕上的方向。在默认情况下,与图像传感器方向一致。在相机API中可以通过setDisplayOrientation()设置相机预览方向。在默认情况下,这个值为0,与图像传感器方向一致。

下面是Camera.setDisplayOrientation的注释文档:

/**
 * Set the clockwise rotation of preview display in degrees. This affects
 * the preview frames and the picture displayed after snapshot. This method
 * is useful for portrait mode applications. Note that preview display of
 * front-facing cameras is flipped horizontally before the rotation, that
 * is, the image is reflected along the central vertical axis of the camera
 * sensor. So the users can see themselves as looking into a mirror.
 *
 * <p>This does not affect the order of byte array passed in {@link
 * PreviewCallback#onPreviewFrame}, JPEG pictures, or recorded videos. This
 * method is not allowed to be called during preview.     
 */

public native final void setDisplayOrientation(int degrees);

注释中的第二段,描述了这个API修改的仅仅是Camera的预览方向而已,并不会影响到PreviewCallback回调、生成的JPEG图片和录像文件的方向,这些数据的方向依然会跟图像Sensor的方向一致。

注意:设置预览方向并不会改变拍出照片的方向

  • 对于横屏(这个activity设置的是横屏显示)来说,由于屏幕方向和预览方向一致,预览图像和看到的实物方向一致。
  • 对于竖屏(这个activity设置的是竖屏显示),屏幕方向和预览方向垂直,所以会出现颠倒90度现象,无论怎么旋转手机,显示在UI预览界面的画面与人眼看到的实物始终成90度(UI预览界面逆时针转了90度)。为了得到一致的预览画面,需要将相机的预览方向旋转90(setDisplayOrientation(90)),保持与屏幕方向一致,

如图3所示(红色箭头表示相机预览的x轴方向,蓝色箭头表示屏幕的x轴方向)。

这里写图片描述

看到这里,相信你肯定有疑问,按照这样旋转的话,看到的UI预览图应该是顺时针旋转90度才对啊,但为什么看到的是逆时针旋转90度

举例

下图是MI3手机的屏幕“自然”方向和后置相机的图像传感器方向。

后置相机的orientation是90

这里写图片描述

1、图像传感器获得到图像后,就知道了这幅图像每个坐标的像素值,但是要显示到屏幕上,就要按照屏幕的坐标系来显示,于是就需要将图像传感器的坐标系逆时针旋转90度,才能显示到屏幕的坐标系上,
2、于是看到的图像逆时针旋转了90度,因此需要顺时针旋转90度(setDisplayOrientation(90)),才能与手机的自然方向保持一致。

这里写图片描述

1.3、 前置摄像头的镜像效果

Android相机硬件有个特殊设定,就是对于前置摄像头,

  • 在预览图像是真实场景的镜像

  • 拍出的照片和真实场景一样。

以MI3为例

通过程序调用取到的后置相机的orientation是90,前置相机的orientation是270。

orientation可以理解为图像传感器方向顺时针旋转到屏幕自然方向的角度。

下图所示是MI3手机的后置相机和前置相机对准同一个小人,后置前置相机采集到的图像、前置相机预览的图像。

后置

对于后置相机,相机预览的图像和相机采集到的图像是一样的。只需要旋转后置相机orientation度,即90度即可和屏幕方向保持一致;

前置

对于前置相机来说,相机预览的图像和相机采集到的图像是镜像关系, 注意下图小人头发在前置采集的图像和预览图像中头发是相反的。

因此在MI3手机上做竖屏应用时,

采集的图像:顺时针旋转270度后,与屏幕方向一致

预览的图像:顺时针旋转90度后,与屏幕方向一致.(因为底层相机对采集的图像做了镜像处理,所以只需转90度)

这里写图片描述

1 、图像传感器获得到图像后,就知道了这幅图像每个坐标的像素值,但是要显示到屏幕上,就要按照屏幕的坐标系来显示,于是就需要将图像传感器的坐标系顺时针旋转90度,才能显示到屏幕的坐标系上,于是看到的图像顺时针旋转了90度

2、也就是说前置摄像头获取到的图像(未经镜像处理)需要顺时针旋转270,才能和手机的自然方向一致(上图第二行)

3、但是在预览的时候,做了镜像处理后,只需要顺时针旋转90度,就能和手机的自然方向一致。(如上图第三行):

下图显示了,屏幕的坐标系和前置摄像头的坐标系

这里写图片描述

下图简单显示了,横屏和竖屏的情况

这里写图片描述

此外,由于拍摄图像并没有做水平翻转,所以对于前置摄像头拍出来的照片,用户会发现跟预览时所见的是左右翻转的。可以根据自己的需求进行处理。

1.4、官方推荐的相机预览方向适配做法

通过orientation属性的含义可以知道,我们可以用它和应用的方向来做相机预览方向的适配,下面代码是官方网站推荐的。

但并不是所有手机的orientation值都靠谱,比如VIVO V1手机第一次获取后置相机的CameraInfo的orientation值是90,而当执行了mCamera = Camera.open();之后再获取CameraInfo的orientation值就是0,而且以后获取的都是 0 ,除非重启手机。无论是这款手机上的哪个应用,只要执行了一次Camera.open()之后,其他所有程序中获取CameraInfo的orientation都是是0。

因此按照此方法做适配不能解决所有手机上的问题。

 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的拍照方向

当你点击拍照按钮,得到的图片方向不一定与画面中预览的方向一致,这是因为拍摄的照片是将图像Sensor采集到的数据直接存储到SDCard上的,因此,Camera的拍照方向与上述的Camera的图像Sensor方向一致。

为了演示这个问题,我用手机的Camera对同一个场景拍了两张照片,第一张是横着拿手机拍的,第二张是竖着拿手机拍的。然后用在电脑上打开得到的图片(实际场景中的杯子是竖着的),效果如下所示:

这里写图片描述

由此可见,如果横向拿手机拍照,由于正好与Camera的拍照方向一致,因此得到的照片是“正确”的;而竖着拿手机拍照的话,Camera的图像Sensor依然以上面描述的角度在采集图像并存储到SDCard上,所以得到的图片就是右图这样的,因为竖着拿手机正好与图像Sensor的方向相差了90度。由此,大家应该明白了为什么我们用手机拍出的照片经常需要旋转90度才能看到“正确”的画面了吧?

3. SurfaceView预览图像、拍摄照片拉伸变形

说明这个问题之前,同样先说一下几个跟相机有关的尺寸。

  • SurfaceView尺寸:即自定义相机应用中用于显示相机预览图像的View的尺寸,当它铺满全屏时就是屏幕的大小。这里surfaceview显示的预览图像暂且称作手机预览图像。

  • Previewsize:相机硬件提供的预览帧数据尺寸。预览帧数据传递给SurfaceView,实现预览图像的显示。这里预览帧数据对应的预览图像暂且称作相机预览图像。

  • Picturesize:相机硬件提供的拍摄帧数据尺寸。拍摄帧数据可以生成位图文件,最终保存成.jpg或者.png等格式的图片。这里拍摄帧数据对应的图像称作相机拍摄图像。图4说明了以上几种图像及照片之间的关系。手机预览图像是直接提供给用户看的图像,它由相机预览图像生成,拍摄照片的数据则来自于相机拍

这里写图片描述

下面说下我在开发过程中遇到的三种拉伸变形现象:

  1. 手机预览画面中物体被拉伸变形。
  2. 拍摄照片中物体被拉伸变形。
  3. 点击拍照瞬间,手机预览画面会停顿下,此时的图像是拉伸变形的,然后预览画面恢复后图像又正常了。

现象1的原因是SurfaceView和Previewsize的长宽比率不一致。因为手机预览视图的图像是由相机预览图像根据SurfaceView大小缩放得来的,当长宽比不一致时必然会导致图像变形。

后两个现象的原因则是Previewsize和Picturesize的长宽比率不一致所致,查了相关的资料,发现其具体原因跟某些手机相机硬件的底层实现有关。

总之为了避免以上几种变形现象的发生,在开发时最好将SurfaceView、PreviewSize、PictureSize三个尺寸保证长宽比例一致。具体实现可以先通过camera.getSupportedPreviewSizes()和camera.getSupportedPictureSizes()获得相机硬件支持的所有预览和拍摄尺寸,然后在里面筛选出和SurfaceView的长宽比一致并且大小合适的尺寸,通过camera.setPrameters来更新设置。注意:市场上手机相机硬件支持的尺寸一般都是主流的4:3或者16:9,所以SurfaceView尺寸不能太奇葩,最好也设置成这样的长宽比。

4. 各种crash

这里写图片描述

这里写图片描述

这里写图片描述

前两个Crash的原因是:相机硬件在聚焦和拍照前必须要保证已经连接到surface,并且开启相机预览,surface有收到预览数据。如果在还没有执行camera. setPreviewDisplay或者未调用camera. startPreview之前,就调用camera.autofocus或camera.takepicture,就会出现这个运行时异常。

对应到自定义相机的代码中,要注意在拍照按钮事件响应中执行camera.autofocus或camera.takepicture前,一定要检验camera有没有设置预览Surfaceview并开启了相机预览。这里有个方法可以判断预览状态:Camera.setPreviewCallback是预览帧数据的回调函数,它会在SurfaceView收到相机的预览帧数据时被调用,因此在里面可以设置是否允许对焦和拍照的标志位。

这里写图片描述

还有一点要注意,camera.takePicture()在执行过程中会执行camera.stopPreview来获取拍摄帧数据,表现为预览画面卡住,而如果此时用户点击了按钮的话,也就是调用camera.takepicture,也会出现上面的crash,因此在开发时,可能还需要屏蔽拍照按钮的连续点击。
第三个crash则涉及图像的裁剪,由于要支持1:1或者4:3尺寸镜头,所以会需要对预览视图进行裁剪,由于是竖屏应用,所以裁剪区域的坐标系跟相机传感器方向是成90度角的,表现在裁剪里就是,屏幕上的x方向,对应在拍摄图像上是高度方向,而屏幕上的y方向,对应到拍摄图像上则是宽度方向。因此在计算时要一定注意坐标系的转换以及越界保护。

5. 锁屏下相机资源的释放问题

为了节省手机电量,不浪费相机资源,在开发的自定义相机里,如果预览图像已不需要显示,如按Home键盘切换后台或者锁屏后,此时就应该关闭预览并把相机资源释放掉。

参考官方API文档,

  • 当surfaceView变成可见时,会创建surface并触发surfaceHolder.callback接口中surfaceCreated回调函数。

  • 而surfaceview变成不可见时,则会销毁surface,并触发surfacedestroyed回调函数。

我们可以在对应的回调函数里,处理相机的相关操作,如连接surface、开启/关闭预览。 至于相机资源释放,则可以放在Acticity的onpause里执行。相应的,要重新恢复预览图像时,可以把相机资源申请和初始化放在Acticity的onResume里执行,然后通过创建surfaceview,将camera和surface相连并开启预览。
这里写图片描述

但是在开发过程中发现,对于按HOME键切后台场景,程序可以正常运行。对于锁屏场景,则在重新申请相机资源时会发生crash,说相机资源访问失败。那么原因是什么呢?我在代码里增加了调试log, 检查了代码的执行顺序,结果如下:

在自定义相机页面按HOME键时的执行流程:

  • 程序运行->按HOME键
    Activity调用的顺序是onPause->onStop
    SurfaceView调用了surfaceDestroyed方法
    然后再切回程序
    Activity调用的顺序是onRestart->onStart->onResume
    SurfaceView调用了surfaceCreated->surfaceChanged方法

  • 而对于锁屏,其执行流程则是:
    Activity只调用onPause方法
    解锁后Activity调用onResume方法
    SurfaceView中surfaceholder.callback的所有方法都没有执行

    问题找到了,由于锁屏时,callback的回调方法没有执行,导致相机和预览的连接还没有断开,相机资源就被释放了,所以导致在重新申请相机资源时,系统报crash。根据上面的文档,推测是锁屏下系统并没有改变surfaceview的可见性,于是我尝试在onPause和onResume时通过手动设置surfaceview的visibile属性,结果发现可以正常触发回调函数了。由于在切后台或者锁屏时,用户本来就应该看不到surfaceview,因此这种手动更改surfaceview的可见性的方法,并不会对用户的体验造成影响。

这里写图片描述

参考:

Android相机开发那些坑

Android开发实践:掌握Camera的预览方向和拍照方向

Android 开发之解决相机预览上下颠倒问题

【腾讯优测干货分享】Android 相机预览方向及其适配探索

关于Android Camera几点须知

关注我的公众号,轻松了解和学习更多技术
这里写图片描述

展开阅读全文

没有更多推荐了,返回首页