Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG (一)

news2024/12/28 20:14:43

1. 前言

这段时间,在使用 natario1/CameraView 来实现带滤镜的预览拍照录像功能。
由于CameraView封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView的使用进入深水区,逐渐出现满足不了我们需求的情况。
特别是对于使用MultiFilter,叠加2个滤镜拍照是正常的,叠加2个以上滤镜拍照,预览时正常,拍出的照片就会全黑。
Github中的issues中,也有不少提这个BUG的,但是作者一直没有修复该问题。

在这里插入图片描述
那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,尝试性地去解决这个问题。
好在功夫不负有心人,花费数个工作日后,这个问题终于被我解决了。
而这篇文章就是来记录,该如何解决的这个BUG

2. 复现BUG

首先,我们来明确CameraView滤镜是如何调用的,同时也让我们明确当遇到该问题时的代码大致情况,来复现下这个BUG。

2.1 前置操作

新建一个Android项目,Activity设为横屏,确保添加好相机相关权限,并申请权限后,依赖CameraView的依赖库

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

2.2 编写XML布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MyMainActivity">

    <com.otaliastudios.cameraview.CameraView
        android:id="@+id/camera_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:cameraFacing="front"
        app:cameraEngine="camera2"
        app:cameraExperimental="true"
        app:cameraMode="video" />

    <ImageView
        android:id="@+id/img"
        android:layout_width="300dp"
        android:layout_height="200dp" />

    <Button
        android:id="@+id/btn_take_picture"
        android:layout_gravity="right|bottom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="拍照" />

</FrameLayout>

2.3 初始化CameraView并添加滤镜

binding.cameraView.setLifecycleOwner(this)
val multiFilter = MultiFilter()

val contrastFilter = ContrastFilter()
contrastFilter.contrast = 1.05F
multiFilter.addFilter(contrastFilter)

val brightnessFilter = Filters.BRIGHTNESS.newInstance() as BrightnessFilter
brightnessFilter.brightness = 1.2F
multiFilter.addFilter(brightnessFilter)

val saturationFilter = Filters.SATURATION.newInstance() as SaturationFilter
saturationFilter.saturation = 1F
multiFilter.addFilter(saturationFilter)

binding.cameraView.filter = multiFilter

2.4 进行拍照

binding.btnTakePicture.setOnClickListener {
	//带滤镜拍照
    binding.cameraView.takePictureSnapshot()
}

binding.cameraView.addCameraListener(object : CameraListener() {
    override fun onPictureTaken(result: PictureResult) {
        super.onPictureTaken(result)
        //拍照回调
        val bitmap = BitmapFactory.decodeByteArray(result.data, 0, result.data.size)
        bitmap?.also {
            runOnUiThread {
                Toast.makeText(this@Test2Activity, "拍照成功", Toast.LENGTH_SHORT).show()
                //将Bitmap设置到ImageView上
                binding.img.setImageBitmap(it)
            }
            val file = getNewImageFile()
            //保存图片到指定目录
            ImageUtils.save(it, file, Bitmap.CompressFormat.JPEG)
        }
    }
})

2.5 运行程序

运行程序,点击拍照,我们就可以复现这个BUG了 : 左上角的图片是拍照后全黑的效果。

在这里插入图片描述

3. takePictureSnapshot源码解析

接下来,我们来分析下CameraView带滤镜拍照的流程。

3.1 takePictureSnapshot

分析的起点从cameraView.takePictureSnapshot()这个带滤镜拍照的API开始。

public void takePictureSnapshot() {
    PictureResult.Stub stub = new PictureResult.Stub();
    mCameraEngine.takePictureSnapshot(stub);
}

PictureResult.Stub stub是一个参数封装类,用来传递配置参数

public static class Stub {
    Stub() {}
    public boolean isSnapshot;
    public Location location;
    public int rotation;
    public Size size;
    public Facing facing;
    public byte[] data;
    public PictureFormat format;
}

这里我们主要来看mCameraEngine.takePictureSnapshot,具体实现是在CameraBaseEngine中的takePictureSnapshot()方法中。
这里给stub赋值了一些参数,然后调用了onTakePictureSnapshot()

public /* final */ void takePictureSnapshot(final @NonNull PictureResult.Stub stub) {
    // Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
    final boolean metering = mPictureSnapshotMetering;
    getOrchestrator().scheduleStateful("take picture snapshot", CameraState.BIND,
            new Runnable() {
        @Override
        public void run() {
            LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture());
            if (isTakingPicture()) return;
            stub.location = mLocation;
            stub.isSnapshot = true;
            stub.facing = mFacing;
            stub.format = PictureFormat.JPEG;
            // Leave the other parameters to subclasses.
            //noinspection ConstantConditions
            AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT));
            onTakePictureSnapshot(stub, ratio, metering);
        }
    });
}

3.2 onTakePictureSnapshot

onTakePictureSnapshot是个接口中的方法,具体实现有Camera1EngineCamera2Engine,由于我们使用的是Camera2,所以这里直接来看Camera2Engine

@Override
protected void onTakePictureSnapshot(@NonNull final PictureResult.Stub stub,
                                     @NonNull final AspectRatio outputRatio,
                                     boolean doMetering) {
    //...省略不重要代码...
    
    // stub.size is not the real size: it will be cropped to the given ratio stub.
    // rotation will be set to 0 - we rotate the texture instead.
    stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
    stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
    mPictureRecorder = new Snapshot2PictureRecorder(stub, this,
            (RendererCameraPreview) mPreview, outputRatio);
    mPictureRecorder.take();
}

这里实际就是调用了mPictureRecorder.take()mPictureRecorder是一个PictureRecorder接口,具体实现有Snapshot1PictureRecorderSnapshot2PictureRecorderSnapshotPictureRecorder

这里我们用的是Camera2,所以来看Snapshot2PictureRecorder

public void take() {
    //...省略不重要代码...
	
	super.take();
}

Snapshot2PictureRecorder是继承自Snapshot2PictureRecorder,也就是说Snapshot2PictureRecorder最终调用的是SnapshotPictureRecorder

3.3 SnapshotPictureRecorder.take

来看SnapshotPictureRecordertake(),这里注册了RendererFrameCallback,并在onRendererFrame()回调方法中,移除了RendererFrameCallback,并调用onRendererFrame()

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

    });
}

onRendererFrame()最终调用的是takeFrame()

protected void onRendererFrame(@NonNull final SurfaceTexture surfaceTexture,
                             final int rotation,
                             final float scaleX,
                             final float scaleY) {
    final EGLContext eglContext = EGL14.eglGetCurrentContext();
    WorkerHandler.execute(new Runnable() {
        @Override
        public void run() {
            takeFrame(surfaceTexture, rotation, scaleX, scaleY, eglContext);
        }
    });
}

takeFrame()就是拍照部分的核心代码所在了

3.4 带滤镜拍照核心代码

SnapshotGlPictureRecorder中的takeFrame()就是带滤镜拍照的核心代码了,这里先贴出代码,下一篇文章我们会再来详细分析。

protected void takeFrame(@NonNull SurfaceTexture surfaceTexture,
                         int rotation,
                         float scaleX,
                         float scaleY,
                         @NonNull EGLContext eglContext) {

    // 0. EGL window will need an output.
    // We create a fake one as explained in javadocs.
    final int fakeOutputTextureId = 9999;
    SurfaceTexture fakeOutputSurface = new SurfaceTexture(fakeOutputTextureId);
    fakeOutputSurface.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());

    // 1. Create an EGL surface
    final EglCore core = new EglCore(eglContext, EglCore.FLAG_RECORDABLE);
    final EglSurface eglSurface = new EglWindowSurface(core, fakeOutputSurface);
    eglSurface.makeCurrent();
    final float[] transform = mTextureDrawer.getTextureTransform();

    // 2. Apply preview transformations
    surfaceTexture.getTransformMatrix(transform);
    float scaleTranslX = (1F - scaleX) / 2F;
    float scaleTranslY = (1F - scaleY) / 2F;
    Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0);
    Matrix.scaleM(transform, 0, scaleX, scaleY, 1);

    // 3. Apply rotation and flip
    // If this doesn't work, rotate "rotation" before scaling, like GlCameraPreview does.
    Matrix.translateM(transform, 0, 0.5F, 0.5F, 0); // Go back to 0,0
    Matrix.rotateM(transform, 0, rotation + mResult.rotation, 0, 0, 1); // Rotate to OUTPUT
    Matrix.scaleM(transform, 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
    Matrix.translateM(transform, 0, -0.5F, -0.5F, 0); // Go back to old position

    // 4. Do pretty much the same for overlays
    if (mHasOverlay) {
        // 1. First we must draw on the texture and get latest image
        mOverlayDrawer.draw(Overlay.Target.PICTURE_SNAPSHOT);

        // 2. Then we can apply the transformations
        Matrix.translateM(mOverlayDrawer.getTransform(), 0, 0.5F, 0.5F, 0);
        Matrix.rotateM(mOverlayDrawer.getTransform(), 0, mResult.rotation, 0, 0, 1);
        Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
        Matrix.translateM(mOverlayDrawer.getTransform(), 0, -0.5F, -0.5F, 0);
    }
    mResult.rotation = 0;

    // 5. Draw and save
    long timestampUs = surfaceTexture.getTimestamp() / 1000L;
    LOG.i("takeFrame:", "timestampUs:", timestampUs);
    mTextureDrawer.draw(timestampUs);
    if (mHasOverlay) mOverlayDrawer.render(timestampUs);
    mResult.data = eglSurface.toByteArray(Bitmap.CompressFormat.JPEG);

    // 6. Cleanup
    eglSurface.release();
    mTextureDrawer.release();
    fakeOutputSurface.release();
    if (mHasOverlay) mOverlayDrawer.release();
    core.release();
    dispatchResult();
}

4. 其他

4.1 Android Camera2 系列

更多Camera2相关文章,请看
十分钟实现 Android Camera2 相机预览_氦客的博客-CSDN博客
十分钟实现 Android Camera2 相机拍照_氦客的博客-CSDN博客
十分钟实现 Android Camera2 视频录制_氦客的博客-CSDN博客

4.2 Android 相机相关文章

Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作_氦客的博客-CSDN博客
Android 从零开发一个简易的相机App_android开发简易app_氦客的博客-CSDN博客
Android 使用Camera1实现相机预览、拍照、录像_android 相机预览_氦客的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1196062.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

程序员千万不能去这些公司,听一下我这个学长的经验。

俗话说“条条大路通罗马”&#xff0c;但是对于程序员来说&#xff0c;有些路千万别走&#xff0c;走得越久越难以抽身&#xff0c;甚至说毁掉你的职业生涯。 今天来跟大家讲一下&#xff0c;作为程序员&#xff0c;有些公司千万不要进去&#xff0c;你以为稀松平常&#xff0…

华为防火墙vrrp+hrp双机热备负载分担(两端为交换机)

主要配置&#xff1a; FW1 hrp enable hrp interface GigabitEthernet1/0/2 remote 172.16.0.2 interface GigabitEthernet1/0/0 这里可以假想为接两条外线&#xff0c;一条外线对应一个vrrid undo shutdown ip address 1.1.1.2 255.255.255.0 vrrp vrid 3 virtual-ip 1.1.1…

基于SpringBoot的SSMP整合案例(实体类开发与数据层开发)

实体类开发 导入依赖 Lombok&#xff0c;一个Java类库&#xff0c;提供了一组注解&#xff0c;简化POJO实体类开发<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId> </dependency>lombok版本由SpringB…

LeetCode【215】数组中第k大的元素

题目&#xff1a; 思路&#xff1a; https://zhuanlan.zhihu.com/p/59110615 代码&#xff1a; public int findKthLargest(int[] nums, int k) {PriorityQueue<Integer> queue new PriorityQueue<>((o1, o2) -> o1 - o2);for (int i 0; i < nums.lengt…

刚学C语言太无趣 推荐一个好用易学的可视化框架:EasyX。VC6.0就能写

很多同学在大一刚学C语言时&#xff0c;是不是很好奇为什么别人编程都在做软件&#xff0c;而自己只能面对着黑窗口进行 printf &#xff1f; EasyX&#xff0c;C语言可视化编程。 分享我大一时候做的一个项目&#xff0c;用 VC6.0 开发的一款画图软件&#xff1a; 这个软件源…

02MyBatisPlus条件构造器,自定义SQL,Service接口

一、条件构造器 1.MyBatis支持各种复杂的where条件&#xff0c;满足开发的需求 Wrapper是条件构造器&#xff0c;构建复杂的where查询 AbstractWrapper有构造where条件的所有方法&#xff0c;QueryWrapper继承后并有自己的select指定查询字段。UpdateWrapper有指定更新的字段的…

bat脚本设置变量有空格踩到的坑

SET PATH c:\xxx;%PATH% 我想把一个路径作为path环境变量最前面的一个&#xff0c;所以使用了上面的语句。 但是没有生效&#xff0c;我还以为是其他什么原因&#xff0c;后来又有一个类似的需求&#xff1a; set output output\x64 结果在使用 %output% 的时候是一个空格&…

常见排序算法之快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。 基本思想为∶任取待排序元素序列中的某元素作为基准值&#xff0c;按照该排序码将待排序集合分割成两子序列&#xff0c;左子序列中所有元素均小于基准值&#xff0c;右子序列中所有元素均大于基准值&#xff0c;…

Flutter 实战:构建跨平台应用

文章目录 一、简介二、开发环境搭建三、实战案例&#xff1a;开发一个简单的天气应用1. 项目创建2. 界面设计3. 数据获取4. 实现数据获取和处理5. 界面展示6. 添加动态效果和交互7. 添加网络错误处理8. 添加刷新功能9. 添加定位功能10. 添加通知功能11. 添加数据持久化功能 《F…

【Unity细节】Failed importing package???Unity导包失败?

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 &#x1f636;‍&#x1f32b;️收录于专栏&#xff1a;unity细节和bug &#x1f636;‍&#x1f32b;️优质专栏 ⭐【…

计算机服务器中了mallox勒索病毒怎么解决,勒索病毒解密,数据恢复

企业的计算机服务器为企业的数据存储提供了极大便利&#xff0c;也让企业的生产运行效率得到了极大提升&#xff0c;但是网络数据安全威胁随着技术的不断发展也不断增加。近期&#xff0c;云天数据恢复中心接到很多企业的求助&#xff0c;企业的计算机服务器遭到了mallox勒索病…

图神经网络 (GNN)

目录 一、GNN介绍1.1引入1.1.1图的介绍1.1.2怎样将内容表示成图1.1.4图神经网络是在做什么 1.2基本概念 二、GNN流程2.1聚合2.2更新2.2.1一次GNN操作 2.3循环2.3.1多层GNN操作2.3.2能做什么 三、GNN算法原理3.1数据3.2变量定义3.3GNN算法3.3.1Forward3.3.2Backward 四、GNN优势…

Unity 调用自己封装好的DLL库

因为做项目时会用到很多重复的方法&#xff0c;每次都重新写有点浪费时间&#xff0c;就可以将这些方法封装成DLL类库&#xff0c;用的时候直接引入调用就行。 首先在VS里面创建类库文件 注&#xff1a;.NET Framework要选3.5以下 然后定义好命名空间名字和类名就可以写自己要…

互联网按摩预约小程序开发;

随着移动互联网的普及&#xff0c;越来越多的人开始通过手机预约按摩服务。按摩预约小程序是一种方便快捷的预约方式&#xff0c;可以让用户随时随地预约按摩服务。那么&#xff0c;按摩预约小程序的开发周期要多久&#xff1f;按摩预约小程序的功能有哪些呢&#xff1f;本文将…

振南技术干货集:研发版本乱到“妈不认”? Git!(1)

注解目录 1、关于 Git 1.1Git 今生 (Git 和 Linux 的生父都是 Linus&#xff0c;振南给你讲讲当初关于 Git 的爱恨情愁&#xff0c;其背后其实是开源与闭源两左阵营的明争暗斗。) 1.2Git的爆发 (Git 超越时代的分布式思想。振南再给你讲讲旧金山三个年轻人创办 GitHub&…

ABAQUS分析步笔记

定义原则&#xff1a; 每个step的所有边界条件&#xff0c;载荷条件累加构成本step的仿真效果&#xff1b; step2需要在step1的状态基础上进行载荷运动等限定时&#xff0c;需要确保在step2中传递了step1的想要保留的特征&#xff0c;如&#xff1a; 1、BC-1 这里的BC-1的固…

2024最新fl studio 21.2.0.3842中文版完整下载

FL Studio 21.2.0.3842中文版完整下载是最好的音乐开发和制作软件也称为水果音乐软件。它是最受欢迎的工作室&#xff0c;因为它包含了一个主要的听觉工作场所。2024最新fl studioFL Studio 21版有不同的功能&#xff0c;如它包含图形和音乐音序器&#xff0c;帮助您使完美的配…

JRebel热部署——效率提升100倍(程序员工具必备)

1. 下载JRebel 2.激活程序 这里推荐一个免费获取jrebel激活服务器地址和激活邮箱的地址:点击进入 进入网站之后就可以获取到激活链接和邮箱 点击进入激活 复制过去激活就可以 然后就可以看到激活成功了 3.如何使用 代码修改后&#xff0c;直接CtrlShitF9 即可重新启动 4…

阿里云服务器怎么样?阿里云服务器优势、价格及常见问题介绍

阿里云&#xff08;Alibaba Cloud&#xff09;是阿里巴巴集团旗下的云计算服务提供商&#xff0c;其提供的云服务器&#xff08;ECS&#xff09;是其核心服务之一。在云计算市场中&#xff0c;阿里云服务器备受用户的青睐&#xff0c;那么&#xff0c;阿里云服务器究竟怎么样呢…

基于SSM的房屋租售信息管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…