图像编辑器 Monica 之实现好玩的人脸替换功能

科技   2024-11-18 22:18   江苏  

一. 图像编辑器 Monica

Monica 是一款跨平台的桌面图像编辑软件,使用 Kotlin Compose Desktop 作为 UI 框架。应用层使用 Kotlin 编写,基于 mvvm 架构,使用 koin 作为依赖注入框架。

screenshot.png
screenshot-version.png

从上述图中可以看到,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(02550), 480);

            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(00255), -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系统化学习


深度学习系统化学习

推荐阅读

OpenCV4.8+YOLOv8对象检测C++推理演示

ZXING+OpenCV打造开源条码检测应用

攻略 | 学习深度学习只需要三个月的好方法

三行代码实现 TensorRT8.6 C++ 深度学习模型部署

实战 | YOLOv8+OpenCV 实现DM码定位检测与解析

对象检测边界框损失 – 从IOU到ProbIOU

初学者必看 | 学习深度学习的五个误区


OpenCV学堂
三本书《Java数字图像处理-编程技巧与应用实践》、《OpenCV Android开发实战》、《OpenCV4应用开发-入门、进阶与工程化实践》作者。OpenCV实验大师平台 软件作者,OpenCV开发专家、OpenCV研习社创始人。
 最新文章