赞
踩
从本篇开始,会在上一篇搭建的滤镜框架的基础上,介绍具体的滤镜效果该如何制作。本篇会先介绍大眼滤镜,先来看一下效果,原图如下:
使用手机后置摄像头对眼部放大后的效果:
制作大眼滤镜所需的主要知识点:
下面让我们一步步来实现这个效果吧。
要对眼部进行放大,那么一定需要识别到图像中眼睛的位置,通常我们会先识别到人脸,再去识别人眼,这样比从整张图片的范围内直接定位人眼要快。
人脸定位我们使用 OpenCV,虽然 OpenCV 也提供了人眼定位的模型文件,但是由于准确率一般,因此我们使用中科院开源的 SeetaFace 定位人眼。为人眼添加滤镜效果的任务自然落到 OpenGL 上。
OpenCV 在 Android Studio 上的配置,在OpenCV 入门(一) —— OpenCV 基础中已经讲过,去参考那篇文章,这里就不再赘述,下面只介绍 SeetaFace 的配置。
中科院开源的人脸识别引擎 SeetaFace,比 OpenCV 自带模型的识别率要好一些。GitHub 下载 SeetaFaceEngine:
可以看到包括三个核心模块:
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.cpp
src/face_alignment.cpp
src/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(
opencv
log
opencv_java4 # 链接 OpenCV 动态库
android # 因为要用 ANativeWindow 渲染,因此要链接 libandroid
seeta_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' } } }
有关 OpenCV 人脸识别的内容,我们在 OpenCV 系列文章的OpenCV 入门(六)—— Android 下的人脸识别中详细讲过。虽然这里又添加了 SeetaFace 进行人眼识别,但主要过程没有太大的变化。
从代码结构上说,识别工作要分为两层:
从过程上说,主要分为以下几个步骤:
接下来结合代码详细说明上述实现步骤。
先将 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: Int
while (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 = 0L
init {
mFaceTracker = nativeInit(opencvModelPath, seetaFaceModelPath)
}
fun startTracking() {
nativeStart(mFaceTracker)
}
private external fun nativeInit(opencvModelPath: String, seetaFaceModelPath: String): Long
private 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();
}
}
进入识别流程,需要将图像数据传给 Native 层进行识别,图像数据来自 CameraHelper:
typealias CameraPreviewCallback = (data: ByteArray) -> Unit
class 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.PreviewCallback
override 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.初始化 CameraHelper
mCameraHelper = 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) {
// 先移除之前的消息,保持检测最新的 data
mHandler.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 对象返回给上层
...
}
主要任务有两项:
下面分别来看这两项内容实现。
大致过程如下:
我们先来看 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 转换为 RGBA
cvtColor(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 人脸矩形信息 FaceInfo
seeta::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);
}
}
}
过程梳理:
人脸宽高数据在本节的大眼滤镜中用不到,但是在下一篇添加贴纸效果时有用,由于是很小的点,也不适宜在下一篇中单独拿出来说,因此就在这里直接保存这个信息了。
这里的上层对象就是指 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 构造方法中的关键点集合 FloatArray
int 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
前面的工作保证我们能获取到人眼坐标,接下来就是使用 OpenGL 在绘制时添加滤镜效果了。
顶点着色器使用 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.0
return (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 / dis
newCoord = 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 进行绘制了。
大眼滤镜,包括我们后续要实现的美颜滤镜和贴纸,它们都是绘制在 FBO 上的,而不是直接渲染到屏幕上,因此可以抽出一个基类将 FBO 的共同操作放入其中:
/**
* 使用 FBO 绘制的 Filter 基类
*/
open class BaseFrameFilter(context: Context, mVertexSourceId: Int, mFragmentSourceId: Int) :
BaseFilter(context, mVertexSourceId, mFragmentSourceId) {
protected var mFrameBuffers: IntArray? = null
protected var mFrameBufferTextures: IntArray? = null
override fun onReady(width: Int, height: Int) {
super.onReady(width, height)
// 1.先清空 mFrameBuffers 的残留数据
mFrameBuffers?.let {
releaseFrameBuffer()
}
// 2.创建 FBO
mFrameBuffers = 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)
// 绑定 FBO
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])
// 将纹理附加到 FBO
glFramebufferTexture2D(
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: Int
private val rightEye: Int
private var leftBuffer: FloatBuffer
private var rightBuffer: FloatBuffer
private var face: Face? = null
init {
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.判断不符合绘制条件的情况,直接返回上一层的纹理 ID
val landmarks = face?.landmarks
val imgWidth = face?.imgWidth
val imgHeight = face?.imgHeight
if (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] / imgWidth
var y = landmarks[3] / imgHeight
leftBuffer.clear()
leftBuffer.put(x)
leftBuffer.put(y)
leftBuffer.position(0)
glUniform2fv(leftEye, 1, leftBuffer)
// 右眼坐标
x = landmarks[4] / imgWidth
y = landmarks[5] / imgHeight
rightBuffer.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)
// 解绑 FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glBindTexture(GL_TEXTURE_2D, 0)
return mFrameBufferTextures!![0]
}
}
核心思路就是从 Face 中提取出左眼和右眼的坐标,传递给片元着色器 big_eyes_fragment 中定义的两个变量 left_eye 和 right_eye。
在渲染器中创建 BigEyesFilter 并将其添加到绘制的责任链中:
private lateinit var mBigEyesFilter: BigEyesFilter
override 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)
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。