本篇我们来介绍贴纸效果与美颜滤镜的实现。
1、贴纸效果
贴纸实际上是一个图片,用 Bitmap 加载图片后用 OpenGL 渲染到指定的位置上。我们举例添加一个耳朵贴纸:
1.1 获取人脸位置
上一篇我们在讲大眼滤镜时,在 Native 层除了获取到人脸 5 个特征点的坐标之外,还保存了人脸起始点坐标与宽高,这个数据其实是我们添加贴纸时才会用到的:
void FaceTracker::detect(const Mat &src, std::vector<Rect2f> &rectangles) {
std::vector<Rect> faces;
tracker->process(src);
tracker->getObjects(faces);
if (!faces.empty()) {
// 先只处理一个人脸,将其位置信息保存到 rectangles 中备用
Rect face = faces[0];
// 保存人脸起始点(左上角)以及宽高
rectangles.emplace_back(face.x, face.y, face.width, face.height);
seeta::ImageData imageData = seeta::ImageData(src.cols, src.rows);
imageData.data = src.data;
seeta::FaceInfo faceInfo;
seeta::Rect bbox;
bbox.x = face.x;
bbox.y = face.y;
bbox.width = face.width;
bbox.height = face.height;
faceInfo.bbox = bbox;
seeta::FacialLandmark landmarks[5];
faceAlignment->PointDetectLandmarks(imageData, faceInfo, landmarks);
for (auto & landmark : landmarks) {
rectangles.emplace_back(landmark.x, landmark.y, 0, 0);
}
}
}
该数据最终被封装到 Face 中,通过 landmarks[0]、landmarks[1] 可以获取起始点坐标,通过 faceWidth、faceHeight 获取人脸宽高。
1.2 贴纸滤镜实现
添加贴纸不需要增加新的着色器,大致步骤如下:
- 用 Bitmap 加载贴纸资源,在准备阶段创建一个贴纸纹理与该 Bitmap 绑定
- 绘制阶段先将贴纸滤镜前面的滤镜绘制的内容绘制出来,然后再绘制贴纸
- 绘制贴纸时,重要的是计算好贴纸的起始位置与宽高并设置给 glViewport()
绘制贴纸之前的代码是常规套路前面已经说过多次,这里直接贴出,不再赘述:
class StickFilter(context: Context) :
BaseFrameFilter(context, R.raw.base_vertex, R.raw.base_fragment) {
private val mBitmap: Bitmap
// 绘制贴纸的纹理
private lateinit var mTextureId: IntArray
private var mFace: Face? = null
init {
mBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.erduo_000)
}
override fun initCoordinator() {
// 转 180° 调正
val texture = floatArrayOf(
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f
)
mTextureBuffer.clear()
mTextureBuffer.put(texture)
}
override fun onReady(width: Int, height: Int) {
super.onReady(width, height)
// 1.生成并绑定贴纸的纹理 ID
mTextureId = IntArray(1)
TextureHelper.generateTextures(mTextureId)
glBindTexture(GL_TEXTURE_2D, mTextureId[0])
// 2.将 Bitmap 的像素数据加载到 OpenGL 的纹理对象中
GLUtils.texImage2D(GL_TEXTURE_2D, 0, mBitmap, 0)
// 3.解绑
glBindTexture(GL_TEXTURE_2D, 0)
}
override fun onDrawFrame(textureId: Int): Int {
// 1.如果数据不足无法绘制贴纸,就返回上一层的纹理 ID
val landmarks = mFace?.landmarks
val imgWidth = mFace?.imgWidth ?: 0
val imgHeight = mFace?.imgHeight ?: 0
if (landmarks == null || imgWidth == 0 || imgHeight == 0) {
return textureId
}
// 2.渲染前的设置
// 2.1 设置视窗
glViewport(0, 0, mWidth, mHeight)
// 2.2 绑定 FBO
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])
// 2.3 使用着色器程序
glUseProgram(mProgramId)
// 3.给顶点着色器的顶点和纹理坐标变量传值
mVertexBuffer.position(0)
glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer)
glEnableVertexAttribArray(vPosition)
mTextureBuffer.position(0)
glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer)
glEnableVertexAttribArray(vCoord)
// 4.绘制前面滤镜的内容
// 4.1 激活图层
glActiveTexture(GL_TEXTURE0)
// 4.2 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId)
// 4.3 给采样器传参
glUniform1i(vTexture, 0)
// 4.4 通知 OpenGL 绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
// 4.5 解绑 FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glBindTexture(GL_TEXTURE_2D, 0)
// 5.绘制贴纸
drawStick(landmarks, imgWidth, imgHeight)
return mFrameBufferTextures!![0]
}
}
主要看绘制贴纸的方法 drawStick():
private fun drawStick(landmarks: FloatArray, imgWidth: Int, imgHeight: Int) {
// 1.混合模式
// 1.1 开启混合模式
glEnable(GL_BLEND)
// 1.2 设置混合模式:
// GL_ONE 表示原图全部绘制
// GL_ONE_MINUS_SRC_ALPHA 表示目标图因子 = 1 - 源图 alpha
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
// 2.设置贴纸的绘制区域
// 2.1 计算人脸起始点坐标(图像内坐标转换为屏幕内坐标)
val xInScreen = landmarks[0] / imgWidth * mWidth
val yInScreen = landmarks[1] / imgHeight * mHeight
// 2.2 设置贴纸绘制的起始点与宽高
glViewport(
xInScreen.toInt(),
// yInScreen 是人脸的起始点纵坐标,而贴纸需要放在头上,向上移适当距离
(yInScreen - mBitmap.height / 2).toInt(),
// 贴纸宽度要根据人脸矩形宽度在屏幕内等比例缩放,记得先用 Float 计算否则误差较大
((mFace?.faceWidth ?: 0).toFloat() / imgWidth * mWidth).toInt(),
mBitmap.height
)
// 3.绑定 FBO、设置着色器程序
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])
glUseProgram(mProgramId)
// 4.为顶点坐标和纹理坐标赋值
mVertexBuffer.position(0)
glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer)
glEnableVertexAttribArray(vPosition)
mTextureBuffer.position(0)
glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer)
glEnableVertexAttribArray(vCoord)
// 5.绘制与解绑
// 激活图层
glActiveTexture(GL_TEXTURE0)
// 绑定
glBindTexture(GL_TEXTURE_2D, mTextureId[0])
// 传递参数
glUniform1i(vTexture, 0)
// 通知 OpenGL 绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
// 解绑 FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glBindTexture(GL_TEXTURE_2D, 0)
// 关闭混合模式
glDisable(GL_BLEND)
}
需要注意的就是 2.2 设置绘制的起点与宽高的问题:
- 如果起始点的纵坐标不向上移动适当距离,那么贴纸就会贴在眼睛上方,而不是头上方
- 贴纸的宽度需要随着人脸矩形的宽度变化,否则人离屏幕很远的情况下,脸变小了,但贴纸还是原来的大小就很违和
最后将 StickFilter 添加到渲染器的责任链中进行绘制。这里我们做了一点改动,就是在 UI 上添加了各个滤镜的开关,当开启滤镜时,才进行绘制。因此要从 UI 将开启状态经过 FilterSurfaceView 同步给渲染器 GLRender:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// 各个滤镜的控制开关
mBinding.cbBeauty.setOnCheckedChangeListener { _, isChecked ->
mBinding.myGlSurfaceView.enableBeauty(isChecked)
}
mBinding.cbBigEye.setOnCheckedChangeListener { _, isChecked ->
mBinding.myGlSurfaceView.enableBigEye(isChecked)
}
mBinding.cbStick.setOnCheckedChangeListener { _, isChecked ->
mBinding.myGlSurfaceView.enableStick(isChecked)
}
}
FilterSurfaceView 只做简单的传递:
fun enableBigEye(checked: Boolean) {
mGLRender.enableBigEye(checked)
}
fun enableStick(checked: Boolean) {
mGLRender.enableStick(checked)
}
GLRender 收到后要在 OpenGL 的渲染线程中做滤镜对象的创建工作:
fun enableBigEye(checked: Boolean) {
mGLSurfaceView.queueEvent {
if (checked) {
mBigEyesFilter = BigEyesFilter(mContext)
// 同步宽高信息
mBigEyesFilter?.onReady(mWidth, mHeight)
} else {
mBigEyesFilter?.release()
mBigEyesFilter = null
}
}
}
fun enableStick(checked: Boolean) {
mGLSurfaceView.queueEvent {
if (checked) {
mStickFilter = StickFilter(mContext)
mStickFilter?.onReady(mWidth, mHeight)
} else {
mStickFilter?.release()
mStickFilter = null
}
}
}
queueEvent() 会将任务提交到 OpenGL ES 渲染线程执行。因为 Android OpenGL ES 的渲染操作必须在渲染线程(也就是 GLSurfaceView 中的 GLThread)上执行,以避免多线程访问 OpenGL ES 上下文导致的竞态条件和不一致性。因此,当我们需要在非渲染线程上执行 OpenGL ES 操作时,就需要使用 queueEvent
来将任务提交到渲染线程执行。
在绘制时也要做相应修改:
private lateinit var mScreenFilter: ScreenFilter
private lateinit var mCameraFilter: CameraFilter
private var mBigEyesFilter: BigEyesFilter? = null
private var mStickFilter: StickFilter? = null
override fun onDrawFrame(gl: GL10?) {
...
// 3.交给滤镜进行具体的绘制工作
mCameraFilter.setMatrix(mMatrix)
var textureId = mCameraFilter.onDrawFrame(mTextureIds[0])
mBigEyesFilter?.setFace(mFaceTracker.getFace())
textureId = mBigEyesFilter?.onDrawFrame(textureId) ?: textureId
mStickFilter?.setFace(mFaceTracker.getFace())
textureId = mStickFilter?.onDrawFrame(textureId) ?: textureId
mScreenFilter.onDrawFrame(textureId)
}
2、美颜滤镜
美颜滤镜的实现的重点是在美颜算法,算法是写在片元着色器中的,我们先来看其实现。
2.1 美颜着色器
美颜效果主要是通过着色器中的算法代码实现的,算法有很多种,这里我们提供一种算法放在 beauty_fragment.glsl 中:
precision mediump float;
varying mediump vec2 aCoord;
uniform sampler2D vTexture;
// 图片(纹理)宽高
uniform int width;
uniform int height;
// 高斯模糊的 20 个采样点
vec2 blurCoordinates[20];
void main() {
// 1.高斯模糊
// 像素点步长
vec2 singleStepOffset = vec2(1.0 / float(width), 1.0 / float(height));
// aCoord 是 GPU 当前渲染的像素点, 整个公式就是求出距离当前正在渲染的像素点在
// X 轴方向左侧 10 个像素单位的点的坐标。后续的采样点也是类似的含义
blurCoordinates[0] = aCoord.xy + singleStepOffset * vec2(0.0, -10.0);
blurCoordinates[1] = aCoord.xy + singleStepOffset * vec2(0.0, 10.0);
blurCoordinates[2] = aCoord.xy + singleStepOffset * vec2(-10.0, 0.0);
blurCoordinates[3] = aCoord.xy + singleStepOffset * vec2(10.0, 0.0);
blurCoordinates[4] = aCoord.xy + singleStepOffset * vec2(5.0, -8.0);
blurCoordinates[5] = aCoord.xy + singleStepOffset * vec2(5.0, 8.0);
blurCoordinates[6] = aCoord.xy + singleStepOffset * vec2(-5.0, 8.0);
blurCoordinates[7] = aCoord.xy + singleStepOffset * vec2(-5.0, -8.0);
blurCoordinates[8] = aCoord.xy + singleStepOffset * vec2(8.0, -5.0);
blurCoordinates[9] = aCoord.xy + singleStepOffset * vec2(8.0, 5.0);
blurCoordinates[10] = aCoord.xy + singleStepOffset * vec2(-8.0, 5.0);
blurCoordinates[11] = aCoord.xy + singleStepOffset * vec2(-8.0, -5.0);
blurCoordinates[12] = aCoord.xy + singleStepOffset * vec2(0.0, -6.0);
blurCoordinates[13] = aCoord.xy + singleStepOffset * vec2(0.0, 6.0);
blurCoordinates[14] = aCoord.xy + singleStepOffset * vec2(6.0, 0.0);
blurCoordinates[15] = aCoord.xy + singleStepOffset * vec2(-6.0, 0.0);
blurCoordinates[16] = aCoord.xy + singleStepOffset * vec2(-4.0, -4.0);
blurCoordinates[17] = aCoord.xy + singleStepOffset * vec2(-4.0, 4.0);
blurCoordinates[18] = aCoord.xy + singleStepOffset * vec2(4.0, -4.0);
blurCoordinates[19] = aCoord.xy + singleStepOffset * vec2(4.0, 4.0);
// 正在渲染(采样)的点,即所有采样点的中心点的颜色矩阵
vec4 currentColor = texture2D(vTexture, aCoord);
// 计算 21 个点的颜色总和
vec3 totalRGB = currentColor.rgb;
for (int i = 0; i < 20; i++) {
totalRGB += texture2D(vTexture, blurCoordinates[i].xy).rgb;
}
vec4 blur = vec4(totalRGB * 1.0 / 21.0, currentColor.a);
// 2.高反差保留
// 用原图减去高斯模糊的图
// https://shaderific.com/glsl/common_functions.html
// OpenGL 内置函数参考网站
vec4 highPassColor = currentColor - blur;
// clamp 会返回三个参数中大小在中间的那个数
// 计算强度系数,对每个颜色通道取反向
highPassColor.r = clamp(2.0 * highPassColor.r * highPassColor.r * 24.0, 0.0, 1.0);
highPassColor.g = clamp(2.0 * highPassColor.g * highPassColor.g * 24.0, 0.0, 1.0);
highPassColor.b = clamp(2.0 * highPassColor.b * highPassColor.b * 24.0, 0.0, 1.0);
vec4 highPassBlur = vec4(highPassColor.rgb, 1.0);
// 3.磨皮(融合)
// 蓝色分量
float blue = min(currentColor.b, blur.b);
float value = clamp((blue - 0.2) * 5.0, 0.0, 1.0);
// 取 RGB 三个分量重最大的值
float maxChannelColor = max(max(currentColor.r, currentColor.g), currentColor.b);
// 磨皮强度
float intensity = 1.0;
float currentIntensity = (1.0 - maxChannelColor / (maxChannelColor + 0.2)) * value * intensity;
// mix 返回线性混合的 xy,如 x(1 - a) + ya
vec3 r = mix(currentColor.rgb, blur.rgb, currentIntensity);
gl_FragColor = vec4(r, 1.0);
}
2.2 美颜滤镜
滤镜代码前面已经添加过几次,都是固定套路了:
/**
* 美颜:反向、高反差保留、高斯模糊
*/
class BeautyFilter(context: Context) :
BaseFrameFilter(context, R.raw.base_vertex, R.raw.beauty_fragment) {
// 着色器中定义的宽高变量
private val width: Int = glGetUniformLocation(mProgramId, "width")
private val height: Int = glGetUniformLocation(mProgramId, "height")
override fun initCoordinator() {
val texture = floatArrayOf(
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f
)
mTextureBuffer.clear()
mTextureBuffer.put(texture)
}
override fun onDrawFrame(textureId: Int): Int {
// 1.设置视窗
glViewport(0, 0, mWidth, mHeight)
// 绑定 FBO
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])
// 2.使用着色器程序
glUseProgram(mProgramId)
// 3.为着色器中定义的变量赋值
mVertexBuffer.position(0)
glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer)
glEnableVertexAttribArray(vPosition)
mTextureBuffer.position(0)
glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer)
glEnableVertexAttribArray(vCoord)
glUniform1i(width, mWidth)
glUniform1i(height, mHeight)
// 4.后续常规操作,OpenGL 绘制
// 激活图层
glActiveTexture(GL_TEXTURE0)
// 绑定
glBindTexture(GL_TEXTURE_2D, textureId)
// 传递参数
glUniform1i(vTexture, 0)
// 通知 OpenGL 绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
// 解绑 FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glBindTexture(GL_TEXTURE_2D, 0)
return mFrameBufferTextures!![0]
}
}
最后在渲染器中添加美颜滤镜:
private var mBeautyFilter: BeautyFilter? = null
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
...
mBeautyFilter?.onReady(width, height)
...
}
override fun onDrawFrame(gl: GL10?) {
...
// 3.交给滤镜进行具体的绘制工作
mCameraFilter.setMatrix(mMatrix)
var textureId = mCameraFilter.onDrawFrame(mTextureIds[0])
mBigEyesFilter?.setFace(mFaceTracker.getFace())
textureId = mBigEyesFilter?.onDrawFrame(textureId) ?: textureId
mStickFilter?.setFace(mFaceTracker.getFace())
textureId = mStickFilter?.onDrawFrame(textureId) ?: textureId
textureId = mBeautyFilter?.onDrawFrame(textureId) ?: textureId
mScreenFilter.onDrawFrame(textureId)
}
fun enableBeauty(checked: Boolean) {
mGLSurfaceView.queueEvent {
if (checked) {
mBeautyFilter = BeautyFilter(mContext)
// 同步宽高信息
mBeautyFilter?.onReady(mWidth, mHeight)
} else {
mBeautyFilter?.release()
mBeautyFilter = null
}
}
}
结果如文章开头演示所示,至此,滤镜系列完结。
参考资料:
高反差保留算法: https://www.jianshu.com/p/bb702124d2ad
图层混合强光模式:https://blog.csdn.net/matrix_space/article/details/22426633
开源美颜相机工程参考:https://github.com/wuhaoyu1990/MagicCamera
美白着色器代码参考:https://github.com/smzhldr/AGLFramework/blob/master/aglframework/src/main/res/raw/light_f.glsl