音视频SurfaceViewOpenGL

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

2019-05-23  本文已影响0人  蓝师傅_Android

最近比较忙,还花时间写了几篇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》相机预览,这样讲就好理解了

上一篇 下一篇

猜你喜欢

热点阅读