美文网首页AndroidAndroid日记程序猿学习
Android 关于美颜/滤镜 从OpenGl录制视频的一种方案

Android 关于美颜/滤镜 从OpenGl录制视频的一种方案

作者: 某金 | 来源:发表于2017-04-08 18:15 被阅读9456次

前言

这篇文章是有感而发,从一开始做实时美颜视频录制到现在大概能真正开始用,找了无数资料,也经历了很长一段时间,真的感觉比较艰难,我现在写这篇文章也希望能帮助到更多的人。

首先我需要特别感谢下程序员扛把子同学,讲真,我感觉是技术大佬没的说,最最关键具有共享精神,要不是他的《Android+JNI+OpenGL开发自己的美图秀秀》我估计根本没任何办法完成美颜录制。

代码已上传至GitHub

特别附上MagicCamera工程

提示:工程需要下载NDK

正文

其实一开始叫我做美颜录制我是拒绝的,因为不能你叫我做我就做,最关键的是......

我不会啊!!!

我就是一开发Android的,你叫我做这个明显是超出的能力范围的啊!!!

好吧,领导最大,你叫我做我就做,然后我就开始找这方面的资料,最先找到的就是GPUImage,后来找到了MagicCamera,可以说它已经实现了全部的功能,你唯一需要考虑的就是如何将它移植到你的工程中去。


其实上面的都不是本文的重点n(≧▽≦)n**

重点来了

我上面说过MagicCamera已经实现了全部的功能,包括实时滤镜浏览到最后的录制成MP4,但问题就出在录制这里。

MagicCamera采用的是录制方案来自于grafika,这是个神器,你可以从里面了解到GLSurfaceview的很多知识,据说是Google工程师fadden业余时间写的。

这个方案实际上最终就是利用MediaCodec录制,MediaCodec的颜色模式里有个MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface常量,设置MediaFormat.KEY_COLOR_FORMAT后就可以通过mEncoder.createInputSurface()创建一个Surface,然后你就可以通过OpenGL将图像绘制到这个Surface上,MediaCodec就可以通过这个Surface录制出H264,具体代码可以看VideoEncoderCore

上面方案其实非常好,效率也非常高,因为是直接硬编码的H264,比起一般使用ffmpng的软编码效率要高不少,但是有个非常致命的缺点,无法设置fps

MediaCodec有个MediaFormat.KEY_FRAME_RATE参数,它可以设置fps,但是我发现我不管设置什么最终读出的fps都是25帧,后来我谷歌到了《MediaCodec KEY_FRAME_RATE seems to be ignored》才知道MediaCodec不会丢帧,如果要降低fps,必须减少传给MediaCodec的帧数,但事实上我就算减少了帧数,最终录制出来的H264的fps还是25帧,我不知道为什么。

这里我跟yasea的作者begeekmyfriend讨论过《关于取得美颜数据的效率问题》还是没得到结果,如果有谁知道请留言告诉我下,谢谢

如果你对MediaCodec不熟悉的话,可以看下Google官方文档的翻译版本《Android多媒体--MediaCodec 中文API文档》然后结合grafika学习。

解决问题

然后我没办法只能寻找其他方法解决。

我这里列举下我查找过的方法

  • GLES20.glReadPixels():OpenGL方法,用于读取像素,但我测试过只有在索尼或者三星的手机上效率可以,在国产的机子上效率低的可以差不多接近100ms,这是完全不可接受的。
  • PBO:详细请看我的另一篇文章《Android 关于美颜/滤镜 利用PBO从OpenGL录制视频》
  • EGLImageKHR:它要用到一个叫GraphicBuffer的类,但是谷歌后我才知道这个类早在2.X就已经在NDK删除了,不允许你使用,后来我找到了替代GraphicBuffer,但是想想还是算了,既然被删除了就一定有他的道理。
  • ImageReader:这个就是我最终的方案,效率完全可以接受,但是要求4.4以上。

ImageReader

这个方案是我从《Android5.0录屏方案》中得到的灵感,这篇文章说MediaProjection是通过createVirtualDisplay()创建的,并且它接受一个Surface,通过创建ImageReader后调用getSurface()方法得到一个Surface后传递给MediaProjection,然后就可以通过ImageReader相关的API进行录制。

这我发现跟MediaCodec创建Surface的方式一模一样,唯一不同的就是MediaCodec录制出来的是H264,而ImageReader拿出来的是BGRA的,用《Android ImageReader使用》的话说就是**ImageReader类允许应用程序直接访问呈现表面的图像数据 **。

ImageReader首先要求4.4及以上(稍微有点瑕疵,但是现在4.4以下的机子应该比较少了),并且拿出来的是BGRA的(跟Bitmap的ARGB_8888是不一样的,这里要注意),而且在使用过程中要注意内存对齐的问题。

这里需要注意,我在Demo中是480x640的,如果是原来的1280x1080那这效率还是远远跟不上的。

接下来我们的思路就很清楚了

  1. 创建ImageReader后获得Surface。
  • TextureMovieEncoder获取这个Surface,然后将图像绘制到这个Surface。
  • 通过setOnImageAvailableListener设置回调,然后通过acquireNextImage()方法获取每一帧。
  • 将取出来的数据BGRA转换为YUV420,通过MediaCodec或者ffmpng编码成H264。

我们现在要做的就是替换VideoEncoderCore,创建一个ImageEncoderCore。

完整代码请看ImageEncoderCore

public class ImageEncoderCore {
    private static final int MAX_IMAGE_NUMBER = 25;//这个值代表ImageReader最大的存储图像

    private ImageReader mImageReader;
    private Surface mInputSurface;

    public ImageEncoderCore(int width, int height) {
        this.mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, MAX_IMAGE_NUMBER);

        mInputSurface = mImageReader.getSurface();

        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                Image image = reader.acquireNextImage();//获取下一个
                Image.Plane[] planes = image.getPlanes();
                int width = image.getWidth();//设置的宽
                int height = image.getHeight();//设置的高
                int pixelStride = planes[0].getPixelStride();//像素个数,RGBA为4
                int rowStride = planes[0].getRowStride();//这里除pixelStride就是真实宽度
                int rowPadding = rowStride - pixelStride * width;//计算多余宽度

                byte[] data = new byte[rowStride * height];//创建byte
                ByteBuffer buffer = planes[0].getBuffer();//获得buffer
                buffer.get(data);//将buffer数据写入byte中

                //到这里为止就拿到了图像数据,你可以转换为yuv420,或者录制成H264

                //这里我提供一段转换为Bitmap的代码

                //这是最终数据,通过循环将内存对齐多余的部分删除掉
                // 正常ARGB的数据应该是width*height*4,但是因为是int所以不用乘4
                int[] pixelData = new int[width * height];

                int offset = 0;
                int index = 0;
                for (int i = 0; i < height; ++i) {
                    for (int j = 0; j < width; ++j) {
                        int pixel = 0;
                        pixel |= (data[offset] & 0xff) << 16;     // R
                        pixel |= (data[offset + 1] & 0xff) << 8;  // G
                        pixel |= (data[offset + 2] & 0xff);       // B
                        pixel |= (data[offset + 3] & 0xff) << 24; // A
                        pixelData[index++] = pixel;
                        offset += pixelStride;
                    }
                    offset += rowPadding;
                }

                Bitmap bitmap = Bitmap.createBitmap(pixelData,
                        width, height,
                        Bitmap.Config.ARGB_8888);//创建bitmap

                image.close();//用完需要关闭
            }
        }, null);
    }

    /**
     * 返回surface
     */
    public Surface getInputSurface() {
        return mInputSurface;
    }

    /**
     * 释放
     */
    public void release() {
        if (mImageReader != null) {
            mImageReader.close();
            mImageReader = null;
        }
    }
}

工程我直接用的是MagicCamera,将VideoEncoderCore替换成ImageEncoderCore,然后将获取的图像传回主界面显示。

结尾

实时上ImageReader对于获取大尺寸的图像的效率还是不是非常高,所以我在工程中使用的是480x640的分辨率,老实讲我还是觉得MediaCodec的方法应该更加的好,毕竟ImageReader还需要多2步转换过程(BGRA转ARGB转YUV420)。

就这些吧,共勉。

相关文章

网友评论

  • 830605ff668a:请问这个project如何编译?
  • Frank_X:怎么加入声音通道啊,这个录制视频没有录制声音哦
    某金:@Frank_X 用AudioTrack录制
  • 小白学AI:如果想使用多个纹理,比如再向shader中传入一张图片的纹理,然后将摄像头的纹理和图片的纹理融合,F = a*img1 + (1-a)*img2。我在楼主的项目基础上做了修改:各种滤波器不再继承GPUImage而是继承GPUImageTwoInputFilter。这样就可以使用GPUImageTwoInputFilter的成员函数读入一张图片,获取纹理,然后在shader中摄像头的纹理进行融合。
    但是上传到shader中后,无法显示,直接报错退出。mFilterSecondTextureCoordinateAttribute = GLES20.glGetAttribLocation(getProgram(), "inputTextureCoordinate2");这种的返回值也都是正数或者0(我理解正数或者0应该是正确的返回结果)。这样问题应该定位在shader中。但是在shader是GPUImage官方的进行纹理融合的范例,也是我在iOS平台上测试通过的。
    困扰中。希望楼主或者其他人能给点建议。
  • 899e2d74bdeb:请问你说ImageEncoderCore这个可以控制fps,但我没看出代码哪里控制了fps,它到最后还不是一样要用MediaCodec进行硬编码。请解答,谢谢?
    899e2d74bdeb:@某金 谢谢
    某金:我这里的确没做控制,ImageReader可以读出每一帧,你需要根据时间自己忽略多余的帧
    899e2d74bdeb:this.mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, MAX_IMAGE_NUMBER);是不是通过MAX_IMAGE_NUMBER这个参数进行控制。
  • 3c591aaea3df:想问一下,你是怎么控制到fps的了?用ImageReader读到没一帧以后,自己控制然后还是丢给MediaCodec来编码吗?还是说改成ffmpeg的软编码了?
    某金:软编
  • f9320167088f:保存带滤镜视频需要怎么做啊…,能提供个思路不,刚接触这个滤镜。
  • 1c72477e2e9c:为啥你要纠结设置帧率的问题
  • 57f8383c791f:大神,请问怎么把这个放在IM视频通话呢?
    某金:把rgba编码成h264吧,你可以去查找下直播相关的技术,原理是一样的
  • 心海泡沫:请问怎么将这个demo录制成mp4啊,刚干这行没多久,感觉一点头绪都没有。
  • 40861814d719:大神你这个demo好像没有保存录制好的视频,怎么保存
  • master_or_dev:录制的视频根本没有保存下来啊
  • 6dc6bde0b0ab:大神,如何对本地视频应用这些滤镜效果并播放,然后保存到本地
  • 567dacd0f6dd:赞,楼主,你的代码能录制视频吗,录制后怎么查看
  • 0d2686552350:我在使用magiccamera的时候录制的视频都是可变帧率的,我是使用MP4parse做拼接(断点拍摄),但这个框架不支持不同帧率的视频拼接,怎么样才能让帧率恒定下来
  • chiu丶:请问MagicCamera为什么没有保存视频的代码,还有保存带滤镜效果的图片是黑屏~
  • 8f6d3c21c926:怎么设置暂停,继续录制,类似断点录制
    830605ff668a:请问下,这个project如何编译呀,安装了ndk,仍然编译不过~~~
    刘春_2d5a:录制出来的视频为什么模糊,还有就是,如何分段保存视频
    某金: @何以平此生 一般画面有个时间戳,你在录制的时候需要根据时间戳去筛选符合时间的帧,断点录制就是暂停获取时间段的帧,恢复录制后重新获取帧
  • 程序员小跃:楼主,请问下,你实现了相机实时预览的时候磨皮和美白的功能吗?
    程序员小跃:@某金 MagicCamera貌似没有,是导入的图片支持磨皮美白,在相机预览的时候还没的
    某金: @5d8faa06be1f 有啊,你看下MagicCamera
  • kevin_nazgul:市面上大多数带美颜和特效的软件都是使用MediaCodec,因为效率最好(觉得fps不是什么大事)。

    如果MediaCodec不能正常使用(有些机器改得有BUG),就需要从OpenGL读出数据,然后再使用ffmpeg编码,读数据采用的方案如你所说:
    1. EGLImageKHR方案,速度最快,稳定性不好,因为厂商会改它的实现,导致接口有变更。且7.0之后禁止使用非公开API。
    2. PBO方案:速度次于1,但是好歹是个公开API,缺点是要求OpenGL 3.0,虽然3.0是从很早就有接口,但是具体实现可能是用2.0进行模拟的。
    3. ImageReader:公开API,速度次于PBO方案。你提的BGRA的问题,也许你可以在OpenGL里面就故意倒着画,没试验过。其次,你用java去转换,如果你用jni转换速度会更快些。
    某金:MediaCodec的确是更快,我也提倡用这个,如果不是有蛋疼的FPS要求:joy: ,BGRA的问题应该是RGB数据在内存中从下而上存储的问题[http://blog.csdn.net/g0ose/article/details/52116453]
  • 1ecb7e63e384:MediaCodec无论怎么设置fps都是25帧,我猜测您用的应该是华为的设备!?
    某金:@Elemelpo :sob:
    1ecb7e63e384:@某金 据说、华为的产品的前置摄像头(还有一些品牌的平板类的产品),无论如何在代码里面设置帧率最终都会执行一遍芯片产商的编码类(而且很有可能是软编),而后置摄像头则不会有这个问题,我也是最近发现的这个问题,就我身边的一些品牌来说,就华为有这个问题(不过身边也就几种品牌):smile: 至于解决方案,目前也还在努力的寻找中.......
    某金:@Elemelpo 额,的确有华为,可是其他设备也会出现同样的情况啊,话说你有什么更好的解决方案吗?
  • 77a3d68117f4:想问一下,有没有办法,对本地视频文件应用这些shader滤镜?
    某金: @没有人是李妖迟 跟录制没关系啊你解码成yuv,然后通过opengl或者ffmpeg过滤镜后重新编码
    77a3d68117f4:@某金 相当于重新录制一遍? 需要和视频长度一样的时间?
    某金: @没有人是李妖迟 先解码绘制到surface(OpenGL)上过滤镜,然后在重新编码
  • 熊皮皮:既然ImageReader要求Android 4.4以上,那不如使用多PBO方式,这是OpenGL ES 3.0的接口,Android 4.3就支持OpenGL ES 3.0了。
    某金: @熊皮皮 你说的对用多个PBO的确能实现很好的效果,非常感谢提出。
    熊皮皮:@某金 PBO的代码,很多人参考这个链接http://www.songho.ca/opengl/gl_pbo.html。另外,BGRA转YUV420用libyuv一步应该能实现。
    某金: @熊皮皮 额,我之前也用过pbo,但是在华为等机器上效率很慢,根本达不到要求,我对opengl不是很懂,你有什么文章可以让我参考下吗?
  • 41f358492b3d:兄弟,0个issue 不敢用啊.给你star了
    某金: @面对疾风 不不不,我只是提供一种方案而已,你可以自己去搜索ImageReader,美颜部分不是我写的,你可以去看原文。
  • 迷途小书童nb:赞👍
    某金: @Adley 谢谢😄

本文标题:Android 关于美颜/滤镜 从OpenGl录制视频的一种方案

本文链接:https://www.haomeiwen.com/subject/mfkkattx.html