《OpenGL从入门到放弃09》滤镜拍照,了解一下?

最近比较忙,还花时间写了几篇Xposed的文章,所以OpenGL 更新有点慢,不过还是要坚持呀~

上一篇文章《OpenGL从入门到放弃08》相机预览,这样讲就好理解了详细介绍了相机预览,并且封装了一个 Camera2BaseFilter 类,
主要是为了方便实现不同滤镜效果。

跟滤镜有关的内容还有滤镜预览、滤镜拍照、开滤镜录视频,这些都会讲,这篇文章主要学习一下滤镜预览、滤镜拍照,而滤镜录视频比较麻烦一些,所以后面单独写一篇吧~

如果各位有读完上一篇文章,这篇文章理解起来就很简单了。

一、滤镜预览

先增加各种不同滤镜对应的片元着色器,然后在 ShaderManager中预加载着色器代码, ShaderManager《OpenGL从入门到放弃07》滤镜,一定要看,不亏 这一篇文章里有说明了,本次就新增着色器,照猫画虎

先看下最终实现的滤镜预览效果,gif压缩了不清晰,效果还是可以看出来的,大家可以自己去实践一下~


滤镜-预览.gif

1、黑白滤镜着色器

顶点着色器
camera2_vertex_shader_base.glsl

attribute vec4 aPosition;
uniform mat4 uMVPMatrix;
attribute vec4 aTexCoord;
varying vec2 vTextureCoord; //传给片元着色器
varying vec4 vPosition; //传给片元着色器
void main()
{
    vTextureCoord = (uMVPMatrix * aTexCoord).xy;
    gl_Position = aPosition;
    vPosition = aPosition;
}

片元着色器
camera2_fragment_shader_gray.glsl

#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES uTextureSampler;
varying vec2 vTextureCoord;
void main()
{
  vec4 vCameraColor = texture2D(uTextureSampler, vTextureCoord);
  float fGrayColor = (0.3*vCameraColor.r + 0.59*vCameraColor.g + 0.11*vCameraColor.b);
  gl_FragColor = vec4(fGrayColor, fGrayColor, fGrayColor, 1.0);
}

看到这里,是不是很熟悉
《OpenGL从入门到放弃07》滤镜,一定要看,不亏
这一篇文章介绍了好几种滤镜实现,都是关于图片的,这里相机预览同样适用,只不过纹理不一样了,相机预览,用到的纹理是扩展纹理samplerExternalOES,替换一下,然后其它基本一样。

2、黑白滤镜实现类

public class Camera2FilterGray extends Camera2BaseFilter {

    public Camera2FilterGray(Context context, int textureId) {
        super(context, textureId);
    }

    @Override
    protected ShaderManager.Param getProgram() {
        return ShaderManager.getParam(ShaderManager.CAMERA_GRAY_SHADER);
    }
}

黑白着色器的加载在ShaderManager

        //黑白
        insertParam(CAMERA_GRAY_SHADER, GLUtil.loadFromAssetsFile(context, "shader/camera2/camera2_vertex_shader_base.glsl")
                , GLUtil.loadFromAssetsFile(context, "shader/camera2/camera2_fragment_shader_gray.glsl"));

添加一个滤镜就只需要以上步骤,然后在切换滤镜的点击事件中替换滤镜即可。
其它滤镜就不再讲了,如果前面文章都看懂,那么这个对你来说应该是so easy了。

二、拍照

这里思考一个问题:
相机返回的预览数据流其实我们并没有更改,只是在渲染到GLSurfaceView的时候我们增加滤镜显示,所以我们拍照保存肯定不能用Camera2的那套,而是要另辟蹊径。

onDrawFrame这个方法是会一直调用的,每调用一次,就渲染一帧图片,所以我们就可以从onDrawFrame这个方法入手,当点击拍照的时候,获取渲染的这一帧数据,然后保存到本地。同理录制视频就应该是不断的保存每一帧图片,这个后面会介绍。

2.1 如何获取纹理图片数据?

我们现在是要获取GLSurfaceView中渲染的纹理图片数据,然后保存到本地,
GLES20.glReadPixels(...) 上场


public static native void glReadPixels(
        int x, //0    左下角的坐标
        int y, //0
        int width, //输出的宽度
        int height, //输出的高度
        int format, //GLES20.GL_RGBA
        int type, //GLES20.GL_UNSIGNED_BYTE
        java.nio.Buffer pixels //接收输出Buffer
    );

这个方法可以获取渲染在GLSurfaceView中的图片数据,
各个参数的意思:
x, y

图像的左下角的坐标,如果想截取整个opengl显示窗口,取0,0即可

width,height

截屏范围

format:指的是opengl中像素存储的格式,一般有:

GL_ALPHA
GL_RGB
GL_RGBA

type:这个值要和format匹配上,

GL_UNSIGNED_BYTE,(0-255)
GL_UNSIGNED_SHORT_5_6_5,
GL_UNSIGNED_SHORT_4_4_4_4,
oGL_UNSIGNED_SHORT_5_5_5_1,
这个每一个通道的范围在0-2n次方的范围内

pixels:

接收输出Buffer

2.2 上代码

2.2.1 onDrawFrame

    boolean mTakePicture = false;

    public void takePicture() {
        mTakePicture = true;
    }
    public void onDrawFrame(GL10 gl) {
        ...
        mCamera2BaseFilter.draw(mTransformMatrix);

        if (mTakePicture) {
            doTakePicture();
        }

    }

onDrawFrame 中,用一个boolean变量标识是否要拍照,在点击拍照的时候设置true,然后在doTakePicture 中设置为false

2.2.3 doTakePicture

    //回调数据的宽,init 赋值
    public int mFrameCallbackWidth;
    public int mFrameCallbackHeight;
    //用于存储拍照回调数据的buffer,大小是宽乘以高
    private IntBuffer mOutPutIntBuffer;

    private void doTakePicture() {
        mTakePicture = false;
        if (mOutPutIntBuffer == null) {
            mOutPutIntBuffer = IntBuffer.allocate(mFrameCallbackWidth *
                    mFrameCallbackHeight);
        }

        //todo 这里可以用离屏渲染的方式,增加水印,后面再加
//        mCamera2BaseFilter.draw(mTransformMatrix);

        //通过这个api获取画的这一帧数据
        GLES20.glReadPixels(0, 0, mFrameCallbackWidth, mFrameCallbackHeight,
                GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mOutPutIntBuffer);

        //获取到图片数据了
        int[] origin = mOutPutIntBuffer.array();
        int[] result = new int[mFrameCallbackWidth * mFrameCallbackHeight];
        //解决方向颠倒问题
        for (int i = 0; i < mFrameCallbackHeight; i++) {
            for (int j = 0; j < mFrameCallbackWidth; j++) {
                int pix = origin[i * mFrameCallbackWidth + j];
                int pb = (pix >> 16) & 0xff;
                int pr = (pix << 16) & 0x00ff0000;
                int pix1 = (pix & 0xff00ff00) | pr | pb;
                result[(mFrameCallbackHeight - i - 1) * mFrameCallbackWidth + j] = pix1;
            }
        }

        //帧数据回调, Activity中做保存图片操作
        if (mCameraCallback != null) {
            mCameraCallback.onFrameCallBack(result);
        }
    }

基本都注释清楚了,这里再整理一下这几个步骤:

  1. 初始化要输出的 IntBuffer mOutPutIntBuffer,大小是宽乘以高
  2. 调用 GLES20.glReadPixels,传宽高和接收图片的 mOutPutIntBuffer
  3. 得到mOutPutIntBuffer之后,这时候如果直接保存图片,会发现图片上下颠倒,所以要先解决图片上下颠倒问题(for循环自己去理解理解)
  4. 最终将数据回调到Activity中

2.2.4 onFrameCallBack

回调到Activity中,大概看一下

    public void onFrameCallBack(final int[] bytes) {

        final int width = mCameraV2Renderer.mFrameCallbackWidth;
        final int height = mCameraV2Renderer.mFrameCallbackHeight;
        Log.d(TAG, "onFrameCallBack: bitmap width = "+ width);
        Log.d(TAG, "onFrameCallBack: bitmap height = "+ height);
        new Thread(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                IntBuffer byteBuffer = IntBuffer.wrap(bytes);
                bitmap.copyPixelsFromBuffer(byteBuffer);
                saveBitmap(bitmap);
                bitmap.recycle();
            }
        }).start();
    }

int数组转换成IntBuffer,然后通过Bitmap的copyPixelsFromBuffer方法,将数据拷贝到Bitmap中,然后就常规保存Bitmap即可

2.2.5 保存Bitmap

    //图片保存
    public void saveBitmap(Bitmap bitmap) {
        String path = getSD() + "/OpenGLDemo/photo/";
        File folder = new File(path);
        if (!folder.exists() && !folder.mkdirs()) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(Camera2Demo_SurfaceView_Activity.this, "无法保存照片", Toast.LENGTH_SHORT).show();
                }
            });

            return;
        }
        long dataTake = System.currentTimeMillis();
        final String jpegName = path + "_" + dataTake + ".jpg";
        try {
            FileOutputStream fos = new FileOutputStream(jpegName);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
            bos.flush();
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        Log.w(TAG, "saveBitmap: 保存成功,耗时:" + (System.currentTimeMillis() - dataTake));
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(Camera2Demo_SurfaceView_Activity.this, "保存成功,路径:" + jpegName, Toast.LENGTH_SHORT).show();
            }
        });

    }

    protected String getSD() {
        return Environment.getExternalStorageDirectory().getAbsolutePath();
    }

6.0 动态权限就不说了,我提前申请的



接下来就是见证效果的时候了,点击拍照,打开图片


第一次拍照效果

方向对了,不过图片不太对,右边黑了一块?


看下日志打印的大小

D/Camera2Demo_SurfaceView: onFrameCallBack: bitmap width = 1670
D/Camera2Demo_SurfaceView: onFrameCallBack: bitmap height = 1252

图片的大小:


image.png

这个是个bug,这个大小是Camer2获取到的大小,而实际上拍照应该是取GLSurfaceView的大小

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        glViewport(0, 0, width, height);
        Log.i(TAG, "onSurfaceChanged: " + width + ", " + height);
        mFrameCallbackWidth = width;
        mFrameCallbackHeight = height;
    }

onSurfaceChanged 中获取这个大小,然后重新拍照看下

拍照

嗯,这就对了,

最终几个滤镜拍照效果:


滤镜拍照.gif
拍照结果.gif

gif有点模糊,原图是很清晰的~

拍照到这里就结束了,后面还有拍照添加贴纸、拍视频等功能要讲,敬请期待吧~

源码学习
之前的文章
《OpenGL从入门到放弃01 》一些基本概念
《OpenGL从入门到放弃02 》GLSurfaceView和Renderer
《OpenGL从入门到放弃03 》相机和视图
《OpenGL从入门到放弃04 》画一个长方形
《OpenGL从入门到放弃05 》着色器语言
《OpenGL从入门到放弃06 》纹理图片显示
《OpenGL从入门到放弃07》滤镜,一定要看,不亏
《OpenGL从入门到放弃08》相机预览,这样讲就好理解了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容