解决相机库CameraView多滤镜拍照错乱的BUG (二) : 解决BUG

2024-01-14 22:28

本文主要是介绍解决相机库CameraView多滤镜拍照错乱的BUG (二) : 解决BUG,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 前言

这段时间,在使用 natario1/CameraView 来实现带滤镜的预览拍照录像功能。
由于CameraView封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github中的issues中,有些BUG作者一直没有修复。

那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
上一篇文章我们已经复现了CameraView在使用多滤镜MultiFilter的时候哦度会遇到拍照错乱的BUG,这篇文章我们来解决这个BUG

以下源码解析基于CameraView 2.7.2

implementation("com.otaliastudios:cameraview:2.7.2")

为了在博客上更好的展示,本文贴出的代码进行了部分精简

在这里插入图片描述

2. CameraView滤镜预览的流程

关于CameraView带滤镜预览的流程,我们在Android 相机库CameraView源码解析 (四) : 带滤镜预览中已经详细说明过了,这里我们在来简单说明一下。
在这里插入图片描述

  • 首先在CamerView中,会调用View生命周期的onAttachedToWindow,去初始化GlCameraPreview
  • GlCameraPreviewonCreateView中,会初始化GLSurfaceView,并调用GLSurfaceView.setRenderer()GLSurfaceViewRenderer建立关联
  • 然后,GlCameraPreview会回调onSurfaceCreate()onSurfaceChanged()
  • 当我们手动调用requestRender后,会调用onDrawFrame()来重新渲染
  • 拍照实现了RendererFrameCallback回调,会在回调中的onRendererTextureCreated()onRendererFilterChanged()onRendererFrame()中,来实现带滤镜拍照功能

3. GLSurfaceView保存的图片尺寸的决定因素

AndroidGLSurfaceView保存的图片尺寸,是和相机支持的尺寸有关,还是和GLSurfaceView的尺寸有关呢 ?

GLSurfaceViewAndroid中用于显示OpenGL渲染的视图,它的大小决定了OpenGL渲染的区域。
当相机的原始图像被用于OpenGL渲染时,会根据GLSurfaceView的尺寸进行缩放或裁剪。
当你从glSurfaceView中获取或保存图片时,获取到的是OpenGL渲染在这个视图上的内容,因此图片的尺寸会和GLSurfaceView的尺寸相同。

4. 预览过程中是怎么确定尺寸的

在预览过程中,也就是在GlCameraPreview类中,回调onSurfaceChanged()时,会传入宽高。

  • 会调用gl.glViewport()确定OpenGL在窗口中显示的区域范围
  • 会调用Filter.setSize()将宽高尺寸设置给Filter滤镜
  • dispatchOnSurfaceAvailable()中会调用crop()确定裁剪、缩放参数
public void onSurfaceChanged(GL10 gl, final int width, final int height) {gl.glViewport(0, 0, width, height);mCurrentFilter.setSize(width, height);if (!mDispatched) {dispatchOnSurfaceAvailable(width, height);mDispatched = true;} else if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) {dispatchOnSurfaceSizeChanged(width, height);}
}

crop()中会计算得到mCroppingmCropScaleXmCropScaleY,从而确定裁剪、缩放参数

protected void crop(@Nullable final CropCallback callback) {if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0&& mOutputSurfaceHeight > 0) {float scaleX = 1f, scaleY = 1f;AspectRatio current = AspectRatio.of(mOutputSurfaceWidth, mOutputSurfaceHeight);AspectRatio target = AspectRatio.of(mInputStreamWidth, mInputStreamHeight);if (current.toFloat() >= target.toFloat()) {// We are too short. Must increase height.scaleY = current.toFloat() / target.toFloat();} else {// We must increase width.scaleX = target.toFloat() / current.toFloat();}mCropping = scaleX > 1.02f || scaleY > 1.02f;mCropScaleX = 1F / scaleX;mCropScaleY = 1F / scaleY;getView().requestRender();}if (callback != null) callback.onCrop();
}

5. 拍照过程中是怎么确定尺寸的

在带滤镜拍照过程中,也就是在SnapshotGlPictureRecorder中调用take()方法的时候,会实现RendererFrameCallback回调接口。

public void take() {mPreview.addRendererFrameCallback(new RendererFrameCallback() {@RendererThreadpublic void onRendererTextureCreated(int textureId) {SnapshotGlPictureRecorder.this.onRendererTextureCreated(textureId);}@RendererThread@Overridepublic void onRendererFilterChanged(@NonNull Filter filter) {SnapshotGlPictureRecorder.this.onRendererFilterChanged(filter);}@RendererThread@Overridepublic void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,int rotation, float scaleX, float scaleY) {mPreview.removeRendererFrameCallback(this);SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture,rotation, scaleX, scaleY);}});
}

5.1 onRendererTextureCreated

onRendererTextureCreated()中,会调用computeCrop来计算得到适合的尺寸,然后赋值给mResult.size

protected void onRendererTextureCreated(int textureId) {mTextureDrawer = new GlTextureDrawer(textureId);// Need to crop the size.Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio);mResult.size = new Size(crop.width(), crop.height());//...省略了无关代码...
}

5.2 onRendererFilterChanged

onRendererFilterChanged中,会调用filter.copy(),拷贝一份滤镜,然后将拷贝的滤镜设置给GlTextureDrawer

mTextureDrawer.setFilter(filter.copy());

6. Filter.copy

我们再来看一下Filter.copy的逻辑

6.1 BaseFilter.copy

BaseFilter中,内部调用了getClass().newInstance()来反射得到一个新的BaseFilter,并赋值了Size,如果实现了OneParameterFilterTwoParameterFilter接口,还会给拷贝相关的参数过来。

protected Size size;@Override
public void setSize(int width, int height) {size = new Size(width, height);
}public final BaseFilter copy() {BaseFilter copy = onCopy();if (size != null) {copy.setSize(size.getWidth(), size.getHeight());}if (this instanceof OneParameterFilter) {((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1());}if (this instanceof TwoParameterFilter) {((TwoParameterFilter) copy).setParameter2(((TwoParameterFilter) this).getParameter2());}return copy;
}protected BaseFilter onCopy() {try {return getClass().newInstance();} catch (IllegalAccessException e) {throw new RuntimeException("Filters should have a public no-arguments constructor.", e);} catch (InstantiationException e) {throw new RuntimeException("Filters should have a public no-arguments constructor.", e);}
}

6.2 MultiFilter.copy

MultiFilter中有一个filters滤镜列表,用来存储多个子滤镜。

  • setSize的时候,会赋值给size变量,并遍历filters列表调用maybeSetSize()
    • maybeSetSize()内部会根据filter取到state,如果sizestate.size不同,就会将size赋值给state.size,并调用filter.size()将size赋值给filter,确保filter中的filter是最新的
  • copy的时候
    • 会新创建一个MultiFilter,并调用setSize()
    • 遍历filters列表,调用filter.copy(),并调用MultiFilter.addFilter()将拷贝的filter添加到MultiFilter
final List<Filter> filters = new ArrayList<>();
final Map<Filter, State> states = new HashMap<>();
private Size size = null;@Override
public void setSize(int width, int height) {size = new Size(width, height);synchronized (lock) {for (Filter filter : filters) {maybeSetSize(filter);}}
}private void maybeSetSize(@NonNull Filter filter) {State state = states.get(filter);if (size != null && !size.equals(state.size)) {state.size = size;state.sizeChanged = true;filter.setSize(size.getWidth(), size.getHeight());}
}@Override
public Filter copy() {synchronized (lock) {MultiFilter copy = new MultiFilter();if (size != null) {copy.setSize(size.getWidth(), size.getHeight());}for (Filter filter : filters) {copy.addFilter(filter.copy());}return copy;}
}

7. 造成多滤镜拍照错乱的原因分析

上篇文章我们总结了下这个BUG,是跟CameraView的尺寸和摄像头选取的分辨率匹配有关。

  • 使用单个滤镜
    • 一切正常
  • 使用多个滤镜,预览正常,但是
    • 手机选用的摄像头分辨率比CameraView分辨率高 : 照片得到的画面会放大
    • 手机选用的摄像头分辨率比CameraView分辨率低 : 拍照得到的画面会缩小,会有黑边

结合我们上面分析了源码,那么为什么会导致这个BUG呢 ? 我们再来理一下逻辑

  • 预览的时候
    • onSurfaceChanged(width, height)
      • glViewport(0, 0, width, height) : 确定OpenGL窗口的显示范围
      • Filter.setSize(width, height) : 将宽高设置给Filter
  • 带滤镜拍照的时候
    • onRendererTextureCreated
      • computeCrop() : 确定裁剪尺寸,并赋值给mResult.size
    • onRendererFilterChanged()
      • filter.copy() : 拷贝滤镜,并赋值给GlTextureDrawer
        • 这个时候拷贝后的filter中的尺寸是预览时候的GlSurfaceView的宽高

再来打印下日志 (预览摄像头分辨率选用1080*1920,屏幕分辨率1080*2412)的情况下

11:02:27.349  I  CameraActivity onCreate
11:02:27.351  I  CameraActivity onStart
11:02:27.351  I  CameraActivity onResume
11:02:27.385  I  屏幕尺寸:width:1080 height:2412
11:02:27.385  I  CameraView尺寸:width:1080 height:2412
11:02:27.389  I  GlCameraPreview.onSurfaceCreated
11:02:27.389  I  GlCameraPreview.onSurfaceChanged width:1080 height:2412
11:02:27.495  I  选取的摄像头预览尺寸(setPreviewStreamSize): 1080x1920
11:02:27.622  I  MultiFilter FrameBufferCreated:CrossProcessFilter width:1080 height:2412
11:02:34.688  I  CameraActivity ---- 点击拍照(takePictureSnapshot) ----
11:02:34.712  I  SnapshotGlPictureRecorder onRendererTextureCreated size:860x1920
11:02:34.712  I  SnapshotGlPictureRecorder onRendererFilterChanged copyFilter.size:1080x2412
11:02:34.732  I  SnapshotGlPictureRecorder onRendererFrame->takeFrame size:860x1920 rotation:0 scaleX:0.79602 scaleY:1.0
11:02:34.758  I  MultiFilter FrameBufferCreated:CrossProcessFilter width:1080 height:2412
11:02:34.820  I  MultiFilter maybeDestroyFramebuffer

现在我们可以来解答这个BUG了

7.1 为什么会出现拍照错乱的情况 ?

根据这个逻辑,我们可以推测出,是带滤镜拍照的时候的filter宽高用的GlSurfaceView的宽高(比如1080x2316),而实际上带滤镜拍照的EglSurface的宽高是mResult.size(通过computeCrop估算得到,比如1910x4096),两者是不一致的,导致最终拍照出现了错乱。

public class CropHelper {public static Rect computeCrop(@NonNull Size currentSize, @NonNull AspectRatio targetRatio) {int currentWidth = currentSize.getWidth();int currentHeight = currentSize.getHeight();if (targetRatio.matches(currentSize, 0.0005F)) {return new Rect(0, 0, currentWidth, currentHeight);}AspectRatio currentRatio = AspectRatio.of(currentWidth, currentHeight);int x, y, width, height;if (currentRatio.toFloat() > targetRatio.toFloat()) {height = currentHeight;width = Math.round(height * targetRatio.toFloat());y = 0;x = Math.round((currentWidth - width) / 2F);} else {width = currentWidth;height = Math.round(width / targetRatio.toFloat());y = Math.round((currentHeight - height) / 2F);x = 0;}return new Rect(x, y, x + width, y + height);}
}

7.2 为什么预览时正常的,拍照才出现这个问题 ?

这个详见我的这篇文章 为什么相机库CameraView预览和拍照的效果不一致 ?,本质是因为在CameraView中,GlSurfaceView是专门用来预览,而作者自己实现的EglSurface是用来拍照时候存储图像的,所以可能会出现预览效果和拍照的实际效果不一致的情况。

7.3 为什么使用单个滤镜的时候,没有这个问题,而使用多个滤镜就有问题了 ?

因为在MultiFilter中,如果有多个滤镜,需要通过创建一个新的GlTexture,并传入widthheight,从而实现多个滤镜叠加。
而单个滤镜的情况下,是不需要多这一步操作的,所以单个滤镜情况下,直接就return了,没有走后面的逻辑,所以就不会有这个问题。

private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isFirst, boolean isLast) {State state = states.get(filter);if (isLast) {state.sizeChanged = false;//单个滤镜的情况下,直接returnreturn;}//多个滤镜才会走这里的逻辑if (state.sizeChanged) {maybeDestroyFramebuffer(filter);state.sizeChanged = false;}if (!state.isFramebufferCreated) {state.isFramebufferCreated = true;state.outputTexture = new GlTexture(GLES20.GL_TEXTURE0,GLES20.GL_TEXTURE_2D,state.size.getWidth(),state.size.getHeight());state.outputFramebuffer = new GlFramebuffer();state.outputFramebuffer.attach(state.outputTexture);}
}

8 .其他

8.1 CameraView源码解析系列

Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客
Android 相机库CameraView源码解析 (三) : 滤镜相关类说明-CSDN博客
Android 相机库CameraView源码解析 (四) : 带滤镜拍照-CSDN博客
Android 相机库CameraView源码解析 (五) : 保存滤镜效果-CSDN博客

这篇关于解决相机库CameraView多滤镜拍照错乱的BUG (二) : 解决BUG的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/606727

相关文章

如何解决线上平台抽佣高 线下门店客流少的痛点!

目前,许多传统零售店铺正遭遇客源下降的难题。尽管广告推广能带来一定的客流,但其费用昂贵。鉴于此,众多零售商纷纷选择加入像美团、饿了么和抖音这样的大型在线平台,但这些平台的高佣金率导致了利润的大幅缩水。在这样的市场环境下,商家之间的合作网络逐渐成为一种有效的解决方案,通过资源和客户基础的共享,实现共同的利益增长。 以最近在上海兴起的一个跨行业合作平台为例,该平台融合了环保消费积分系统,在短

pip-tools:打造可重复、可控的 Python 开发环境,解决依赖关系,让代码更稳定

在 Python 开发中,管理依赖关系是一项繁琐且容易出错的任务。手动更新依赖版本、处理冲突、确保一致性等等,都可能让开发者感到头疼。而 pip-tools 为开发者提供了一套稳定可靠的解决方案。 什么是 pip-tools? pip-tools 是一组命令行工具,旨在简化 Python 依赖关系的管理,确保项目环境的稳定性和可重复性。它主要包含两个核心工具:pip-compile 和 pip

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo

Android 10.0 mtk平板camera2横屏预览旋转90度横屏拍照图片旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,拍照保存的图片 依然是竖屏的,所以说同样需要将图片也保存为横屏图标了,所以就需要看下mtk的camera2的相关横屏保存图片功能, 如何实现实现横屏保存图片功能 如图所示: 2.mtk

速盾高防cdn是怎么解决网站攻击的?

速盾高防CDN是一种基于云计算技术的网络安全解决方案,可以有效地保护网站免受各种网络攻击的威胁。它通过在全球多个节点部署服务器,将网站内容缓存到这些服务器上,并通过智能路由技术将用户的请求引导到最近的服务器上,以提供更快的访问速度和更好的网络性能。 速盾高防CDN主要采用以下几种方式来解决网站攻击: 分布式拒绝服务攻击(DDoS)防护:DDoS攻击是一种常见的网络攻击手段,攻击者通过向目标网

Jenkins 插件 地址证书报错问题解决思路

问题提示摘要: SunCertPathBuilderException: unable to find valid certification path to requested target...... 网上很多的解决方式是更新站点的地址,我这里修改了一个日本的地址(清华镜像也好),其实发现是解决不了上述的报错问题的,其实,最终拉去插件的时候,会提示证书的问题,几经周折找到了其中一遍博文

Redis中使用布隆过滤器解决缓存穿透问题

一、缓存穿透(失效)问题 缓存穿透是指查询一个一定不存在的数据,由于缓存中没有命中,会去数据库中查询,而数据库中也没有该数据,并且每次查询都不会命中缓存,从而每次请求都直接打到了数据库上,这会给数据库带来巨大压力。 二、布隆过滤器原理 布隆过滤器(Bloom Filter)是一种空间效率很高的随机数据结构,它利用多个不同的哈希函数将一个元素映射到一个位数组中的多个位置,并将这些位置的值置

linux 下Time_wait过多问题解决

转自:http://blog.csdn.net/jaylong35/article/details/6605077 问题起因: 自己开发了一个服务器和客户端,通过短连接的方式来进行通讯,由于过于频繁的创建连接,导致系统连接数量被占用,不能及时释放。看了一下18888,当时吓到了。 现象: 1、外部机器不能正常连接SSH 2、内向外不能够正常的ping通过,域名也不能正常解析。

proxy代理解决vue中跨域问题

vue.config.js module.exports = {...// webpack-dev-server 相关配置devServer: {host: '0.0.0.0',port: port,open: true,proxy: {'/api': {target: `https://vfadmin.insistence.tech/prod-api`,changeOrigin: true,p

解决Office Word不能切换中文输入

我们在使用WORD的时可能会经常碰到WORD中无法输入中文的情况。因为,虽然我们安装了搜狗输入法,但是到我们在WORD中使用搜狗的输入法的切换中英文的按键的时候会发现根本没有效果,无法将输入法切换成中文的。下面我就介绍一下如何在WORD中把搜狗输入法切换到中文。