OpenGL 入门(四)—— 贴纸与美颜滤镜

28 篇文章 1 订阅

本篇我们来介绍贴纸效果与美颜滤镜的实现,还是先来看效果,原图:

2024-5-9.全部滤镜效果演示(使用前)1

开启滤镜后:

2024-5-9.全部滤镜效果演示(使用后)1

1、贴纸效果

贴纸实际上是一个图片,用 Bitmap 加载图片后用 OpenGL 渲染到指定的位置上。我们举例添加一个耳朵贴纸:

erduo_000

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 贴纸滤镜实现

添加贴纸不需要增加新的着色器,大致步骤如下:

  1. 用 Bitmap 加载贴纸资源,在准备阶段创建一个贴纸纹理与该 Bitmap 绑定
  2. 绘制阶段先将贴纸滤镜前面的滤镜绘制的内容绘制出来,然后再绘制贴纸
  3. 绘制贴纸时,重要的是计算好贴纸的起始位置与宽高并设置给 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

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值