android opengl美颜功能,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个大小,屏幕大小、摄像头大小、录制大小可以各不相同。
流程图.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日。
android opengl美颜功能,Android 关于美颜/滤镜 利用PBO从OpenGL录制视频相关推荐
- android 美颜录像,Android 关于美颜/滤镜 利用PBO从OpenGL录制视频
前言 上次我写了一遍文章<Android 关于美颜/滤镜 从OpenGl录制视频的一种方案>,里面利用ImageReader来从获取Surface上获取数据,但是经过@熊皮皮的提醒,我发现 ...
- android 视频美颜代码,Android 关于美颜/滤镜 利用PBO从OpenGL录制视频
前言 上次我写了一遍文章<Android 关于美颜/滤镜 从OpenGl录制视频的一种方案>,里面利用ImageReader来从获取Surface上获取数据,但是经过@熊皮皮的提醒,我发现 ...
- android opengl美颜功能,Android短视频中如何实现720P磨皮美颜录制
在Android上要实现一个录制功能,需要有几个方面的知识储备:自定义相机的开发.视频数据格式的了解.编码相关知识以及视频合成技术,同时如果需要美颜.磨皮等滤镜操作还需要一定的openGL的知识.如果 ...
- android的UDC功能,Android实现搜索历史功能
本文实例为大家分享了Android实现搜索历史的具体代码,供大家参考,具体内容如下 SharedPreferences实现本地搜索历史功能,覆盖搜索重复的文本,可清空 1. 判断搜索内容是否含表情,不 ...
- android 添加附件功能,Android实现带附件的邮件发送功能
本文实例讲解了基于基于jmail实现android邮件发送功能,分享给大家供大家参考,具体内容如下 在android上发送邮件方式: 第一种:借助gmail app客户端,缺点是必须使用gmail帐号 ...
- android sharesdk分享功能,Android ShareSDK快速实现分享功能
第一步 :获取ShareSDK 为了集成ShareSDK,您首先需要到ShareSDK官方网站注册并且创建应用,获得ShareSDK的Appkey,然后到SDK的下载页面下载SDK的压缩包,解压以后可 ...
- android系统应用功能,Android系统应用(12)
如何成为系统应用 方法一:在Manifest中声明android:sharedUserId的值为:android.uid.system,android.uid.phone,android.uid.lo ...
- android相册幻灯片功能,Android实现幻灯片式图片浏览器
我们来实现一个幻灯片式图片浏览器: 最下面一个画廊视图,选中画廊中的图片,会在上面的ImageSwitcher控件中显示大图. 效果图如图 实现方法: 在布局文件中添加图片切换控件ImageSwitc ...
- android放微信@功能,Android仿微信语音消息的录制和播放功能
一.简述 效果: 实现功能: 长按Button时改变Button显示文字,弹出Dialog(动态更新音量),动态生成录音文件,开始录音: 监听手指动作,规定区域.录音状态下手指划出规定区域取消录音,删 ...
- android 本地提醒功能,android中的本地定时推送到通知栏
一.使用系统定义的Notification 以下是使用示例代码: import android.app.Notification; import android.app.NotificationMan ...
最新文章
- MySQL优化篇:排序分组优化
- 机器学习数据预处理之缺失值:后向填充
- JavaIO简单代码实例
- php 可以做pc客户端吗,vue.js能做pc端吗
- 直流电动机matlab仿真实验,直流电动机的MATLAB仿真.doc
- Google Chrome —— 离线安装/安装包下载解决方案
- 【Tomcat Eclipse】8080端口被占用问题记录
- 使用Stream API的类Java产量
- 云计算之路-阿里云上:基于Xen的IO模型进一步分析“黑色0.1秒”问题
- 关于源码,反码,补码(正数--负数)---------(-128)自己的理解
- html返回顶部_Jquery实现一键返回顶部
- 智能手机射频前端架构初识: Phase 2/3/5/6/6L/7/7L/7LE
- python程序设计题库-知到智慧树_Python程序设计基础_完整免费答案
- 锐浪报表使用技巧Gird++
- Java面试知识点(零)Java零碎知识点
- 微信小程序中生成二维码工具以及扫一扫
- 2017诺贝尔文学奖揭晓!1901-2017年最全诺奖书单来了
- Mac 此账户尚未用于app store_iOS、Android 本周不能错过的 14 款新 App
- 明日之后怎么跳过实名认证_明日之后,怎么能跳过教学
- R语言实例:diamonds 数据可视化分析报告
热门文章
- 用Java实现24点游戏
- Navicat在输入da..时自动关闭解决方法(手心输入法)
- mina框架CumulativeProtocolDecoder.doDecode方法浅析
- Win10下连接树莓派ZeroW(附win10虚拟网卡驱动下载)
- cdr添加节点快捷键_【CDR干货】常用cdr快捷键命令汇总,快来收藏!
- 阿铭Linux_总览部分学习笔记20190114
- 【书记舞】MMD动作+镜头+配音下载
- MIMO系统的信号检测
- 面试官的窒息逼问: 到底什么是面向接口编程?
- 软件工程各种UML总结