美文网首页
iOS视觉-- (11) OpenGL ES+GLSL实现大眼和

iOS视觉-- (11) OpenGL ES+GLSL实现大眼和

作者: 桀骜不驯的搬砖者 | 来源:发表于2022-11-28 14:31 被阅读0次

    前面我们学过摄像头的渲染、单滤镜、多滤镜的处理的流程。接下来要学习的是大眼和瘦脸的技能了。这里会使用到人脸识别的技术,刚开始打算用的是Vision原生框架来做,无奈,脱离时代的iPhone6太卡了。难当次重任,后面使用了第三方框架。测试版可以随便玩玩。真的不错哦。效率很高,特征点106个。还是OK的。
    借鉴博客:iOS原生框架Vision实现瘦脸大眼特效仿抖音特效相机之大眼瘦脸
    本文达成效果如下图:

    效果图
    106个特征点如下图 特征点
    原理解析

    主要是以下3点,具体请前往参考博客原理解析

    • 1.圆内放大
    • 2.圆内缩小
    • 3.向某一点拉伸

    用一张gif图来概括上面的内容,也是本文章最终的达成的效果,如开头所展示的效果图

    经过前面我们了解了

    • 日常开发中OpenGL开发流程
    • 1.设置图层
    • 2.设置图形上下文
    • 3.设置渲染缓冲区(renderBuffer)
    • 4.设置帧缓冲区(frameBuffer)
    • 5.编译、链接着色器(shader)
    • 6.设置VBO (Vertex Buffer Objects)
    • 7.设置纹理
    • 8.渲染

    这些基本步骤大致是不变的。这章是摄像头渲染+"多滤镜"渲染思想的结合提现。内容是感觉是增加了,但是实际的开发流程还是一样的。接下来让我们进入正题。
    经过分析我们主要有以下3个工作:


    需求图

    核心代码:

        ///绘制面部特征点
        func renderFacePoint() {
            //MARK: - 1.绘制摄像头
            //使用着色器
            glUseProgram(renderProgram)
            //绑定frameBuffer
            glBindFramebuffer(GLenum(GL_FRAMEBUFFER), facePointFrameBuffer)
            
            //设置清屏颜色
            glClearColor(0.0, 0.0, 0.0, 1.0)
            //清除屏幕
            glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
            
            //1.设置视口大小
            let scale = self.contentScaleFactor
            glViewport(0, 0, GLsizei(self.frame.size.width * scale), GLsizei(self.frame.size.height * scale))
            
           
    
    #warning("注意⚠️:想要获取shader里面的变量,这里要记住要在glLinkProgram后面、后面、后面")
            //----处理顶点数据-------
            //将顶点数据通过renderProgram中的传递到顶点着色程序的position
            /*1.glGetAttribLocation,用来获取vertex attribute的入口的.
              2.告诉OpenGL ES,通过glEnableVertexAttribArray,
              3.最后数据是通过glVertexAttribPointer传递过去的。
             */
            //注意:第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
            let position = glGetAttribLocation(renderProgram, "position")
    
            //设置合适的格式从buffer里面读取数据
            glEnableVertexAttribArray(GLuint(position))
    
            //设置读取方式
            //参数1:index,顶点数据的索引
            //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
            //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
            //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
            //参数5:stride,连续顶点属性之间的偏移量,默认为0;
            //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    //        glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 0))
            glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, standardVertex)
    
    
            //----处理纹理数据-------
            //1.glGetAttribLocation,用来获取vertex attribute的入口的.
            //注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
            let textCoord = glGetAttribLocation(renderProgram, "textCoordinate")
    
            //设置合适的格式从buffer里面读取数据
            glEnableVertexAttribArray(GLuint(textCoord))
    
            //3.设置读取方式
            //参数1:index,顶点数据的索引
            //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
            //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
            //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
            //参数5:stride,连续顶点属性之间的偏移量,默认为0;
            //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    //        glVertexAttribPointer(GLuint(textCoord), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 3))
            glVertexAttribPointer(GLuint(textCoord), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, standardVerticalInvertFragment)
    
            
            //法一:使用 CVOpenGLESTexture进行加载,打开下面
            glActiveTexture(GLenum(GL_TEXTURE0))
            glUniform1i(glGetUniformLocation(self.renderProgram, "colorMap"), 0)
            
            //法二:使用 glTexImage2D 方式加载,打开下面
    //        glActiveTexture(GLenum(GL_TEXTURE1))
    //        glBindTexture(GLenum(GL_TEXTURE_2D), originalTexture)
    //        glUniform1i(glGetUniformLocation(self.renderProgram, "colorMap"), 1) //单个纹理可以不用设置
    
            glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
            
            
            //MARK: - 2.绘制面部特征点
            if drawLandMark {
                //注意⚠️:不能清屏。否则看不到照相机画面
                //        glClearColor(0.0, 0.0, 0.0, 1.0)
                //清除屏幕
                //        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
                //1.设置视口大小
                glViewport(0, 0, GLsizei(self.frame.size.width * scale), GLsizei(self.frame.size.height * scale))
                
                //使用着色器
                glUseProgram(faceProgram)
                
                for faceInfo in FaceDetector.shareInstance().faceModels {
                    
                    var tempPoint: [GLfloat] = [GLfloat].init(repeating: 0, count: faceInfo.landmarks.count * 3)
                    var indices: [GLubyte] = [GLubyte].init(repeating: 0, count: faceInfo.landmarks.count)
                    for i in 0..<faceInfo.landmarks.count {
                        let point = faceInfo.landmarks[i].cgPointValue
                        tempPoint[i*3+0] = GLfloat(point.x * 2 - 1)
                        tempPoint[i*3+1] = GLfloat(point.y * 2 - 1)
                        tempPoint[i*3+2] = 0.0
                        indices[i] = GLubyte(i)
                        
                    }
                    
                    let position = glGetAttribLocation(faceProgram, "position")
                    glEnableVertexAttribArray(GLuint(position))
                    //这种方式得先把顶点数据提交到GPU
                    //            glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 3), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 0))
                    glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, tempPoint)
                    
                    
                    let lineWidth = faceInfo.bounds.size.width / CGFloat(self.frame.width * scale)
                    let sizeScaleUniform = glGetUniformLocation(self.faceProgram, "sizeScale")
                    glUniform1f(GLint(sizeScaleUniform), GLfloat(lineWidth * 20))
                    
                    //            var scaleMatrix = GLKMatrix4Identity//GLKMatrix4Scale(GLKMatrix4Identity, 1/Float(lineWidth), 1/Float(lineWidth), 0)
                    //            let scaleMatrixUniform = shader.uniformIndex("scaleMatrix")!
                    //            glUniformMatrix4fv(GLint(scaleMatrixUniform), 1, GLboolean(GL_FALSE), &scaleMatrix.m.0)
                    
                    glDrawElements(GLenum(GL_POINTS), GLsizei(indices.count), GLenum(GL_UNSIGNED_BYTE), indices)
                }
            }
            
           
    
            
            
            //MARK: - 3.绘制纹理完毕,开始瘦脸
            renderThinFace()
        }
    
    //MARK: - 绘制瘦脸
        ///绘制瘦脸
        func renderThinFace() {
            //使用着色器
            glUseProgram(thinFaceProgram)
            //绑定frameBuffer
            glBindFramebuffer(GLenum(GL_FRAMEBUFFER), thinFaceFrameBuffer)
            
            let faceInfo = FaceDetector.shareInstance().oneFace
            if faceInfo.landmarks.count == 0 {
                glUniform1i(hasFaceUniform, 0)
                //3.绘制纹理完毕,开始渲染到屏幕上
                displayRenderToScreen(facePointTexture)
                return
            }
            glClearColor(0.0, 0.0, 0.0, 1.0)
            //清除屏幕
            glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
            
            //1.设置视口大小
            let scale = self.contentScaleFactor
            glViewport(0, 0, GLsizei(self.frame.size.width * scale), GLsizei(self.frame.size.height * scale))
            
            hasFaceUniform = glGetUniformLocation(self.thinFaceProgram, "hasFace")
            aspectRatioUniform = glGetUniformLocation(self.thinFaceProgram, "aspectRatio")
            facePointsUniform = glGetUniformLocation(self.thinFaceProgram, "facePoints")
            thinFaceDeltaUniform = glGetUniformLocation(self.thinFaceProgram, "thinFaceDelta")
            bigEyeDeltaUniform = glGetUniformLocation(self.thinFaceProgram, "bigEyeDelta")
            
            glUniform1i(hasFaceUniform, 1)
            let aspect: Float = Float(inputTextureW / inputTextureH)
            glUniform1f(aspectRatioUniform, aspect)
            
            glUniform1f(thinFaceDeltaUniform, thinFaceDelta)
            glUniform1f(bigEyeDeltaUniform, bigEyeDelta)
            
            let size = 106 * 2
            var tempPoint: [GLfloat] = [GLfloat].init(repeating: 0, count: size)
            var index = 0
            for i in 0..<faceInfo.landmarks.count {
                let point = faceInfo.landmarks[i].cgPointValue
                tempPoint[i*2+0] = GLfloat(point.x)
                tempPoint[i*2+1] = GLfloat(point.y)
                
                index += 2
                if (index == size) {
                    break
                }
            }
            glUniform1fv(facePointsUniform, GLsizei(size), tempPoint)
    
            //注意:第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
            let position = glGetAttribLocation(thinFaceProgram, "position")
            glEnableVertexAttribArray(GLuint(position))
            glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, standardVertex)
    
    
            //----处理纹理数据-------
            let textCoord = glGetAttribLocation(thinFaceProgram, "inputTextureCoordinate")
            //设置合适的格式从buffer里面读取数据
            glEnableVertexAttribArray(GLuint(textCoord))
            glVertexAttribPointer(GLuint(textCoord), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, standardVerticalInvertFragment)
            
            glActiveTexture(GLenum(GL_TEXTURE1))
            glBindTexture(GLenum(GL_TEXTURE_2D), facePointTexture)
            glUniform1i(glGetUniformLocation(self.thinFaceProgram, "inputImageTexture"), 1) //单个纹理可以不用设置
            
            glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
    
            
            //MARK: - 3.绘制纹理完毕,开始渲染到屏幕上
            displayRenderToScreen(thinFaceTexture)
        }
    
        //8.渲染到屏幕上
        private func displayRenderToScreen(_ texture: GLuint) {
            //注意⚠️:打破之前的纹理绑定关系,使OpenGL的纹理绑定状态恢复到默认状态。
            glBindTexture(GLenum(GL_TEXTURE_2D), 0) //将2D纹理绑定到默认的纹理,一般用于打破之前的纹理绑定关系,使OpenGL的纹理绑定状态恢复到默认状态。
            glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)//将framebuffer绑定到默认的FBO处,一般用于打破之前的FBO绑定关系,使OpenGL的FBO绑定状态恢复到默认状态。
            
            //设置清屏颜色
            glClearColor(0.0, 0.0, 0.0, 1.0)
            //清除屏幕
            glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
            
            //1.设置视口大小
            let scale = self.contentScaleFactor
            glViewport(0, 0, GLsizei(self.frame.size.width * scale), GLsizei(self.frame.size.height * scale))
            
            //使用着色器
            glUseProgram(displayProgram)
            //绑定frameBuffer
            glBindFramebuffer(GLenum(GL_FRAMEBUFFER), frameBuffer)
    
    #warning("注意⚠️:想要获取shader里面的变量,这里要记住要在glLinkProgram后面、后面、后面")
            //----处理顶点数据-------
            //将顶点数据通过renderProgram中的传递到顶点着色程序的position
            /*1.glGetAttribLocation,用来获取vertex attribute的入口的.
              2.告诉OpenGL ES,通过glEnableVertexAttribArray,
              3.最后数据是通过glVertexAttribPointer传递过去的。
             */
            //注意:第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
            let position = glGetAttribLocation(displayProgram, "position")
    
            //设置合适的格式从buffer里面读取数据
            glEnableVertexAttribArray(GLuint(position))
    
            //设置读取方式
            //参数1:index,顶点数据的索引
            //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
            //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
            //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
            //参数5:stride,连续顶点属性之间的偏移量,默认为0;
            //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    //        glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 0))
            glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, standardVertex)
    
    
            //----处理纹理数据-------
            //1.glGetAttribLocation,用来获取vertex attribute的入口的.
            //注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
            let textCoord = glGetAttribLocation(displayProgram, "textCoordinate")
    
            //设置合适的格式从buffer里面读取数据
            glEnableVertexAttribArray(GLuint(textCoord))
    
            //3.设置读取方式
            //参数1:index,顶点数据的索引
            //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
            //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
            //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
            //参数5:stride,连续顶点属性之间的偏移量,默认为0;
            //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    //        glVertexAttribPointer(GLuint(textCoord), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 5), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 3))
            glVertexAttribPointer(GLuint(textCoord), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 0, standardVerticalInvertFragment)
    
            glActiveTexture(GLenum(GL_TEXTURE0))
            glBindTexture(GLenum(GL_TEXTURE_2D), texture)
            glUniform1i(glGetUniformLocation(self.displayProgram, "inputImageTexture"), 0) //单个纹理可以不用设置
    
            glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
            
        
            if (EAGLContext.current() == myContext) {
                myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
            }
            
        }
    

    这里值得注意的是:绘制特征点的时候不能进行Clear清屏操作,否则会看不摄像头所捕获的内容

    大眼片元着色器算法:

     //圓內放大
     vec2 enlargeEye(vec2 textureCoord, vec2 originPosition, float radius, float delta) {
        
        float weight = distance(vec2(textureCoord.x, textureCoord.y / aspectRatio), vec2(originPosition.x, originPosition.y / aspectRatio)) / radius;
        
        weight = 1.0 - (1.0 - weight * weight) * delta;
        weight = clamp(weight,0.0,1.0);
        textureCoord = originPosition + (textureCoord - originPosition) * weight;
        return textureCoord;
    }
    
    vec2 bigEye(vec2 currentCoordinate) {
        
        vec2 faceIndexs[2];
        faceIndexs[0] = vec2(74., 72.);//如下图中,以74为圆心,74到72作为半径R
        faceIndexs[1] = vec2(77., 75.);
        
        for(int i = 0; i < 2; I++)
        {
            int originIndex = int(faceIndexs[i].x);
            int targetIndex = int(faceIndexs[i].y);
            
            vec2 originPoint = vec2(facePoints[originIndex * 2], facePoints[originIndex * 2 + 1]);
            vec2 targetPoint = vec2(facePoints[targetIndex * 2], facePoints[targetIndex * 2 + 1]);
            
            float radius = distance(vec2(targetPoint.x, targetPoint.y / aspectRatio), vec2(originPoint.x, originPoint.y / aspectRatio));
            radius = radius * 5.;
            currentCoordinate = enlargeEye(currentCoordinate, originPoint, radius, bigEyeDelta);
        }
        return currentCoordinate;
    }
    

    textureCoord表示当前要修改的坐标,originPosition表示圆心坐标,radius表示圆的半径,delta用来控制变形强度。 和瘦脸的算法类似,根据originPositiontargetPosition确定一个圆,圆内的坐标会参与计算,圆外的不变。 圆内的坐标围绕圆心originPosition在变化,最终的坐标完全是由weight的值决定,weight越大,最终的坐标变化越小,当weight为1,即坐标处于圆边界或圆外时,最终的坐标不变;当weight小于1时,最终的坐标会落在原坐标和圆点之间,也就是说最终返回的像素点比原像素点距离圆点更近,这样就产生了以圆点为中心的放大效果。

    如下图中,以74为圆心,74到72作为半径R


    1.png

    瘦脸片元着色器算法:

    vec2 curveWarp(vec2 textureCoord, vec2 originPosition, vec2 targetPosition, float delta) {
         
        vec2 offset = vec2(0.0);
        vec2 result = vec2(0.0);
        vec2 direction = (targetPosition - originPosition) ;
        
        float radius = distance(vec2(targetPosition.x, targetPosition.y / aspectRatio), vec2(originPosition.x, originPosition.y / aspectRatio));
        float ratio = distance(vec2(textureCoord.x, textureCoord.y / aspectRatio), vec2(originPosition.x, originPosition.y / aspectRatio)) / radius;
        
        ratio = 1.0 - ratio;
        ratio = clamp(ratio, 0.0, 1.0);
        offset = direction * ratio * delta;
        
        result = textureCoord - offset;
        
        return result;
    }
    
    //指定9对 圆心坐标和目标坐标,如下图
    vec2 thinFace(vec2 currentCoordinate) {
        
        vec2 faceIndexs[9];
        faceIndexs[0] = vec2(3., 44.);
        faceIndexs[1] = vec2(29., 44.);
        faceIndexs[2] = vec2(7., 45.);
        faceIndexs[3] = vec2(25., 45.);
        faceIndexs[4] = vec2(10., 46.);
        faceIndexs[5] = vec2(22., 46.);
        faceIndexs[6] = vec2(14., 49.);
        faceIndexs[7] = vec2(18., 49.);
        faceIndexs[8] = vec2(16., 49.);
        
        for(int i = 0; i < 9; I++)
        {
            int originIndex = int(faceIndexs[i].x);
            int targetIndex = int(faceIndexs[i].y);
            vec2 originPoint = vec2(facePoints[originIndex * 2], facePoints[originIndex * 2 + 1]);
            vec2 targetPoint = vec2(facePoints[targetIndex * 2], facePoints[targetIndex * 2 + 1]);
            currentCoordinate = curveWarp(currentCoordinate, originPoint, targetPoint, thinFaceDelta);
        }
        return currentCoordinate;
    }
    

    textureCoord表示当前要修改的坐标,originPosition表示圆心坐标,targetPosition表示目标坐标,delta用来控制变形强度。

    上述shader方法可以这样理解,首先确定一个以originPosition为圆心、targetPositionoriginPosition之间的距离为半径的圆,然后将圆内的像素朝着同一个方向移动一个偏移值,且偏移值在距离圆心越近时越大,最终将变换后的坐标返回。

    如果将方法简化为这样的表达式变换后的坐标 = 原坐标 - (目标坐标 - 圆心坐标) * 变形强度,也就是说,方法的作用就是要在原坐标的基础上减去一个偏移值,而(targetPosition - originPosition)决定了移动的方向和最大值。

    • 指定9对 圆心坐标和目标坐标,如下图
    2.png

    刚开始想的是实现像开头动图那样的效果,但是在实现的时候遇到了一些问题。刚开始的想法是这样的,如下图

    3.png

    后面想到在实现多滤镜的时候,上一个片元着色器的输出,作为下一个片元着色器的输入, 如下图所示:

    流程图

    具体详情请查看源码。

    本文Demo:码云Github

    相关文章

      网友评论

          本文标题:iOS视觉-- (11) OpenGL ES+GLSL实现大眼和

          本文链接:https://www.haomeiwen.com/subject/mkllortx.html