NDK OpenGL与OpenCV实现大眼萌特效

news2024/9/22 15:31:56

NDK​系列之OpenGL与OpenCV实现大眼萌特效,本节主要是在上一节OpenGL仿抖音极快极慢录制特效视频上增加大眼萌的特效。

OpenGL视频特效系列:

NDK OpenGL渲染画面效果

NDK OpenGL离屏渲染与工程代码整合

NDK OpenGL仿抖音极快极慢录制特效视频

NDK OpenGL与OpenCV实现大眼萌特效

NDK OpenGL实现美颜功能

实现效果:

实现逻辑:

1.集成OpenCV,实现检测人脸框框;

2.集成中科院FaceAlignment,在人脸框框基础上,进行人脸关键点定位,有5个点

3.用C++构建实例化Face.java(人脸类,包含关键点5个点坐标值)

4.通过MyGLRenderer自定义渲染器触发着色器执行放大眼睛效果。

一、OpenCV集成到AS

1)复制OpenCV源文件到cpp目录下,动态库文件复制到jniLibs目录下: 

2)在CMakeLists文件中,导入源文件和库文件

二、集成中科院FaceAlignment

1)开源的 SeetaFace 人脸识别引擎是由中科院计算所山世光研究员带领的人脸识别研究组研发。代码基于 C++实现,且不依赖于任何第三方的库函。SeetaFace 人脸识别引擎包括了搭建一套全自动人脸识别系统所需的三个核心模块,即:人脸检测模块(SeetaFace Detection)、面部特征点定位模块(SeetaFaceAlignment)以及人脸特征提取与比对模块(SeetaFace Identification)。
SeetaFace 项目网址为 https://github.com/seetaface/SeetaFaceEngin 
我们这里只需要定位人眼,集成的是面部特征点定位模块(SeetaFaceAlignment),复制FaceAlignment源文件到cpp目录下:

2)配置FaceAlignment源文件中的CMakeLists文件

3)在CMakeLists文件中,导入源文件和库文件

三、人脸跟踪/人脸关键点定位代码实现

1)创建java层的Face类,用于保存人脸关键点的坐标等信息

public class Face {
    /**
     * 人脸框的x和y,不等于 width,height,所以还是单独定义算了(没啥关联)
     * float[] landmarks 细化后如下:12个元素
     * 0下标(保存:人脸框的 x)
     * 1下标(保存:人脸框的 y)
     * <p>
     * 2下标(保存:左眼x)
     * 3下标(保存:左眼y)
     * <p>
     * 4下标(保存:右眼x)‘
     * 5下标(保存:右眼y)
     * <p>
     * 6下标(保存:鼻尖x)
     * 7下标(保存:鼻尖y)
     * <p>
     * 8下标(保存:左边嘴角x)
     * 9下标(保存:左边嘴角y)
     * <p>
     * 10下标(保存:右边嘴角x)
     * 11下标(保存:右边嘴角y)
     */
    public float[] landmarks;

    public int width;        // 保存人脸的框 的宽度
    public int height;       // 保存人脸的框 的高度
    public int imgWidth;    // 送去检测的所有宽 屏幕
    public int imgHeight;   // 送去检测的所有高 屏幕

    public Face(int width, int height, int imgWidth, int imgHeight, float[] landmarks) {
        this.landmarks = landmarks;
        this.width = width;
        this.height = height;
        this.imgWidth = imgWidth;
        this.imgHeight = imgHeight;
    }
}

 2)创建Java层FaceTrack.java人脸追踪类,人脸与关键点的定位追踪 api 类, 与C++层交互。

public class FaceTrack {
    static {
        System.loadLibrary("native-lib");
    }

    private CameraHelper mCameraHelper; // 手机相机预览工具类(之前的内容)
    private Handler mHandler; // 此Handler方便开启一个线程
    private HandlerThread mHandlerThread; // 此HandlerThread方便开启一个线程

    private long self; // FaceTrack.cpp对象的地址指向long值
    private Face mFace; // 最终人脸跟踪的结果

    /**
     * @param model        OpenCV人脸的模型的文件路径
     * @param seeta        中科院的那个模型(五个关键点的特征点的文件路径)
     * @param cameraHelper 需要把CameraID传递给C++层
     */
    public FaceTrack(String model, String seeta, CameraHelper cameraHelper) {
        mCameraHelper = cameraHelper;
        self = native_create(model, seeta); // 传入人脸检测模型到C++层处理,返回FaceTrack.cpp的地址指向

        // 开启一个线程:去执行 人脸检测定位
        mHandlerThread = new HandlerThread("FaceTrack");
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // 子线程 耗时再久 也不会对其他地方 (如:opengl绘制线程) 产生影响
                synchronized (FaceTrack.this) {
                    // 定位 线程中检测
                    Log.i("FaceTrack", "开始人脸定位 线程中检测");
                    mFace = native_detector(self, (byte[]) msg.obj, mCameraHelper.getCameraID(), 800, 480);
                    if (mFace != null) {
                        Log.e("拍摄了人脸mFace.toString:", mFace.toString()); // 看看打印效果
                    }
                }
            }
        };
    }

    public void startTrack() { // 启动跟踪器 OpenCV
        native_start(self);
    }

    public void stopTrack() { // 停止跟踪器 OpenCV
        synchronized (this) {
            mHandlerThread.quitSafely();
            mHandler.removeCallbacksAndMessages(null);
            native_stop(self);
            self = 0;
        }
    }

    // 开始检测人脸 byte[] data == NV21 Camera的数据 byte[]
    public void detector(byte[] data) { // 要把相机的数据,给C++层做人脸追踪
        // 把积压的 11号任务移除掉
        mHandler.removeMessages(11);
        // 加入新的11号任务
        Message message = mHandler.obtainMessage(11);
        message.obj = data;
        mHandler.sendMessage(message);
    }

    public Face getFace() { // 这个函数很重要
        return mFace; // 如果能拿到mFace,就证明 有人脸最终信息 和 5个关键点信息
    }

    /**
     * 传入人脸检测模型到C++层处理
     *
     * @param model OpenCV人脸模型
     * @param seeta Seeta中科院的人脸关键点模型
     * @return FaceTrack.cpp地址指向long值
     */
    private native long native_create(String model, String seeta);

    private native void native_start(long self); // 开始追踪

    private native void native_stop(long self); // 停止追踪

    /**
     * 执行真正的人脸探测工作
     *
     * @param self     Face.java对象的地址指向long值
     * @param data     Camera相机 byte[] data NV21摄像头的数据
     * @param cameraId Camera相机ID,前置摄像头,后置摄像头
     * @param width    宽度
     * @param height   高度
     * @return 若Face==null:代表没有人脸信息+人脸5特征,  若Face有值:人脸框x/y,+ 5个特侦点(本次只需要 人脸框x/y + 双眼关键点)
     */
    private native Face native_detector(long self, byte[] data, int cameraId, int width, int height);
}

C++层初始化OpenCV和Seeta人脸关键点定位

Java_com_ndk_opengl_face_FaceTrack_native_1create(JNIEnv *env, jobject thiz, jstring model_,
                                                  jstring seeta_) {
    const char *model = env->GetStringUTFChars(model_, 0);
    const char *seeta = env->GetStringUTFChars(seeta_, 0);

    FaceTrack *faceTrack = new FaceTrack(model, seeta);

    env->ReleaseStringUTFChars(model_, model);
    env->ReleaseStringUTFChars(seeta_, seeta);
    return reinterpret_cast<jlong>(faceTrack);
}
FaceTrack::FaceTrack(const char *model, const char *seeta) {
    Ptr<CascadeDetectorAdapter> mainDetector = makePtr<CascadeDetectorAdapter>(makePtr<CascadeClassifier>(model)); // OpenCV主探测器
    Ptr<CascadeDetectorAdapter> trackingDetector = makePtr<CascadeDetectorAdapter>(makePtr<CascadeClassifier>(model)); // OpenCV跟踪探测器
    DetectionBasedTracker::Parameters detectorParams;
    // OpenCV创建追踪器,为了下面的(开始跟踪,停止跟踪)
    tracker = makePtr<DetectionBasedTracker>(mainDetector, trackingDetector, detectorParams);

    // TODO >>>>>>>>>>>>>>>>>>>>>>> 上面是OpenCV模板代码人脸追踪区域, 下面是Seeta人脸关键点代码+OpenCV >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    faceAlignment = makePtr<seeta::FaceAlignment>(seeta); // Seeta中科院关键特征点
}

3)当Surface改变时,会回调到自定义渲染器MyGlRendere的onSurfaceChanged()函数,在这里创建人脸检测跟踪器;

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
	Log.i(TAG, "onSurfaceChanged");
	mWidth = width;
	mHeight = height;

	// 创建人脸检测跟踪器 // TODO 【大眼相关代码】
	mFaceTrack = new FaceTrack(modelPath, seetaPath, mCameraHelper);
	mFaceTrack.startTrack(); // 启动跟踪器

	mCameraHelper.startPreview(mSurfaceTexture); // 开始预览
	mCameraFilter.onReady(width, height);
	mScreenFilter.onReady(width, height);
}

C++层OpenCV开启追踪器

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_opengl_face_FaceTrack_native_1start(JNIEnv *env, jobject thiz, jlong self) {
    if (self == 0) {
        return;
    }
    FaceTrack *faceTrack = reinterpret_cast<FaceTrack *>(self);
    faceTrack->startTracking();
}

4)当Camera画面有数据时,会回调到自定义渲染器MyGlRendere的onPreviewFrame()函数,在这里真正开始检测人脸,把相机的数据,给C++层做人脸追踪;

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
	if (mFaceTrack != null)
		mFaceTrack.detector(data);
}

C++层执行人脸追踪

extern "C"
JNIEXPORT jobject JNICALL
Java_com_ndk_opengl_face_FaceTrack_native_1detector(JNIEnv *env, jobject thiz, jlong self,
                                                    jbyteArray data_, jint camera_id,
                                                    jint width, jint height) {
    if (self == 0) {
        return NULL;
    }

    jbyte *data = env->GetByteArrayElements(data_, 0);
    FaceTrack *faceTrack = reinterpret_cast<FaceTrack *>(self); // 通过地址反转CPP对象
    LOGI("OpenCV旋转数据操作");
    // OpenCV旋转数据操作
    Mat src(height * 3 / 2, width, CV_8UC1, data); // 摄像头数据data 转成 OpenCv的 Mat
   // imwrite("/sdcard/camerajin.jpg", src); // 做调试的时候用的(方便查看:有没有摆正,有没有灰度化 等)
    cvtColor(src, src, CV_YUV2RGBA_NV21); // 把YUV转成RGBA

    LOGI("OpenCV旋转数据操作 camera_id %d", camera_id);
    if (camera_id == 1) { // 前摄
        rotate(src, src, ROTATE_90_COUNTERCLOCKWISE); // 逆时针90度
        flip(src, src, 1); // y 轴 翻转(镜像操作)
    } else {  // 后摄
        rotate(src, src, ROTATE_90_CLOCKWISE);
    }
    LOGI("OpenCV基础操作");
    // OpenCV基础操作
    cvtColor(src, src, COLOR_RGBA2GRAY); // 灰度化
    equalizeHist(src, src); // 均衡化处理(直方图均衡化,增强对比效果)
    vector<Rect2f> rects;
    faceTrack->detector(src, rects); // 送去定位,要去做人脸的检测跟踪了
    env->ReleaseByteArrayElements(data_, data, 0);

    // rects 他已经有丰富的人脸框框的信息,接下来就是,关键点定位封装操作Face.java

    // TODO 注意:上面的代码执行完成后,就拿到了 人脸检测的成果 放置在rects中

    // C++ 反射 实例化 Face.java 并且保证 Face.java有值

    int imgWidth = src.cols; // 构建 Face.java的 int imgWidth; 送去检测图片的宽
    int imgHeight = src.rows; // 构建 Face.java的 int imgHeight; 送去检测图片的高
    int ret = rects.size(); // 如果有一个人脸,那么size肯定大于0
    LOGI("OpenCV基础操作 ret %d", ret);
    if (ret) { // 注意:有人脸,才会进if
        jclass clazz = env->FindClass("com/ndk/opengl/face/Face");
        jmethodID construct = env->GetMethodID(clazz, "<init>", "(IIII[F)V");
        // int width, int height,int imgWidth,int imgHeight, float[] landmark
        int size = ret * 2; // 乘以2是因为,有x与y, 其实size===2,因为rects就一个人脸

        // 构建 Face.java的 float[] landmarks;
        jfloatArray floatArray = env->NewFloatArray(size);
        for (int i = 0, j = 0; i < size; ++j) {  // 前两个就是人脸的x与y
            float f[2] = {rects[j].x, rects[j].y};
            env->SetFloatArrayRegion(floatArray, i, 2, f);
            i += 2;
        }
        Rect2f faceRect = rects[0];
        int faceWidth = faceRect.width; // 构建 Face.java的 int width; 保存人脸的宽
        int faceHeight = faceRect.height; // 构建 Face.java的 int height; 保存人脸的高
        // 实例化Face.java对象,都是前面JNI课程的基础
        jobject face = env->NewObject(clazz, construct, faceWidth, faceHeight, imgWidth, imgHeight,
                                      floatArray);
        rectangle(src, faceRect, Scalar(0, 0, 255)); // OpenCV内容
        for (int i = 1; i < ret; ++i) { // OpenCV内容
            circle(src, Point2f(rects[i].x, rects[i].y), 5, Scalar(0, 255, 0));
        }
        imwrite("/sdcard/srcjin.jpg", src); // 做调试的时候用的(方便查看:有没有摆正,有没有灰度化 等)
        return face; // 返回 jobject == Face.java(已经有值了,有人脸所有的信息了,那么就可以开心,放大眼睛)
    }
    src.release(); // Mat释放工作
    return NULL;
}

四、开启大眼效果

1)单独创建大眼过滤器BigEyeFilter,方便后扩展,每增加一种新特效就新增一个过滤器来实现;

public class BigEyeFilter extends BaseFrameFilter {

    private int left_eye; // 左眼坐标的属性索引
    private int right_eye; // 右眼坐标的属性索引
    private FloatBuffer left; // 左眼的buffer
    private FloatBuffer right; // 右眼的buffer
    private Face mFace; // 人脸追踪+人脸5关键点 最终的成果

    public BigEyeFilter(Context context) {
        super(context, R.raw.base_vertex, R.raw.bigeye_fragment);
        left_eye = glGetUniformLocation(mProgramId, "left_eye"); // 左眼坐标的属性索引
        right_eye = glGetUniformLocation(mProgramId, "right_eye"); // 右眼坐标的属性索引

        left = ByteBuffer.allocateDirect(2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();  // 左眼buffer申请空间
        right = ByteBuffer.allocateDirect(2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); // 右眼buffer申请空间
    }

    @Override
    public int onDrawFrame(int textureID) {
        if (null == mFace) {
            return textureID; // 如果这个对象为null,证明没有检测到人脸,啥事都不用做
        }

        // 1:设置视窗
        glViewport(0, 0, mWidth, mHeight);
        // 这里是因为要渲染到FBO缓存中,而不是直接显示到屏幕上
        glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[0]);

        // 2:使用着色器程序
        glUseProgram(mProgramId);

        // 渲染 传值
        // 1:顶点数据
        mVertexBuffer.position(0);
        glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer); // 传值
        glEnableVertexAttribArray(vPosition); // 传值后激活

        // 2:纹理坐标
        mTextureBuffer.position(0);
        glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer); // 传值
        glEnableVertexAttribArray(vCoord); // 传值后激活

        float[] landmarks =  mFace.landmarks; // TODO 传 mFace 眼睛坐标 给着色器

        /* landmarks[2] / mFace.imgWidth ?
           landmarks 他的相对位置是,C++层里面得到的坐标,这个坐标是相对屏幕的
           但是我们的OpenGL纹理坐标才行,因为OpenGL着色器代码 纹理 是 0~1 范围
           所以需要  landmarks[2] / mFace.imgWidth 转换一下
         */

        // 左眼: 的 x y 值,保存到 左眼buffer中
        float x = landmarks[2] / mFace.imgWidth;
        float y = landmarks[3] / mFace.imgHeight;
        left.clear();
        left.put(x);
        left.put(y);
        left.position(0);
        glUniform2fv(left_eye, 1, left);

        // 右眼: 的 x y 值,保存到 右眼buffer中
        x = landmarks[4] / mFace.imgWidth;
        y = landmarks[5] / mFace.imgHeight;
        right.clear();
        right.put(x);
        right.put(y);
        right.position(0);
        glUniform2fv(right_eye, 1, right);

        // 片元 vTexture
        glActiveTexture(GL_TEXTURE0); // 激活图层
        glBindTexture(GL_TEXTURE_2D, textureID); // 绑定
        glUniform1i(vTexture, 0); // 传递参数

        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 通知opengl绘制

        // 解绑fbo
        glBindTexture(GL_TEXTURE_2D, 0);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        return mFrameBufferTextures[0];//返回fbo大眼后的纹理id
    }

    public void setFace(Face mFace) { // C++层把人脸最终5关键点成果的(mFaceTrack.getFace()) 赋值给此函数
        this.mFace = mFace;
    }
}

大眼的片元着色器代码,使用局部放大的算法实现

// TODO 片元着色器(大眼专用的, 局部放大的算法)

// 着色器坐标: 0 ~ 1

precision mediump float; // float 数据的精度

varying vec2 aCoord; // 顶点着色器传过来的 采样点的坐标
uniform sampler2D vTexture; // 采样器

uniform vec2 left_eye; // 左眼 x/y
uniform vec2 right_eye; // 右眼 x/y

// 着色器代码,最好加 .0,防止有问题
// 把公式转成着色器代码
// r:    原来的点 距离眼睛中心点距离(半径)
// rmax: 局部放大 最大半径 / 2
float fs(float r, float rmax) {
    float a = 0.8; // 放大系数,如果你的a==0,我会直接返回r(啥事不做)

    // 内置函数:求平方 pow
    return (1.0 - pow(r / rmax - 1.0, 2.0) * a);
}

// TODO 目的:把正常眼睛的纹理坐标,搬到 放大区域   纹理坐标搬到外面
// oldCoord 整个屏幕的纹理坐标
// eye 眼睛坐标
// rmax: 局部放大 最大半径 / 2
vec2 newCoord(vec2 oldCoord, vec2 eye, float rmax) {
    vec2 newCoord = oldCoord;
    float r = distance(oldCoord, eye); // 求两点之间的距离

    // 必须是眼睛范围才做事情,
    if (r > 0.0f && r < rmax) { // 如果进不来if,那么还是返回原来的点,啥事不做
        float fsr = fs(r, rmax);

        //    新点 - 眼睛     /  老点 - 眼睛   = 新距离;
        // (newCoord - eye) / (coord - eye) = fsr;

        // newCoord新点 =    新距离  * (老点     - 眼睛)  +  眼睛
        newCoord       =    fsr    * (oldCoord - eye)   +  eye;
    }
    return newCoord;
}

// 那个max应该是可以随便设置的吧,配置一半的限制,是为了避免两眼重叠很奇怪

void main(){
    // gl_FragColor = texture2D(vTexture, aCoord);

    // 两眼间距的一半  识别区域宽度/2吗
    float rmax = distance(left_eye, right_eye) / 2.0; // distance 求两点的距离(rmax两眼间距) 注意是放大后的间距

    // aCoord是整副图像,
    vec2 newCoord = newCoord(aCoord, left_eye, rmax); // 求左眼放大位置的采样点
    newCoord = newCoord(newCoord, right_eye, rmax); // 求右眼放大位置的采样点
    // 此newCoord就是大眼像素坐标值
    gl_FragColor = texture2D(vTexture, newCoord);
}

2)用户操作UI开启大眼效果

((CheckBox) findViewById(R.id.chk_bigeye)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
	@Override
	public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
		mGLSurfaceView.enableBigEye(isChecked);
	}
});

将事件分发到自定义渲染器MyGlRendere,在渲染器统一处理效果,开启大眼特效

public void enableBigEye(final boolean isChecked) {
	// BigEyeFilter bigEyeFilter = new BigEyeFilter(); // 这样可以吗  不行,必须在EGL线程里面绘制
	myGLSurfaceView.queueEvent(new Runnable() { // 把大眼渲染代码,加入到, GLSurfaceView 的 内置EGL 的 GLTHread里面
		public void run() {
			if (isChecked) {
				mBigEyeFilter = new BigEyeFilter(myGLSurfaceView.getContext());
				mBigEyeFilter.onReady(mWidth, mHeight);
			} else {
				mBigEyeFilter.release();
				mBigEyeFilter = null;
			}
		}
	});
}

3)相机绘制一帧图像时,会回调到自定义渲染器MyGlRendere的onDrawFrame()函数,获取纹理对象的图像数据,先通过CamreaFilter相机过滤器,实现相关效果,再将其FBO的纹理ID传递给大眼过滤器BigEyeFilter,大眼过滤器增加完特效后,又将包含大眼特效的纹理ID,传递给ScreenFilter屏幕过滤器,将最终成果的纹理ID通过OpenGL渲染到屏幕;

@Override
public void onDrawFrame(GL10 gl) {
	Log.i(TAG, "onDrawFrame");
	// 每次清空之前的:例子:上课擦黑白 是一个道理
	glClearColor(255, 0, 0, 0); // 屏幕清理成颜色 红色,清理成红色的黑板一样
	// mask 细节看看此文章:https://blog.csdn.net/z136411501/article/details/83273874
	// GL_COLOR_BUFFER_BIT 颜色缓冲区
	// GL_DEPTH_BUFFER_BIT 深度缓冲区
	// GL_STENCIL_BUFFER_BIT 模型缓冲区
	glClear(GL_COLOR_BUFFER_BIT);

	// 绘制摄像头数据
	mSurfaceTexture.updateTexImage();  // 将纹理图像更新为图像流中最新的帧数据【刷新一下】
	// 画布,矩阵数据,通过Native层将数据存储到mtx
	mSurfaceTexture.getTransformMatrix(mtx);
	// 相机过滤器,绘制一帧图像,不可见
	mCameraFilter.setMatrix(mtx);
	int textureId = mCameraFilter.onDrawFrame(mTextureID[0]); // 摄像头,矩阵,都已经做了
	// 增加其他特效
	/*textureId = 美白.onDrawFrame(textureId);
	textureId = 大眼.onDrawFrame(textureId);
	textureId = xxx.onDrawFrame(textureId);*/
	// TODO 【大眼相关代码】 textureId = 大眼Filter.onDrawFrame(textureId);
	if (null != mBigEyeFilter) {
		mBigEyeFilter.setFace(mFaceTrack.getFace());
		textureId = mBigEyeFilter.onDrawFrame(textureId);
	}
	// 屏幕过滤器,绘制一帧图像,屏幕显示
	mScreenFilter.onDrawFrame(textureId); // textureId == 最终成果的纹理ID

	mMediaRecorder.encodeFrame(textureId, mSurfaceTexture.getTimestamp());
}

大眼过滤器BigEyeFilter绘制大眼效果

@Override
public int onDrawFrame(int textureID) {
	if (null == mFace) {
		return textureID; // 如果这个对象为null,证明没有检测到人脸,啥事都不用做
	}

	// 1:设置视窗
	glViewport(0, 0, mWidth, mHeight);
	// 这里是因为要渲染到FBO缓存中,而不是直接显示到屏幕上
	glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[0]);

	// 2:使用着色器程序
	glUseProgram(mProgramId);

	// 渲染 传值
	// 1:顶点数据
	mVertexBuffer.position(0);
	glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer); // 传值
	glEnableVertexAttribArray(vPosition); // 传值后激活

	// 2:纹理坐标
	mTextureBuffer.position(0);
	glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer); // 传值
	glEnableVertexAttribArray(vCoord); // 传值后激活

	float[] landmarks =  mFace.landmarks; // TODO 传 mFace 眼睛坐标 给着色器

	/* landmarks[2] / mFace.imgWidth ?
	   landmarks 他的相对位置是,C++层里面得到的坐标,这个坐标是相对屏幕的
	   但是我们的OpenGL纹理坐标才行,因为OpenGL着色器代码 纹理 是 0~1 范围
	   所以需要  landmarks[2] / mFace.imgWidth 转换一下
	 */

	// 左眼: 的 x y 值,保存到 左眼buffer中
	float x = landmarks[2] / mFace.imgWidth;
	float y = landmarks[3] / mFace.imgHeight;
	left.clear();
	left.put(x);
	left.put(y);
	left.position(0);
	glUniform2fv(left_eye, 1, left);

	// 右眼: 的 x y 值,保存到 右眼buffer中
	x = landmarks[4] / mFace.imgWidth;
	y = landmarks[5] / mFace.imgHeight;
	right.clear();
	right.put(x);
	right.put(y);
	right.position(0);
	glUniform2fv(right_eye, 1, right);

	// 片元 vTexture
	glActiveTexture(GL_TEXTURE0); // 激活图层
	glBindTexture(GL_TEXTURE_2D, textureID); // 绑定
	glUniform1i(vTexture, 0); // 传递参数

	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 通知opengl绘制

	// 解绑fbo
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	return mFrameBufferTextures[0];//返回fbo大眼后的纹理id
}

至此,OpenGL与OpenCV实现大眼萌特效已完成。

源码:

NdkOpenGLPlay: NDK OpenGL渲染画面效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/759306.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通讯录实现

普通版 需求 通讯录可以用来存储1000个人的信息&#xff0c;每个人的信息包括&#xff1a;姓名、性别、年龄、电话、住址 提供方法&#xff1a; 添加联系人信息删除指定联系人信息查找指定联系人信息修改指定联系人信息显示所有联系人信息清空所有联系人以名字排序所有联系…

【Linux后端服务器开发】UDP协议

目录 一、端口号 二、UDP报头格式 三、UDP的特点 四、UDP协议实现网络聊天群 一、端口号 端口号port标识了一个主机上进行通信的不同的应用程序。 0 ~ 1023&#xff1a;系统端口号&#xff0c;HTTP、FTP、SSH等这些广为使用的应用层协议&#xff0c;它们的端口号都是固定…

Windows软件开发常用技巧总结

本文总结了本人在日常工作学习中遇到的问题及其解决方法&#xff0c;没有固定的涉及领域 目的就是为了在下一次遇到类似问题的时候方便查找&#xff0c;从而快速解决问题 本文不定时更新~ 目录 Windows使用 如何实现桌面图标随意排列 文件资源管理器相关 显示隐藏文件 修改…

Linux--获取最近一次的进程退出码:echo $?

举例&#xff1a; #include <stdio.h> int main() { printf("hello world,pid: %d,ppid: %…

JavaFx 用户界面控件3——TableView

1.表格视图 TableView ableView是JavaFX提供的一个强大的控件&#xff0c;可以用于显示表格数据。它通过为TableView设定items属性&#xff08;存储行数据的ObservableList对象&#xff09;和列属性&#xff08;TableColumn对象&#xff09;来完成数据填充与展示。 以下是一个…

如何做一线leader

文章目录 道领导力五个层次关键&#xff1a;信任 处事原则 术避坑指南事急则乱员工沟通向上管理人才招聘人才培养裁人员工关怀 道 领导力 五个层次 职位 当面交代事情&#xff0c;观察眼神、语气。反复确认有没有问题&#xff0c;如果有可以及时讨论策略&#xff0c;准备资源…

IDEA连接达梦数据库

在 IntelliJ IDEA 中连接达梦数据库&#xff0c;可以按照以下步骤进行操作&#xff1a; 1. 打开 IntelliJ IDEA&#xff0c;进入项目。 2. 在顶部菜单栏选择 "View" -> "Tool Windows" -> "Database"&#xff0c;打开数据库工具窗口。 3.…

自旋锁的优势和特点

ucos为何没自旋锁&#xff1f; UC/OS是一个适用于嵌入式系统的实时操作系统&#xff0c;它的设计目标是提供一种轻量级的任务调度和同步机制。相比于一般的操作系统&#xff0c;UC/OS在实现上更加精简&#xff0c;因此并没有像Linux那样的完整的锁机制。 UC/OS提供了一些基本…

17 | 从后端到前端:微服务后,前端如何设计?

微服务架构通常采用前后端分离的设计方式。作为企业级的中台&#xff0c;在完成单体应用拆分和微服务建设后&#xff0c;前端项目团队会同时面对多个中台微服务项目团队&#xff0c;这时候的前端人员就犹如维修电工一样了。 面对如此多的微服务暴露出来的 API 服务&#xff0c…

ChatGPT与Claude对比分析

一 简介 1、ChatGPT: 访问地址&#xff1a;https://chat.openai.com/ 由OpenAI研发,2022年11月发布。基于 transformer 结构的大规模语言模型,包含1750亿参数。训练数据集主要是网页文本,聚焦于流畅的对话交互。对话风格友好,回复通顺灵活,富有创造性。存在一定的安全性问题,可…

山西电力市场日前价格预测【2023-07-17】

日前价格预测 预测明日&#xff08;2023-07-17&#xff09;山西电力市场全天平均日前电价为335.50元/MWh。其中&#xff0c;最高日前电价为377.51元/MWh&#xff0c;预计出现在06: 00。最低日前电价为271.94元/MWh&#xff0c;预计出现在13: 30。 价差方向预测 1&#xff1a;实…

力扣 45. 跳跃游戏 II

题目来源&#xff1a;https://leetcode.cn/problems/jump-game-ii/description/ C题解1&#xff1a;因为每一步都要能走到下一个更远的地方&#xff0c;就比如 [2,3,1,1,4]&#xff0c;第一步虽然可以到索引2的位置&#xff0c;但是到索引1的位置下一步可以走更远。所以需要记录…

工厂方法模式详解

文章目录 前言一、工厂方法模式的定义二、举个例子三、工厂方法模式的缺点总结 前言 工厂方法模式是应用比较广泛的一种设计模式&#xff0c;它相对于简单工厂模式进行了一些优化&#xff0c;如果再增加一个具体产品不用修改代码&#xff0c;也不会违反开闭原则。 一、工厂方法…

Spring 6【什么是Spring 6、Spring框架介绍 、Spring IoC/DI 详解 】(一)-全面详解(学习总结---从入门到深化)

目录 一、Spring 6 二、Spring框架介绍 三、Spring IoC/DI 详解 一、Spring 6 1. 为什么要学习Spring 6 Spring 作为 Java程序员必会武功&#xff0c;无论是Spring的哪个版本&#xff0c;至少需要会一版本。再此基础上不会一套 组合拳SSM或SSH&#xff0c;出门都不好意思…

DAO(Data Access Object)

ProductDAO.java Search.java (Servlet) Compile Tomcat http://localhost:8080/book/chapter15/search.jsp Insert.java Compile http://localhost:8080/book/chapter15/insert.jsp

介绍用户协议报UDP

作者&#xff1a;爱塔居 专栏&#xff1a;计算机网络 文章目录 目录 文章目录 一、UDP概述 二、UDP的首部格式 一、UDP概述 用户数据报协议UDP只是在IP的数据报服务之上增加了很少一点的功能&#xff1a;复用和分用的功能以及差错检测的功能。 重点要记住UDP的主要特点&am…

JDK 下载 华为云镜像站 地址

通常去 Oracle 官网下载 JDK&#xff0c;速度很慢而且需要账号登入 Oracle 官网下载地址 https://www.oracle.com/cn/java/technologies/downloads/archive/ JDK 下载 华为云镜像站 地址 https://repo.huaweicloud.com/java/jdk/ 我们下期见&#xff0c;拜拜&#xff01;

C++程序生成文件

文章目录 VS2019 Debug 模式VS2019 Release 模式exe连接动态库和静态库的流程 VS2019 Debug 模式 在Visual Studio的Debug模式下运行程序时&#xff0c;会生成以下文件&#xff1a; 可执行文件&#xff08;.exe&#xff09;&#xff1a;生成的可执行文件&#xff08;.exe&#…

深度学习常用激活函数总结

激活函数的特点 非线性。引入非线性能提升模型的表达能力可微分。如果激活函数不可微&#xff0c;那就无法使用梯度下降方法更新参数非饱和性。饱和指在某些区间上激活函数的梯度接近于零&#xff0c;使参数无法更新取值范围有界。这样可以使网络更稳定&#xff0c;即使有很大…

dede tag不以关键词作为url以数字作为url方法修改

dede tag不以关键词作为url以数字作为url方法修改 打开/include/taglib/tag.lib.php 查找以下代码&#xff1a; $row[link] $cfg_cmsurl."/tags.php?/".urlencode($row[keyword])."/"; 替换为&#xff1a; $row[link] $cfg_cmsurl."/tags.php?/&q…