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博客
Android 相机库CameraView源码解析 (六) : 保存滤镜效果-CSDN博客

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

氦客

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值