最近项目中有研究背景虚化功能,需求是通过写一个GPUImage的滤镜,结合TensorFLow Lite来实现对图片中指定物体的背景虚化功能。这部分内容基本都是通过看官方文档和自己摸索学习,这里总结并整理一份笔记,内容主要包括Android接入TensorFlow Lite,通过运行AI模型来识别图片中物体,并对其做背景虚化。一共分为三部分,本文是第二部分,用GPUImage库来实现目标物体的背景虚化。
什么是GPUImage
GPUImage是一个用来给图片加滤镜的开源框架。不止图片,也可以动态的给照相机镜头加滤镜,使得拍出来的照片自带滤镜。所以如果有做相机类app、图片的修图功能等,可以使用这个库。这个库的地址在这里:
是的你没看错,他确实是一个iOS的库,当然Android也有一个由他魔改而来的 android-gpuimage 。不过毕竟是从iOS搬运过来的,这个Android库存在很多坑,而且他所支持的滤镜数量也不及原版。因此在使用过程中,如果有一些实在无法解决的问题,可以参考iOS的实现,自己改一改。
这个库的引入也很简单,这里就不细说了,具体直接参考README文档即可,接下里介绍下代码中具体的使用方法。
基本的滤镜使用
使用起来也很简单,首先在xml文件中这也写:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<jp.co.cyberagent.android.gpuimage.GPUImageView
android:id="@+id/gpuImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
app:gpuimage_surface_type="texture_view" />
</RelativeLayout>
这个GPUImageView就是GPUIMage库自带的一个控件,可以当做ImageView来使用。要加载图片,也是跟ImageView控件一样:
gpuImageView.setImage(bitmap)
然后就是给图片加滤镜了。GPUImage自带有很多滤镜,这里取其中几个举个例子,比如 GPUImageBrightnessFilter滤镜,就是调节图片亮度的,他的调节范围是-1到1,默认是0:
GPUImageBrightnessFilter: Adjusts the brightness of the image
brightness: The adjusted brightness (-1.0 - 1.0, with 0.0 as the default)
添加方式:
val filter = GPUImageBrightnessFilter()
filter.setBrightness(0.5f)//亮度调节
gpuImageView.filter = GPUImageBrightnessFilter()
最终的效果就是这样的:
image分别是原图、亮度强度为0.3、亮度强度-0.3的效果。
背景虚化的原理
实现目标背景虚化的原理很简单:找出图片中的目标物体,只对目标物体之外的背景部分做模糊处理,目标物体不变。所以大体上是有两套方案:
- 自己写一个模糊滤镜,在滤镜中自己判断是否需要模糊
- 使用自带的模糊滤镜对图片整体模糊,在显示的时候再去判断如果是目标物体就显示原图,非目标物体则显示模糊图
这两种方案,显然方案1会复杂一点,方案2实现起来相对简单,我目前使用的也是方案2。后面有空的话再去研究下方案1的实现。
方案二,比较笨的办法是写两个ImageView,上层是抠出来的原图,下层是模糊的图片,不过这个写法实在有点蠢。另外一个比较好的办法就是自己写一个滤镜,这个滤镜的输入是一张原图和一个可以确定目标物体轮廓的数据,在滤镜中做背景虚化并输出,这样一来这个滤镜也方便在其他地方使用。不过因为Opengl不能直接接受数据输入,所以需要把轮廓数据转换为一张Mask图片作为纹理传入。
这个方案具体如何做呢?
- 模糊效果是可以直接用GPUImage自带的模糊滤镜;
- 轮廓可以用之前用Tensorflow Lite生成的Mask图;
- 自定义滤镜A,写一个能够接受多个图片作为输入源的滤镜。GPUImage是基于OpenGL ES实现的,且已经封装好了很多OpenGL的方法,我们只需要写着色器即可。对OpenGL ES不太了解的话可以先大概学习一下基本概念:LearnOpenGL;
- 在上一步的滤镜中,将原图和目标物体轮廓图作为输入源加入到滤镜中;
- GPUImage提供了一个滤镜叫GPUImageFilterGroup,这是一个滤镜组,可以接收多个滤镜;所以需要重写GPUImageFilterGroup,往里面依次加入模糊滤镜和我们前面写的滤镜A,在这个滤镜的片元着色器中具体去操作像素点;
具体实现
接下来就是具体的实现方式了。
自定义 GPUImageThreeInputFilter 滤镜
首先,要写一个可以接收多个图片作为输入源的滤镜。其实这个滤镜在iOS的库里面是有的,可以接收两个、三个乃至四个图片的输入,但是Android版本的库里面只有一个仅能接收两个图片输入的 GPUImageTwoInputFilter 滤镜。而我们所需要的滤镜要能接收三张图片:加了滤镜后的图片、原图、描出目标物体的Mask图,所以需要参考 GPUImageTwoInputFilter 自己写一个。具体代码如下:
open class GPUImageThreeInputFilter(vertexShader: String?, fragmentShader: String?) :
GPUImageFilter(vertexShader, fragmentShader) {
private var filterSecondTextureCoordinateAttribute = 0
private var filterInputTextureUniform2 = 0
private var filterSourceTexture2 = OpenGlUtils.NO_TEXTURE
private var texture2CoordinatesBuffer: ByteBuffer? = null
private var bitmap2: Bitmap? = null
private var filterThirdTextureCoordinateAttribute = 0
private var filterInputTextureUniform3 = 0
private var filterSourceTexture3 = OpenGlUtils.NO_TEXTURE
private var texture3CoordinatesBuffer: ByteBuffer? = null
private var bitmap3: Bitmap? = null
constructor(fragmentShader: String?) : this(VERTEX_SHADER, fragmentShader) {}
override fun onInit() {
super.onInit()
filterSecondTextureCoordinateAttribute = GLES20.glGetAttribLocation(program, "inputTextureCoordinate2")
filterInputTextureUniform2 = GLES20.glGetUniformLocation(program, "inputImageTexture2")
// This does assume a name of "inputImageTexture2" for second input texture in the fragment shader
GLES20.glEnableVertexAttribArray(filterSecondTextureCoordinateAttribute)
filterThirdTextureCoordinateAttribute = GLES20.glGetAttribLocation(program, "inputTextureCoordinate3")
filterInputTextureUniform3 = GLES20.glGetUniformLocation(program, "inputImageTexture3")
// This does assume a name of "inputImageTexture3" for second input texture in the fragment shader
GLES20.glEnableVertexAttribArray(filterThirdTextureCoordinateAttribute)
}
override fun onInitialized() {
super.onInitialized()
if (bitmap2 != null && !bitmap2!!.isRecycled && bitmap3 != null && !bitmap3!!.isRecycled) {
setBitmap(bitmap2, bitmap3)
}
}
fun setBitmap(bitmap2: Bitmap?, bitmap3: Bitmap?) {
if ((bitmap2 != null && bitmap2.isRecycled)
&& (bitmap3 != null && bitmap3.isRecycled)) {
return
}
this.bitmap2 = bitmap2
this.bitmap3 = bitmap3
if (this.bitmap2 == null || this.bitmap3 == null) {
return
}
runOnDraw(Runnable {
if (filterSourceTexture2 == OpenGlUtils.NO_TEXTURE
&& filterSourceTexture3 == OpenGlUtils.NO_TEXTURE) {
if ((bitmap2 == null || bitmap2.isRecycled)
|| (bitmap3 == null || bitmap3.isRecycled)) {
return@Runnable
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE3)
filterSourceTexture2 = OpenGlUtils.loadTexture(bitmap2, OpenGlUtils.NO_TEXTURE, false)
GLES20.glActiveTexture(GLES20.GL_TEXTURE4)
filterSourceTexture3 = OpenGlUtils.loadTexture(bitmap3, OpenGlUtils.NO_TEXTURE, false)
}
})
}
override fun onDestroy() {
super.onDestroy()
GLES20.glDeleteTextures(1, intArrayOf(filterSourceTexture2), 0)
filterSourceTexture2 = OpenGlUtils.NO_TEXTURE
GLES20.glDeleteTextures(1, intArrayOf(filterSourceTexture3), 0)
filterSourceTexture3 = OpenGlUtils.NO_TEXTURE
}
override fun onDrawArraysPre() {
GLES20.glEnableVertexAttribArray(filterSecondTextureCoordinateAttribute)
GLES20.glActiveTexture(GLES20.GL_TEXTURE3)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, filterSourceTexture2)
GLES20.glUniform1i(filterInputTextureUniform2, 3)
texture2CoordinatesBuffer!!.position(0)
GLES20.glVertexAttribPointer(filterSecondTextureCoordinateAttribute, 2, GLES20.GL_FLOAT, false, 0, texture2CoordinatesBuffer)
GLES20.glEnableVertexAttribArray(filterThirdTextureCoordinateAttribute)
GLES20.glActiveTexture(GLES20.GL_TEXTURE4)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, filterSourceTexture3)
GLES20.glUniform1i(filterInputTextureUniform3, 4)
texture3CoordinatesBuffer!!.position(0)
GLES20.glVertexAttribPointer(filterThirdTextureCoordinateAttribute, 2, GLES20.GL_FLOAT, false, 0, texture3CoordinatesBuffer)
}
fun setRotation2(rotation: Rotation?, flipHorizontal: Boolean, flipVertical: Boolean) {
val buffer = TextureRotationUtil.getRotation(rotation, flipHorizontal, flipVertical)
val bBuffer = ByteBuffer.allocateDirect(32).order(ByteOrder.nativeOrder())
val fBuffer = bBuffer.asFloatBuffer()
fBuffer.put(buffer)
fBuffer.flip()
texture2CoordinatesBuffer = bBuffer
}
fun setRotation3(rotation: Rotation?, flipHorizontal: Boolean, flipVertical: Boolean) {
val buffer = TextureRotationUtil.getRotation(rotation, flipHorizontal, flipVertical)
val bBuffer = ByteBuffer.allocateDirect(32).order(ByteOrder.nativeOrder())
val fBuffer = bBuffer.asFloatBuffer()
fBuffer.put(buffer)
fBuffer.flip()
texture3CoordinatesBuffer = bBuffer
}
companion object {
private const val VERTEX_SHADER =
"attribute vec4 position;\n" +
"attribute vec4 inputTextureCoordinate;\n" +
"attribute vec4 inputTextureCoordinate2;\n" +
"attribute vec4 inputTextureCoordinate3;\n" +
" \n" +
"varying vec2 textureCoordinate;\n" +
"varying vec2 textureCoordinate2;\n" +
"varying vec2 textureCoordinate3;\n" +
" \n" +
"void main()\n" +
"{\n" +
" gl_Position = position;\n" +
" textureCoordinate = inputTextureCoordinate.xy;\n" +
" textureCoordinate2 = inputTextureCoordinate2.xy;\n" +
" textureCoordinate3 = inputTextureCoordinate3.xy;\n" +
"}"
}
init {
setRotation2(Rotation.NORMAL, false, false)
setRotation3(Rotation.NORMAL, false, false)
}
}
说是接受三个图片作为输入,但是为什么 setBitmap()
方法只接受两个参数呢?其实这个滤镜在通过· setFilter()
或其他方式加到图片上之后,就会默认把当前状态的图片作为一个输入源。在顶点着色器里面的 inputTextureCoordinate
就是代表了默认的输入源坐标。
自定义 BokehBluFilter 滤镜
在上篇文章中,我们已经获取到了一张由目标物体的轮廓绘制出的Mask图。以猫为例,假如我们绘制出的Mask图中,猫的部分红色,非猫部分黑色,即猫的部分色值 r=255,g=0,b=0
, 非猫的背景部分色值为 r=0,g=0,b=0
,那么效果如下:
原图:
image结果图:
image然后自定义一个 BokehBlurFilter,继承自 GPUImageFilterGroup,代码如下:
class BokehBlurFilter(val original: Bitmap? = null, segMask: Bitmap? = null) : GPUImageFilterGroup() {
init {
if (original != null && segMask != null) {
val threeInputFilter = GPUImageThreeInputFilter(fragmentShader)
threenputFilter.setBitmap(original, segMask)
addFilter(GPUImageGaussianBlurFilter())
addFilter(threeInputFilter)
}
}
fun setBlurSize(intensity: Float) {
(filters[0] as GPUImageGaussianBlurFilter).setBlurSize(intensity * MAX_BLUR_SIZE)
updateMergedFilters()
}
companion object {
var fragmentShader =
"varying highp vec2 textureCoordinate;\n" +
"varying highp vec2 textureCoordinate2;\n" +
"varying highp vec2 textureCoordinate3;\n" +
"\n" +
" uniform sampler2D inputImageTexture;\n" +
" uniform sampler2D inputImageTexture2;\n" +
" uniform sampler2D inputImageTexture3;\n" +
" \n" +
" void main()\n" +
" {\n" +
" mediump vec4 textureBlur = texture2D(inputImageTexture, textureCoordinate);\n" +
" mediump vec4 textureOriginal = texture2D(inputImageTexture2, textureCoordinate2);\n" +
" mediump vec4 textureMask = texture2D(inputImageTexture3, textureCoordinate3);\n" +
" gl_FragColor = textureMask;\n" +
" }"
}
}
代码很简单,就是继承了 GPUImageFilterGroup,在初始化时向父类所持有的滤镜列表中添加两个滤镜,第一个是模糊滤镜,这里我用了自带的高斯模糊滤镜;第二个是前面写的 GPUImageThreeInputFilter 滤镜,并且在 GPUImageThreeInputFilter 滤镜中依次把原图和目标物体轮廓Mask图作为输入源。这里在创建 GPUImageThreeInputFilter 对象时传入了一个 fragmentShader
参数作为片元着色器。在片元着色器对每个像素点做具体操作。这里只是举个例子,显示了Mask图。了解了glsl语言就知道这里该怎么写。我们的背景虚化具体就是在这里做处理了。
片元着色器
在片元着色器里面这样写就可以了:
const val fragmentShader =
"varying highp vec2 textureCoordinate;\n" +
"varying highp vec2 textureCoordinate2;\n" +
"varying highp vec2 textureCoordinate3;\n" +
"\n" +
" uniform sampler2D inputImageTexture;\n" +
" uniform sampler2D inputImageTexture2;\n" +
" uniform sampler2D inputImageTexture3;\n" +
" \n" +
" void main()\n" +
" {\n" +
" mediump vec4 textureBlur = texture2D(inputImageTexture, textureCoordinate);\n" +
" mediump vec4 textureMask = texture2D(inputImageTexture2, textureCoordinate2);\n" +
" mediump vec4 textureOriginal = texture2D(inputImageTexture3, textureCoordinate3);\n" +
" if(textureMask.r == 0.0){\n" +
" gl_FragColor = textureOriginal;\n" +
" }else{\n" +
" gl_FragColor = textureBlur;\n" +
" }\n" +
" }"
很简单,textureBlur
是模糊后图片的纹理, textureMask
是目标物体轮廓Mask图的纹理, textureOriginal
是原图的纹理。以前面那个猫的图片为例,因为Mask图中,猫的部分是黑色,非猫部分是红色,也就是说猫的部分,像素点的r为0,非猫部分则为1,所以在着色器中根基r值是否为0来判断当前点显示的纹理是原图还是模糊图。
最后,将这个滤镜设置给原图,效果如下:
image可以看到,模糊对猫的背景虚化是成功了,但是还存在一些问题,比如:猫的边缘比较生硬等,另外在实际使用过程中也还是有一些坑。下一篇文章中具体来总结一下遇到的问题和优化方案。
网友评论