解决相机库CameraView多滤镜拍照错乱的BUG (二) : 解决BUG 我不是女神ヾ 2024-04-25 20:24 5阅读 0赞 ### 1. 前言 ### 这段时间,在使用 [natario1/CameraView][natario1_CameraView] 来实现带滤镜的`预览`、`拍照`、`录像`功能。 由于`CameraView`封装的比较到位,在项目前期,的确为我们节省了不少时间。 但随着项目持续深入,对于`CameraView`的使用进入深水区,逐渐出现满足不了我们需求的情况。 `Github`中的`issues`中,有些`BUG`作者一直没有修复。 那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。 [上一篇文章][Link 1]我们已经复现了`CameraView`在使用多滤镜`MultiFilter`的时候哦度会遇到拍照错乱的`BUG`,这篇文章我们来解决这个`BUG`。 以下源码解析基于`CameraView 2.7.2` implementation("com.otaliastudios:cameraview:2.7.2") > 为了在博客上更好的展示,本文贴出的代码进行了部分精简 ![在这里插入图片描述][b56e198a1fec4d1894f881778ce7c7db.png] ### 2. CameraView滤镜预览的流程 ### 关于`CameraView`带滤镜预览的流程,我们在[Android 相机库CameraView源码解析 (四) : 带滤镜预览][Android _CameraView_ _ _]中已经详细说明过了,这里我们在来简单说明一下。 ![在这里插入图片描述][d41b051670944d6f8d572cd1d251dd5c.png] * 首先在`CamerView`中,会调用`View`生命周期的`onAttachedToWindow`,去初始化`GlCameraPreview` * 在`GlCameraPreview`的`onCreateView`中,会初始化`GLSurfaceView`,并调用`GLSurfaceView.setRenderer()`将`GLSurfaceView`和`Renderer`建立关联 * 然后,`GlCameraPreview`会回调`onSurfaceCreate()`和`onSurfaceChanged()` * 当我们手动调用`requestRender`后,会调用`onDrawFrame()`来重新渲染 * 拍照实现了`RendererFrameCallback`回调,会在回调中的`onRendererTextureCreated()`、`onRendererFilterChanged()`、`onRendererFrame()`中,来实现带滤镜拍照功能 ### 3. GLSurfaceView保存的图片尺寸的决定因素 ### `Android`中`GLSurfaceView`保存的图片尺寸,是和相机支持的尺寸有关,还是和`GLSurfaceView`的尺寸有关呢 ? `GLSurfaceView`是`Android`中用于显示`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()`中会计算得到`mCropping`、`mCropScaleX`、`mCropScaleY`,从而确定裁剪、缩放参数 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() { @RendererThread public void onRendererTextureCreated(int textureId) { SnapshotGlPictureRecorder.this.onRendererTextureCreated(textureId); } @RendererThread @Override public void onRendererFilterChanged(@NonNull Filter filter) { SnapshotGlPictureRecorder.this.onRendererFilterChanged(filter); } @RendererThread @Override public 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`,如果实现了`OneParameterFilter`或`TwoParameterFilter`接口,还会给拷贝相关的参数过来。 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`,如果`size`和`state.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. 造成多滤镜拍照错乱的原因分析 ### [上篇文章][Link 1]我们总结了下这个`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_],本质是因为在`CameraView`中,`GlSurfaceView`是专门用来预览,而作者自己实现的`EglSurface`是用来拍照时候存储图像的,所以可能会出现预览效果和拍照的实际效果不一致的情况。 #### 7.3 为什么使用单个滤镜的时候,没有这个问题,而使用多个滤镜就有问题了 ? #### 因为在`MultiFilter`中,如果有多个滤镜,需要通过创建一个新的`GlTexture`,并传入`width`和`height`,从而实现多个滤镜叠加。 而单个滤镜的情况下,是不需要多这一步操作的,所以单个滤镜情况下,直接就`return`了,没有走后面的逻辑,所以就不会有这个问题。 private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isFirst, boolean isLast) { State state = states.get(filter); if (isLast) { state.sizeChanged = false; //单个滤镜的情况下,直接return return; } //多个滤镜才会走这里的逻辑 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. 如何解决该BUG ### 经过上文,我们已经知道 : 预览的时候的 `filter` 宽高用的 `GlSurfaceView` 的宽高,而实际上拍照的 `EglSurface` 的宽高是`mResult.size` ,两者是不一致的,导致最终拍照出现了错乱。 所以需要在`filter.copy()`拷贝滤镜之后,再设置一下 `Size` ,确保尺寸和 `EglSurface` 的尺寸一样,就可以解决这个问题了。 **解决办法** : 将`CameraView`源码中的`SnapshotGlPictureRecorder.java`的`onRendererFilterChanged`方法 protected void onRendererFilterChanged(@NonNull Filter filter) { mTextureDrawer.setFilter(filter.copy()); } 修改为如下代码即可 protected void onRendererFilterChanged(@NonNull Filter filter) { Filter copyFilter = filter.copy(); copyFilter.setSize(mResult.size.getWidth(), mResult.size.getHeight()); mTextureDrawer.setFilter(copyFilter); } ### 9. 其他 ### #### 9.1 CameraView源码解析系列 #### [Android 相机库CameraView源码解析 (一) : 预览-CSDN博客][Android _CameraView_ _ _ _-CSDN] [Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客][Android _CameraView_ _ _ _-CSDN 1] [Android 相机库CameraView源码解析 (三) : 滤镜相关类说明-CSDN博客][Android _CameraView_ _ _ _-CSDN 2] [Android 相机库CameraView源码解析 (四) : 带滤镜拍照-CSDN博客][Android _CameraView_ _ _ _-CSDN 3] [Android 相机库CameraView源码解析 (五) : 保存滤镜效果-CSDN博客][Android _CameraView_ _ _ _-CSDN 4] [natario1_CameraView]: https://github.com/natario1/CameraView [Link 1]: https://blog.csdn.net/EthanCo/article/details/135033368 [b56e198a1fec4d1894f881778ce7c7db.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/25/d3f856550dd4431f96cb757d071fe7c8.png [Android _CameraView_ _ _]: https://blog.csdn.net/EthanCo/article/details/135202176 [d41b051670944d6f8d572cd1d251dd5c.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/25/4806a7a43f704dc7bba639bc8ad4e40a.png [CameraView_]: https://blog.csdn.net/EthanCo/article/details/134829443 [Android _CameraView_ _ _ _-CSDN]: https://blog.csdn.net/EthanCo/article/details/134511622 [Android _CameraView_ _ _ _-CSDN 1]: https://blog.csdn.net/EthanCo/article/details/134545086 [Android _CameraView_ _ _ _-CSDN 2]: https://blog.csdn.net/EthanCo/article/details/134517249 [Android _CameraView_ _ _ _-CSDN 3]: https://blog.csdn.net/EthanCo/article/details/134517154 [Android _CameraView_ _ _ _-CSDN 4]: https://blog.csdn.net/EthanCo/article/details/134691849
相关 Android 相机库CameraView源码解析 (四) : 带滤镜预览 Android 相机库CameraView源码解析 (四) : 带滤镜预览 太过爱你忘了你带给我的痛/ 2024年04月25日 20:24/ 0 赞/ 8 阅读
相关 解决相机库CameraView多滤镜拍照错乱的BUG (二) : 解决BUG 解决相机库CameraView多滤镜拍照错乱的BUG (二) : 解决BUG 我不是女神ヾ/ 2024年04月25日 20:24/ 0 赞/ 6 阅读
相关 解决相机库CameraView多滤镜拍照错乱的BUG (一) : 复现BUG 解决相机库CameraView多滤镜拍照错乱的BUG 女爷i/ 2024年04月25日 20:24/ 0 赞/ 10 阅读
相关 为什么相机库CameraView预览和拍照的效果不一致 ? 从源码解析 : 为什么CameraView预览和拍照的效果会不一致呢 ? 我就是我/ 2024年04月25日 20:24/ 0 赞/ 9 阅读
相关 Android 相机库CameraView源码解析 (六) : 保存滤镜效果 Android 相机库CameraView源码解析 : 保存滤镜效果部分 约定不等于承诺〃/ 2024年04月25日 20:24/ 0 赞/ 6 阅读
相关 Android 相机库CameraView源码解析 (二) : 拍照 Android 相机库CameraView源码解析 : 拍照部分 ╰+哭是因爲堅強的太久メ/ 2024年04月25日 20:24/ 0 赞/ 8 阅读
相关 Android 相机库CameraView源码解析 (三) : 滤镜相关类说明 Android 相机库CameraView源码解析 : 滤镜相关类说明 - 日理万妓/ 2024年04月25日 20:24/ 0 赞/ 7 阅读
相关 Android 相机库CameraView源码解析 (五) : 带滤镜拍照 Android 相机库CameraView源码解析 : 带滤镜拍照 我不是女神ヾ/ 2024年04月25日 20:23/ 0 赞/ 5 阅读
相关 Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG(二) : 解决BUG 解决CameraView叠加2个以上滤镜拍照黑屏的BUG Bertha 。/ 2024年04月25日 20:23/ 0 赞/ 7 阅读
相关 Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG (一) : 复现BUG 解决CameraView叠加2个以上滤镜拍照黑屏的BUG 我会带着你远行/ 2024年04月25日 20:23/ 0 赞/ 5 阅读
还没有评论,来说两句吧...