预览图片前置后置角度?别傻傻弄不清|android相机角度解析

2,527 阅读20分钟

前言

很高兴遇见你~

很长一段时间没有写博客了,最近这段时间也真的是比较忙。

这篇文章的话,和之前的面试热点知识不同,可能做相机开发的同学比较感兴趣。相信做相机开发的时候都会遇到过这个问题,幸幸苦苦做出来的demo,显示出来的却是这样:

出现了显示方向屏幕方向不对应的情况。但是,当我们旋转一下手机:

诶?显示方向居然没问题了?这是怎么回事?

针对这个问题,官方也提供了接口给我们处理:setDisplayOrientation()和setRotation()

文章涉及到的相机api,均为camera1,因为camera1的接口更加简单,会更容易理解。

这两个接口官方也给我们做了注释,同时,网络上也有很多的博客对此进行了解析。甚至我们都不需要理解,直接把代码copy一份,就完美解决这个问题了。但看完了注释以及相关博客,依旧似懂非懂,特别是对于前置摄像头的旋转角度问题,更是无法理解,甚至很多的解析都是自相矛盾。在我思考了多天解决这个问题后,写成这篇文章,希望对后来遇到此问题的同学有所帮助。

文章主要的内容是如何将相机的数据以正确的方向显示和存储,包括预览和拍照。文中仅涉及到极少相关的系统相机api,侧重于讲解原理机制,没有开发经验但感兴趣的同学,也非常建议阅读。如果是急于解决问题的开发者,可以划到最后直接cv代码。

那,我们开始吧。

相机显示基础

在讲如何解决这个问题之前,首先要做一些知识的铺垫,才能帮助我们从本质上去理解他。只有掌握了本质,才能以不变应万变,任何相机角度的问题,都可以手到擒来。

屏幕方向(手机自然方向)

屏幕方向其实很好理解,但也非常重要。屏幕方向,就是垂直于地面向上,坐标的原点永远在当前屏幕的左上角。例如 :

  • 手机横置的时候:屏幕的方向向上,坐标在左上角

  • 手机竖直的时候:

这个方向也称为手机的自然方向。后续我们将用【自然方向】来表示我们此处讨论硬件屏幕方向,读者要记住。

摄像头方向

手机的自然方向,虽然我们手机的旋转始终竖直向上,这在我们看来是合理的。但是相机设备则不会了。摄像头的方向并不是随着我们手机的旋转始终向上,他的方向是固定的。如下:

  • 当我们的手机横置的时候:其中的箭头表示摄像头的方向

  • 当我们的手机竖直的时候:摄像头的方向是向右的,他不会随着手机的旋转保持向上。

到这里就发现区别了吧,两个方向是不同的。

  • 手机的自然方向:随着手机设备的旋转始终向上
  • 摄像头方向:固定不变,随着手机设备的选择而旋转

摄像头的安装方向

既然摄像头的方向不会改变,那么他被安装到手机上的时候,方向就是被固定的了。主流的手机安装方向是:朝向手机竖直握持的右边。如下图:

诶?为什么在竖直握持的右侧而不是向上呢?这样你把手机逆时针旋转90度,是不是手机的自然方向和摄像头方向就一致了啦?这个问题我们后面会详细展开。这里只需要知道每个摄像头在手机上都有安装方向的,主流的方向在竖直握持的右侧,也就是有些非主流的,安装在左侧也完全没问题。

屏幕显示与相机取景原理

虽然写的是原理,但是很简单,并不复杂。

屏幕的显示原理很简单,把一帧或者一张照片的像素,从上到下一行行地显示。这里屏幕只认准两个方向:手机的自然方向和照片的方向,他不会去管照片的内容是哪个方向。例如:

屏幕他非常“傻”,他只会把拿到的像素一行行地显示。

相机取景得到的数据结果是一帧或一张照片数据,照片的方向是摄像头的方向,如下图:

  • 相机方向向右进行拍照,景物以正常的方向被相机取景
  • 相机取景后的成片方向保存为相机的方向,也就是向右
  • 当我们让照片的方向垂直向上时,需要将照片逆时针旋转90度

摄像头中没有传感器,所以他很傻,他不知道这个时候自己是已经顺时针旋转了90度,所以保存的照片的方向和内容的真实方向并不是一致的

然后我们把屏幕的显示和摄像头的取景原理结合起来,屏幕拿到摄像头给的数据,会按照手机的自然方向和照片的方向一致的角度进行显示,换句话说,就是让照片的方向垂直向上。而如果内容的真实方向和照片方向不一致,那么就会出现显示方向错乱的问题了。而前面我们讲到,主流的摄像头都是安装在竖直握持的屏幕右侧,当我们逆时针旋转设备90度时,刚好,摄像头的方向和景物方向一致,那么最终的显示方向,就是正常。

这,就是相机预览显示与成片方向错乱的本质原因

到这里是否有一种恍然大悟的感觉。别急,还有另一个非常重要的问题需要了解,他将是理解前置摄像头成片角度的核心要点,也是我思考了这么多天一直忽略的问题。

拍摄角度问题

之前有一张非常有趣的图片,有助于我们理解拍照角度问题:

图片来源于网络

  • 图中两个人看食物的角度不同,看到的内容也不同。

同样,我们看到的,和摄像头看到的,也许是不同的。 如果我们使用后置摄像头,那么当我们手持手机进行拍照的时候,摄像头看到的,和我们眼睛看到的是一致的。而如果使用前置摄像头,那就完全不同了。

前置摄像头是相对于我们从前往后排,而我们是从后往前看,正如上面图片中的两个人,看的角度是完全不同的。所以我们看到的,和摄像头拍到的,是一个镜像的关系。如下图:

笔者太菜了不会画3d图,读者自己想象一下吧。。有什么3D绘图的工具也可以评论区推荐一下。

  • 摄像头从后往我们这边看,看到的东西和我们看到的是镜像的。

到这一步可以理解前置和我们的镜像关系了,我们继续。

前面我们讲到摄像头的方向都是安装在竖直握持的屏幕右侧,前置摄像头也是一样。但是!!!注意,这里非常重要:生成的照片却是向左,而不是和后摄一样的朝右

昂?不是说照片的方向和摄像头的方向一致?怎么前置就不一样了?

首先,这里的【照片的方向和摄像头的方向一致】是有一个前提的,这个前提就是:【我们观察的方法必须和摄像头的取景方向一致】。而前置摄像头和我们的观察角度是不同的

前置摄像头是从后往前拍,我们以为他是在屏幕右侧,但这是从我们的角度来看的。如果从摄像头的角度来看,他其实是在屏幕的左侧 ,并不是我们所看到的右侧。可能有点不好理解哈,我再用两个图解释一下:

  • 我们从前面看,前置是安装在右侧的,如下图:

  • 但是从后往前看,顺着摄像头的取景方向,其实是在左侧:

这样应该好理解一点了吧(不理解我也莫得法子咯)。

所以前置摄像头,我们看着以为他在右侧,其实他在左侧

此外还有一个很重要的问题:如果是后摄,当我们顺时针旋转设备的时候,摄像头也跟着顺时针旋转;而如果是前摄,如果我们顺时针旋转设备,摄像头是逆时针旋转。 这点非常的重要,后续的角度计算都是基于这个结论。

这个结论有了前面的铺垫,应该也很好理解。如果还是不好理解,建议拿着自己的手机,跟我一样弄个笔,然后顺时针旋转,再换个角度观察,立刻秒懂。

小结

好了,到这里相机基础内容就讲得差不多,或者说我们今天的核心内容已经讲得差不多了。而接下来,就是如何根据系统api,结合我们上面讲到的结论,让内容正确显示到屏幕上。

如何解决显示方向问题

系统api相关

这里我们首先要明确一个问题,需要解决方向有两个:拍照生成的图片方向和预览的方向。

  • 拍照生成的图片是指按下快门之后,保存在文件中的图片的方向。
  • 预览是我们在拍照前所看到的画面

这两者的旋转有不同的方法,且两者参数不互相影响。也就是,我给预览设置了顺时针旋转90度,但是生成的图片的方向是不受影响的。这点需要注意。而且这两者需要旋转的角度,在前置摄像头下,是不同的,后续会讲到。

  • 给预览设置旋转的方法是:camera.setDisplayOrientation(int orientation),参数必须是0、90、180、270其中的一个,下同。表示预览画面顺时针旋转的角度。注意在android4.4以下的版本需要先停止预览,再设置角度
  • 给图片设置旋转的方法:Parameters.setRotation(int rotation) ,让图片顺时针旋转的角度

接下来我们需要做的就是,如何通过系统提供的参数,计算出预览和图片需要旋转的角度。

两个关键参数

如何正确显示摄像头获取的数据,需要两个相关的参数:

  • 摄像头是如何安装的
  • 当前设备旋转了多少角度

获取这两个数据之后,通过计算可以得到让预览画面旋转的角度和让照片旋转的角度来让其显示正常的角度。

通过系统api我们可以得到两个参数:

  • activity.getDisplay().getRotation()

    获取当前手机设备逆时针旋转的角度。这里官方api解释是屏幕显示顺时针旋转的角度,这应该比较好理解。当我们逆时针旋转设备90度,那么屏幕为了保持竖直向上就需要顺时针旋转90度。但是这里理解为设备逆时针旋转90度,可以帮助我们后续的理解。

    同时注意,这里所有的旋转,包括后续的其他旋转,都只有4个角度:0、90、180、270。这其实也不难理解,我们要把手机旋转90度,屏幕才会旋转嘛。如下图:

    细心的读者可能会发现,getRotation方法返回的是0、1、2、3四个其中之一,而不是0、90、180、270。是的,没错,还需要再加一层的转换。但文章为了方便叙述,就直接认为是这四个角度了,读者要分辨一下。

  • cameraInfo.orientaion

    当我们竖直握持设备的时候,拍摄出来的照片让其正向需要顺时针旋转的角度。也可以理解为,手机竖直握持的方向和摄像头的安装方向的顺时针夹角。为什么这两者是相等的?先看下图:

    拿90度举例子,摄像头获取的图片的方向是摄像头的方向,要和自然方向契合就必须逆时针旋转90度,那么,生成的图片就必须顺时针旋转90度才能正确显示。前面我们分析了很多,这点应该不难理解。

后置摄像头的适配

后置摄像头的适配非常简单。我们先明确两个参数对于我们的意义:

  • cameraInfo.orientaion:相机安装方向与屏幕方向的夹角,一般情况下靠右安装为90度 。(a)
  • activity.getDisplay().getRotation():手机设备逆时针旋转的角度,屏幕顺时针旋转的角度(b)

当我们逆时针旋转设备的时候,摄像头也跟着逆时针旋转,相机得到的图片会进行逆时针旋转。那么最终的结果就是两者之差求360度的正相位。

当我们竖直握持设备与设备逆时针旋转90度时的情况如下图:(左边是竖直握持手机,相机方向朝右;右边是设备逆时针旋转90度,相机方向向上)

这样我们就可以更好的理解旋转摄像头和图片本身的旋转角度问题。

那么需要旋转的角度:orientation = (a-b+360)%360

其中 a-b 表示需要顺时针的角度,然后加上360取正相,最后求余即可。对于后置来说,预览和图片需要旋转的角度是一致的,因为后置摄像头没有对预览进行其他的操作。

前置摄像头的适配

前置摄像头有一个关键是:当我们顺时针旋转设备的时候,对于我们而言是顺时针,对于摄像头而言是逆时针旋转 ,这在前面讨论过。所以我们首先还是先来明确参数的意义:

  • cameraInfo.orientaion:相机安装方向与屏幕方向的夹角,一般情况下靠右安装,前置为270度 。(a)
  • activity.getDisplay().getRotation():设备已经逆时针旋转的角度,屏幕顺时针旋转的角度,也就是摄像头顺时针旋转的角度。(b)

摄像头顺时针旋转会导致得到的图片也跟着顺时针旋转,因为最终需要旋转的角度就是这两者之和求360的余数。

需要旋转的角度:orientationPicture = (a+b)%360

这个结论对于生成的图片来说,是没有问题的,因为默认情况下,拍照图片数据没有进行镜像处理。但是预览和旋转之前,将显示在屏幕上的图像进行一次镜像翻转。也即,竖屏拍摄时(或者说旋转角度为90或者270时),需要对旋转角度求360的反相。预览需要旋转的角度是:orientationPreview = (360-orientationPicture)%360,也就是orientationPreview = (360-(a+b)%360)%360

这里直接对所有角度进行求反相的原因在于,0和180求反相依旧是本身。

到这里关于适配的内容就讲完了,后半部分的内容是以前半部分为前提的。如果直接看到后半部分,可能会觉得不知所云,这是正常的,这时候建议先去阅读前面的内容。

渲染与算法配置

前面的配置中,对于采用GLSurfaceView,或者TextureView直接上屏,显示上是没有问题的。同时对于拍照数据,直接存储到磁盘中进行读取与显示,也是没有问题的。

在渲染与算法业务流程中,我们获取相机帧数据之后,并不是直接上屏显示,而是需要经过一系列的渲染处理,例如美颜、滤镜等,再上屏。此时获取的帧数据,在旋转方向上,与前面的设置的内容是有所不同的。这里针对渲染与算法业务流程,对旋转计算进行单独的解析。

预览

获取相机帧数据,有两种方式:

  1. 通过给相机设置surfaceTexture,数据类型为GPU纹理数据,一般用于渲染
  2. 通过onPreviewFrame函数回调,获取的是CPU的yuv数据,一般用于算法

上面两种数据,也可以互相转换,即CPU<-->GPU之间的数据转化。当我们获取到这些数据时,图像并没有按照我们设置的角度进行纠正,而是给了我们一个原始的图像和一个角度数据,我们需要根据角度数据自行进行纠正:

  1. 纹理数据,旋转角度存储在Matrix中,通过surfaceTexture.getTransformMatrix()方法可以获取到UVMatrix,此矩阵保存了旋转、镜像等处理。
  2. yuv数据,onPreviewFrame函数回调并没有附带旋转函数,我们需要利用前面我们计算的预览旋转角度。

到此,如何获取数据以及旋转方向的获取都知道了。对于后置摄像头,上述的旋转角度可以直接使用,并没有问题,但在前置摄像头中,存在一个镜像问题,会导致存在一个巨大的坑。

前置特殊处理:

当我们初始化Matrix时,旋转角度与镜像反转是会相互影响,可以理解为,先进行镜像翻转,再进行旋转。这也是为什么,我们在前置设置预览旋转角度时,需要再求360度的反相。

但前面两种方式获取到的图像,是没有经过任何旋转与镜像处理,直接从相机获取到的原始图像。

因此如果不需要镜像处理,将图像转正,则需要将我们设置给相机的预览旋转角度displayOrientation进行求360度的反相。 渲染处理中,通过纹理数据获取到的Matrix无需进行任何额外计算。

但是注意,如果我们需要从旋转角度来计算Matrix,需要额外注意镜像与旋转角度之间的相互影响。例如从onPreviewFrrame获取到YUV数据并进行矩阵转化,此时无需对·displayOrientation`进行求360度反相,原因是两者在矩阵中会互相影响,相当于先镜像再旋转。

拍照

拍照之后获取到的为jpeg数据,我们前面给相机设置的pictureRotation会存储在其exif数据中。jepg分为两个部分,exif额外信息数据与图像数据,而在其exif数据中会记录图片数据需要旋转多少度才能使图片正向。

存储在图像数据的图像分为两种情况:已经旋转完成的图像,其exif::orientation为0;原始图像,没有经过处理,其orientation不为0。不同的机型可能出现不同的情况,例如鸿蒙系统手机为前者,小米手机为后者。

这也是为什么这里我们不能使用前面设置给相机的pictureOrientation角度,而必须使用jepg的exif旋转角度。

注意,对于前置摄像头,拍照的图片均没有进行镜像处理

一点思考

不知道有没有读者跟我一样的想法:为啥摄像头要安装在屏幕的右侧,直接安装在顶边不就没有这么多角度问题了吗?即使手机旋转了,我们也只需要处理一个设备的旋转角度问题,不是更好吗?

诶!好像确实如此吼?的确,如果和竖直握持手机的自然方向安装摄像头,确实少了很多的麻烦,因为我们不需要去关心cameraInfo.orientation的值。这里我思考了一下猜测:历史遗留原因和兼容。

安装在屏幕的右侧好处是:当我们横向拍照的时候,不需要去处理方向问题,也就是设备逆时针旋转90度。相机设备都是这种横屏的状态、可以带来更广的水平视野,更加符合我们的肉眼正常视野。所以摄像头硬件以及手机硬件的设计都是按照横向来进行设计。如果现在需要更改这个摄像头的安装方向,那么更改的是整个供应链的设计标准,显然不可能。

第二个是兼容不同的设备。android设备和iphone不同,没有办法做到统一。A厂商摄像头安装在右侧,那么B厂商完全可以安装在左侧。为了兼容不同的设备情况,android提供了orientation参数来让开发者去适配这些情况。

当然,这只是我的个人推测。

读者可以发现我前半段的知识铺垫写的非常多,后面的解决方案就很快了。这其实有点类似于基础知识和上层运用的关系。如果对于这些角度的变换问题无法掌握,那么即使知道如何解决这个问题,也是无法从本质上去理解到底为什么。这其实是一个基础知识和上层知识关系的很好例子。抓住原理和本质,那么上层如何变换,也是手到擒来。

所以,要打好基础呀。

总结

屏幕显示内容或图片的方向错位,源于手机的自然方向与摄像头方向不同。我们通过屏幕的旋转角度和生成的图片需要顺时针旋转的角度cameraInfo.orientaion(一般前置为270,后置90),计算出图片和预览需要旋转的角度,再设置给相机即可。

  • cameraInfo.orientaion:a

  • activity.getDisplay().getRotation():b【注意这里的rotation需要转化成0、90、180、270,这里为了叙述方便去掉转化】

  • 后置摄像头:

    • orientation = (a-b+360)%360
  • 前置摄像头

    • 图片:orientationPicture = (a+b)%360
    • 预览:orientationPreview = (360-(a+b)%360)%360
  • 渲染与算法

    • 获取surfaceTexture的matrix直接使用
    • 通过displayOrientation直接转化为matrix,注意矩阵中翻转与旋转的影响
    • 图片需要获取jepg的EXIF数据,获取真实的图片旋转角度
    • 获取yuv数据,对displayOrientation要进行求360度反相

好了,那么这片文章就到这里。如果文章对你有帮助,还希望能够留下一个赞鼓励一下作者,会让我开心一整天的 : )