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

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博客

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux系统初步了解

Linux系统由4个主要部分组成&#xff1a;内核、Shell、文件系统和应用程序。 本专题主要是围绕这四个来展开的。 POSIX&#xff08;可移植操作系统接口&#xff09;定义了操作系统应该为应用程序提供的标准接口&#xff0c;其意愿是获得源码级别的软件可移植性。所以Linux选择…

elemenui的Upload上传整合成数组对象

1. 普通直接上传 <el-upload action"" :before-upload"doBeforeUpload"><el-button type"success" size"mini">导入</el-button></el-upload> methods:{doBeforeUpload(file) {let reader new FileReader(…

Postman基本页面和请求/响应页签介绍

近期在复习Postman的基础知识&#xff0c;在小破站上跟着百里老师系统复习了一遍&#xff0c;也做了一些笔记&#xff0c;希望可以给大家一点点启发。 一、Postman的界面介绍 Home主页、Workspace工作空间、Collections集合、Environments环境变量、Mock Server虚拟服务器、Mo…

GEE:计算有效像素占比(统计有效像素数量、像素总数)

作者:CSDN @ _养乐多_ 在GEE中进行遥感数据处理的时候,经常会由于去云,导致影像出现空洞,只有部分像素可用,或者在进行特殊处理时,只对有效像素进行处理,但是我们不知道有效像素数量和占比,无法对结果做出准确的分析。这个时候就需要统计有效像素数量占比。 本文记录…

智链引擎CEO李智:游戏化增长中台,让裂变营销快十倍、便宜十倍、好十倍丨数据猿专访...

大数据产业创新服务媒体 ——聚焦数据 改变商业 双十一电商大战一触即发&#xff0c;各个垂类的App也都希望能够借力双十一营销季&#xff0c;实现用户和营收双增长。MarTech在这个风口上&#xff0c;又成为2B赛道关注的焦点。 业内人士指出&#xff0c;MarTech的引入催生营销…

【MySQL基本功系列】第二篇 InnoDB存储引擎的架构设计

通过上一篇文章&#xff0c;我们简要了解了MySQL的运行逻辑&#xff0c;从用户请求到最终将数据写入磁盘的整个过程。当数据写入磁盘时&#xff0c;存储引擎扮演着关键的角色&#xff0c;它负责实际的数据存储和检索。在MySQL中&#xff0c;有多个存储引擎可供选择&#xff0c;…

HCIA-DHCP+DHCP中继

DHCPDHCP中继 实验拓扑配置步骤第一步 配置Eth-Trunk聚合链路&二层VLAN第二步 配置IP地址第三步 配置DHCPDHCP中继 配置验证查看PC1 PC2是否正确的获得了IP地址 实验拓扑 配置步骤 第一步 配置Eth-Trunk聚合链路&二层VLAN SW1 sysname SW1 # undo info-center enabl…

Linux 内核启动流程

目录 链接脚本vmlinux.ldsLinux 内核启动流程分析Linux 内核入口stext__mmap_switched 函数start_kernel 函数rest_init 函数init 进程 看完Linux 内核的顶层Makefile 以后再来看Linux 内核的大致启动流程&#xff0c;Linux 内核的启动流程要比uboot 复杂的多&#xff0c;涉及到…

SparkAi创作系统ChatGPT网站源码+详细搭建部署教程+AI绘画系统+支持GPT4.0+Midjourney绘画

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

VulnHub Nullbyte

一、信息收集 1.nmap扫描 arp-scan -l扫描内网存活主机 ┌──(root&#x1f480;kali)-[~/桌面] └─# nmap -sS -A -p- 192.168.103.201/24 -sS 半扫描 -A 扫描详细信息 -p- 扫描全端口发现开放了80、111、777、50978端口 且发现777端口开放了ssh服务&#xff0c;说明他把…

深度学习之各种配置环境

如何使用python进行深度学习&#xff0c;我们需要配置相应的环境 第一步&#xff1a;先安装python python的官网地址&#xff1a;https://www.python.org/ 点进去&#xff0c;点击 Downloads&#xff0c;然后点击 Windows 等待下载完成&#xff0c;安装步骤请参考下文&#x…

主题模型LDA教程:一致性得分coherence score方法对比(umass、c_v、uci)

文章目录 主题建模潜在迪利克雷分配&#xff08;LDA&#xff09;一致性得分 coherence score1. CV 一致性得分2. UMass 一致性得分3. UCI 一致性得分4. Word2vec 一致性得分5. 选择最佳一致性得分 主题建模 主题建模是一种机器学习和自然语言处理技术&#xff0c;用于确定文档…

Linux程序的地址空间

Linux程序的地址空间 &#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;Linux &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容深刻理解了什么程序或者进程的地址…

【性能测试】非GUI模式Jemter压测+TPS性能拐点详细,一篇带你打通...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 非GUI模式执行Jem…

YOLOv8 Ultralytics:使用Ultralytics框架训练RT-DETR实时目标检测模型

YOLOv8 Ultralytics&#xff1a;使用Ultralytics框架训练RT-DETR实时目标检测模型 前言相关介绍前提条件实验环境安装环境项目地址LinuxWindows 制作自己的数据集训练自己的数据集创建自己数据集的yaml文件football.yaml文件内容 进行训练进行验证进行预测 数据集获取参考文献 …

python 对全局变量的修改,需要使用global关键字

is_debug Falsedef get_is_debug():return is_debugdef set_is_debug(dbg):global is_debugis_debug dbg代码review的时候有个同事&#xff08;我们主要都是开发c代码的&#xff0c;python也会写&#xff0c;但是用的少&#xff09;说&#xff0c;set_is_debug函数中 is_debu…

Leetcode-94 二叉树的中序遍历

递归实现 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

网络安全基础之php开发文件下载的实现

前言 php是网络安全学习里必不可少的一环&#xff0c;简单理解php的开发环节能更好的帮助我们去学习php以及其他语言的web漏洞原理 正文 在正常的开发中&#xff0c;文件下载的功能是必不可少&#xff0c;比如我们在论坛看到好看图片好听的歌时&#xff0c;将其下载下来时就…

蓝桥杯算法竞赛第一周题型总结

本专栏内容为&#xff1a;蓝桥杯学习专栏&#xff0c;用于记录蓝桥杯的学习经验分享与总结。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;C &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &#x1f339;&#x1f33…

为什么单片机内不继承晶振

一、晶振是什么&#xff1f;有什么作用&#xff1f; 晶振&#xff08;Crystal Oscillator&#xff09;是一种基于晶体材料的振荡器&#xff0c;用于提供稳定的振荡信号。晶体是一种可通过机械振动在电场作用下产生相应电压的物质。晶振由晶体与电路共同组成&#xff0c;晶体负责…