当前位置: 首页 > news >正文

OpenGL 入门(三)—— OpenGL 与 OpenCV 共同打造大眼滤镜

从本篇开始,会在上一篇搭建的滤镜框架的基础上,介绍具体的滤镜效果该如何制作。本篇会先介绍大眼滤镜,先来看一下效果,原图如下:

2024-5-7.大眼滤镜效果演示(使用前)

使用手机后置摄像头对眼部放大后的效果:

2024-5-7.大眼滤镜效果演示(使用后)

制作大眼滤镜所需的主要知识点:

  • OpenCV 人脸定位
  • SeetaFace 五官定位
  • OpenGL 绘制大眼特效

下面让我们一步步来实现这个效果吧。

1、项目配置

要对眼部进行放大,那么一定需要识别到图像中眼睛的位置,通常我们会先识别到人脸,再去识别人眼,这样比从整张图片的范围内直接定位人眼要快。

人脸定位我们使用 OpenCV,虽然 OpenCV 也提供了人眼定位的模型文件,但是由于准确率一般,因此我们使用中科院开源的 SeetaFace 定位人眼。为人眼添加滤镜效果的任务自然落到 OpenGL 上。

OpenCV 在 Android Studio 上的配置,在OpenCV 入门(一) —— OpenCV 基础中已经讲过,去参考那篇文章,这里就不再赘述,下面只介绍 SeetaFace 的配置。

1.1 配置 SeetaFace

中科院开源的人脸识别引擎 SeetaFace,比 OpenCV 自带模型的识别率要好一些。GitHub 下载 SeetaFaceEngine:

2024-4-10.SeetaFace

可以看到包括三个核心模块:

  1. SeetaFace Alignment:面部特征点定位模块
  2. SeetaFace Detection:人脸检测模块
  3. SeetaFace Identification:人脸特征提取与比对模块

SeetaFace_config.docx 是在 Windows 的 VS 配置 SeetaFace 的文档,这里我们要将其配置到 AS 中。步骤如下:

  1. 将 FaceAlignment 目录下的 include 和 src 两个目录以及 CMakeLists.txt 拷贝到 /src/main/cpp/SeetaFace 目录下,其中 src 目录中有一个 test 目录我们并不需要,可以删除 test 目录,但是该目录下的 face_alignment_test.cpp 可以指导我们如何使用 SeetaFace

  2. 修改 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()]]
    
  3. 修改主 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 静态库
    )
    
  4. 修改模块的 build.gradle,因为 SeetaFace 的 CMakeLists.txt 中声明了使用 C++11,gradle 要做出相应的配置:

    android {defaultConfig {externalNativeBuild {cmake {cppFlags "-std=c++11"}}}
    }
    
  5. 将人脸识别模型 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 进行人眼识别,但主要过程没有太大的变化。

从代码结构上说,识别工作要分为两层:

  1. Native 层:具体的识别工作都是交由 OpenCV 和 SeetaFace 在 Native 层完成的,我们需要将识别的结果(人脸的坐标和宽高数据以及五官坐标)封装成一个上层的 Face 对象并返回给上层
  2. 上层:定义封装人脸数据的 Face 类,同时还需要一个 FaceTracker 作为上层与 Native 层沟通的桥梁,一方面接收外界的指令通知 Native 进行初始化、人脸检测等工作,另一方面接收 Native 层的识别结果存入 Face 并提供给外界作为 OpenGL 添加各种滤镜的依据

从过程上说,主要分为以下几个步骤:

  1. 初始化:使用指定的识别模型在 Native 层初始化 OpenCV 和 SeetaFace
  2. 开始识别:开启 OpenCV 的跟踪识别
  3. 人脸识别:OpenCV 进行人脸识别,识别到的结果保存到一个集合中,SeetaFace 再对集合中的每个人脸进行特征点识别,将包含两眼位置信息的特征点数据保存起来
  4. 反射构造上层对象:将人脸信息和特征点信息通过反射的方式封装到上层的 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 对象返回给上层...
}

主要任务有两项:

  1. 利用 OpenCV 和 SeetaFace 检测人脸信息并保存
  2. 将人脸信息封装到上层的 Face 对象中并返回

下面分别来看这两项内容实现。

检测过程

大致过程如下:

  1. 根据原图像生成用于 OpenCV 识别的图片对象 Mat
  2. 将 Mat 由 YUV NV21 格式转换为 RGBA 格式,并且旋转图像将其调正
  3. 取原图的灰度图和直方图均衡化,准备正式开始识别
  4. 调用 OpenCV 的 API 进行检测,结果保存在集合中
  5. 从 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);}}
}

过程梳理:

  1. 先用 OpenCV 检测人脸,调用 tracker->process(src) 对原图进行检测,再通过 tracker->getObjects(faces) 将检测到的人脸矩形保存到 faces 集合中
  2. 调用 SeetaFace 的 faceAlignment->PointDetectLandmarks() 检测人脸的 5 个特征点,该函数需要三个参数:
    • ImageData:保存图像信息的对象,包括图像宽高以及像素数据,如果通过构造函数创建该对象,可以不用显式指定 num_channels,构造函数会为其赋默认值为 1
    • FaceInfo:人脸信息,主要是指定它的 bbox 字段,包含人脸矩形的左上角坐标以及矩形宽高
    • FacialLandmark:人脸特征点(关键点),SeetaFace 会将左眼、右眼、鼻子、左嘴角、右嘴角这 5 个特征点的坐标检测出来,这里声明了 FacialLandmark 类型的数组就是用来接收这 5 个点的
  3. 将人脸信息(起始点和宽高)以及 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 页:

2024-4-17.大眼公式

用该公式可以计算出放大后的点到眼睛的距离,当然这个距离不能大于 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 共同打造大眼滤镜

从本篇开始&#xff0c;会在上一篇搭建的滤镜框架的基础上&#xff0c;介绍具体的滤镜效果该如何制作。本篇会先介绍大眼滤镜&#xff0c;先来看一下效果&#xff0c;原图如下&#xff1a; 使用手机后置摄像头对眼部放大后的效果&#xff1a; 制作大眼滤镜所需的主要知识点&…...

Linux服务器安全基础 - 查看入侵痕迹

1. 常见系统日志 /var/log/cron 记录了系统定时任务相关的日志 /var/log/dmesg 记录了系统在开机时内核自检的信息&#xff0c;也可以使用dmesg命令直接查看内核自检信息 /var/log/secure:记录登录系统存取数据的文件;例如:pop3,ssh,telnet,ftp等都会记录在此. /var/log/btmp:记…...

Java反射机制的实战应用:探索其魅力与局限

引言 Java作为一种面向对象的编程语言&#xff0c;其灵活性和强大的功能使其成为众多开发者的首选。而Java反射机制作为Java语言中的一项重要特性&#xff0c;为程序员提供了一种在运行时检查和操作类、方法、属性等信息的能力。本文旨在深入探讨Java反射机制的实战应用&#…...

vue3项目 文件组成

从头捋顺一遍vue3项目文件目录 前置知识JS模块化什么是依赖&#xff1f;安装依赖webpack能做什么&#xff1f;vue基本使用 不借助vue-cli&#xff0c;从0开始搭建vue项目。index.html、main.js、App.vue引入npm引入webpack引入babel引入vue-loaderwebpack配置webpack配置 前置知…...

C语言关键字 typedef 的功能是什么?

一、问题 语⾔有 32 个关键字&#xff0c;其中 int 的功能是声明整型变量&#xff0c;struct 的功能是声明结构体变量&#xff0c;那么 typedef 的功能是什么呢&#xff1f; 二、解答 1. typedef 的功能 在 C 语⾔中除了可以使⽤标准类型名&#xff08;如 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 文档数据库系统&#xff0c;可以在水平方向上很好地扩展&#xff0c;并通过键值系统实现数据存储。作为 Web 应用程序和网站的热门选择&#xff0c;MongoDB 易于实现并可以通过编程方式访问。 MongoDB 通过一种称为“分片”的技术实现扩展。分片是将…...

阿里云VOD视频点播流程(1)

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

Python爬虫获取豆瓣电影Top100

大家好&#xff0c;我是秋意零。 今天分析一篇&#xff0c;Python爬虫获取豆瓣电影Top100。 在此之前&#xff0c;我没有学习过爬虫&#xff0c;只有一丢丢的Python基础。下面效果的实现源码几乎没经过我&#xff0c;而是AI百老师。我主要负责了对应的调试以及根据我想要的功…...

动态规划专训8——背包问题

动态规划题目中&#xff0c;常出现背包的相关问题&#xff0c;这里单独挑出来训练 A.01背包 1.01背包模板题 【模板】01背包_牛客题霸_牛客网 (nowcoder.com) 你有一个背包&#xff0c;最多能容纳的体积是V。 现在有n个物品&#xff0c;第i个物品的体积为&#x1d463;&am…...

软件杯 深度学习花卉识别 - python 机器视觉 opencv

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

学习笔记:【QC】Android Q - IMS 模块

一、IMS init 流程图 高清的流程图参考&#xff1a;【高清图&#xff0c;保存后可以放大看】 二、IMS turnon 流程图 高清的流程图参考&#xff1a;【高清图&#xff0c;保存后可以放大看】 三、分析说明 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 设计原则

原文&#xff1a;Increase - 2024.04.26 &#xff08;注&#xff1a;Increase 是一家提供金融技术服务的公司。&#xff09; API 资源是 API 的实体或对象。决定如何为这些实体命名和建模可以说是设计 API 最难也是最重要的部分。您所公开的资源组织了用户对您的产品如何工作…...

mybatis调用数据库存储过程

mybatis调用数据库存储过程及常见属性详解 调用mapper String visitCode mapper.getVisitCode(objectMap);Dao层&#xff0c;xml文件代码编写 <select id"getVisitCode" parameterType"map" resultType"string" statementType"CALLAB…...

【git】发生冲突后回滚提交

gerrit 冲突&#xff0c; 无法合并到主干 那么先回滚 参考这里的 reset 操作&#xff1a; 回滚 到上一个提交 $ git reset --soft HEAD~1 # 數字表示移動到 HEAD後面第幾個刚提交的会撤回&#xff0c; stash 刚刚提交的 然后去pull 最新的 修改冲突&#xff1a; 最后再…...

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&#xff08;Real-Time Streaming Protocol&#xff09;是 TCP/IP 协议体系中的一个应用层协议&#xff0c;由哥伦比亚大学、网景和 RealNetworks 公司提交的 IETF RFC 标准。该协议定义了一对多应用程序如何有效地通过 IP 网络传送多媒体数据。RTSP 在体系…...

Mac 电脑安装 Raptor 流程图软件的方法

0. 安装逻辑 &#xff08;1&#xff09;运行 raptor&#xff0c;本质上需要 mac 能够运行 windows 程序&#xff0c;因此需要安装 .NET Runtime 7.0&#xff0c;这是微软程序运行必须的文件。 &#xff08;2&#xff09;运行 raptor 还需要安装依赖文件 mono-libgdiplus。 &am…...

W801学习笔记二十:宋词学习应用

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

EPAI手绘建模APP转换模型和坐标系

(11) 模型转换 图 273 转换工具栏 ① 实体转成曲面&#xff0c;先选择需要转成曲面的实体模型&#xff0c;再点击该按钮。将选择的实体模型转成多个曲面。 ② 曲线转成NURBS样条曲线&#xff0c;先选择需要转成NURBS样条曲线的边模型&#xff0c;修改转换参数&#xff0c;将选…...

STM32快速入门(串口传输之USART)

STM32快速入门&#xff08;串口传输之USART&#xff09; 前言 USART串口传输能实现信息在设备之间的点对点传输&#xff0c;支持单工、半双工、全全双工&#xff0c;一般是有三个引脚&#xff1a;TX、RX、SW_RX&#xff08;共地&#xff09;。不需要一根线来同步时钟。最大优…...

什么是网络安全和网络隐私?

什么是网络安全?这个是我最感兴趣的话题,网络安全说白了就是在网络上的安全,跟现实中一样,现实中为了家里的安全,我们会给家门上锁,会装监控,农村的话可能还会养一条狗,只有我们让别人进我们家,别人才能进来,对于计算机来说也是一样的,我们会设置账户的密码,会设置防火墙,会安…...

树莓派变小路由器放出热点wifi

环境 树莓派4Bubuntu20 作用 树莓派放出wifi后&#xff0c;笔记本电脑连接树莓派的wifi&#xff0c;并且ip配置在一个网段&#xff0c;就可以互相通信&#xff08;笔记本放出wifi&#xff0c;树莓派连接效果一样&#xff09;&#xff0c;这样的好处是树莓派只要一上电就会自…...

数据猎手:使用Java和Apache HttpComponents库下载Facebook图像

引言 在信息驱动的时代&#xff0c;互联网上的数据成为了无可比拟的宝藏。本文旨在探讨如何通过利用Java和Apache HttpComponents库&#xff0c;从全球最大的社交网络平台Facebook上获取图像数据。 作为全球最大的社交网络平台&#xff0c;Facebook聚集了数以亿计的用户&#…...

uniapp——阻止冒泡

点击事件阻止冒泡 click.stop"onSubmit"其他类型&#xff0c;比如视频&#xff1a; 最后加了一个 click.stop <view class"videoBox" v-if"item.video_url"><video :src"i.image(item.video_url)" :controls"true&quo…...

Jmeter性能测试(四)

一、遇到问题解决思路 1、检查请求头是否正确 2、检查请求参数是否正确 3、检查鉴权信息是否正确 4、检查变量作用域 5、检查数据提取是否正确(正则/json提取器) 二、请求头检查 1、在Http信息头管理器查看 2、注意这里的变量作用域是全局的 三、请求参数检查 1、在查看结…...

从零开始精通RTSP之传输ADPCM等音频流

概述 在上一篇文章中&#xff0c;我们详细介绍了使用RTP传输AAC音频流的打包方法。除了AAC编码算法外&#xff0c;常用的音频编码算法还有ADPCM、G711A、G711U、G726等。接下来&#xff0c;我们继续介绍RTP传输ADPCM等音频流的打包方法。 封装方法 RTP封装ADPCM等音频数据时&am…...

box-decoration-break 使用介绍

box-decoration-break属性的使用 一、定义 box-decoration-break是CSS片段模块&#xff08;CSS Fragmentation Module Level 3&#xff09;中的一个属性&#xff0c;主要用于指定背景&#xff08;background&#xff09;、内边距&#xff08;padding&#xff09;、边框&#…...

技术分享 | 京东商品API接口|京东零售数据可视化平台产品实践与思考

导读 本次分享题目为京东零售数据可视化平台产品实践与思考。 主要包括以下四个部分&#xff1a; 1.京东API接口介绍 2. 平台产品能力介绍 3. 业务赋能案例分享 01 京东API接口介绍 02 平台产品能力介绍 1. 产品矩阵 数据可视化产品是一种利用数据分析和可视化技术&…...

信用渭南网站建设/企业网站seo优化

#include <stdio.h> #define T 10int main() {int i, j, a[T];printf("请输入%d个数字&#xff0c;空格分隔&#xff1a;\n", T - 1);for (i 1; i < T; i) //a[0] 存放交换时的临时数据scanf("%d", &a[i]);for (i 1; i < T; i) …...

Javaweb网站建设/百度广告联盟

管理节点 docker swarm leave --force普通节点 docker swarm leave欢迎小伙伴讨论&#xff0c;如有错误请在评论区评论或发私聊消息&#xff0c;谢谢你。...

创建wordpress插件/自己建网站怎么弄

控制系统的方块图方块图的组成方块信号线分支点相加点方块图的化简方块图的等效变换比较点的等效变换引出点的等效变换系统的方块图&#xff0c;是描述系统中每个元件之间的功能和信号传递关系的数学模型&#xff0c;它表示系统各变量之间的因果关系以及对各变量所进行的运算&a…...

自己做的网站怎么维护/网站性能优化的方法有哪些

文章目录1. 前提条件2. 功能3. 概念4. Helm组件5. 安装5.1 安装客户端5.2 安装服务端6. helm init6.1 --history-max6.2 --node-selectors6.3 --override6.4 --output6.5 其他参数7. 部署示例参考资料&#xff1a; 阳明官方文档&#xff08;含有更多细节&#xff09;kubernete…...

wordpress iframe插件/品牌推广外包公司

人社部公告&#xff0c;正式将互联网营销师列入中国新十大职业&#xff0c;成为国家认证的新兴职业。 互联网营销师&#xff0c;简单讲&#xff0c;其实就是指直播带货主播&#xff0c;代表就是李佳琪。薇娅&#xff0c;散打哥&#xff0c;辛巴&#xff0c;罗永浩等网红大佬&…...

视频网站如何做盗链/威海网站制作

文章目录1 报错2 解决办法1 报错 尝试在linux上通过Pycharm IDE使用matplotlib包。当我运行此代码时&#xff1a; from matplotlib import pyplot结果报错&#xff1a; ImportError: No module named tkinter2 解决办法 ubuntu的系统&#xff1a; sudo apt-get install py…...