不操千曲而后晓声,观千剑而后识器。这篇文章主要讲述Android Camera API/Camera2 API 相机预览及滤镜贴纸等处理相关的知识,希望能为你提供帮助。
android Lollipop 增加了Camera2 API,
并将原来的Camera API标记为废弃了。相对原来的Camera API来说,
Camera2是重新定义的相机 API,
也重构了相机 API 的架构。初看之下,
可能会感觉Camera2使用起来比Camera要复杂,
然而使用过后,
你也许就会喜欢上使用Camera2了。无论是Camera还是Camera2,
当相机遇到OpenGL就比较好玩了。
问题及思路 Camera的预览比较常见的是使用SurfaceHolder来预览,
Camera2比较常见的是使用TextureView来预览。如果利用SurfaceHolder作为Camera2的预览,
利用TextureView作为Camera的预览怎么做呢?
实现起来可能也很简单,
如果预览之前,
要做美颜、磨皮或者加水印等等处理呢?
实现后又如何保证使用Camera还是Camera2 API,
使用SurfaceHolder还是TextureView预览,
或者是直接编码不预览都可以迅速的更改呢?
本篇博客示例将数据、处理过程、预览界面分离开,
使得无论使用Camera还是Camera2,
只需关注相机自身。无论最终期望显示在哪里,
都只需提供一个显示的载体。详细点说来就是:
- 以一个SurfaceTexture作为接收相机预览数据的载体, 这个SurfaceTexture就是处理器的输入。
- SurfaceView、TextureView或者是Surface, 提供SurfaceTexture或者Surface给处理器作为输出, 来接收处理结果。
- 重点就是处理器了。处理器利用GLSurfaceView提供的GL环境, 以相机数据作为输入, 进行处理, 处理的结果渲染到视图提供的输出点上, 而不是GLSurfaceView内部的Surface上。当然, 也可以不用GLSurfaceView, 自己利用EGL来实现GL环境, 问题也不大。具体实现就参照GLSurfaceView的源码来了。
文章图片
文章图片
文章图片
具体实现 根据上述思路来, 主要涉及到以下问题:
- 使用GLSurfaceView创建GL环境, 但是要让这个环境为离屏渲染服务, 而不是直接渲染到GLSurfaceView的Surface上。在这其中还涉及到其他的一些问题, 具体的问题, 在下面再说。
- OpenGL 的使用。相关文章
- 务必使相机、处理过程、显示视图分离。以便能够自由的替换数据源、显示视图, 只需要关注处理过程。
首先, 我们使用GLSurfaceView, 是希望利用它的GL环境, 而不是它的视图, 所以, 我们需要改变它的渲染位置为我们期望的位置:
//这句是必要的,
避免GLSurfaceView自带的Surface影响渲染
getHolder().addCallback(null);
//指定外部传入的surface为渲染的window surface
setEGLWindowSurfaceFactory(new GLSurfaceView.EGLWindowSurfaceFactory() {
@
Override
public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig
config, Object window) {
//这里的surface由外部传入,
可以为Surface、SurfaceTexture或者SurfaceHolder
return egl.eglCreateWindowSurface(display,config,surface,null);
}@
Override
public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
egl.eglDestroySurface(display, surface);
}
});
另外, GLSurfaceView的GL环境是受View的状态影响的, 比如View的可见与否, 创建和销毁, 等等。我们需要尽可能的让GL环境变得可控。因此, GLSurfaceView有两个方法一顶要暴露出来:
public void attachedToWindow(){
super.onAttachedToWindow();
}public void detachedFromWindow(){
super.onDetachedFromWindow();
}
这里就又有问题了, 因为GLSurfaceView的onAttachedToWindow和onDetachedFromWindow是需要保证它有parent的。所以, 在这里必须给GLSurfaceView一个父布局。
//自定义的GLSurfaceView
mGLView=
new GLView(mContext);
//避免GLView的attachToWindow和detachFromWindow崩溃
new ViewGroup(mContext) {
@
Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {}
}.addView(mGLView);
另外, GLSurfaceView的其他设置:
setEGLContextClientVersion(2);
setRenderer(TextureController.this);
setRenderMode(RENDERMODE_WHEN_DIRTY);
setPreserveEGLContextOnPause(true);
这样, 我们就可以愉快的使用GLSurfaceView来提供GL环境, 给指定的Surface或者SurfaceTexture渲染图像了。
数据的接收 我们针对的是相机的处理, 相机的图像数据和视频的图像数据, 在Android中都可以直接利用SurfaceTexture来接收, 所以我们可以提供一个SurfaceTexture给相机, 然后将SurfaceTexture的数据拿出来, 调整好方向, 作为原始数据。具体处理和相机普通的预览类似, 不同的是, 我们是不希望直接显示到屏幕上的, 而且在后续我们还会对这个图像做其他处理。所以我们时将相机的当前帧数据渲染到一个2d的texture上, 作为后续处理过程的输入。所以在渲染时, 需要绑定FrameBuffer。
@
Override
public void draw() {
boolean a=
GLES20.glIsEnabled(GLES20.GL_DEPTH_TEST);
if(a){
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
}
if(mSurfaceTexture!=
null){
mSurfaceTexture.updateTexImage();
mSurfaceTexture.getTransformMatrix(mCoordOM);
mFilter.setCoordMatrix(mCoordOM);
}
EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]);
GLES20.glViewport(0,0,width,height);
mFilter.setTextureId(mCameraTexture[0]);
mFilter.draw();
Log.e("
wuwang"
,"
textureFilter draw"
);
EasyGlUtils.unBindFrameBuffer();
if(a){
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
}
}
上面所使用的mFilter就是用来渲染相机数据的Filter, 该Filter所起的作用就是将相机数据的方向调整正确。然后通过绑定FrameBuffer并制定接受渲染的Texture, 就可以将相机数据以一个正确的方向渲染到这个指定的Texture上了。
mFilter的顶点着色器为:
attribute vec4 vPosition;
attribute vec2 vCoord;
uniform mat4 vMatrix;
uniform mat4 vCoordMatrix;
varying vec2 textureCoordinate;
void main(){
gl_Position =
vMatrix*vPosition;
textureCoordinate =
(vCoordMatrix*vec4(vCoord,0,1)).xy;
}
其片元着色器为:
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 textureCoordinate;
uniform samplerExternalOES vTexture;
void main() {
gl_FragColor =
texture2D( vTexture, textureCoordinate );
}
渲染相机数据到指定窗口上 在之前利用OpenGLES预览Camera的博客中, 我们是直接将相机的数据“draw”到屏幕上了。在上面的处理中, 我们在绘制之前调用了
EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0])
,
这个方法是让我们后续的渲染,
渲染到fTexture[0]这个纹理上。具体可以参考前面的博客FBO离屏渲染。 所以, 通过上面的方式接收数据, 然后利用自定义的GLSurfaceView指定渲染窗口并执行渲染后, 我们依旧无法看到相机的预览效果。为了将相机的数据渲染到屏幕上, 我们需要将fTexture[0]的内容再渲染到制定的窗口上。这个渲染比之前的接收相机数据, 渲染到fTexture[0]上更为简单:
AFilter filter=
new NoFilter(getResource());
...void onDrawFrame(GL10 gl){
GLES20.glViewPort(0,0,width,height)
filter.setMatrix(matrix);
filter.setTexture(fTexture[0]);
filter.draw();
}...
NoFilter的顶点着色器为:
attribute vec4 vPosition;
attribute vec2 vCoord;
uniform mat4 vMatrix;
varying vec2 textureCoordinate;
void main(){
gl_Position =
vMatrix*vPosition;
textureCoordinate =
vCoord;
}
片元着色器为:
precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D vTexture;
void main() {
gl_FragColor =
texture2D( vTexture, textureCoordinate );
}
没错, 就是显示超简单的渲染一张图片的着色器, 看过前面的博客的应该见过这段着色器代码。
增加滤镜、贴纸效果 如果仅仅是预览相机, 我们这种做法简直就是多次一句, 直接指定渲染窗口, 渲染出来就万事大吉了。但是仅仅是这样的话, 就太没意思了。而现在要做的就是相机有意思的起点了。很多有意思的相机应用, 都可以通过这样的方式去实现, 比如我们常见美妆、美颜、色彩处理( 滤镜) , 甚至瘦脸、大眼, 或者其他的让脸变胖的, 以及一些给相机中的人带眼镜、帽子、发箍( 这些一般需要做人脸识别特征点定位) 等等等等。
通过上面的接收数据, 和渲染相机数据到指定的窗口上, 我们是已经可以看到渲染的结果了的。
然后我们要在渲染到指定窗口前, 增加其他的Filter, 为了保证易用性, 我们增加一个GroupFilter, 让其他的Filter, 直接加入到GroupFilter中来完成处理。
public class GroupFilter extends AFilter{private Queue<
AFilter>
mFilterQueue;
private List<
AFilter>
mFilters;
private int width=
0, height=
0;
private int size=
0;
public GroupFilter(Resources res) {
super(res);
mFilters=
new ArrayList<
>
();
mFilterQueue=
new ConcurrentLinkedQueue<
>
();
}@
Override
protected void initBuffer() {}public void addFilter(final AFilter filter){
//绘制到frameBuffer上和绘制到屏幕上的纹理坐标是不一样的
//Android屏幕相对GL世界的纹理Y轴翻转
MatrixUtils.flip(filter.getMatrix(),false,true);
mFilterQueue.add(filter);
}public boolean removeFilter(AFilter filter){
boolean b=
mFilters.remove(filter);
if(b){
size--;
}
return b;
}public AFilter removeFilter(int index){
AFilter f=
mFilters.remove(index);
if(f!=
null){
size--;
}
return f;
}public void clearAll(){
mFilterQueue.clear();
mFilters.clear();
size=
0;
}public void draw(){
updateFilter();
textureIndex=
0;
if(size>
0){
for (AFilter filter:mFilters){
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[textureIndex%2], 0);
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
GLES20.glViewport(0,0,width,height);
if(textureIndex=
=
0){
filter.setTextureId(getTextureId());
}else{
filter.setTextureId(fTexture[(textureIndex-1)%2]);
}
filter.draw();
unBindFrame();
textureIndex+
+
;
}
}}private void updateFilter(){
AFilter f;
while ((f=
mFilterQueue.poll())!=
null){
f.create();
f.setSize(width,height);
mFilters.add(f);
size+
+
;
}
}@
Override
public int getOutputTexture(){
return size=
=
0?getTextureId():fTexture[(textureIndex-1)%2];
}@
Override
protected void onCreate() {}@
Override
protected void onSizeChanged(int width, int height) {
this.width=
width;
this.height=
height;
updateFilter();
createFrameBuffer();
}//创建离屏buffer
private int fTextureSize =
2;
private int[] fFrame =
new int[1];
private int[] fRender =
new int[1];
private int[] fTexture =
new int[fTextureSize];
private int textureIndex=
0;
//创建FrameBuffer
private boolean createFrameBuffer() {
GLES20.glGenFramebuffers(1, fFrame, 0);
GLES20.glGenRenderbuffers(1, fRender, 0);
genTextures();
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, fRender[0]);
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width,
height);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[0], 0);
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
//int status =
GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
//if(status=
=
GLES20.GL_FRAMEBUFFER_COMPLETE){
//return true;
//}
unBindFrame();
return false;
}//生成Textures
private void genTextures() {
GLES20.glGenTextures(fTextureSize, fTexture, 0);
for (int i =
0;
i <
fTextureSize;
i+
+
) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fTexture[i]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
}
}//取消绑定Texture
private void unBindFrame() {
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}private void deleteFrameBuffer() {
GLES20.glDeleteRenderbuffers(1, fRender, 0);
GLES20.glDeleteFramebuffers(1, fFrame, 0);
GLES20.glDeleteTextures(1, fTexture, 0);
}}
将这个FilterGroup加入到已有的流程中, 只需要将保持的相机数据的Texture作为FilterGroup的输入, 然后将FilterGroup的输出作为渲染到指定窗口的Filter的输入即可:
@
Override
public void onDrawFrame(GL10 gl) {
if(isParamSet.get()){
mCameraFilter.draw();
mGroupFilter.setTextureId(mCameraFilter.getOutputTexture());
mGroupFilter.draw();
GLES20.glViewport(0,0,mWindowSize.x,mWindowSize.y);
mShowFilter.setMatrix(SM);
mShowFilter.setTextureId(mGroupFilter.getOutputTexture());
mShowFilter.draw();
if(mRenderer!=
null){
mRenderer.onDrawFrame(gl);
}
callbackIfNeeded();
}
}
然后将需要增加的其他的Filter, 依次增加到GroupFilter中即可:
WaterMarkFilter filter=
new WaterMarkFilter(getResources());
filter.setWaterMark(BitmapFactory.decodeResource(getResources(),R.mipmap.logo));
filter.setPosition(300,50,300,150);
mController.addFilter(filter);
//mController.addFilter(new GrayFilter(getResources()));
mController.setFrameCallback(720, 1280, Camera2Activity.this);
其他 如果之前没有接触过OpenGL, 这篇博客看下来可能也是云里雾里。主要是因为篇幅有限, 加上之前的博客分享的也是从零开始学习OpenGLES的内容, 所以在这篇博客中没有赘述, 如有问题, 可在评论区留言, 或给我发邮件, 共同探讨。另外, 分享一下我们公司的项目——AiyaEffectSDK, 可以快速实现各种好玩的美颜、贴纸效果, 欢迎Star和Fork。
源码 所有的代码全部在一个项目中, 托管在Github上——Android OpenGLES 2.0系列博客的Demo
【Android Camera API/Camera2 API 相机预览及滤镜贴纸等处理】欢迎转载, 转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/61207844]
推荐阅读
- Android开发之深入理解Android 7.0系统权限更改相关文档
- android工程化开发看法
- Android APK用途及使用小结(待续)
- Android之 服务
- Android -- 自定义ScrollView实现放大回弹效果
- android屏蔽BACK键HOME键和多任务键
- Chromimum Android渲染介绍
- Android类加载之PathClassLoader和DexClassLoader
- Android 使用Java8新特性之"方法引用"