Camera 在音视频的世界中, 就像我们的眼睛一样, 是负责图像采集的入口, 要想实现一款高可用的 Camera, 我认为需要关注以下几个方面
OpenGL ES 主要用于图像的渲染, 因此这里主要聚焦于相机的预览, 想要让相机的数据呈现到我们屏幕上, 主要有如下几个步骤
可以看到每一步想要做好都是挺有挑战性的, 本篇主要分析 相机预览帧尺寸的选取 和 相机预览帧旋转角度的设定 这两个问题
若是使用 JetPack 组件 CameraX 的话, 基本上无需考虑这两个问题, 不过为了解析其实现思路, 这里先以 Camera1 为例, 后续会给出 CameraX 的实现代码
在刚开始接触自定义相机的时候, 这个问题一度困扰着我, 是因为面对这个问题我一直无法给予一个很好的解决方案, 之前我的方案是, 交由用户传入一个预览的尺寸, 然后设计一个算法从相机中找寻与这个尺寸最为接近的, 这个方案看似挺合理的, 写 demo 时可以使用, 但却存在着如下的隐患
因此让外界指定预览尺寸的方式有些不太合理, 于是我们把目光转移到大厂的系统相机上, One UI 的系统相机如下
可以发现, 它们仅提供的是尺寸的比例, 它的优势如下
因此我们可以尝试采用外界指定比例, 在同比例中选取最接近预览控件尺寸的方式作为预览帧的采集的尺寸
若想实现上述的方式, 我们第一步要做的是对相机的预览尺寸按照比例进行归类, 方便从后续从中筛选出最合适的尺寸, 接下来我们便看看第一步的操作
private final SizeMap mPreviewSizes = new SizeMap();
// 变量所有的预览尺寸, 按照比例分类
for (Camera.Size size : mCameraParams.getSupportedPreviewSizes()) {
mPreviewSizes.add(new Size(size.width, size.height));
}
这里遍历了该 Camera 支持的所有预览尺寸, SizeMap.add 方法如下
class SizeMap {
private final ArrayMap<AspectRatio, SortedSet<Size>> mRatios = new ArrayMap<>();
/**
* Add a new {@link Size} to this collection.
*
* @param size The size to add.
* @return {@code true} if it is added, {@code false} if it already exists and is not added.
*/
public boolean add(Size size) {
for (AspectRatio ratio : mRatios.keySet()) {
if (ratio.matches(size)) {
final SortedSet<Size> sizes = mRatios.get(ratio);
if (sizes.contains(size)) {
return false;
} else {
sizes.add(size);
return true;
}
}
}
// None of the existing ratio matches the provided size; add a new key
SortedSet<Size> sizes = new TreeSet<>();
sizes.add(size);
mRatios.put(AspectRatio.of(size.getWidth(), size.getHeight()), sizes);
return true;
}
}
通过这种方式, 我们的 mPreviewSizes 便按照比例完成分类了, 接下来看看那尺寸的选取
// 1. 获取用户期望的比例的集合
SortedSet<Size> previewSizes = mPreviewSizes.sizes(aspectRatio);
if (previewSizes == null) {
// 1.1 用户期望的比例不存在, 选取获取默认比例
previewSizes = mPreviewSizes.sizes(chooseDefaultAspectRatio());
}
// 2. 选择最合适的预览帧尺寸
final Size previewSize = chooseOptimalPreviewSize(previewSizes);
// 参数注入
mCameraParams.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
这里有两个需要注意的点
接下来逐一解析这两个算法
private AspectRatio chooseDefaultAspectRatio() {
AspectRatio result = null;
for (AspectRatio ratio : mPreviewSizes.ratios()) {
result = ratio;
if (AspectRatio.DEFAULT.equals(ratio)) {
break;
}
}
return result;
}
关于选取默认的比例代码比较简单, 即遍历预览比例集合, 判断是否存在默认比例, 若存在则返回默认比例, 反之则选取比例集合中的最后一个
当用户指定的比例不支持, 为什么不选取与用户设置最接近的比例呢?
为了回答这个问题, 先看看使用最接近比例算法最后选取的预览结果
E/TAG: desire aspect is 1:1, closely is 11:9
E/TAG: desired size is: [1080, 1080]; choose preview size is: [352x288]
可以看到从这个比例中选取的预览尺寸远低于 View 的尺寸, 从数据上看几乎会进行 3 倍的上采样, 即使上采样算法再优, 也难以保证预览的清晰度
我们无法控制用户心仪的比例, 因此当用户指定的比例不存在时, 采用指定的默认比例是能够保证预览清晰度的比较好的方式, 接下来看看如何从比例中选择最合适的预览尺寸
private Size chooseOptimalPreviewSize(SortedSet<Size> sizes) {
// 1. 确定期望的宽高
int desiredWidth;
int desiredHeight;
// 横屏状态, 直接使用 view 的宽高
if (isLandscape(screenOrientationDegrees)) {
desiredWidth = viewWidth;
desiredHeight = viewHeight;
}
// 竖屏状态, 反转宽高
else {
desiredWidth = viewHeight;
desiredHeight = viewWidth;
}
// 2. 查找最合适的预览尺寸
Size result = null;
for (Size size : sizes) {// sizes 已按照从小到大排序
result = size;
// 选取宽高都比期望稍大的
if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {
return size;
}
}
return result;
}
在确定期望的宽高的过程中, 竖屏状态下会有一个宽高翻转的操作
尺寸查找的主要的思路是选取比期望宽高稍大的预览尺寸
通过上面的选取方式, 验证的结果如下
// 期望比例 4:3
E/TAG: desired size is: [1440, 1080]; choose preview size is: [1600x1200]
// 期望比例 16:19
E/TAG: desired size is: [1920, 1080]; choose preview size is: [1920x1080]
// 期望比例 1:1
E/TAG: desired size is: [1440, 1080]; choose preview size is: [1600x1200]
可以看到最终选择的结果, 是符合我们上面算法预期的
好的, 预览帧采集的尺寸已经确定好了, 接下来看看预览帧旋转角度的设定
想要正确的实现预览效果, 设定相机预览帧的旋转角度是必须要面对的问题, 想要解决这个问题, 我们需要先了解一下相机 sensor 的坐标系
以手机屏幕为参考系, 相机 Sensor 取景坐标系的指向与其安装的方位有关, 以后置相机为例, 其相机传感器的坐标系如下所示
可以看到, 一般相机的 sensor 坐标系如上图所示, 与手机逆时针旋转 90 度后的屏幕坐标系重合, 接下来根据这个坐标看看在手机横竖屏时, 图像的呈现效果
可以看到横屏(逆时针 90°)状态下, Sensor 和屏幕的坐标系重合, 图像可以直接映射
可以看到竖屏状态下的映射就出现问题了, 我们可以很自然的想到顺时针旋转 90° 来解决
了解了 sensor 坐标系和屏幕坐标系之间的映射, 接下来看看如何撰写相关代码
关于修改相机旋转角度的方法 Camera.setDisplayOrientation 定义如下
public class Camera {
/**
* ......
* <pre>
* 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);
* }
* </pre>
* ......
*/
public native final void setDisplayOrientation(int degrees);
}
我们只需要将旋转角度计算好, 传入这个方法便可以让 Camera 输出符合屏幕预览的图像了, 关于这个旋转角度的计算, 其注释上有明确的代码示例, 其主要思想如下
可以看到在计算前后摄像头旋转角度时除了用到屏幕的旋转角度, 还用到了 CameraInfo.orientation 这个变量, 接下来我们逐个分析
关于 CameraInfo.orientation 其定义如下
public class Camera {
public static class CameraInfo {
/**
* <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>
*
* .......
*/
public int orientation;
}
}
注释的译文为: orientation 表示相机图像的方向。它的值是相机图像顺时针旋转到设备自然方向一致时的图像,它可能是 0、90、180、270 四种。对于竖屏应用来说,后置相机传感器是横屏安装的,当你面向屏幕时,如果后置相机传感器顶边和设备自然方向的右边是平行的,那么后置相机的 orientation 是 90。如果是前置相机传感器顶边和设备自然方向的右边是平行的,则前置相机的 orientation 是 270。
注释看起来可能比较抽象, 它的意思是指屏幕竖置时, 传感器采集的图像想要以符合人眼观感的样式显示到屏幕上, 所需要顺时针旋转的角度, 我们通过流程图来加深一下对其的理解
可以看到将 sensor 坐标系映射到屏幕坐标系之后, 还需要顺时针旋转 90° 才能正常显示, 那么其 CameraInfo.orientation 即为 90
好的, 理解了 CameraInfo.orientation 这个参数我们再回过头来看看前后摄像头的旋转算法
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
......
int degrees = 0;
......
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
......
}
// back-facing
else {
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
可以看到后置镜头的计算过程是非常简单的, 摄像头图像方向要恢复到自然方向需要顺时针旋转, 而屏幕逆时针旋转正好抵掉了摄像头的旋转, 所以两者相减, 然后再加上 360 取模运算, 便可以获得最终结果
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
......
int degrees = 0;
......
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // 抵消镜像
}
// back-facing
else {
......
}
camera.setDisplayOrientation(result);
}
前置镜头的代码比后置镜头复杂一些, 我们图解分析一下
前置的采集流程如上图所示(前置镜头的 Sensor 坐标系与后置方向不同, 与安装方式有关)
可以看到若是直接按照 CameraInfo.orientation 进行旋转 270°, 那么得到的图像是非镜像的, 显然这是不符合自拍预览效果的
因此系统对其进行了镜像操作(图中的文字没有完全镜像, 请见谅), 那么只需要旋转 90°, 便可以得到想要的预览效果了, 现在再回看上面的代码, 就比较清晰了
CameraX 是 Jetpack 基于 Camera2.0 封装的相机组件, 向 5.0 以上的版本提供服务
关于预览相机相关的配置如下
PreviewConfig config = new PreviewConfig.Builder()
// CameraX 的宽高比和 Camera1 相反, 为 3:4 9:16......
.setTargetAspectRatio(new Rational(aspectRatio.getY(), aspectRatio.getX()))
// 分辨率
.setTargetResolution(new android.util.Size(previewWidth, previewHeight))
// 前置与否
.setLensFacing(facing == Constants.FACING_FRONT ? CameraX.LensFacing.FRONT
: CameraX.LensFacing.BACK)
.build();
Preview preview = new Preview(config);
CameraX.bindToLifecycle(mLifecycleOwner, preview);
可以看到短短几行代码便可以实现最佳预览尺寸的选择, 其旋转的角度在图像通过 SurfaceTexture 输出时, 记录在其 matrix 中, 我们可以很方便进行旋转操作(没有考虑屏幕的旋转, 需要自己矫正)
关于 CameraX 的用法, 想了解更多用法请查看, 这里就不进行赘述了
关于相机预览尺寸的选取和旋转角度的设定到这里便分析结束了, 这里再简单的回顾一下
其中 旋转角度的设定 较之 尺寸的选取 要更为困难, 其中牵扯到坐标系的映射和映射后的旋转, 前置相机还需要考虑镜像的抵消, 不过所幸这里将其梳理清楚了
虽然有 CameraX 这种简单的 API 供我们使用, 但对于 5.0 以下的机型, 仍然需要我们自定义, 因此我们了解其原理是非常重要的, 而且 探索思考的过程比结果更重要 不是?
文中所有的代码地中,如有不解,请自行查看。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- cepb.cn 版权所有 湘ICP备2022005869号-7
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务