android 美颜录像,Android 关于美颜/滤镜 利用PBO从OpenGL录制视频

前言

上次我写了一遍文章《Android 关于美颜/滤镜 从OpenGl录制视频的一种方案》,里面利用ImageReader来从获取Surface上获取数据,但是经过@熊皮皮的提醒,我发现多PBO的确可以实现跟ImageReader一样的效果,并且版本要求仅为Android4.3。

代码已上传至GitHub

提示:工程需要下载NDK和CMake

正文

1.原理

什么是PBO?PBO就是PixelBufferObject(像素缓存对象),它跟VBO很相似,只不过一个存像素数据,一个存顶点数据,你可以通过《OpenGL像素缓冲区对象(PBO)》了解。

其实上篇文章里我列举的几个方法里面已经有PBO了,但是因为我之前用的是单个PBO,结果测试发现效率不行就放弃了。

单PBO获取像素信息如下:

//绑定到PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(0));

//从FBO中读取数据写入到PBO中

GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0);

//将OpenGL缓存区映射到客户端内存

ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);

//取消内存映射

GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);

//解除PBO绑定

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);

这上面代码其实没有什么问题,包括GLES30.glReadPixels()时间都已经降为0,但就是在执行函数 GLES30.glMapBufferRange()映射内存的时候非常慢。

后来经过提醒后我重新翻看了《OpenGL像素缓冲区对象(PBO)》后发现我之前忽略了二点。

第一个问题是 GLES30.glMapBufferRange()这个函数实际会等待GPU完成了对相应缓冲区对象的操作后才会返回,所以我使用单个PBO并不能显著的提高传输效率,而PBO的主要优点在于可以通过DMA(Direct Memory Access)进行异步传输数据,从而不影响CPU的时钟周期,所以使用2个PBO, 一个PBO拷贝数据、一个PBO映射内存,交替使用,效率将大大提高。

第二个问题就是字节对齐问题,OpenGLES默认以4字节对齐,也就是说我取得的rowStride应该是4的整数倍,计算公式如下:

int align = 4;//4字节对齐

int rowStride = (width * pixelStride + (align - 1)) & ~(align - 1);

而我在GLES30.glReadPixels()中使用的参数是GLES30.GL_RGBA,pixelStride应该等于4,那么就有(width * 4 + (4 - 1)) & ~(4 - 1) == width * 4,从这个道理上来讲,我的width无论取得什么应该都是内存对齐的,效率不应该会降低,事实上大部分机子都没有问题,但是在索尼Z2上效率下降了。

经过我实验后发现如果我是128字节对齐,那么效率不会降低,代码如下:

int align = 128;//128字节对齐

int rowStride = (width * mPixelStride + (align - 1)) & ~(align - 1);

事实上这里我很奇怪,理论上GLES20.glPixelStore()最大值应该是8,怎么都不可能是128,我怀疑这个值应该跟硬件和屏幕分辨率有关,因为ImageReader计算出来的rowStride和我计算出来的值不一样,但是我没有在网上找到相关的资料,如果有谁知道请留言告知我下,谢谢。

关于内存对齐你可以通过《关于内存对齐的那些事》了解。

修改后多PBO获取像素信息如下:

//绑定到第一个PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));

//从FBO中读取数据写入到PBO中

GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0);

//绑定到第二个PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboNewIndex));

//将OpenGL缓存区映射到客户端内存

ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);

//取消内存映射

GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);

//解除PBO绑定

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);

//交换索引

mPboIndex = (mPboIndex + 1) % 2;

mPboNewIndex = (mPboNewIndex + 1) % 2;

经过修改后,2个PBO轮流交替使用,就完全可以满足需求。

2.实现

实际上面讲完,这篇文章就可以结束了,但是我怎么会满足呢!所以我对MagicCamera进行了一些修改。

1.去除grafika方法

在使用PBO之后,grafika方法就已经失去作用了,并且在MagicCamera的写法中过了2次滤镜(绘制到本地窗口一次,绘制到Surface一次),所以开启录制后OpenGL的计算量将加倍。

这里直接删除encoder文件夹。

2.修改原来的绘制方案

原来的绘制方案是先将摄像头数据绘制到FBO,然后将返回的纹理经过滤镜后绘制到本地窗口。

但是因为要使用PBO,所以我先将摄像头数据过滤镜后绘制到FBO,然后以屏幕大小绘制到本地窗口,和以录制大小绘制到另一个FBO在通过PBO获取数据。

这样做的好处就是3个大小,屏幕大小、摄像头大小、录制大小可以各不相同。

3bc4db687546?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

流程图.png

这样需要注意一点因为屏幕大小和录制大小不相同,所以它们的顶点坐标和纹理坐标也不相同,需要重新计算屏幕坐标和录制坐标。

3.开始绘制

接下来就可以开始绘制了,首先将摄像头数据经过滤镜后绘制到FBO。

1.初始化FBO,完整代码请看GPUImageFilter

//生成FBO

GLES20.glGenFramebuffers(1, mFrameBuffers, 0);

//生成纹理

GLES20.glGenTextures(1, mFrameBufferTextures, 0);

//绑定到纹理

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);

//...省略设置纹理参数

//将纹理关联到FBO

GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);

//解除绑定纹理

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

//解除绑定FBO

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

上面将纹理关联到FBO,这样就可以直接绘制到纹理上。

2.将摄像头数据经过滤镜后绘制到FBO,完整代码请看GPUImageFilter

//设定为摄像头大小

GLES20.glViewport(0, 0, 480, 640);

//绑定到FBO

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);

//...省略其他代码

//设置矩阵,该矩阵从摄像头获得

GLES20.glUniformMatrix4fv(mTextureTransformMatrixLocation, 1, false, mTextureTransformMatrix, 0);

//选择活跃纹理

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

//绑定到纹理,这里需要注意GL_TEXTURE_EXTERNAL_OES是特殊的

GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);

GLES20.glUniform1i(mGLUniformTexture, 0);

//...省略其他代码

//解除绑定纹理

GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);

//解除绑定FBO

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

//设定为屏幕大小

GLES20.glViewport(0, 0, 1080, 1920);

上面的矩阵通过mSurfaceTexture.getTransformMatrix(mtx)获得,顶点着色器需要添加参数。

attribute vec4 position;

attribute vec4 inputTextureCoordinate;

varying vec2 textureCoordinate;

uniform mat4 textureTransform;

void main() {

textureCoordinate = (textureTransform * inputTextureCoordinate).xy;

gl_Position = position;

}

这里的GL_TEXTURE_EXTERNAL_OES必须要注意,当我们使用mSurfaceTexture.updateTexImage()时,图像会被隐式的绑定到GL_TEXTURE_EXTERNAL_OES,所以这里跟我们一般使用的纹理GL_TEXTURE_2D不同。

所以片段着色器也必须要修改,下面是没有滤镜的实现,其他的看Raw。

#extension GL_OES_EGL_image_external : require

varying highp vec2 textureCoordinate;

uniform samplerExternalOES inputImageTexture;

void main(){

gl_FragColor = texture2D(inputImageTexture, textureCoordinate);

}`

3.将返回的纹理绘制到本地窗口,完整代码请看GPUImageFilter

//...省略其他代码

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

//绑定纹理,这里的纹理是GL_TEXTURE_2D

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

GLES20.glUniform1i(mGLUniformTexture, 0);

//...省略其他代码

//解除绑定纹理

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

这里的顶点着色器和片段着色器需要去除矩阵和OES参数。

4.如果开始录制将返回的纹理绘制到FBO然后通过PBO获得数据,完整代码请看MagicRecordFilter

final int align = 128;//128字节对齐

mRowStride = (width * mPixelStride + (align - 1)) & ~(align - 1);

mPboIds = IntBuffer.allocate(2);

//生成2个PBO

GLES30.glGenBuffers(2, mPboIds);

//绑定到第一个PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(0));

//设置内存大小

GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, mPboSize, null,GLES30.GL_STATIC_READ);

//绑定到第而个PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(1));

//设置内存大小

GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, mPboSize, null,GLES30.GL_STATIC_READ);

//解除绑定PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);

2.绘制2D纹理到FBO,完整代码请看MagicRecordFilter

//设定为录制大小

GLES20.glViewport(0, 0, 240, 320);

//绑定到FBO

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);

//...省略其他代码

//设置矩阵

GLES20.glUniformMatrix4fv(mTextureTransformMatrixLocation, 1, false, mTextureTransformMatrix, 0);

//选择活跃纹理

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

//绑定到纹理

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

GLES20.glUniform1i(mGLUniformTexture, 0);

//...省略其他代码

//解除绑定纹理

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

//解除绑定FBO

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

//设定为屏幕大小

GLES20.glViewport(0, 0, 1080, 1920);

这里也需要设置矩阵,但是这个矩阵不是从摄像头获取的,而是我自己把它垂直翻转了下。

mTextureTransformMatrix = new float[]{

-1f, 0f, 0f, 0f,

0f, 1f, 0f, 0f,

0f, 0f, 1f, 0f,

1f, 0f, 0f, 1f});

为什么我要垂直翻转呢,因为RGB图像在内存中存储的时候是从下到上的,如果你直接把数据赋值给Bitmap,那么你将得到一张倒置的并且颜色为BGRA的图像,这也可以解释为什么我们最终要将BGRA转换为ARGB,因为Bitmap需要的是Bitmap.Config.ARGB_8888。

private void bindPixelBuffer() {

//绑定到第一个PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));

//调用glReadPixels获取数据,这里需要注意原生的Java里面没有与PBO配合的glReadPixels方法

MagicJni.glReadPixels(0, 0, mRowStride, mInputHeight, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE);

//第一帧没有数据跳出

if (mInitRecord) {

unbindPixelBuffer();

mInitRecord = false;

return;

}

//绑定到第二个PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboNewIndex));

//glMapBufferRange会等待DMA传输完成,所以需要交替使用pbo

//映射内存

ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, mPboSize, GLES30.GL_MAP_READ_BIT);

//解除映射

GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);

unbindPixelBuffer();

//交给mRecordHelper录制

mRecordHelper.onRecord(byteBuffer, mInputWidth, mInputHeight, mRowStride, mLastTimestamp);

}

//解绑pbo

private void unbindPixelBuffer() {

//解除绑定PBO

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);

//交换索引

mPboIndex = (mPboIndex + 1) % 2;

mPboNewIndex = (mPboNewIndex + 1) % 2;

}

这里必须要注意,要与PBO配合使用glReadPixels()最后一个参数必须为0,但是原生Java层的glReadPixels()最后一个参数是Buffer,而最后参数为int的glReadPixels()24版本才有,所以这里需要使用jni去调用原生的glReadPixels()方法,代码在MagicJni。

关于RecordHelper我就不讲了,跟上篇一样,这里可以用libyuv代替,我这只是作为测试浏览用。

我这里JNI采用CMake编译,编译指令在CMakeLists.txt,更多可以参考谷歌官方文档《向您的项目添加 C 和 C++ 代码》。

结尾

其实在篇文章我早就写完了,但是一直搞不清楚rowStride的计算方式,最终我决定还是不拖了,直接发布希望有谁知道的能指点下,谢谢。

最后,如果它有解决你的问题的话,请下点个赞,谢谢。

这是我个人的第四篇文章,发布于2017年5月15日。

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

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

相关文章

Java对象到对象映射器

我在该项目上使用了Dozer一段时间。 但是,最近我遇到了一个非常有趣的错误,它促使我环顾四周,并尝试使用其他“对象到对象”映射器。 这是我找到的工具列表: 推土机:推土机是Java Bean到Java Bean的映射器&#xff…

android媒体播放框架,Android 使用超简单的多媒体播放器JiaoZiVideoPlayer

在之前的项目中用到了视频播放的功能,在网上看了看使用了大家用的比较多的一个开源项目JiaoZiVideo可以迅速的实现视频播放的相关功能。JiaoZiVideo的简单使用集成了JiaoZiVideo后仅需这几行代码就可以实现播放视频JZVideoPlayerStandard jzVideoPlayerStandard (J…

送福利:ROKID 语音开发板免费送,开启你的物联网之旅

都让一让,我说个事情:掘金联合 Rokid 开发者社区给大家发福利啦! 掘金联合 Rokid 开发者社区为大家准备了一些福利,只要秀出你的 skill 和技术栈,就有可能获得 Rokid 全栈语音智能开发套件。 ? Rokid开箱试用活动 活…

如何使用JavaScript控制台改进工作流程

作为Web开发人员,很有必要了解如何调试代码。后台开发我们经常使用外部库来记录日志,并在某些情况下格式化显示日志,前端我们会使用断点和控制台,但是我们浏览器的控制台比我们想象的要强大得多。 当我们考虑控制台时&#xff0c…

select、poll、epoll之间的区别总结[整理]

原文:https://www.cnblogs.com/Anker/p/3265058.html 好文章收藏下,慢慢品味 select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者…

JPA(七):映射关联关系------映射双向多对一的关联关系

映射双向多对一的关联关系 修改Customer.java package com.dx.jpa.singlemanytoone;import java.util.Date; import java.util.HashSet; import java.util.Set;import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; impo…

如何优雅的绘制一棵省市区三级可选择的树?

开始 总结一下 开发过程中的思路想法 各位大佬们看看就好 首先你拥有的数据结构 所有省市区的信息列表 以及已经选中的信息 用的是element-ui的 el-tree const cityStorage {provinceList:[{id: 1, provinceId: "110000", name: "北京市"}],//所有省ci…

html click事件 参数,vue 实现click同时传入事件对象和自定义参数

这篇文章主要介绍了vue 实现click同时传入事件对象和自定义参数,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧仅仅传入自定义参数HTMLdddddJS代码new Vue({el:#app,methods:{tm:function(e){console.log(e);}}})仅仅传入事件对象HTML…

Android学习(七)—— Android布局

Android布局 1、LinearLayout 线性布局,这种布局在平时的开发中用的最多,内部控件只能水平或竖直进行排列,在搭建较复杂的界面时会有点麻烦。 常用属性 android:orientation 控制控件排列方向,属性值为垂直(vertical…

不一样的ZTree,权限树.js插件

每一个有趣的创新,都源于苦逼的生活。在最近的工作中,遇到一个做权限管理筛选的需求。 简单总结需求: 1展示一个组织中的组织结构 2通过点击组织结构中的任意一个节点可以向上向下查询对应的组织结构 如果你不想苦逼的重复劳动,还…

JavaFX 2:如何加载图像

这是有关如何在JavaFX 2应用程序中加载图像的JavaFX教程。 使用ImageView可以轻松完成此操作。 ImageView是一个节点,用于绘制加载有Image类的图像。 因此,您将首先使用Image类加载图像,然后使用ImageView显示它。 我还将在这里演示如何从本地…

记HTML5 a 标签的一个小坑

今天写了段简单的代码&#xff0c;点击<a>标签时却抛出了这个错误&#xff1a;Uncaught TypeError: download is not a function。代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><…

C#线程 ---- 线程同步详解

线程同步 说明&#xff1a;接上一篇&#xff0c;注意分享线程同步的必要性和线程同步的方法。 测试代码下载&#xff1a;https://github.com/EkeSu/C-Thread-synchronization-C-.git 一、什么是线程同步&#xff1a; 在同一时间只允许一个线程访问资源的情况称为线程同步。 二、…

响应式方案调研及前端开发管理思考

网易首页响应式风格实现技术调研网易首页实现页面&#xff08;字体&#xff09;响应式风格的方式是在不同尺寸的视口中使用不同的容器类&#xff0c;如图 1所示。当视口大于等于1420px时&#xff0c;使用大尺寸容器类 &#xff08;index2017_1200_wrap&#xff0c;width: 1200p…

linux nexus启动_Linux一键部署Nexus 3私服仓库自动化部署脚本

此脚本是Linux一键部署Nexus 3私服仓库自动化脚本&#xff0c;有需要朋友可以参考&#xff0c;脚本内容如下&#xff1a;环境准备&#xff1a;操作系统&#xff1a;CentOS Linux release 7.8.2003软件版本&#xff1a;Docker&#xff1a;docker-ce-19.03.12[rootlocalhost ~]# …

flex.css快速入门,极速布局

什么是flex.css? css3 flex 布局相信很多人已经听说过甚至已经在开发中使用过它&#xff0c;但是我想我们都会有一个共同的经历&#xff0c;面对它的各种版本&#xff0c;各种坑&#xff0c;傻傻的分不清楚&#xff0c;flex.css就是对flex布局的一种封装&#xff0c;通过简洁…

优化Angularjs的$watch方法

Angularjs的$watch相信大家都知道&#xff0c;而且也经常使用&#xff0c;甚至&#xff0c;你还在为它的某些行为感到恼火。比如&#xff0c;一进入页面&#xff0c;它就会调用一次&#xff0c;我明明希望它在我初始化之后&#xff0c;值再次变动才调用。这种行为给我们带来许多…

JavaFX中的塔防(2)

在上一部分中&#xff0c;我们创建了一个简单的编辑器&#xff0c;让我们放置炮塔。 现在&#xff0c;我们将在敌人起源的地方添加一个生成点&#xff0c;并为其定义攻击目标。 首先&#xff0c;我将通过对象层向地图添加更多信息。 这是标准的TMX&#xff0c;因此我们可以在Ti…

12面魔方公式图解法_三阶魔方入门

一、魔方的构造这里只讲常见的普通三阶魔方。三阶魔方一共有26个色块&#xff0c;分三个层&#xff0c;从上到下分别为顶层、中间层、底层。26个色块按位置分为中心块、角色块、棱色块。中心块6个&#xff0c;角色块8个&#xff0c;棱色块12个。中心块为每一个面最中央的色块。…

Linux ls命令详解

ls常见命令参数 ls: -F 给不同的文件添加不同表示,添加帽子 d/ l* s -a: 显示隐藏文件 以.开头的文件 -p: 只给目录添加/ -t: 按照修改时间排序 time --time-stylelong-iso: ls -l --time-stylelong-iso 显示友好长格式时间 -r: 倒着排序 reverse -S: 按照文件…