在Android中使用opengl 片元shader实现在录像过程对人脸进行识别并自动打马赛

一、 使用fragment shader实现画面打马赛克的核心原理解释

OpenGL管线采样一张纹理的过程可以近似地理解为,从纹理坐标左下角(0, 0)到右上角(1, 1)的坐标片元范围内,不停地问fragment shader这个片元坐标应该是什么颜色,然后由我自定义的规则处理并获取纹理对应的像素颜色,最简单的情况下甚至可以就写一句"返回红色"就可以,这样所有坐标获取到颜色都是红色,最后就是一整个片元范围都呗着色为红色。正经来说,也可以把传入的图片按照坐标一一对应地采样并返回颜色,这样显示出来的图片本身的模样。也可以像今天介绍的fragment shader一样,一些范围直接一一对应采样,一些坐标压缩成间断范围采样实现降采样,从而实现部分区域马赛克一样的效果。

代码如下:

cpp 复制代码
#extension GL_OES_EGL_image_external : require
precision mediump float;

varying vec2 vTextureCoord;
uniform samplerExternalOES sTexture;

// 1. 定义最大支持的人脸数量 (可根据需求调整,通常 5-10 足够)
#define MAX_FACES 5

// 2. 外部传入的归一化矩形数组 (x=minU, y=minV, z=maxU, w=maxV)
uniform vec4 uFaceRects[MAX_FACES];
uniform int uFaceCount; // 实际检测到的人脸数量

// 3. 归一化的马赛克块大小 (例如 vec2(0.02, 0.03))
uniform vec2 uMosaicBlockSize;

void main() {
    vec2 uv = vTextureCoord;
    bool applyMosaic = false;
    float alpha = 1.0; // 颜色强度 (0.0-1.0)

    // 4. 遍历所有人脸矩形,判断当前像素是否在其中
    // 注意:GLSL ES 2.0 要求 for 循环的边界必须是常量,所以必须循环 MAX_FACES 次
    for (int i = 0; i < MAX_FACES; i++) {
        if (i < uFaceCount) {
            vec4 rect = uFaceRects[i];
            // 判断 UV 坐标是否在矩形内
            if (uv.x >= rect.x && uv.x <= rect.z && uv.y >= rect.y && uv.y <= rect.w) {
                applyMosaic = true;
                break; // 只要在任意一个矩形内,就标记并跳出循环
            }
        }
    }

    // 5. 核心:高性能马赛克算法 (坐标离散化)
    if (applyMosaic) {
        // 将连续坐标除以块大小 -> 向下取整对齐到网格 -> 加 0.5 采样网格中心 -> 乘回块大小
        vec2 grid = floor(uv / uMosaicBlockSize);
        uv = (grid + 0.5) * uMosaicBlockSize;
        alpha = 0.5; // 可选:降低颜色强度,增强马赛克效果
    }
    // 6. 最终采样 (无论是否马赛克,都只采样 1 次,性能拉满)
    gl_FragColor = vec4(texture2D(sTexture, uv).rgb * alpha, 1.0);
}

视频画面采样过程给特定范围打马赛克的核心原理代码可以看vec2 grid = floor(uv / uMosaicBlockSize)这一句,其实就是把传入的uv坐标除以uMosaicBlockSize,这时uv坐标只用经过uMosaicBlockSize为单位的量时,才会整整增加uMosaicBlockSize,也就是说uv在整个范围内,增量不再是连续的,而是以n*uMosaicBlockSize呈现,这样就会把画面采样过程中把uMosaicBlockSize \*n, uMosaicBlockSize \* (n + 1)都变成了采样纹理中uMosaicBlockSize *n位置的像素,从而把这个范围的像素降采样成1 / uMosaicBlockSize:

具体来说,可以把采样位置看成是白色的圆点,白色方框是纹理的像素,要整个纹理图片原样显示就需要每个方框采样一次,而如果按照上面执行的操作,把uMosaicBlockSize设置为2,那(3,2) (4,1) (4,2)这几个领近点采样时都会映射为(3,1)的像素值,对比周边精细度就成了1/2,这块区域就会现得像素特别低,也就是马赛克特效。

二、 录制逻辑:

其实和其他音频视频例子的架构基本都是差不多的,就是MediaRecorder提供surface,渲染内容到这个通过这个surface获取EglSurface的FBO中。

流程的关键代码如下:

绘制内容到FBO,然后再把FBO作为纹理绘制到glsurfaceview的FBO 0和MediaRecord surface的FBO的流程:

Kotlin 复制代码
package com.facedetectandmosaic
import android.graphics.RectF
import android.graphics.SurfaceTexture
import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLContext
import android.opengl.EGLDisplay
import android.opengl.EGLSurface
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.util.Log
import android.view.Surface
import com.cjztest.glShaderEffect.ShaderUtil
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import javax.microedition.khronos.opengles.GL10

class CameraGLRenderer(private val glSurfaceView: GLSurfaceView) : GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {

    var surfaceTexture: SurfaceTexture? = null
        private set
    var onSurfaceTextureReady: ((SurfaceTexture) -> Unit)? = null

    private var oesTextureId = -1
    private var fboId = -1
    private var fboTextureId = -1

    private var oesProgram = 0
    private var shader2DProgram = 0

    private val transformMatrix = FloatArray(16)
    private lateinit var vertexBuffer: FloatBuffer
    private lateinit var textureBuffer: FloatBuffer

    // 录制相关独立 EGL 环境
    @Volatile private var isRecording = false
    private var recordSurface: Surface? = null
    private var recordEglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY
    private var recordEglContext: EGLContext = EGL14.EGL_NO_CONTEXT
    private var recordEglSurface: EGLSurface = EGL14.EGL_NO_SURFACE

    private var videoWidth = 1280
    private var videoHeight = 720
    private var screenWidth = 0
    private var screenHeight = 0

    /**马赛克人脸相关**/
    private var uFaceRectsHandle = 0
    private var uFaceCountHandle = 0
    private var uMosaicBlockSizeHandle = 0

    private val MAX_FACES = 5

    // 用于存储传递给 Shader 的 FloatArray (长度必须是 MAX_FACES * 4)
    private val faceRectsArray = FloatArray(MAX_FACES * 4)
    private var currentFaceCount = 0

    // OES 顶点着色器(带矩阵转换)
    private val vertexShaderCode = """
        uniform mat4 uSTMatrix;
        attribute vec4 aPosition;
        attribute vec4 aTextureCoord;
        varying vec2 vTextureCoord;
        void main() {
            gl_Position = aPosition;
            vTextureCoord = (uSTMatrix * aTextureCoord).xy;
        }
    """.trimIndent()

    // 滤镜片元着色器
    private val fragmentOesShaderCode: String by lazy {
        ShaderUtil.loadFromAssetsFile(
            "mosaic/mosaicFrag.glsl",
            glSurfaceView.context.resources
        )
    }

    // 2D 顶点着色器(无矩阵转换,因为 FBO 出来的已经是标准正向正方形纹理)
    private val vertex2DShaderCode = """
        attribute vec4 aPosition;
        attribute vec4 aTextureCoord;
        varying vec2 vTextureCoord;
        void main() {
            gl_Position = aPosition;
            vTextureCoord = aTextureCoord.xy;
        }
    """.trimIndent()

    private val fragment2DShaderCode = """
        precision mediump float;
        varying vec2 vTextureCoord;
        uniform sampler2D sTexture;
        void main() {
            gl_FragColor = texture2D(sTexture, vTextureCoord);
        }
    """.trimIndent()

    init {
        val cubeCoords = floatArrayOf(-1f, -1f, 0f, 1f, -1f, 0f, -1f, 1f, 0f, 1f, 1f, 0f)
        vertexBuffer = ByteBuffer.allocateDirect(cubeCoords.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().apply { put(cubeCoords).position(0) }

        // 渲染到 FBO 时需要上下翻转纹理坐标,校正相机镜像
        val textureCoords = floatArrayOf(0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f)
        textureBuffer = ByteBuffer.allocateDirect(textureCoords.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().apply { put(textureCoords).position(0) }
    }

    override fun onSurfaceCreated(gl: GL10?, config: javax.microedition.khronos.egl.EGLConfig?) {
        // 编译两套 Program
        oesProgram = createProgram(vertexShaderCode, fragmentOesShaderCode)
        shader2DProgram = createProgram(vertex2DShaderCode, fragment2DShaderCode)

        // 创建 OES 纹理
        val textures = IntArray(1)
        GLES20.glGenTextures(1, textures, 0)
        oesTextureId = textures[0]
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId)
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())

        surfaceTexture = SurfaceTexture(oesTextureId).apply { setOnFrameAvailableListener(this@CameraGLRenderer) }
        glSurfaceView.post { onSurfaceTextureReady?.invoke(surfaceTexture!!) }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        Log.e("cjztest", "CameraGLRenderer, onSurfaceChanged, width:$width, height:$height")
        screenWidth = width
        screenHeight = height
        // 动态根据屏幕/相机尺寸初始化 FBO 缓冲区
        initFBO(width, height)
    }

    // 外部调用:更新人脸数据 (传入的是基于原始图像的归一化 UV 坐标)
    fun updateFaceRects(uvRects: List<RectF>) {
        currentFaceCount = minOf(uvRects.size, MAX_FACES)

        // 将 List<FloatArray> 展平为一维 FloatArray
        for (i in 0 until MAX_FACES) {
            if (i < currentFaceCount) {
                val rect = uvRects[i] // [minU, minV, maxU, maxV]
                //旋转过90度,还左右镜像过,所有right和left颠倒,top和bottom不变
                faceRectsArray[i * 4 + 0] = rect.top / screenHeight.toFloat()
                faceRectsArray[i * 4 + 1] = 1f - rect.right / screenWidth.toFloat()
                faceRectsArray[i * 4 + 2] = rect.bottom / screenHeight.toFloat()
                faceRectsArray[i * 4 + 3] = 1f - rect.left / screenWidth.toFloat()
            } else {
                // 填充无效数据,防止脏数据干扰
                faceRectsArray[i * 4 + 0] = -1.0f
                faceRectsArray[i * 4 + 1] = -1.0f
                faceRectsArray[i * 4 + 2] = -1.0f
                faceRectsArray[i * 4 + 3] = -1.0f
            }
        }
    }

    private fun initFBO(w: Int, h: Int) {
        if (fboId != -1) {
            GLES20.glDeleteFramebuffers(1, intArrayOf(fboId), 0)
            GLES20.glDeleteTextures(1, intArrayOf(fboTextureId), 0)
        }

        val fbos = IntArray(1)
        GLES20.glGenFramebuffers(1, fbos, 0)
        fboId = fbos[0]

        val texs = IntArray(1)
        GLES20.glGenTextures(1, texs, 0)
        fboTextureId = texs[0]

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fboTextureId)
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, w, h, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())

        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId)
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, fboTextureId, 0)

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
    }

    override fun onDrawFrame(gl: GL10?) {
        val surfaceTex = surfaceTexture ?: return
        synchronized(this) {
            surfaceTex.updateTexImage()
            surfaceTex.getTransformMatrix(transformMatrix)
        }

        if (fboId == -1) return

        // 步骤 1:滤镜处理 -> 先渲染到离屏 FBO 中
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId)
        GLES20.glViewport(0, 0, screenWidth, screenHeight)
        drawOesToFbo()
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)

        // 步骤 2:将 FBO 处理好的画面投递到手机屏幕
        GLES20.glViewport(0, 0, screenWidth, screenHeight)
        drawFboToScreen()

        // 步骤 3:如果正在录制,利用专属独立 EGL 纯离屏渲染投递至 MediaRecorder 录制表面
        if (isRecording && recordEglSurface != EGL14.EGL_NO_SURFACE) {
            // 保存当前屏幕环境
            val oldDisplay = EGL14.eglGetCurrentDisplay()
            val oldDrawSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)
            val oldReadSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_READ)
            val oldContext = EGL14.eglGetCurrentContext()

            // 切换到录制环境
            EGL14.eglMakeCurrent(recordEglDisplay, recordEglSurface, recordEglSurface, recordEglContext)
            GLES20.glViewport(0, 0, videoWidth, videoHeight)
            drawFboToScreen() // 将画面复刻一份塞入编码器
            EGL14.eglSwapBuffers(recordEglDisplay, recordEglSurface)

            // 还原主屏环境,防止 GLSurfaceView 崩溃
            EGL14.eglMakeCurrent(oldDisplay, oldDrawSurface, oldReadSurface, oldContext)
        }
    }

    private fun drawOesToFbo() {
        GLES20.glClearColor(0f, 0f, 0f, 1f)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glUseProgram(oesProgram)

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId)

        val posHandle = GLES20.glGetAttribLocation(oesProgram, "aPosition")
        vertexBuffer.position(0)
        GLES20.glVertexAttribPointer(posHandle, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer)
        GLES20.glEnableVertexAttribArray(posHandle)

        val texHandle = GLES20.glGetAttribLocation(oesProgram, "aTextureCoord")
        textureBuffer.position(0)
        GLES20.glVertexAttribPointer(texHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer)
        GLES20.glEnableVertexAttribArray(texHandle)

        val matrixHandle = GLES20.glGetUniformLocation(oesProgram, "uSTMatrix")
        GLES20.glUniformMatrix4fv(matrixHandle, 1, false, transformMatrix, 0)

        // 获取 打马赛克相关的Uniform 句柄
        uFaceRectsHandle = GLES20.glGetUniformLocation(oesProgram, "uFaceRects")
        uFaceCountHandle = GLES20.glGetUniformLocation(oesProgram, "uFaceCount")
        uMosaicBlockSizeHandle = GLES20.glGetUniformLocation(oesProgram, "uMosaicBlockSize")

        // 传递人脸矩形数组
        GLES20.glUniform4fv(uFaceRectsHandle, MAX_FACES, faceRectsArray, 0)
        GLES20.glUniform1i(uFaceCountHandle, currentFaceCount)

        // 传递马赛克块大小 (假设想要 20x20 像素的马赛克,图像是 1280x720)
        // 归一化大小 = 像素大小 / 图像宽高
        val blockW = 40.0f / screenWidth.toFloat()
        val blockH = 40.0f / screenHeight.toFloat()
        GLES20.glUniform2f(uMosaicBlockSizeHandle, blockW, blockH)

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
    }

    private fun drawFboToScreen() {
        GLES20.glClearColor(0f, 0f, 0f, 1f)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glUseProgram(shader2DProgram)

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fboTextureId)

        val posHandle = GLES20.glGetAttribLocation(shader2DProgram, "aPosition")
        vertexBuffer.position(0)
        GLES20.glVertexAttribPointer(posHandle, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer)
        GLES20.glEnableVertexAttribArray(posHandle)

        val texHandle = GLES20.glGetAttribLocation(shader2DProgram, "aTextureCoord")
        textureBuffer.position(0)
        GLES20.glVertexAttribPointer(texHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer)
        GLES20.glEnableVertexAttribArray(texHandle)

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
    }

    override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
        glSurfaceView.requestRender()
    }

    // 核心重构:为录制 Surface 创建独立隔离的全新 WindowSurface 环境
    fun startRecording(surface: Surface, width: Int, height: Int) {
        videoWidth = width
        videoHeight = height
        recordSurface = surface

        val sharedContext = EGL14.eglGetCurrentContext()
        recordEglDisplay = EGL14.eglGetCurrentDisplay()

        // 强行指定独立标志位,允许录制专用的 Buffer 标记
        val attribList = intArrayOf(
            EGL14.EGL_RED_SIZE, 8,
            EGL14.EGL_GREEN_SIZE, 8,
            EGL14.EGL_BLUE_SIZE, 8,
            EGL14.EGL_ALPHA_SIZE, 8,
            EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
            0x3142, 1, // 核心:显式通知系统这个底层是拿来录像的
            EGL14.EGL_NONE
        )

        val configs = arrayOfNulls<EGLConfig>(1)
        val numConfigs = intArrayOf(0)
        EGL14.eglChooseConfig(recordEglDisplay, attribList, 0, configs, 0, 1, numConfigs, 0)

        val ctxAttribs = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
        // 与主线程共享 Context,实现免内存拷贝复用 FBO 纹理
        recordEglContext = EGL14.eglCreateContext(recordEglDisplay, configs[0], sharedContext, ctxAttribs, 0)

        val surfaceAttribs = intArrayOf(EGL14.EGL_NONE)
        recordEglSurface = EGL14.eglCreateWindowSurface(recordEglDisplay, configs[0], recordSurface, surfaceAttribs, 0)

        isRecording = true
    }

    fun stopRecording() {
        isRecording = false
        if (recordEglSurface != EGL14.EGL_NO_SURFACE) {
            GLES20.glFinish()
            EGL14.eglDestroySurface(recordEglDisplay, recordEglSurface)
            EGL14.eglDestroyContext(recordEglDisplay, recordEglContext)
            recordEglSurface = EGL14.EGL_NO_SURFACE
            recordEglContext = EGL14.EGL_NO_CONTEXT
            recordEglDisplay = EGL14.EGL_NO_DISPLAY
        }
        recordSurface = null
    }

    private fun createProgram(vertex: String, fragment: String): Int {
        val vShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER).also { GLES20.glShaderSource(it, vertex); GLES20.glCompileShader(it) }
        val fShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER).also { GLES20.glShaderSource(it, fragment); GLES20.glCompileShader(it) }
        return GLES20.glCreateProgram().apply {
            GLES20.glAttachShader(this, vShader)
            GLES20.glAttachShader(this, fShader)
            GLES20.glLinkProgram(this)
        }
    }
}

google ML kit人脸识别框架调用:

Kotlin 复制代码
package com.facedetectandmosaic

import android.annotation.SuppressLint
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import android.util.Size
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetectorOptions
import kotlin.math.max

class FaceAnalyzer(
    private val previewSize: Size, // GLSurfaceView 的预览尺寸 (宽高)
    private val isFrontCamera: Boolean = false // 是否前置摄像头
) : ImageAnalysis.Analyzer {

    interface OnFacesDetectedListener {
        fun onFacesDetected(screenRects: List<RectF>)
    }

    private var onFacesDetectedListener: OnFacesDetectedListener? = null
    
    // 设置人脸检测回调监听器(可为 null 以移除监听)
    fun setOnFacesDetectedListener(listener: OnFacesDetectedListener?) {
        this.onFacesDetectedListener = listener
    }

    
    
    private val options = FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) // 优先速度
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
        .build()
    private val faceDetector = FaceDetection.getClient(options)

    @SuppressLint("UnsafeOptInUsageError")
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image ?: run {
            imageProxy.close()
            return
        }

        // 1. 将 ImageProxy 转为 ML Kit 需要的 InputImage
        val inputImage = InputImage.fromMediaImage(
            mediaImage,
            imageProxy.imageInfo.rotationDegrees
        )

        // 获取图像旋转后的逻辑宽高 (用于后续坐标转换)
        val rotation = imageProxy.imageInfo.rotationDegrees

        Log.e("cjztest", "analyze: imageProxy width: ${imageProxy.width}, height: ${imageProxy.height}, rotation: $rotation")

        val imgW = if (rotation == 90 || rotation == 270) imageProxy.height else imageProxy.width
        val imgH = if (rotation == 90 || rotation == 270) imageProxy.width else imageProxy.height

        faceDetector.process(inputImage)
            .addOnSuccessListener { faces ->
                val screenRects = faces.map { face ->
                    // 2. 核心:坐标系转换 (图像坐标 -> 屏幕坐标)
                    transformRect(face.boundingBox, previewSize, imgW, imgH, isFrontCamera)
                }
                //在这里将 screenRects 传递给 GLSurfaceView 的 Renderer,进行绘制
                onFacesDetectedListener?.onFacesDetected(screenRects)
            }
            .addOnFailureListener { e ->
                Log.e("FaceAnalyzer", "Detection failed", e)
            }
            .addOnCompleteListener {
                // 必须关闭 imageProxy,否则不会收到下一帧
                imageProxy.close()
            }
    }

    /**
     * 将 ML Kit 返回的图像坐标,转换为 OverlayView 的屏幕坐标
     * 假设你的 GLSurfaceView 采用的是 Center Crop (居中裁剪填满屏幕) 策略
     */
    private fun transformRect(rect: Rect, previewSize: Size, imgW: Int, imgH: Int, isFront: Boolean): RectF {
        val viewW = previewSize.width.toFloat()
        val viewH = previewSize.height.toFloat()
        if (viewW == 0f || viewH == 0f) return RectF(rect)

        // 计算缩放比例 (Center Crop 逻辑:取较大的缩放比以填满屏幕)
        val scale = max(viewW / imgW, viewH / imgH)

        // 计算裁剪导致的偏移量
        val offsetX = (viewW - imgW * scale) / 2f
        val offsetY = (viewH - imgH * scale) / 2f

        var left = rect.left * scale + offsetX
        var top = rect.top * scale + offsetY
        var right = rect.right * scale + offsetX
        var bottom = rect.bottom * scale + offsetY

        // 如果是前置摄像头,画面通常会被水平镜像 (Mirror),框也需要镜像翻转
        if (isFront) {
            val tempLeft = viewW - right
            val tempRight = viewW - left
            left = tempLeft
            right = tempRight
        }

        return RectF(left, top, right, bottom)

        //cjztest:
//        val left = rect.top.toFloat() / imgH.toFloat() * viewW
//        val top = (1f - rect.right.toFloat() / imgW.toFloat()) * viewH
//        val right = rect.bottom.toFloat() / imgH.toFloat() * viewW
//        val bottom = (1f - rect.left.toFloat() / imgW.toFloat()) * viewH
//        return RectF(left, top, right, bottom)


//        return RectF(0f, 0f, viewW / 2, viewH / 2)  //用来做控制变量法测试
    }
}

三、 完整代码可以参考:

GitHub - cjzjolly/learnopengl at cjztest/face_dectect_and_moasic · GitHub

四、 实现效果:

【类执法仪设备_open gl es fragment shader人脸打马赛克_0】 https://www.bilibili.com/video/BV1kP7X6cEQW/?share_source=copy_web&vd_source=c870102a8d6b63ae5be697515b19d95f

还可以改一下颜色,可编程渲染管线就是灵活:

【类执法仪设备_open gl es fragment shader人脸打马赛克2】 https://www.bilibili.com/video/BV1yw7X6YEpo/?share_source=copy_web\&vd_source=c870102a8d6b63ae5be697515b19d95f

五、 结语

在写这个demo的过程中,除了最核心的shader是自己写之外,大部分代码片段都是AI帮忙直接生成,比以往实现类似难度的demo时间少了三分之一。尽管AI生成的代码有一些问题,我需要review通读后进行修改,也依然大幅度提高了效率,我在这个过程中感觉自己的作用更多的是在AI无法处理好的事情上,理解途中发生了什么,然后再用自己的知识、能力和经验解决,而假如我没有这些技能的话,AI输出的代码出了问题我也无法补救了。所以我觉得AI对于有足够深度知识的程序员来说,能大幅度提升效率。但对AI输出的内容完全无法理解的知识深度不足的程序员来说,AI反而是自己无法驾驭的工具,一旦给的内容出错,由于它给的内容知识超出使用者的知识范畴,这时使用者连兜底的作用也彻底没了,那就......很恐怖了。

相关推荐
tangchao340勤奋的老年?1 天前
C++ OpenGL显示地图
c++·opengl
郝学胜-神的一滴3 天前
[简化版 GAMES 101] 计算机图形学 11:频域·卷积·抗锯齿
c++·unity·图形渲染·opengl·three·unreal
肥or胖5 天前
Qt中OpenGL快速入门
qt·音视频·opengl
郝学胜-神的一滴7 天前
中级OpenGL教程 007:解决背面光照异常高光问题
c++·unity·游戏引擎·three.js·opengl·unreal
郝学胜-神的一滴10 天前
[简化版 GAMES 101] 计算机图形学 10:反走样与深度缓冲核心解析
c++·unity·godot·图形渲染·three.js·unreal engine·opengl
♡すぎ♡13 天前
现代实时渲染管线
计算机图形学·opengl·着色器·渲染管线
郝学胜-神的一滴14 天前
中级OpenGL教程 006:高光反射原理与 Shader 实现
c++·unity·godot·图形渲染·three.js·opengl·unreal
心走20 天前
OpenGL Es渲染相机画面问题记录
opengl
郝学胜-神的一滴21 天前
中级OpenGL教程 005:为球体&平面注入法线灵魂
c++·unity·图形渲染·three.js·opengl·unreal