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

news2025/1/18 11:02:05

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

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

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

相关文章

QT之常用按钮组件

QT之常用按钮组件 导入图标 布局 显示选中 实验结果 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }void Widget::on_push…

‘ChatGLMTokenizer‘ object has no attribute ‘tokenizer‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

六要素金属气象仪-科普小百科

随着科技的发展,气象监测设备也日新月异。今天,我们将为你介绍一款全新的六要素金属气象仪,它能够实时监测环境温度、相对湿度、风速、风向、大气压力、压电雨量等关键气象参数,为你的生活和工作保驾护航。 一、产品简介 WX-Y6S…

浪潮信息:数字化转型的策略与实践

在数字化浪潮的推动下,浪潮信息正致力于将计算创新推向新的高度。作为科技发展的排头兵,浪潮信息深知算力的重要性,因此不断探索前所未有的解决方案。在这个过程中,浪潮信息的研发人员和科技工作者如同探险家,勇敢地迎…

ky10 server x86 设置网卡开机自启

输入命令查看网卡名称 ip a 输入命令编辑网卡信息 vi /etc/sysconfig/network-scripts/*33改成yes 按ESC键,输入:wq,保存

m6A RNA甲基化MeRIP-seq测序分析实验全流程解析

甲基化RNA免疫共沉淀(MeRIP-seq/m6A-seq)实验怎么做,从技术原理、建库测序流程、信息分析流程和研究套路等四方面详细介绍。 一、甲基化RNA免疫共沉淀(MeRIP-seq/m6A-seq)测序技术原理 表观转录组指RNA序列不发生改变的情况下,由RNA上的化学修饰调节基因…

Autosar UDS-CAN诊断开发02-1(CAN诊断帧格式类型详解、CANFD诊断帧格式类型详解、15765-2(CANTP层)的意义)

目录 前言 CANTP层(15765-2协议)存在的意义 CANTP层(15765-2协议)帧类型详细解读(普通CAN格式) 四种诊断报文类型 单帧SingleFrame(SF) 首帧:FirstFrame(FF) 流控帧:FlowCont…

【LabVIEW学习】5.数据通信之TCP协议,控制电脑的一种方式

一。tcp连接以及写数据(登录) 数据通信--》协议--》TCP 1.tcp连接 创建while循环,中间加入事件结构,创建tcp连接,写入IP地址与端口号 2.写入tcp数据 登录服务器除了要知道IP地址以及端口以外,需要用户名与密…

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

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

栈和队列的互相实现

用队列实现栈 OJ链接 请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。 实现 MyStack 类: void push(int x) 将元素 x 压入栈顶。int pop() 移除并返…

Transformer 简介

Transformer 是 Google 在 2017 年底发表的论文 Attention Is All You Need 中所提出的 seq2seq 模型。Transformer 模型的核心是 Self-Attention 机制,能够处理输入序列中的每个元素,并能计算其与序列中其他元素的交互关系的方法,从而能够更…

【Linux】make/Makefile --- 自动化构建项目的工具

目录 一、make/Makefile的简单使用 二、Makefile 的语法规则 三、实现的原理 3.1 make/Makefile识别文件新旧 3.2 .PHONY修饰的伪目标总是被执行 3.3 make/Makefile是具有依赖性的推导能力的 四、语法技巧 五、注意事项 Linux中自动化构建项目最简单的方式:…

shiro Filter加载和执行 源码解析

一、背景 在使用若依框架(前后端不分离包含shiro安全框架)时,发现作者添加了验证码、登录帐号控制等自定义过滤器,于是对自定的过滤器加载和执行流程产生疑问。下面以验证码过滤器为例,对源码解析。注意类之间的继承关…

Kubernetes入门笔记 ——(3)理解pod对象

为什么需要pod 最为熟知的一句话:pod是k8s的最小调度单位。刚开始听到这句话时会想,已经有容器了,k8s为什么还要搞个pod出来?容器和pod是什么关系?容器的本质是进程,而k8s本质上类似操作系统。 熟悉Linux的…

导入PR的视频画面是黑屏的怎么办?

在现代视频编辑领域中,越来越多的人使用Adobe Premiere Pro来编辑和制作视频,但是在某些情况下,用户可能需要透明背景的视频进行创作,那么如何创作透明背景的视频呢? 要制作具有透明背景的视频,我们需要使…

科技改变旅游,道观漫游可视化:智能化管理助力道观游览

道观漫游可视化是一种通过技术手段实现道观游览的可视化展示方式,让游客能够更加直观地了解道观的历史、文化和建筑特色。 随着旅游业的不断发展,道观漫游可视化已经成为了旅游行业中的一个重要方向,吸引了越来越多的游客前来体验。 道观漫游…

5. Jetson Orin Nano CUDA 配置

5. Jetson Orin Nano CUDA 配置 1:安装Jtop jtop安装主要有以下三个步骤: 安装pip3 我们需要使用pip3来安装jtop,所以先安装pip3 sudo apt install python3-pip安装jtop sudo -H pip3 install -U jetson-stats运行jtop服务 sudo -H pip3 in…

芯片量产导入知识

什么是芯片量产 从芯片功能设计到生产制造、测试等环节,每一个环节都至关重要。 对于保障大规模发货后芯片指标表现的一致性,以及产品应用生命周期内的稳定性和可靠性,需要考虑多种因素。以下是一些相关的观点: 可量产性设计&am…

C语言趣练习:两个字符串不用strcmp函数怎么比较大小

目录 1习题一:两个字符串不用strcmp函数怎么比较大小,并输出其差值 2不用strcpy函数将s2字符串中内容复制到s1中 3译密码问题 4总结: 1习题一:两个字符串不用strcmp函数怎么比较大小,并输出其差值 解题思路&#x…

redis 三主三从高可用集群docker swarm

由于数据量过大,单个Master复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只负责存储整个数据集的一部分,这就是Redis的集群,其作用是提供在多个Redis节点间共享数据的程序集。 官网介绍地址 re…