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

一. 图像编辑器 Monica

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

image.png

image.png

从上述图中可以看到,Monica 用到的技术栈包括:Kotlin 编写 UI 和大部分算法(软件使用 JDK 17 进行编译),其余的算法使用 OpenCV C++ 来实现, Kotlin 通过 jni 来调用。另外,软件中用到的大部分深度学习的模型的使用 ONNXRuntime 进行部署和推理,少部分模型使用 OpenCV DNN 进行部署和推理。

Monica 目前还处于开发阶段,当前版本的可以参见 github 地址:https://github.com/fengzhizi7...

二. 人脸替换

Monica 在这个版本中新增了人脸替换的功能,使用的是 facefusion 的换脸模型。

在 AI 实验室中,点击"人脸替换"就可以使用该功能:

image.png

"人脸替换"需要一张源图和加载一张目标图片。

image.png

检测到源图的第一个人脸之后会保存 face embedding,接着检测目标图中的人脸并找到 landmark,然后进行替换。下图是替换的结果。

image.png

并将结果保存。

image.png

"人脸替换"也支持将目标图中所有的人脸进行替换。

image.png

只需要设置一下替换 target 中人脸的数量即可。

image.png

就可以完成目标图中所有的人脸替换。

image.png

当然,这里也有一个缺陷的地方,源图只取第一个人脸。后面可能会考虑源图支持多个人脸的选择。

三. 应用层调用

人脸替换功能的模型也是使用 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/fengzhizi7...

作者:fengzhizi715
来源:Java与Android技术栈

推荐阅读

欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式AI专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。

推荐阅读
关注数
18754
内容数
1314
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息