Android 相机库CameraView源码解析 (五) : 保存滤镜效果

1. 前言

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

那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
上篇文章,我们对带滤镜拍照的整个流程有了大致的了解,这篇文章,我们重点来看如何保存滤镜的效果。

以下源码解析基于CameraView 2.7.2

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

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

在这里插入图片描述

绘制并保存是在SnapshotGlPictureRecorder类中takeFrame()方法的第5部分。

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

这部分代码做了两件事 :

  • mTextureDrawer.draw : 绘制滤镜
  • eglSurface.toByteArray : 转化为JPEG格式的Byte数组

2. 绘制滤镜

首先来看mTextureDrawer.draw()mTextureDrawer上篇文章我们已经介绍过了,通过它,我们最终会调用到mFilter.draw()

public void draw(final long timestampUs) {
    if (mPendingFilter != null) {
        release();
        mFilter = mPendingFilter;
        mPendingFilter = null;

    }

    if (mProgramHandle == -1) {
        mProgramHandle = GlProgram.create(
                mFilter.getVertexShader(),
                mFilter.getFragmentShader());
        mFilter.onCreate(mProgramHandle);
        Egloo.checkGlError("program creation");
    }

    GLES20.glUseProgram(mProgramHandle);
    Egloo.checkGlError("glUseProgram(handle)");
    mTexture.bind();
    mFilter.draw(timestampUs, mTextureTransform);
    mTexture.unbind();
    GLES20.glUseProgram(0);
    Egloo.checkGlError("glUseProgram(0)");
}

可以看到,mTextureDrawer.draw()里,调用顺序如下

  1. 调用GlProgram.create()创建一个OpenGL Program
  2. 调用Filter里的onCreate()
  3. GLES20.glUseProgram(),启用这个Program
  4. 调用mTexture.bind()mTextureGlTexture,这个主要是绑定Texture
  5. 然后调用FilteronDraw方法
  6. 最后,调用mTexture.unbind解除绑定

这里我们重点来看mFilter.onDraw,也就是上文说的Filter接口中的onDraw
所以拍照的绘制,就是在这一块执行的。

3. 转化为JPEG格式的Byte数组

当使用OpenGL绘制到滤镜后,来看接下来的eglSurface.toByteArray()
这里的eglSurfaceEglSurface,可以看到其内部调用了toOutputStream,并最终将ByteArray返回。

public fun toByteArray(format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG): ByteArray {
    val stream = ByteArrayOutputStream()
    stream.use {
        toOutputStream(it, format)
        return it.toByteArray()
    }
}

toOutputStream中调用了GLES20.glReadPixels,作用是从GPU帧缓冲区中读取像素数据。

具体来说,这个函数可以读取当前帧缓冲区或纹理映射到帧缓冲区上的像素数据,并将这些像素数据写入到内存缓冲区中。这是OpenGL提供的用于从帧缓冲区中读取像素数据的函数。在使用glReadPixels()函数捕获屏幕截图时,一般需要先创建一个大小等同于屏幕分辨率的缓冲区对象,并将其与PBO相关联。然后,通过调用glReadPixels()函数来读取帧缓冲区中的像素数据,并将其存储到PBO中。最后,可以使用标准C/C++语法将PBO中的像素数据保存为图片文件,或者进行其他处理和分析。

public fun toOutputStream(stream: OutputStream, format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG) {
    if (!isCurrent()) throw RuntimeException("Expected EGL context/surface is not current")
    
    val width = getWidth()
    val height = getHeight()
    val buf = ByteBuffer.allocateDirect(width * height * 4)
    buf.order(ByteOrder.LITTLE_ENDIAN)
    GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf)
    Egloo.checkGlError("glReadPixels")
    buf.rewind()
    val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    bitmap.copyPixelsFromBuffer(buf)
    bitmap.compress(format, 90, stream)
    bitmap.recycle()
}

调用了GLES20.glReadPixels后,像素数据会被存储在buf中,接着通过buf创建Bitmap,并将Bitmap返回。

4. 分发回调

最后,调用dispatchResult分发回调

protected void dispatchResult() {
    if (mListener != null) {
        mListener.onPictureResult(mResult, mError);
        mListener = null;
        mResult = null;
    }
}

实现了PictureResultListener接口的是CameraBaseEngine

public void onPictureResult(PictureResult.Stub result, Exception error) {
    mPictureRecorder = null;
    if (result != null) {
        getCallback().dispatchOnPictureTaken(result);
    } else {
        getCallback().dispatchError(new CameraException(error,
                CameraException.REASON_PICTURE_FAILED));
    }
}

可以看到这里调用了getCallback().dispatchOnPictureTaken(),最终会调用到CameraView.dispatchOnPictureTaken()

@Override
public void dispatchOnPictureTaken(final PictureResult.Stub stub) {
    LOG.i("dispatchOnPictureTaken", stub);
    mUiHandler.post(new Runnable() {
        @Override
        public void run() {
            PictureResult result = new PictureResult(stub);
            for (CameraListener listener : mListeners) {
                listener.onPictureTaken(result);
            }
        }
    });
}

这里会遍历mListeners,然后调用onPictureTaken方法。
mListeners什么时候被添加呢 ? CameraView中有一个addCameraListener方法,专门用来添加回调。

public void addCameraListener(CameraListener cameraListener) {
    mListeners.add(cameraListener);
}

5. 设置回调

所以我们只要添加了这个回调,并实现onPictureTaken方法,就可以在onPictureTaken()中获取到拍照后的图像信息了。

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 {
            Toast.makeText(this@Test2Activity, "拍照成功", Toast.LENGTH_SHORT).show()
            //将Bitmap设置到ImageView上
            binding.img.setImageBitmap(it)
            
            val file = getNewImageFile()
            //保存图片到指定目录
            ImageUtils.save(it, file, Bitmap.CompressFormat.JPEG)
        }
    }
})

6. 其他

6.1 CameraView源码解析系列

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

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

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

相关文章

若依vue-新建目录及菜单

前面我们把标题和logo换成了自己系统的标题和logo了 接下来就是要建立自己需要的菜单和页面 新建目录解析 在拉下来的代码跑起来后 有一个系统菜单--菜单管理(如图) 在这个菜单的这个页面内有对应的操作功能 修改功能 这个功能可以修改写好了的菜单数据 例如:名称/排序/路由…

(一)五种最新算法(SWO、COA、LSO、GRO、LO)求解无人机路径规划MATLAB

一、五种算法(SWO、COA、LSO、GRO、LO)简介 1、蜘蛛蜂优化算法SWO 蜘蛛蜂优化算法(Spider wasp optimizer,SWO)由Mohamed Abdel-Basset等人于2023年提出,该算法模型雌性蜘蛛蜂的狩猎、筑巢和交配行为&…

vscode 编译运行c++ 记录

一、打开文件夹,新建或打开一个cpp文件 二、ctrl shift p 进入 c/c配置 进行 IntelliSense 配置。主要是选择编译器、 c标准, 设置头文件路径等,配置好后会生成 c_cpp_properties.json; 二、编译运行: 1、选中ma…

SpringBoot的依赖管理和自动配置

与其明天开始,不如现在行动! 文章目录 1 依赖管理机制2 自动配置机制2.1 初步理解2.2 完整流程 💎总结 1 依赖管理机制 为什么导入starter-web后所有相关依赖都会导入进来? 开发什么场景,导入什么场景启动器-spring-bo…

[ROS2] --- action

1 action介绍 ROS通信机制也会被常常用到——那就是动作。从这个名字上就可以很好理解这个概念的含义,这种通信机制的目的就是便于对机器人某一完整行为的流程进行管理。 1.1 客户端/服务器模型 动作和服务类似,使用的也是客户端和服务器模型&#xf…

zabbix 进阶

zabbix的字段发现机制: zabbix客户端主动和服务端联系,将自己的地址和端口发送服务端实现字段添加监控主机。 客户端是主动一方。 缺点:自定义网段中主机数量太多,登记耗时会很久,而且这个自动发现机制不是很稳定。…

c-语言->数据在内存的存储

系列文章目录 文章目录 系列文章目录前言 前言 目的:学习整数在内存的储存,什么是大小端,浮点数的储存。 1. 整数在内存中的存储 在讲解操作符的时候,我们就讲过了下⾯的内容: 整数的2进制表⽰⽅法有三种&#xff0…

Minio保姆级教程

转载自:www.javaman.cn Minio服务器搭建和整合 1、centos安装minio 1.1、创建安装目录 mkdir -p /home/minio1.2、在线下载minio #进入目录 cd /home/minio #下载 wget https://dl.minio.io/server/minio/release/linux-amd64/minio1.3、minio配置 1.3.1、添加…

基于Springboot的校园失物招领系统(有报告)。Javaee项目,springboot项目。

演示视频: 基于Springboot的校园失物招领系统(有报告)。Javaee项目,springboot项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构…

三、jvm中的对象及引用

一、对象在jvm的创建过程 检查加载-->分配内存-->内存空间初始化-->设置-->对象初始化 1) 检查加载 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查类是否已经被加载、解析和初始化过。 虚拟机遇到一条 new 指令时&#xf…

【Deeplearning4j】小小的了解下深度学习

文章目录 1. 起因2. Deeplearning4j是什么3. 相关基本概念4. Maven依赖5. 跑起来了,小例子!6. 鸢尾花分类代码 7. 波士顿房价 回归预测代码 8. 参考资料 1. 起因 其实一直对这些什么深度学习,神经网络很感兴趣,之前也尝试过可能因…

Python实现GUI图片浏览程序

Python实现GUI图片浏览程序 下面程序需要pillow库。pillow是 Python 的第三方图像处理库,需要安装才能实用。pillow是PIL( Python Imaging Library)基础上发展起来的,需要注意的是pillow库安装用pip install pillow,导…

javaEE -14(10000字 JavaScript入门 - 1)

一:初始 JavaScript JavaScript (简称 JS)是世界上最流行的编程语言之一,它是一个脚本语言, 通过解释器运,主要在客户端(浏览器)上运行, 现在也可以基于 node.js 在服务器端运行. JavaScript 和 HTML 和 CSS 之间的关系: HTML…

Rellax.js,一款超酷的 JavaScript 滚动效果库

嗨,大家好,欢迎来到猿镇,我是镇长,lee。 又到了和大家见面的时间,今天和大家分享一款轻松实现视差滚动效果的 JavaScript 库——Rellax.js。无需大量的配置,即可为你的网站增色不少。 什么是Rellax.js&am…

simulinkveristandlabview联合仿真环境搭建

目录 开篇废话 软件版本 明确需求 软件安装 matlab2020a veristand2020 R4 VS2017 VS2010 软件安装验证 软件资源分享 开篇废话 推免之后接到的第一个让人难绷的活,网上开源的软件资料和成功的案例很少,查来查去就那么几篇,而且版本…

以pycharm为例,生成Python项目所需要的依赖库/包文档:requirements.txt

平时我们在编写或者使用别人的Python项目时,往往会看到一个文档requirements.txt,该文档是描述一个Python项目中的第三方库的名称以及版本。本文介绍导出python当前项目依赖包requirements.txt的操作步骤。 方法一:如果每个项目有对应的虚拟…

Java零基础——Elasticsearch篇

1.Elasticsearch简介 Elasticsearch是一个基于Lucene的一个开源的分布式、RESTful 风格的搜索和数据分析引擎。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中&#xf…

13. MySQL 日志

目录 错误日志 binlog日志 概述 日志格式 查询日志 慢查询日志 错误日志 错误日志是MySQL中最重要的日志之一,它记录了当mysqld启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时&#…

用Rust刷LeetCode之26 删除有序数组中的重复项

26. 删除排序数组中的重复项[1] 难度: 简单 老的描述: 新的描述: 注意是 排序数组,非严格递增排列,即已经是排好序的,只不过有重复元素 func removeDuplicates(nums []int) int { if len(nums) 0 { return 0 } i : 0 for j : 1; j < len(nums); j { …

Kubernetes架构及核心部件

文章目录 1、Kubernetes集群概述1.1、概述1.2、通过声明式API即可 2、Kubernetes 集群架构2.1、Master 组件2.1.1、API Server2.1.2、集群状态存储2.1.3、控制器管理器2.1.4、调度器 2.2、Worker Node 组件2.2.1、kubelet2.2.2、容器运行时环境2.2.3、kube-proxy 2.3、图解架构…