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渲染画面效果