OpenGL 入门(三)—— OpenGL 与 OpenCV 共同打造大眼滤镜
从本篇开始,会在上一篇搭建的滤镜框架的基础上,介绍具体的滤镜效果该如何制作。本篇会先介绍大眼滤镜,先来看一下效果,原图如下:
使用手机后置摄像头对眼部放大后的效果:
制作大眼滤镜所需的主要知识点:
- OpenCV 人脸定位
- SeetaFace 五官定位
- OpenGL 绘制大眼特效
下面让我们一步步来实现这个效果吧。
1、项目配置
要对眼部进行放大,那么一定需要识别到图像中眼睛的位置,通常我们会先识别到人脸,再去识别人眼,这样比从整张图片的范围内直接定位人眼要快。
人脸定位我们使用 OpenCV,虽然 OpenCV 也提供了人眼定位的模型文件,但是由于准确率一般,因此我们使用中科院开源的 SeetaFace 定位人眼。为人眼添加滤镜效果的任务自然落到 OpenGL 上。
OpenCV 在 Android Studio 上的配置,在OpenCV 入门(一) —— OpenCV 基础中已经讲过,去参考那篇文章,这里就不再赘述,下面只介绍 SeetaFace 的配置。
1.1 配置 SeetaFace
中科院开源的人脸识别引擎 SeetaFace,比 OpenCV 自带模型的识别率要好一些。GitHub 下载 SeetaFaceEngine:
可以看到包括三个核心模块:
- SeetaFace Alignment:面部特征点定位模块
- SeetaFace Detection:人脸检测模块
- SeetaFace Identification:人脸特征提取与比对模块
SeetaFace_config.docx 是在 Windows 的 VS 配置 SeetaFace 的文档,这里我们要将其配置到 AS 中。步骤如下:
-
将 FaceAlignment 目录下的 include 和 src 两个目录以及 CMakeLists.txt 拷贝到 /src/main/cpp/SeetaFace 目录下,其中 src 目录中有一个 test 目录我们并不需要,可以删除 test 目录,但是该目录下的 face_alignment_test.cpp 可以指导我们如何使用 SeetaFace
-
修改 SeetaFace 的 CMakeLists.txt,注释掉不需要的部分:
# 低于主 CMakeLists 要求的最低版本,干掉 #cmake_minimum_required(VERSION 2.8.4)# 不需要 #project(seeta_fa_lib)# 不需要构建示例,可以和最后的 if (BUILD_EXAMPLES) 一起干掉 # Build options #option(BUILD_EXAMPLES "Set to ON to build examples" ON)# Use C++11 #set(CMAKE_CXX_STANDARD 11) #set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") message(STATUS "C++11 support has been enabled by default.")set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -msse4.1")include_directories(include)set(src_files src/cfan.cppsrc/face_alignment.cppsrc/sift.cpp)# 修改编译为静态库 add_library(seeta_fa_lib STATIC ${src_files}) set(fa_required_libs seeta_fa_lib)#[[if (BUILD_EXAMPLES)message(STATUS "Build with examples.")find_package(OpenCV)if (NOT OpenCV_FOUND)message(WARNING "OpenCV not found. Test will not be built.")else()include_directories(${OpenCV_INCLUDE_DIRS} build)link_directories(build)list(APPEND fa_required_libs ${OpenCV_LIBS} seeta_facedet_lib)add_executable(fa_test src/test/face_alignment_test.cpp)target_link_libraries(fa_test ${fa_required_libs})endif() endif()]]
-
修改主 CMakeLists.txt:
# 指定 SeetaFace 的 CMakeLists 文件 add_subdirectory(${CMAKE_SOURCE_DIR}/SeetaFace/FaceAlignment)# 导入 SeetaFace 的头文件 include_directories(${CMAKE_SOURCE_DIR}/SeetaFace/FaceAlignment/include)target_link_libraries(opencvlogopencv_java4 # 链接 OpenCV 动态库android # 因为要用 ANativeWindow 渲染,因此要链接 libandroidseeta_fa_lib # 链接 SeetaFace 静态库 )
-
修改模块的 build.gradle,因为 SeetaFace 的 CMakeLists.txt 中声明了使用 C++11,gradle 要做出相应的配置:
android {defaultConfig {externalNativeBuild {cmake {cppFlags "-std=c++11"}}} }
-
将人脸识别模型 SeetaFaceEngine-master/FaceAlignment/model/seeta_fa_v1.1.bin 拷贝到 /src/main/assets/ 目录下
SeetaFace 支持识别人脸的 5 个关键点:两只眼睛各 1 个、鼻子 1 个、嘴边两侧各 1 个。
如果 Native 层编辑 cpp 代码时没有代码提示、格式排版,甚至有错也不报,新建文件时没有 C/C++ 的选项,并且 Build -> Refresh Linked C++ Projects 也是灰色的,可能是因为没有在 build.gradle 中添加 Native 编译配置:
android { externalNativeBuild {cmake {path file('src/main/cpp/CMakeLists.txt')version '3.22.1'} } }
2、人脸识别与人眼识别
有关 OpenCV 人脸识别的内容,我们在 OpenCV 系列文章的OpenCV 入门(六)—— Android 下的人脸识别中详细讲过。虽然这里又添加了 SeetaFace 进行人眼识别,但主要过程没有太大的变化。
从代码结构上说,识别工作要分为两层:
- Native 层:具体的识别工作都是交由 OpenCV 和 SeetaFace 在 Native 层完成的,我们需要将识别的结果(人脸的坐标和宽高数据以及五官坐标)封装成一个上层的 Face 对象并返回给上层
- 上层:定义封装人脸数据的 Face 类,同时还需要一个 FaceTracker 作为上层与 Native 层沟通的桥梁,一方面接收外界的指令通知 Native 进行初始化、人脸检测等工作,另一方面接收 Native 层的识别结果存入 Face 并提供给外界作为 OpenGL 添加各种滤镜的依据
从过程上说,主要分为以下几个步骤:
- 初始化:使用指定的识别模型在 Native 层初始化 OpenCV 和 SeetaFace
- 开始识别:开启 OpenCV 的跟踪识别
- 人脸识别:OpenCV 进行人脸识别,识别到的结果保存到一个集合中,SeetaFace 再对集合中的每个人脸进行特征点识别,将包含两眼位置信息的特征点数据保存起来
- 反射构造上层对象:将人脸信息和特征点信息通过反射的方式封装到上层的 Face 对象中并将其返回给上层
接下来结合代码详细说明上述实现步骤。
2.1 初始化
先将 OpenCV 和 SeetaFace 识别人脸的模型文件拷贝到项目的 /src/main/res/raw 目录下,在创建渲染器时将模型文件拷贝到手机中:
class GLRender(private val mGLSurfaceView: GLSurfaceView) : GLSurfaceView.Renderer,SurfaceTexture.OnFrameAvailableListener {private val mOpencvModelPath =Utils.copyAsset2Dir(mGLSurfaceView.context, "lbpcascade_frontalface.xml")private val mSeetaFaceModelPath =Utils.copyAsset2Dir(mGLSurfaceView.context, "seeta_fa_v1.1.bin")
}
工具类参考代码如下:
class Utils {companion object {fun copyAsset2Dir(context: Context, assetName: String): String {val cascadeDir = context.getDir("cascade", Context.MODE_PRIVATE)val cascadeFile = File(cascadeDir, assetName)if (!cascadeFile.exists()) {context.resources.assets.open(assetName).use { inputStream ->FileOutputStream(cascadeFile).use { outputStream ->val buffer = ByteArray(2048)var length: Intwhile (inputStream.read(buffer).also { length = it } > 0) {outputStream.write(buffer, 0, length)}}}}return cascadeFile.absolutePath}}
}
然后在渲染器监听到 GLSurfaceView 的尺寸发生变化时,创建上层的 FaceTracker:
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {...// 创建 FaceTracker 开始检测人脸mFaceTracker = FaceTracker(mCameraHelper, mOpencvModelPath, mSeetaFaceModelPath)mFaceTracker.startTracking()}
FaceTracker 要调用 Native 方法进行初始化和开启检测:
class FaceTracker(private val mCameraHelper: CameraHelper,opencvModelPath: String,seetaFaceModelPath: String
) {// Native 层 FaceTracker 对象的地址private var mFaceTracker = 0Linit {mFaceTracker = nativeInit(opencvModelPath, seetaFaceModelPath)}fun startTracking() {nativeStart(mFaceTracker)}private external fun nativeInit(opencvModelPath: String, seetaFaceModelPath: String): Longprivate external fun nativeStart(faceTracker: Long)
}
nativeInit() 会创建 Native 层的 FaceTracker 对象并将地址返回给上层,这样上层在执行后续的开启识别、结束识别、人脸检测等方法时,将该地址传入便可在 Native 层直接将地址转换成 Native 的 FaceTracker 对象进而执行相应的函数:
#include "FaceTracker.h"extern "C"
JNIEXPORT jlong JNICALL
Java_com_opengl_filters_FaceTracker_nativeInit(JNIEnv *env, jobject thiz,jstring opencv_model_path_,jstring seeta_face_model_path_) {const char *opencv_model_path = env->GetStringUTFChars(opencv_model_path_, nullptr);const char *seeta_face_model_path = env->GetStringUTFChars(seeta_face_model_path_, nullptr);auto faceTracker = new FaceTracker(opencv_model_path, seeta_face_model_path);env->ReleaseStringUTFChars(opencv_model_path_, opencv_model_path);env->ReleaseStringUTFChars(seeta_face_model_path_, seeta_face_model_path);// 将 Native 对象的地址返回给上层return reinterpret_cast<jlong>(faceTracker);
}extern "C"
JNIEXPORT void JNICALL
Java_com_opengl_filters_FaceTracker_nativeStart(JNIEnv *env, jobject thiz, jlong face_tracker) {if (face_tracker) {auto *faceTracker = reinterpret_cast<FaceTracker *>(face_tracker);faceTracker->startTracking();}
}
FaceTracker.h 内需要定义初始化 OpenCV 的跟踪器对象所需的 CascadeDetectorAdapter:
#ifndef OPENGL_FACETRACKER_H
#define OPENGL_FACETRACKER_H#include <opencv2/opencv.hpp>
#include <jni.h>
#include "SeetaFace/FaceAlignment/include/face_alignment.h"using namespace cv;class CascadeDetectorAdapter : public DetectionBasedTracker::IDetector {
public:CascadeDetectorAdapter(cv::Ptr<cv::CascadeClassifier> detector) :IDetector(),Detector(detector) {}// 检测人脸的函数,Mat 相当于 Android 的一张 Bitmap。一张图片有几个人脸就会调用本方法几次void detect(const cv::Mat &Image, std::vector<cv::Rect> &objects) {Detector->detectMultiScale(Image, objects, scaleFactor,minNeighbours, 0, minObjSize, maxObjSize);}virtual ~CascadeDetectorAdapter() = default;private:CascadeDetectorAdapter();cv::Ptr<cv::CascadeClassifier> Detector;
};class FaceTracker {public:FaceTracker(const char *opencv_model_path, const char *seeta_face_model_path);void startTracking();void stopTracking();void detect(const Mat& src, std::vector<Rect2f> &rectangles);private:Ptr<DetectionBasedTracker> tracker = nullptr;Ptr<seeta::FaceAlignment> faceAlignment = nullptr;
};#endif //OPENGL_FACETRACKER_H
FaceTracker 的构造函数要创建 OpenCV 和 SeetaFace 的检测器对象:
FaceTracker::FaceTracker(const char *opencv_model_path, const char *seeta_face_model_path) {// 1.创建 OpenCV 识别对象// 1.1 创建检测器Ptr<CascadeClassifier> detectorClassifier = makePtr<CascadeClassifier>(opencv_model_path);Ptr<CascadeDetectorAdapter> mainDetector = makePtr<CascadeDetectorAdapter>(detectorClassifier);// 1.2 创建跟踪器Ptr<CascadeClassifier> trackerClassifier = makePtr<CascadeClassifier>(opencv_model_path);Ptr<CascadeDetectorAdapter> trackingDetector = makePtr<CascadeDetectorAdapter>(trackerClassifier);// 1.3 创建识别对象DetectionBasedTracker::Parameters detectionParams;tracker = makePtr<DetectionBasedTracker>(mainDetector, trackingDetector, detectionParams);// 2.创建 SeetaFace 识别对象faceAlignment = makePtr<seeta::FaceAlignment>(seeta_face_model_path);
}
初始化完成,至于 startTracking() 仅需调用 tracker 的 run() 即可开始检测:
void FaceTracker::startTracking() {if (tracker) {tracker->run();}
}
2.2 人脸识别
进入识别流程,需要将图像数据传给 Native 层进行识别,图像数据来自 CameraHelper:
typealias CameraPreviewCallback = (data: ByteArray) -> Unitclass CameraHelper(private val mActivity: Activity,private var mCameraId: Int,private var mWidth: Int,private var mHeight: Int
) : Camera.PreviewCallback {private var mPreviewCallback: CameraPreviewCallback? = null// Camera.PreviewCallbackoverride fun onPreviewFrame(data: ByteArray?, camera: Camera?) {data?.let {// 将 mBuffer 继续放入回调队列中接收数据mCamera.addCallbackBuffer(mBuffer)// 将预览画面数据回调给外界mPreviewCallback?.invoke(it)}}fun setPreviewCallback(callback: CameraPreviewCallback) {mPreviewCallback = callback}
}
渲染器设置 PreviewCallback 获取图像数据转发给 FaceTracker 要求检测:
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {// 1.初始化 CameraHelpermCameraHelper = CameraHelper(mGLSurfaceView.context as Activity,Camera.CameraInfo.CAMERA_FACING_BACK,CameraHelper.WIDTH,CameraHelper.HEIGHT)mCameraHelper.setPreviewCallback {mFaceTracker.detect(it)}...}
由于人脸检测是耗时操作肯定放在子线程中,FaceTracker 采用 HandlerThread 来处理检测工作:
init {mFaceTracker = nativeInit(opencvModelPath, seetaFaceModelPath)mHandlerThread = HandlerThread("Face-Detect-Thread")mHandlerThread.start()mHandler = Handler(mHandlerThread.looper) { message ->mFace = nativeDetect(mFaceTracker,message.obj as ByteArray,mCameraHelper.getCameraId(),CameraHelper.WIDTH,CameraHelper.HEIGHT)true}}fun detect(data: ByteArray) {// 先移除之前的消息,保持检测最新的 datamHandler.removeMessages(MSG_DETECT)// 添加新的 data 到消息队列中mHandler.obtainMessage(MSG_DETECT, data).sendToTarget()}
这样就会调用 nativeDetect() 进入 Native 层:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_opengl_filters_FaceTracker_nativeDetect(JNIEnv *env, jobject thiz, jlong face_tracker,jbyteArray data_, jint camera_id, jint width,jint height) {if (!face_tracker) {return nullptr;}jbyte *data = env->GetByteArrayElements(data_, nullptr);auto *faceTracker = reinterpret_cast<FaceTracker *>(face_tracker);// 1.人脸检测...// 2.生成上层的 Face 对象返回给上层...
}
主要任务有两项:
- 利用 OpenCV 和 SeetaFace 检测人脸信息并保存
- 将人脸信息封装到上层的 Face 对象中并返回
下面分别来看这两项内容实现。
检测过程
大致过程如下:
- 根据原图像生成用于 OpenCV 识别的图片对象 Mat
- 将 Mat 由 YUV NV21 格式转换为 RGBA 格式,并且旋转图像将其调正
- 取原图的灰度图和直方图均衡化,准备正式开始识别
- 调用 OpenCV 的 API 进行检测,结果保存在集合中
- 从 OpenCV 的结果中取出数据进行 SeetaFace 识别,将检测的关键点数据保存起来
我们先来看 nativeDetect() 的实现,包含前三步:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_opengl_filters_FaceTracker_nativeDetect(JNIEnv *env, jobject thiz, jlong face_tracker,jbyteArray data_, jint camera_id, jint width,jint height) {if (!face_tracker) {return nullptr;}jbyte *data = env->GetByteArrayElements(data_, nullptr);auto *faceTracker = reinterpret_cast<FaceTracker *>(face_tracker);// 1.人脸检测// 1.1 创建 Mat 对象并做预处理Mat src(height * 3 / 2, width, CV_8UC1, data);// 将 src 的格式由 YUV NV21 转换为 RGBAcvtColor(src, src, COLOR_YUV2RGBA_NV21);// 对原始图像进行旋转调正if (camera_id == 1) {// 前置摄像头需要逆时针旋转 90°rotate(src, src, ROTATE_90_COUNTERCLOCKWISE);// 前置还需要取一个水平方向的镜像,如果传 0 就是竖直方向flip(src, src, 1);} else {// 后置摄像头需要顺时针旋转 90°rotate(src, src, ROTATE_90_CLOCKWISE);}// 1.2 对 Mat 进行人脸检测// 将图片转换为灰度图,可以减少杂色增加识别几率Mat gray;cvtColor(src, gray, COLOR_RGBA2GRAY);// 增强对比度,目的是增强轮廓(因为识别是对轮廓进行识别)equalizeHist(gray, gray);// 检测人脸,结果的矩形保存到 rectangles 中std::vector<Rect2f> rectangles;faceTracker->detect(gray, rectangles);// data 使命结束,及时释放env->ReleaseByteArrayElements(data_, data, 0);...
}
具体的检测工作由 FaceTracker 的 detect() 完成,检测结果保存在 rectangles 中:
void FaceTracker::detect(const Mat &src, std::vector<Rect2f> &rectangles) {// 1.先将 OpenCV 检测到矩形保存到 faces 中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);// 2.使用 SeetaFace 检测人脸以获取五官位置,需要准备三个参数// 2.1 图像数据 ImageData,切记如果使用空参构造函数,一定要为 num_channels 赋值seeta::ImageData imageData = seeta::ImageData(src.cols, src.rows);imageData.data = src.data;// 2.2 人脸矩形信息 FaceInfoseeta::FaceInfo faceInfo;seeta::Rect bbox;bbox.x = face.x;bbox.y = face.y;bbox.width = face.width;bbox.height = face.height;faceInfo.bbox = bbox;// 2.3 人脸 5 个关键点的集合,是一个入参出参seeta::FacialLandmark landmarks[5];// 2.4 执行 SeetaFace 人脸定位faceAlignment->PointDetectLandmarks(imageData, faceInfo, landmarks);// 3.将关键点保存到 rectangles 中for (auto & landmark : landmarks) {// 我们只需要关键点的坐标,而无需宽高数据rectangles.emplace_back(landmark.x, landmark.y, 0, 0);}}
}
过程梳理:
- 先用 OpenCV 检测人脸,调用 tracker->process(src) 对原图进行检测,再通过 tracker->getObjects(faces) 将检测到的人脸矩形保存到 faces 集合中
- 调用 SeetaFace 的 faceAlignment->PointDetectLandmarks() 检测人脸的 5 个特征点,该函数需要三个参数:
- ImageData:保存图像信息的对象,包括图像宽高以及像素数据,如果通过构造函数创建该对象,可以不用显式指定 num_channels,构造函数会为其赋默认值为 1
- FaceInfo:人脸信息,主要是指定它的 bbox 字段,包含人脸矩形的左上角坐标以及矩形宽高
- FacialLandmark:人脸特征点(关键点),SeetaFace 会将左眼、右眼、鼻子、左嘴角、右嘴角这 5 个特征点的坐标检测出来,这里声明了 FacialLandmark 类型的数组就是用来接收这 5 个点的
- 将人脸信息(起始点和宽高)以及 5 个关键点信息(主要是起始点,宽高由于不需要都被设置为 0)共 6 个矩形保存到参数的 rectangles 集合中
人脸宽高数据在本节的大眼滤镜中用不到,但是在下一篇添加贴纸效果时有用,由于是很小的点,也不适宜在下一篇中单独拿出来说,因此就在这里直接保存这个信息了。
创建上层对象
这里的上层对象就是指 Face:
class Face(// 关键点的左上角坐标集合val landmarks: FloatArray,// 人脸宽高val faceWidth: Int,val faceHeight: Int,// 被检测的图像宽高val imgWidth: Int,val imgHeight: Int
)
我们要在 Native 通过反射的方式创建该对象,主要就是先准备好构造方法内的参数数据。其中,landmarks 关键点坐标可以通过上一步中计算出的 rectangles 集合获取;人脸宽高保存在 rectangles 中的第一个矩形内;被检测的图像宽高可以通过灰度图获取。
参考代码如下:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_opengl_filters_FaceTracker_nativeDetect(JNIEnv *env, jobject thiz, jlong face_tracker,jbyteArray data_, jint camera_id, jint width,jint height) {// 2.生成上层的 Face 对象// 2.1 先获取被检测的图片宽高数据备用int imgWidth = gray.cols;int imgHeight = gray.rows;src.release();gray.release();int rectSize = rectangles.size();if (rectSize) {// 2.2 创建 Face 构造方法中的关键点集合 FloatArrayint floatArraySize = rectSize * 2;jfloatArray floatArray = env->NewFloatArray(floatArraySize);for (int i = 0; i < rectSize; ++i) {float f[2] = {rectangles[i].x, rectangles[i].y};env->SetFloatArrayRegion(floatArray, i * 2, 2, f);}// 2.3 获取人脸矩形宽高Rect faceRect = rectangles[0];int faceWidth = faceRect.width;int faceHeight = faceRect.height;// 2.4 获取 Face 类与构造函数的 ID,创建 Face 对象并返回给上层jclass clazz = env->FindClass("com/opengl/filters/Face");jmethodID constructorID = env->GetMethodID(clazz, "<init>", "([FIIII)V");return env->NewObject(clazz, constructorID, floatArray, faceWidth, faceHeight, imgWidth,imgHeight);}return nullptr;
}
这样在 Native 层通过反射的方式创建了上层的 Face 对象并返回给上层的 FaceTracker,后者可以对外提供 Face 对象以供后续 OpenGL 绘制滤镜所用:
fun getFace() = mFace
3、添加大眼滤镜
前面的工作保证我们能获取到人眼坐标,接下来就是使用 OpenGL 在绘制时添加滤镜效果了。
3.1 着色器
顶点着色器使用 base_vertex 即可,需要新建一个片元着色器 big_eyes_fragment.glsl:
// 声明 float 是中等精度的
precision mediump float;// 采样点坐标
varying vec2 aCoord;// 采样器
uniform sampler2D vTexture;
// 左眼坐标
uniform vec2 left_eye;
// 右眼坐标
uniform vec2 right_eye;// 公式,用于计算将眼睛放大后的顶点到放大中心的距离
// r 是未放大前顶点坐标到眼睛中心的距离
// rmax 是放大后顶点到眼睛中心的最大距离
float fs(float r, float rmax) {// 放大系数float a = 0.4;// pow 是内置函数,用于计算幂次,虽然是计算二次方,但是也要写为 2.0return (1.0 - pow(r / rmax - 1.0, 2.0) * a) * r;
}// 计算放大后的点的坐标
// coord 原来的点,eye 眼睛坐标,rmax 放大后的最大距离
vec2 calNewCoord(vec2 oldCoord, vec2 eye, float rmax) {vec2 newCoord = oldCoord;// 原来的点到眼睛中心的距离float dis = distance(oldCoord, eye);// 未到最大距离,可以进行放大if (dis > 0.0f && dis < rmax) {// 求出放大后的点到眼睛的距离float fsr = fs(dis, rmax);// 按比例计算新点坐标:(新点 - 眼睛) / (旧点 - 眼睛) = 放大后距离 / 放大前距离// 即 :(newCoord - eye) / (coord - eye) = fsr / disnewCoord = eye + (oldCoord - eye) * (fsr / dis);}return newCoord;
}void main() {// 两眼间距离除以 2 就是放大后的最大距离float rmax = distance(left_eye, right_eye) / 2.0;// 获取左右眼放大后的坐标,左眼和右眼都要做一次,在哪个眼睛的放大区间就放大哪一个vec2 newCoord = calNewCoord(aCoord, left_eye, rmax);// 注意第一个参数要传 newCoord,如果传了 aCoord 那么就只判断了右眼newCoord = calNewCoord(newCoord, right_eye, rmax);gl_FragColor = texture2D(vTexture, newCoord);
}
fs 函数中的公式来自于 1979 年的一篇论文 “Interactive Image Warping” 的第 41 页:
用该公式可以计算出放大后的点到眼睛的距离,当然这个距离不能大于 rmax,也就是两眼间距的一半。计算出距离后,可以通过 calNewCoord() 计算出放大后点的坐标,然后就可以让 OpenGL 进行绘制了。
3.2 实现滤镜
大眼滤镜,包括我们后续要实现的美颜滤镜和贴纸,它们都是绘制在 FBO 上的,而不是直接渲染到屏幕上,因此可以抽出一个基类将 FBO 的共同操作放入其中:
/*** 使用 FBO 绘制的 Filter 基类*/
open class BaseFrameFilter(context: Context, mVertexSourceId: Int, mFragmentSourceId: Int) :BaseFilter(context, mVertexSourceId, mFragmentSourceId) {protected var mFrameBuffers: IntArray? = nullprotected var mFrameBufferTextures: IntArray? = nulloverride fun onReady(width: Int, height: Int) {super.onReady(width, height)// 1.先清空 mFrameBuffers 的残留数据mFrameBuffers?.let {releaseFrameBuffer()}// 2.创建 FBOmFrameBuffers = IntArray(1)// FBO 个数、保存 FBO ID 的数组、偏移量,用数组的第几个来保存glGenFramebuffers(mFrameBuffers?.size ?: 1, mFrameBuffers, 0)// 3.创建 FBO 的纹理mFrameBufferTextures = IntArray(1)TextureHelper.generateTextures(mFrameBufferTextures!!)// 4.绑定 FBO 与纹理// 将 FBO 纹理 ID 绑定到 GL_TEXTURE_2D 目标上,目标的类型是 2D 纹理glBindTexture(GL_TEXTURE_2D, mFrameBufferTextures!![0])// 更新纹理图像数据glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null)// 绑定 FBOglBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])// 将纹理附加到 FBOglFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,mFrameBufferTextures!![0],0)// 5.解绑glBindFramebuffer(GL_FRAMEBUFFER, 0)glBindTexture(GL_TEXTURE_2D, 0)}override fun release() {super.release()releaseFrameBuffer()}protected fun releaseFrameBuffer() {mFrameBufferTextures?.let {glDeleteTextures(it.size, mFrameBufferTextures, 0)mFrameBufferTextures = null}mFrameBuffers?.let {glDeleteFramebuffers(it.size, mFrameBuffers, 0)mFrameBuffers = null}}
}
大眼滤镜 BigEyesFilter 直接继承该基类:
class BigEyesFilter(context: Context) :BaseFrameFilter(context, R.raw.base_vertex, R.raw.big_eyes_fragment) {private val leftEye: Intprivate val rightEye: Intprivate var leftBuffer: FloatBufferprivate var rightBuffer: FloatBufferprivate var face: Face? = nullinit {leftEye = glGetUniformLocation(mProgramId, "left_eye")rightEye = glGetUniformLocation(mProgramId, "right_eye")leftBuffer = ByteBuffer.allocateDirect(2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()rightBuffer =ByteBuffer.allocateDirect(2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()}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)}fun setFace(face: Face?) {this.face = face}override fun onDrawFrame(textureId: Int): Int {// 1.判断不符合绘制条件的情况,直接返回上一层的纹理 IDval landmarks = face?.landmarksval imgWidth = face?.imgWidthval imgHeight = face?.imgHeightif (imgWidth == null || imgHeight == null || landmarks == null) {return textureId}// 2.绘制前设置:设置视窗、绑定 FBO、使用着色器程序glViewport(0, 0, mWidth, mHeight)glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])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.将眼睛坐标传给片元着色器var x = landmarks[2] / imgWidthvar y = landmarks[3] / imgHeightleftBuffer.clear()leftBuffer.put(x)leftBuffer.put(y)leftBuffer.position(0)glUniform2fv(leftEye, 1, leftBuffer)// 右眼坐标x = landmarks[4] / imgWidthy = landmarks[5] / imgHeightrightBuffer.clear()rightBuffer.put(x)rightBuffer.put(y)rightBuffer.position(0)glUniform2fv(rightEye, 1, rightBuffer)// 5.后续常规操作,OpenGL 绘制// 激活图层glActiveTexture(GL_TEXTURE0)// 绑定glBindTexture(GL_TEXTURE_2D, textureId)// 传递参数glUniform1i(vTexture, 0)// 通知 OpenGL 绘制glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)// 解绑 FBOglBindFramebuffer(GL_FRAMEBUFFER, 0)glBindTexture(GL_TEXTURE_2D, 0)return mFrameBufferTextures!![0]}
}
核心思路就是从 Face 中提取出左眼和右眼的坐标,传递给片元着色器 big_eyes_fragment 中定义的两个变量 left_eye 和 right_eye。
3.3 装配大眼滤镜
在渲染器中创建 BigEyesFilter 并将其添加到绘制的责任链中:
private lateinit var mBigEyesFilter: BigEyesFilteroverride fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {...// 3.创建滤镜对象mScreenFilter = ScreenFilter(mGLSurfaceView.context)mCameraFilter = CameraFilter(mGLSurfaceView.context)mBigEyesFilter = BigEyesFilter(mGLSurfaceView.context)}override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {...// 设置 OpenGL 的绘制视窗mCameraFilter.onReady(width, height)mBigEyesFilter.onReady(width, height)mScreenFilter.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)mScreenFilter.onDrawFrame(textureId)}
相关文章:

OpenGL 入门(三)—— OpenGL 与 OpenCV 共同打造大眼滤镜
从本篇开始,会在上一篇搭建的滤镜框架的基础上,介绍具体的滤镜效果该如何制作。本篇会先介绍大眼滤镜,先来看一下效果,原图如下: 使用手机后置摄像头对眼部放大后的效果: 制作大眼滤镜所需的主要知识点&…...

Linux服务器安全基础 - 查看入侵痕迹
1. 常见系统日志 /var/log/cron 记录了系统定时任务相关的日志 /var/log/dmesg 记录了系统在开机时内核自检的信息,也可以使用dmesg命令直接查看内核自检信息 /var/log/secure:记录登录系统存取数据的文件;例如:pop3,ssh,telnet,ftp等都会记录在此. /var/log/btmp:记…...
Java反射机制的实战应用:探索其魅力与局限
引言 Java作为一种面向对象的编程语言,其灵活性和强大的功能使其成为众多开发者的首选。而Java反射机制作为Java语言中的一项重要特性,为程序员提供了一种在运行时检查和操作类、方法、属性等信息的能力。本文旨在深入探讨Java反射机制的实战应用&#…...
vue3项目 文件组成
从头捋顺一遍vue3项目文件目录 前置知识JS模块化什么是依赖?安装依赖webpack能做什么?vue基本使用 不借助vue-cli,从0开始搭建vue项目。index.html、main.js、App.vue引入npm引入webpack引入babel引入vue-loaderwebpack配置webpack配置 前置知…...
C语言关键字 typedef 的功能是什么?
一、问题 语⾔有 32 个关键字,其中 int 的功能是声明整型变量,struct 的功能是声明结构体变量,那么 typedef 的功能是什么呢? 二、解答 1. typedef 的功能 在 C 语⾔中除了可以使⽤标准类型名(如 int、 char、float …...
【YoloDeployCsharp】基于.NET Framework的YOLO深度学习模型部署测试平台-源码下载与项目配置
基于.NET Framework 4.8 开发的深度学习模型部署测试平台,提供了YOLO框架的主流系列模型,包括YOLOv8~v9,以及其系列下的Det、Seg、Pose、Obb、Cls等应用场景,同时支持图像与视频检测。模型部署引擎使用的是OpenVINO™、TensorRT、ONNX runtime以及OpenCV DNN,支持CPU、IGP…...
如何在 Ubuntu 12.04 VPS 上使用 MongoDB 创建分片集群
简介 MongoDB 是一个 NoSQL 文档数据库系统,可以在水平方向上很好地扩展,并通过键值系统实现数据存储。作为 Web 应用程序和网站的热门选择,MongoDB 易于实现并可以通过编程方式访问。 MongoDB 通过一种称为“分片”的技术实现扩展。分片是将…...

阿里云VOD视频点播流程(1)
一、开通阿里云VOD 视频点播(ApsaraVideo VoD,简称VOD)是集视频采集、编辑、上传、媒体资源管理、自动化转码处理、视频审核分析、分发加速于一体的一站式音视频点播解决方案。登录阿里云,在产品找到视频点播VOD ,点击…...

Python爬虫获取豆瓣电影Top100
大家好,我是秋意零。 今天分析一篇,Python爬虫获取豆瓣电影Top100。 在此之前,我没有学习过爬虫,只有一丢丢的Python基础。下面效果的实现源码几乎没经过我,而是AI百老师。我主要负责了对应的调试以及根据我想要的功…...
动态规划专训8——背包问题
动态规划题目中,常出现背包的相关问题,这里单独挑出来训练 A.01背包 1.01背包模板题 【模板】01背包_牛客题霸_牛客网 (nowcoder.com) 你有一个背包,最多能容纳的体积是V。 现在有n个物品,第i个物品的体积为𝑣&am…...

软件杯 深度学习花卉识别 - python 机器视觉 opencv
文章目录 0 前言1 项目背景2 花卉识别的基本原理3 算法实现3.1 预处理3.2 特征提取和选择3.3 分类器设计和决策3.4 卷积神经网络基本原理 4 算法实现4.1 花卉图像数据4.2 模块组成 5 项目执行结果6 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 &a…...

学习笔记:【QC】Android Q - IMS 模块
一、IMS init 流程图 高清的流程图参考:【高清图,保存后可以放大看】 二、IMS turnon 流程图 高清的流程图参考:【高清图,保存后可以放大看】 三、分析说明 1、nv702870 不创建ims apn pdp 2、nv702811 nv702811的时候才创建…...

NodeMCU ESP8266 操作 SSD1306 OLED显示屏详解(图文并茂)
文章目录 1 模块介绍2 接线介绍3 安装SSD1306驱动库4 源码分析4.1 硬件兼容性4.2 可能存在的问题总结1 模块介绍 我们将在本教程中使用的OLED显示屏是SSD1306型号:单色0.96英寸显示屏,像素为12864,如下图所示。 OLED显示屏不需要背光,这在黑暗环境中会产生非常好的对比度。…...

不抽象:Increase API 设计原则
原文:Increase - 2024.04.26 (注:Increase 是一家提供金融技术服务的公司。) API 资源是 API 的实体或对象。决定如何为这些实体命名和建模可以说是设计 API 最难也是最重要的部分。您所公开的资源组织了用户对您的产品如何工作…...
mybatis调用数据库存储过程
mybatis调用数据库存储过程及常见属性详解 调用mapper String visitCode mapper.getVisitCode(objectMap);Dao层,xml文件代码编写 <select id"getVisitCode" parameterType"map" resultType"string" statementType"CALLAB…...
【git】发生冲突后回滚提交
gerrit 冲突, 无法合并到主干 那么先回滚 参考这里的 reset 操作: 回滚 到上一个提交 $ git reset --soft HEAD~1 # 數字表示移動到 HEAD後面第幾個刚提交的会撤回, stash 刚刚提交的 然后去pull 最新的 修改冲突: 最后再…...

ISO14229 -1 UDS诊断服务记录-001:0x34\0x36\0x37\0x31\0x19\0x14服务报文格式介绍
目录 1、34服务-请求下载 1.1、诊断请求格式 1.2、正响应格式 1.3、负响应格式 1.4、工程应用分析 2、36服务-传输数据 2.1、请求报文格式 2.2、正响应格式 2.3、负响应NRC 3、37服务-退出传输 3.1、报文格式 3.2、正响应格式 3.3、负响应NRC 4、31服务-例程控制 …...

使用 MediaMTX 和 FFmpeg 推拉 RTSP 流媒体
实时流传输协议 RTSP(Real-Time Streaming Protocol)是 TCP/IP 协议体系中的一个应用层协议,由哥伦比亚大学、网景和 RealNetworks 公司提交的 IETF RFC 标准。该协议定义了一对多应用程序如何有效地通过 IP 网络传送多媒体数据。RTSP 在体系…...

Mac 电脑安装 Raptor 流程图软件的方法
0. 安装逻辑 (1)运行 raptor,本质上需要 mac 能够运行 windows 程序,因此需要安装 .NET Runtime 7.0,这是微软程序运行必须的文件。 (2)运行 raptor 还需要安装依赖文件 mono-libgdiplus。 &am…...

W801学习笔记二十:宋词学习应用
前三章完成了唐诗的应用,本章将实现宋词的学习应用。 宋词与唐诗的区别不大,马上开始。 1、我们需要参考前面唐诗的方式,把宋词文本下载下来,并进行格式整理。 W801学习笔记十七:古诗学习应用——上 2、在菜单中添加…...

深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...

汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...

手机平板能效生态设计指令EU 2023/1670标准解读
手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…...