惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
The Last Watchdog
The Last Watchdog
P
Proofpoint News Feed
C
Cybersecurity and Infrastructure Security Agency CISA
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
S
Schneier on Security
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
I
Intezer
A
Arctic Wolf
IT之家
IT之家
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cisco Blogs
AWS News Blog
AWS News Blog
博客园 - 三生石上(FineUI控件)
C
CXSECURITY Database RSS Feed - CXSecurity.com
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
The Exploit Database - CXSecurity.com
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence
D
Docker
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
MongoDB | Blog
MongoDB | Blog
B
Blog
博客园 - 叶小钗
V2EX - 技术
V2EX - 技术
Simon Willison's Weblog
Simon Willison's Weblog
MyScale Blog
MyScale Blog
Hugging Face - Blog
Hugging Face - Blog
Engineering at Meta
Engineering at Meta
NISL@THU
NISL@THU
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
Stack Overflow Blog
Stack Overflow Blog
N
Netflix TechBlog - Medium
The GitHub Blog
The GitHub Blog
V
V2EX
PCI Perspectives
PCI Perspectives
N
News | PayPal Newsroom
V
Visual Studio Blog
Vercel News
Vercel News
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
J
Java Code Geeks
O
OpenAI News
爱范儿
爱范儿

音视频开发进阶

音视频教程-第三节 音视频教程-第二节 真的,AI 可能就是新时代的信息差 充值 Cursor 之后,工作有了哪些变化?🤔 个人'蒸馏'大模型能做哪些有意思的事情 DeepSeek 大模型在 Mac 上的部署和运行 音视频教程-第一节 【WebRTC 专栏】-- Android 开发集成 WebRTC 库的几种方式 【WebRTC 专栏】-- 在 Mac M1 等系列芯片编译和开发 WebRTC-Android 库 Meta Llama3 大模型在 Mac 上的部署和运行 iOS VideoToolBox 解码 HEVC Open-GOP 视频的问题排查 Flutter 状态管理之 InheritedWidget 使用和分析 用 ChatGPT 回答技术问题怎么样 ? 音视频开发系统入门大致路线 UE 4.27 添加自定义 ShadingModel 用 UE4 虚幻引擎做个捏脸小功能~~ UE4 材质练习 之 凹凸贴图偏移的使用 UE4 材质练习系列基础 OpenGL上下文创建以及共享机制 007 | 播放器系列专栏-解析 MP4 文件读取信息 006 | 播放器系列专栏-在 Mac 上查看 MP4 格式信息 干货 | 快速抽取缩略图是怎么练成的? 关于直播、WebRTC、FFmpeg 的那些事 005 | 播放器系列专栏-在 Windows 上查看 MP4 格式信息 将音视频中的花屏、绿屏、黑屏问题一网打尽 关于音视频里面的解码帧率和渲染帧率 004 | 播放器系列专栏-认识MP4视频(下) 003 | 播放器系列专栏-认识MP4视频(上) 入门或者转行音视频,应该要怎么做? H264视频文件如何缩放分辨率 002 | 播放器系列专栏-FFmpeg依赖库的配置 001 | 播放器系列专栏-关于播放器项目的一个小实践 Seek策略以及在有B帧情况下的处理 目前流媒体开发工程师工作内容主要是什么? 一个音视频领域专业问答的小圈子 干货收藏 || Vulkan Game Engine 视频教程 音视频春节假期内卷指南(实操) Vulkan 在 FFmpeg 中的支持 Windows 下 FFmpeg 和 LibX264 的编译和配置 Metal 开发 | 使用 C++ 进行接口调用 音视频开发工作经验分享 || 视频版 FFmpeg 调用 MediaCodec 硬解码到 Surface 上 代码吸猫 | 用 OpenGL 图像渲染的养猫计划 百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!! 老生常谈-FFmpeg 的编译问题轻松搞定 FFmpeg 调用 Android MediaCodec 进行硬解码(附源码) 【WebRTC 专栏】--创建相机预览 Unity Shader 光照基础之 Half Lambert 光照模型 Unity Shader 光照基础之Lambert光照模型 Unity Shader 光照基础内容 Unity Shader 显示一张图片纹理 UnityShader 的基本概念 Unity 物体的基本操作 C++ 模板系列小结07-尾置返回类型 C++ 模板系列小结06-可变参数模板特性 C++ 中的多线程的使用和线程池建设 C++ 模板系列小结05-模板类型作为模板参数 C++ 模板系列小结04-类模板中的成员模板 C++ 模板系列小结03-在模板中指定变量类型 C++ 模板系列小结02-非类型模板参数 C++ 模板系列小结01-函数模板和类模板 从零打造渲染引擎系列01-什么是渲染引擎 iOS开发 - 在 Swift 中去调用 C/C++ 代码 2021 技术新番 - 从零打造渲染引擎系列 iOS 音视频开发的一些基础准备工作 音视频交流群又来啦~~~ 【WebRTC 专栏】WebRTC & Android 开发学习环境搭建~ 【喜大普奔】域名终于备案通过啦 Shader 优化 | OpenGL 绘制网格效果 【音视频连载-011】第二季 FFmpeg 一层一层获取文件信息 KodeLife | Shader 实时编辑预览的强大工具使用实践 推荐几个堪称教科书级别的 Android 音视频入门项目 【音视频连载-010】第二季 FFmpeg 日志打印 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下) 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上) 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示 【音视频连载-004】基础学习篇-SDL 加载图片并显示 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置 LearnOpenGL 源码在 MAC 上的编译与调试 2019 年终总结与回顾 Android NDK 开发的免费技术视频来啦~~ OpenGL 实现视频编辑中的转场效果 OpenGL 实践之贝塞尔曲线绘制 图像库 libjpeg-turbo 编译与实践 图像库 libpng 编译与实践 rust 开发编译 Android 动态库实践 Android NDK 开发 —— 从 Assets 文件夹加载图片并上传纹理 简单易用的图像解码库介绍 —— stb_image 博客图床迁移记 进击的 Vulkan 移动开发之 SwapChain 进击的 Vulkan 移动开发之 Command Buffer 进击的 Vulkan 移动开发之 Instance & Device & Queue 进击的 Vulkan 移动开发(一)之今生前世 Java 显式锁 Lock 与条件队列 C++ 标准容器库小结 一文读懂 YUV 的采样与格式 《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
Android 相机开发中的尺寸和方向问题
音视频开发进阶 · 2018-04-10 · via 音视频开发进阶

一个专注音视频领域的小圈子

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 在 Android Camera 开发中,两个比较闹心的问题就是尺寸和方向了。

其中尺寸指的是:

  • 相机显示预览帧的尺寸
  • 相机拍摄帧的尺寸
  • Android 显示相机预览内容的控件尺寸

而方向指的是

  • 相机显示预览帧的方向
  • 相机拍摄帧的方向
  • Android 手机自身的方向

在开发中要处理好这三个方向和三个尺寸各自的关系才行,这里以 Camera 1.0 版本的 API 作为示例,参考了 Google 的开源项目:cameraviewandroid-Camera2Basic

尺寸


相机作为硬件设备,可以提供两类尺寸:

  • 预览帧尺寸
  • 拍摄帧尺寸

预览帧尺寸

通过 getSupportedPreviewSizes 方法可以得到支持的预览帧的尺寸集合。

		private final SizeMap mPreviewSizes = new SizeMap();
        mCamera = Camera.open(mCameraId);
        mCameraParameters = mCamera.getParameters();
        // Supported preview sizes
        mPreviewSizes.clear();
        for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
            mPreviewSizes.add(new Size(size.width, size.height));
        }

mPreviewSizesSizeMap 类型,查看源码,实际上就是在添加预览帧尺寸的长和宽时,还计算了他们的长宽比,并保存了起来,存储长宽比的结构可以是一对多的关系,也就是长宽比相同,长和宽的尺寸可以有多种,只要他们最后约分后的比例相同。

    // 同一个长宽比,对应多个尺寸
    private final ArrayMap<AspectRatio, SortedSet<Size>> mRatios = new ArrayMap<>();

比如,尺寸是 1920 * 1080 和 1280 * 720 的长宽比都是 16 : 9,而尺寸是 800 * 600 和 640 * 480 的的长宽比都是 4 : 3。

在这里说法是 长宽比,也有说话是 宽高比 的。实际上都是相同的,都是将手机横放时的,较长的那一边比较短的那一边的值。

在计算长宽比时,需要求出宽和高数值的最大公约数,这样才能进行约分计算,根据欧几里得算法,又叫做辗转相除法:两个整数的最大公约数等于其中较小的那个数和两个数相除余数的最大公约数。转换成代码如下:

	// a > b
    private static int gcd(int a, int b) {
        while (b != 0) {
            int c = b;
            b = a % b;
            a = c;
        }
        return a;
    }

拍摄帧尺寸

通过 getSupportedPictureSizes 方法可以得到支持的拍摄帧的尺寸集合。

        // Supported picture sizes;
        private final SizeMap mPictureSizes = new SizeMap();
        mPictureSizes.clear();
        for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
            mPictureSizes.add(new Size(size.width, size.height));
        }

存储结构和预览帧相似,在得到尺寸集合时,也计算了它们对应的长宽比。

而 Android 显示相机预览内容的控件尺寸,在控件对应的方法中可以拿到它的 Width 和 Height 。

计算宽高比

有了这三类尺寸,接下来就是要如何处理了。

为了在预览和拍摄时,图像不会出现拉伸现象,预览帧的长宽比最好和显示控件的长宽比一致,并且拍摄帧的长宽比也和预览帧和显示控件的长宽比一致,总之三者的长宽比最好是一致的,才会有最好的预览和拍摄效果

因为手机预览控件的图像 是由 相机预览帧 根据 控件大小 缩放得来的,当长宽比不一致时必然会导致预览图像变形。而预览帧的长宽比和拍摄帧的长宽比不一致的话,又会导致拍摄的图片变形拉伸。

cameraview 的源码中,首先设定了默认的宽高比为 4 : 3 。

AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(4, 3);

根据这一长宽比,可以从预览帧的尺寸集合中得到那些符合的尺寸列表,再从那些尺寸列表中找到宽和高都刚好大于预览控件的宽高的。若是小于预览控件的宽高则会导致图像被拉伸了。

        SortedSet<Size> sizes = mPreviewSizes.sizes(mAspectRatio);
        if (sizes == null) { // Not supported
            mAspectRatio = chooseAspectRatio();
            // 根据选定的长宽比得到对应的支持的尺寸集合
            sizes = mPreviewSizes.sizes(mAspectRatio);
        }
        // 和预览控件的尺寸相比较,从尺寸集合中找到合适的尺寸
        Size size = chooseOptimalSize(sizes);
        // 把找到的合适尺寸,设置给相机的预览帧
        mCameraParameters.setPreviewSize(size.getWidth(), size.getHeight());

具体找到合适的预览帧尺寸大小的代码如下:

    private Size chooseOptimalSize(SortedSet<Size> sizes) {
        if (!mPreview.isReady()) { // Not yet laid out
            return sizes.first(); // Return the smallest size
        }
        int desiredWidth;
        int desiredHeight;
        // 预览界面的尺寸
        final int surfaceWidth = mPreview.getWidth();
        final int surfaceHeight = mPreview.getHeight();
        // 是否是横屏,若是横屏的话,宽和高相互调换
        if (isLandscape(mDisplayOrientation)) {
            desiredWidth = surfaceHeight;
            desiredHeight = surfaceWidth;
        } else {
            desiredWidth = surfaceWidth;
            desiredHeight = surfaceHeight;
        }

        // 从选定的长宽比支持的尺寸中,找到长和宽都大于或等于控件尺寸的
        Size result = null;
        for (Size size : sizes) { // Iterate from small to large
            if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {
                return size;
            }
            result = size;
        }
        // 实在没有符合条件的,选择支持尺寸中最大的返回。
        return result;
    }

注意到,当屏幕处于横屏模式式,预览控件的宽和高就发生变换了,要相互调换。

找到合适的预览帧的尺寸后,就可以设置给相机了。

而相机拍摄帧的尺寸,也是要根据长宽比来选定。

        final Size pictureSize = mPictureSizes.sizes(mAspectRatio).last();

在宽高比一定的情况下,拍摄帧往往选择尺寸最大的,那样拍摄的图片更清楚,这也是为什么最后使用 last 方法。

这样一来,在确定好了宽高比的情况下,就可以设置对应的尺寸了。

在 Google 的 android-Camera2Basic 工程中,也有这样一段设置尺寸的代码,不同的它是根据拍摄的图片的最大尺寸确定好了长宽比,而不是默认选择普遍的 4 : 3 的比例,之后在此基础之上才进行设置。

       Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
                        new CompareSizesByArea());

可以看到,在相机中设置宽高比还是非常重要的一个环节。

方向


搞定了尺寸问题,还剩下方向了。

相机有两种方向需要处理:

  • 预览帧方向
  • 拍摄帧方向

为了获得更好的相机体验,要处理好预览帧和拍摄帧的方向,保证通过手机屏幕看到的内容都是方向正常的。

首先要明确手机的自然方向:

  • 当手机屏幕 竖立时的自然方向,此时,坐标原点位于左上角,向右为 X 轴正方向,向下为 Y 轴正方向,宽比高短
  • 当手机屏幕 横放时的自然方向,此时,坐标原点位于左上角,向右为 X 轴正方向,向下为 Y 轴正方向,宽比高长

预览帧方向

而相机的图像数据是来自相机硬件图像传感器的,传感器被固定在手机上后有一个默认的取景方向:坐标原点位于手机逆时针横放时的左上角,即与横屏应用的屏幕 X 方向一致。也就是与竖屏应用的屏幕 X 方向呈 90 度角。

这里盗图几张:

back_camera_coordinate

所以,对于横屏应用来说,屏幕的自然方向和相机的图像传感器方向一致,因此看到的图像是正的。而对于竖屏应用来说,预览图像就侧过来了。需要将预览图像顺时针旋转 90 度角才可以正常预览图像。

横屏拍摄结果:

landscape_camera

竖屏拍摄结果:

portrait

关于相机的预览方向和屏幕自然方向存在 90 度角的偏差,在 Camera 的 orientation属性中也有说明:

camera_orientation_description

orientation 表示相机图像的方向。它的值是相机图像顺时针旋转到设备自然方向一致时的图像,它可能是 0、90、180、270 四种。

对于竖屏应用来说,后置相机传感器是横屏安装的,当你面向屏幕时,如果后置相机传感器顶边和设备自然方向的右边是平行的,那么后置相机的 orientation 是 90。如果是前置相机传感器顶边和设备自然方向的右边是平行的,则前置相机的 orientation 是 270 。

对于前置和后置相机传感器 orientation 是不同的,在不同的设备上也可能会有不同的值。

在没有限定 Activity 方向时,采用官方推荐的代码来设置方向:

  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);
    }

首先计算得到设备逆时针旋转的角度,对于后置摄像头传感器的计算:(info.orientation - degrees + 360) % 360 。

因为摄像头图像方向要恢复到自然方向需要顺时针旋转,而屏幕逆时针旋转正好抵掉了摄像头的旋转,所以两者相减,然后再加上 360 取模运算。

对于前置摄像头传感器,因为在使用前置摄像头时,从屏幕竖直方向看到的往往是一个镜像,这是因为摄像头硬件对图像做了水平翻转,也就是将图像内容对着竖直方向对调了,相当于预先旋转了 180 度。之后再只需要旋转 90 度就可以到自然方向了,只不过是个镜像,即左右翻转了。

需要注意的一点是,在 API 14 之前,调用 setDisplayOrientation 方法时要先关闭预览。

最后盗图更清晰明了一下:

拍摄帧方向

确定了预览时的方向,还需要确定拍摄时的方向。

通过 Camera.Parameters.setRotation 函数可以设置相机最终拍出的图片方向。

官方的推荐代码:

    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);
    }

计算旋转的方向,需要考虑到当前屏幕的方向和相机的方向。

OrientationEventListener 和 Camera.orientation 一起配合使用。当屏幕方向改变时,OrientationEventListener 会收到相应的通知,在 onOrientationChanged 的回调方法中去改变相机的拍摄方向,实际上在相机预览方向的改变也是在该回调方法中进行的。

onOrientationChanged 方法的返回值是从 0 ~ 359。而 setRotation 的值只能是 0、90、180、270。所以需要对屏幕方向的 orientation 做一个类似四舍五入的操作。

当然也可以在此回调中根据 Display 类的 getRotation 方法得到方向就行,总之就是有一个回调的通知,然后在此改变屏幕拍摄和预览的方向。

对于前置摄像头,摄像头的 orientation 和屏幕方向的 orientation 两个之差即为要旋转的角度;对于后置摄像头,两者之和即为要旋转的角度。

到这里,就对摄像头开发中的尺寸和方向设置有个更清晰的认识了。

参考


  1. https://blog.csdn.net/Tencent_Bugly/article/details/53375311
  2. https://blog.csdn.net/daiqiquan/article/details/40650055
  3. https://zhuanlan.zhihu.com/p/20559606
  4. http://javayhu.me/blog/2017/09/25/camera-development-experience-on-android/

原创文章,转载请注明来源:    Android 相机开发中的尺寸和方向问题