一. 图像编辑器 Monica
Monica 是一款跨平台的桌面图像编辑软件,使用 Kotlin Compose Desktop 作为 UI 框架。应用层使用 Kotlin 编写,基于 mvvm 架构,使用 koin 作为依赖注入框架。
从上述图中可以看到,Monica 用到的技术栈包括:Kotlin 编写 UI 和大部分算法(软件使用 JDK 17 进行编译),其余的算法使用 OpenCV C++ 来实现, Kotlin 通过 jni 来调用。另外,软件中用到的大部分深度学习的模型的使用 ONNXRuntime 进行部署和推理,少部分模型使用 OpenCV DNN 进行部署和推理。
Monica 目前还处于开发阶段,当前版本的可以参见 github 地址:https://github.com/fengzhizi715/Monica
二. 人脸替换
Monica 在这个版本中新增了人脸替换的功能,使用的是 facefusion 的换脸模型。
在 AI 实验室中,点击"人脸替换"就可以使用该功能:
"人脸替换"需要一张源图和加载一张目标图片。
检测到源图的第一个人脸之后会保存 face embedding,接着检测目标图中的人脸并找到 landmark,然后进行替换。下图是替换的结果。
并将结果保存。
"人脸替换"也支持将目标图中所有的人脸进行替换。
只需要设置一下替换 target 中人脸的数量即可。
就可以完成目标图中所有的人脸替换。
当然,这里也有一个缺陷的地方,源图只取第一个人脸。后面可能会考虑源图支持多个人脸的选择。
三. 应用层调用
人脸替换功能的模型也是使用 ONNXRuntime 进行部署和推理,可以参考该系列前一篇文章。
为了给应用层调用,在 jni 中定义好相关模型的加载和实现人脸替换的功能:
Yolov8Face *yolov8Face = nullptr;
Face68Landmarks *face68Landmarks = nullptr;
FaceEmbedding *faceEmbedding = nullptr;
FaceSwap *faceSwap = nullptr;
FaceEnhance *faceEnhance = nullptr;
JNIEXPORT void JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_initFaceSwap
(JNIEnv* env, jobject,jstring jYolov8FaceModelPath, jstring jFace68LandmarksModePath,
jstring jFaceEmbeddingModePath, jstring jFaceSwapModePath, jstring jFaceSwapModePath2, jstring jFaceEnhanceModePath){
const char* yolov8FaceModelPath = env->GetStringUTFChars(jYolov8FaceModelPath, JNI_FALSE);
const char* face68LandmarksModePath = env->GetStringUTFChars(jFace68LandmarksModePath, JNI_FALSE);
const char* faceEmbeddingModePath = env->GetStringUTFChars(jFaceEmbeddingModePath, JNI_FALSE);
const char* faceSwapModePath = env->GetStringUTFChars(jFaceSwapModePath, JNI_FALSE);
const char* faceSwapModePath2 = env->GetStringUTFChars(jFaceSwapModePath2, JNI_FALSE);
const char* faceEnhanceModePath = env->GetStringUTFChars(jFaceEnhanceModePath, JNI_FALSE);
const std::string& yolov8FaceLogId = "yolov8Face";
const std::string& face68LandmarksLogId = "face68Landmarks";
const std::string& faceEmbeddingLogId = "faceEmbedding";
const std::string& faceSwapLogId = "faceSwap";
const std::string& faceEnhanceLogId = "faceEnhance";
const std::string& onnx_provider = OnnxProviders::CPU;
yolov8Face = new Yolov8Face(yolov8FaceModelPath, yolov8FaceLogId.c_str(), onnx_provider.c_str());
face68Landmarks = new Face68Landmarks(face68LandmarksModePath, face68LandmarksLogId.c_str(), onnx_provider.c_str());
yolov8Face = new Yolov8Face(yolov8FaceModelPath, yolov8FaceLogId.c_str(), onnx_provider.c_str());
faceEmbedding = new FaceEmbedding(faceEmbeddingModePath, faceEmbeddingLogId.c_str(), onnx_provider.c_str());
faceSwap = new FaceSwap(faceSwapModePath, faceSwapModePath2, faceSwapLogId.c_str(), onnx_provider.c_str());
faceEnhance = new FaceEnhance(faceEnhanceModePath, faceEnhanceLogId.c_str(), onnx_provider.c_str());
env->ReleaseStringUTFChars(jYolov8FaceModelPath, yolov8FaceModelPath);
env->ReleaseStringUTFChars(jFace68LandmarksModePath, face68LandmarksModePath);
env->ReleaseStringUTFChars(jFaceEmbeddingModePath, faceEmbeddingModePath);
env->ReleaseStringUTFChars(jFaceSwapModePath, faceSwapModePath);
env->ReleaseStringUTFChars(jFaceSwapModePath2, faceSwapModePath2);
env->ReleaseStringUTFChars(jFaceEnhanceModePath, faceEnhanceModePath);
}
JNIEXPORT jintArray JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_faceLandMark
(JNIEnv* env, jobject,jbyteArray array) {
Mat image = byteArrayToMat(env,array);
Mat dst;
try {
vector<Bbox> boxes;
yolov8Face->detect(image, boxes);
dst = image.clone();
for (auto box: boxes) {
rectangle(dst, cv::Point(box.xmin,box.ymin), cv::Point(box.xmax,box.ymax), Scalar(0, 255, 0), 4, 8, 0);
vector<Point2f> face_landmark_5of68;
face68Landmarks->detect(image, box, face_landmark_5of68);
for (auto point : face_landmark_5of68)
{
circle(dst, cv::Point(point.x, point.y), 4, Scalar(0, 0, 255), -1);
}
}
} catch(...) {
}
jthrowable mException = NULL;
mException = env->ExceptionOccurred();
if (mException != NULL) {
env->ExceptionClear();
jclass exceptionClazz = env->FindClass("java/lang/Exception");
env->ThrowNew(exceptionClazz, "jni exception");
env->DeleteLocalRef(exceptionClazz);
return env->NewIntArray(0);
}
return matToIntArray(env,dst);
}
JNIEXPORT jintArray JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_faceSwap
(JNIEnv* env, jobject,jbyteArray arraySrc, jbyteArray arrayTarget, jboolean status) {
Mat src = byteArrayToMat(env,arraySrc);
Mat target = byteArrayToMat(env,arrayTarget);
vector<Bbox> boxes;
yolov8Face->detect(src, boxes);
int position = 0; // 一张图片里可能有多个人脸,这里只考虑1个人脸的情况
Bbox firstBox = boxes[position];
vector<Point2f> face_landmark_5of68;
face68Landmarks->detect(src, boxes[position], face_landmark_5of68);
vector<float> source_face_embedding = faceEmbedding->detect(src, face_landmark_5of68);
yolov8Face -> detect(target, boxes);
Mat dst = target.clone();
if (!boxes.empty()) {
if (status) {
for (auto box: boxes) {
vector<Point2f> target_landmark_5;
face68Landmarks->detect(dst, box, target_landmark_5);
Mat swap = faceSwap->process(dst, source_face_embedding, target_landmark_5);
dst = faceEnhance->process(swap, target_landmark_5);
}
} else {
Bbox box = boxes[0];
vector<Point2f> target_landmark_5;
face68Landmarks->detect(dst, box, target_landmark_5);
Mat swap = faceSwap->process(dst, source_face_embedding, target_landmark_5);
dst = faceEnhance->process(swap, target_landmark_5);
}
}
return matToIntArray(env,dst);
}
对于应用层,需要编写好调用 jni 层的代码:
object ImageProcess {
val loadPath = System.getProperty("compose.application.resources.dir") + File.separator
init {
// 需要先加载图像处理库,否则无法通过 jni 调用算法
LoadManager.load()
}
......
/**
* 初始化换脸模块
*/
external fun initFaceSwap(yolov8FaceModelPath:String, face68LandmarksModePath:String,
faceEmbeddingModePath:String, faceSwapModePath:String, faceSwapModePath2:String, faceEnhanceModePath:String)
/**
* 人脸 landmark 提取
*/
external fun faceLandMark(src: ByteArray):IntArray
/**
* 替换人脸,将 src 中的人脸替换到 target 中,并展示 target 的图片。
*/
external fun faceSwap(src: ByteArray, target: ByteArray, status: Boolean):IntArray
}
在 Monica 启动时,先加载必须的模型
runInBackground { // 初始化换脸的模块
OpenCVManager.initFaceSwapModule()
}
这里的模型太大了,5个模型差不多1G左右,放在客户端加载实在不是很明智的办法。后面打算放到云端部署。
最后,终于可以在应用层调用了。
fun faceSwap(state: ApplicationState, image: BufferedImage?=null, target: BufferedImage?=null, status:Boolean, onImageChange:OnImageChange) {
if (image!=null && target!=null) {
state.scope.launchWithLoading {
val srcByteArray = image.image2ByteArray()
val (width,height,targetByteArray) = target.getImageInfo()
val outPixels = ImageProcess.faceSwap(srcByteArray, targetByteArray, status)
onImageChange.invoke(BufferedImages.toBufferedImage(outPixels,width,height))
}
}
}
四. 总结
Monica 快要到 1.0.0 版本,后续不会再对软件部署较大的模型, 要是有比较有意思的模型我会优先部署到云端。Monica 使用的主要语言 Kotlin 也会升级到最新的 2.0 之后的版本。
下一阶段的软件规划,可能考虑增加 OpenCV 算法的调试模块,方便快速验证一些算法。
Monica github 地址:https://github.com/fengzhizi715/Monica
OpenCV4系统化学习
推荐阅读
三行代码实现 TensorRT8.6 C++ 深度学习模型部署