图像编辑器 Monica 之重构滤镜模块、云端部署模型

一. 图像编辑器 Monica

Monica 是一款跨平台的桌面图像编辑软件(早期是为了验证一些算法而产生的)。

其技术栈如下:

  • Kotlin 编写 UI(Kotlin Compose Desktop 作为 UI 框架)
  • 基于 mvvm 模式,依赖注入使用 koin,编译使用 JDK 17
  • 部分算法使用 Kotlin 实现
  • 其余的算法使用 OpenCV C++ 来实现,Kotlin 通过 jni 来调用。
  • Monica 所使用的模型,主要使用 ONNXRuntime 加速推理,支持在云端部署。
  • 本地的算法库使用 C++ 17 编译

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

这两个月我重构了滤镜模块,并增加了大量的滤镜,还用 C++ 写了一个简单的 http server 用于部署各个模型。

二. 滤镜模块重构

在该模块重构之前,Monica 就有 28 款滤镜,当前版本增加到 50 多款滤镜,而且相关代码比较独立。因此,我把相关代码拆分出来成为一个独立的 module,未来方便其他的项目也可以移植使用。

后来,考虑增加大量的滤镜,因此修改了软件的 UI,下面展示该模块的入口。

以及该模块的界面

下面举例一些滤镜的效果:

大多数滤镜可以调参,当然各种滤镜效果也可以不断叠加,还可以跟其他功能一起使用。

三. 模型部署到云端

Monica 中有几个功能是调用模型来推理实现的。(可以查看该系列之前的相关文章)

早期 Monica 的版本是把模型文件都放在软件中,这样做导致软件打包之后,打包文件会非常大,我都不敢提供打包文件。

最近,我用 boost 写了一个简单的 http server 用于部署模型。核心代码大概200来行:

cpp 复制代码
#include <boost/program_options.hpp>
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/url/url.hpp>
#include <boost/url/url_view.hpp>
#include <boost/url/parse.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include "GlobalResource.h"
#include "HttpUtils.h"

namespace po = boost::program_options;
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;

#define CONTENT_TYPE_PLAIN_TEXT "text/plain"
#define CONTENT_TYPE_IMAGE_JPEG "image/jpeg"

// 用于处理单个 HTTP 会话
class session : public std::enable_shared_from_this<session> {
public:
    session(tcp::socket socket, std::shared_ptr<GlobalResource> globalResource, size_t maxBodySize)
            : socket_(std::move(socket)), globalResource_(globalResource), maxBodySize_(maxBodySize) {}

    void start() { do_read(); }

private:
    tcp::socket socket_;
    beast::flat_buffer buffer_;
    http::request<http::dynamic_body> req_;
    // 引用全局资源
    std::shared_ptr<GlobalResource> globalResource_;
    size_t maxBodySize_;

    void do_read() {
        auto self = shared_from_this();

        // 创建一个请求解析器,使用 dynamic_body 类型
        auto parser = std::make_shared<http::request_parser<http::dynamic_body>>();
        // 设置允许的最大消息体大小
        parser->body_limit(maxBodySize_);

        // 异步读取请求
        http::async_read(socket_, buffer_, *parser,
                         [self, parser](beast::error_code ec, std::size_t bytes_transferred) {
                             if (ec) {
                                 // 错误处理:输出错误信息并返回响应
                                 std::cerr << "Read error: " << ec.message() << std::endl;
                                 http::response<http::string_body> res{http::status::bad_request, self->req_.version()};
                                 res.set(http::field::content_type, CONTENT_TYPE_PLAIN_TEXT);
                                 res.body() = "Error reading request: " + ec.message();
                                 res.prepare_payload();
                                 self->do_write(res);
                                 return;
                             }
                             // 从解析器中获取请求
                             self->req_ = parser->release();
                             self->handle_request();
                         }
        );
    }

    // 根据请求路径处理请求
    void handle_request() {
        auto target = std::string(req_.target());

        if (target == "/health") {
            http::response<http::string_body> res{http::status::ok, req_.version()};
            res.set(http::field::content_type, CONTENT_TYPE_PLAIN_TEXT);
            res.body() = "OK";
            res.prepare_payload();
            return do_write(res);
        }

        auto method = req_.method();
        // 简单路由:根据 target 分发不同的逻辑
        if (method == http::verb::post) {
            if (target == "/api/sketchDrawing") {
                process_image(target, [this](Mat src) { return globalResource_->processSketchDrawing(src); });
            } else if (target == "/api/faceDetect") {
                process_image(target, [this](Mat src) { return globalResource_->processFaceDetect(src); });
            } else if (target == "/api/faceLandMark") {
                process_image(target, [this](Mat src) { return globalResource_->processFaceLandMark(src); });
            } else if (target.find("/api/faceSwap") == 0) {
                try {
                    // 使用 parse_relative_ref 解析路径 + 查询参数
                    auto targetRes = boost::urls::parse_relative_ref(target);
                    if (!targetRes) {
                        std::cerr << "Invalid URL: " << target << std::endl;
                        return;
                    }

                    boost::urls::url_view url_view = targetRes.value();
                    std::string status_param = "false";  // 默认值
                    // 正确的参数获取方式
                    auto params = url_view.params();
                    if (auto it = params.find("status"); it != params.end()) {
                        auto value = (*it).value;  // 注意:这里可能是 .value 而不是 .value()
                        status_param = std::string(value.data(), value.size());
                    }
                    bool status = (status_param == "true");
                    cout << "status = "<< status << endl;

                    // 解析 multipart/form-data
                    auto parts = parseMultipartFormDataManual(req_);
                    if (parts.find("src") == parts.end() || parts.find("target") == parts.end()) {
                        throw std::runtime_error("Missing images in request.");
                    }

                    Mat src = binaryToCvMat(parts["src"]);
                    Mat target = binaryToCvMat(parts["target"]);
                    Mat dst = globalResource_.get()->processFaceSwap(src, target, status);
                    std::string encodedImage = cvMatToResponseBody(dst, ".jpg");

                    http::response<http::string_body> res{http::status::ok, req_.version()};
                    res.set(http::field::content_type, CONTENT_TYPE_IMAGE_JPEG);
                    res.body() = std::move(encodedImage);
                    res.prepare_payload();
                    do_write(res);
                } catch (const std::exception& e) {
                    http::response<http::string_body> res{http::status::bad_request, req_.version()};
                    res.set(http::field::content_type, CONTENT_TYPE_PLAIN_TEXT);
                    res.body() = "Error processing face swap: " + std::string(e.what());
                    res.prepare_payload();
                    do_write(res);
                }
            }
        } else {
            // 其他接口返回 404
            http::response<http::string_body> res{http::status::not_found, req_.version()};
            res.set(http::field::content_type, CONTENT_TYPE_PLAIN_TEXT);
            res.body() = "Not Found";
            res.prepare_payload();
            do_write(res);
        }
    }

    void process_image(const std::string& target, std::function<Mat(Mat)> processor) {
        try {
            Mat src = requestBodyToCvMat(req_);
            Mat dst = processor(src);
            std::string encodedImage = cvMatToResponseBody(dst, ".jpg");

            http::response<http::string_body> res{http::status::ok, req_.version()};
            res.set(http::field::content_type, CONTENT_TYPE_IMAGE_JPEG);
            res.body() = std::move(encodedImage);
            res.prepare_payload();
            do_write(res);
        } catch (const std::exception& e) {
            http::response<http::string_body> res{http::status::internal_server_error, req_.version()};
            res.set(http::field::content_type, CONTENT_TYPE_PLAIN_TEXT);
            res.body() = "Error: " + std::string(e.what());
            res.prepare_payload();
            do_write(res);
        }
    }


    template<class Response>
    void do_write(Response& res) {
        auto self = shared_from_this();
        auto sp = std::make_shared<Response>(std::move(res));
        http::async_write(socket_, *sp,
                          [self, sp](beast::error_code ec, std::size_t) {
                              self->socket_.shutdown(tcp::socket::shutdown_send, ec);
                          });
    }
};

// HTTP 服务器:监听指定端口,并为每个连接创建一个 session
class server {
public:
    server(net::io_context& ioc, tcp::endpoint endpoint, std::string modelPath, size_t maxBodySize)
            : acceptor_(ioc)
            , globalResource_(std::make_shared<GlobalResource>(modelPath)) // 全局资源初始化,只调用一次
            , maxBodySize_(maxBodySize) {
        beast::error_code ec;
        acceptor_.open(endpoint.protocol(), ec);
        acceptor_.set_option(net::socket_base::reuse_address(true), ec);
        acceptor_.set_option(net::socket_base::receive_buffer_size(1024 * 1024), ec);
        acceptor_.bind(endpoint, ec);
        acceptor_.listen(net::socket_base::max_listen_connections, ec);
    }

    void run() {
        do_accept();
    }

private:
    tcp::acceptor acceptor_;
    std::shared_ptr<GlobalResource> globalResource_;
    size_t maxBodySize_;

    void do_accept() {
        acceptor_.async_accept(
                [this](beast::error_code ec, tcp::socket socket) {
                    if (!ec) {
                        // 将全局资源传递给 session
                        std::make_shared<session>(std::move(socket), globalResource_, maxBodySize_)->start();
                    }
                    do_accept();
                });
    }
};

int main(int argc, char* argv[]) {
    // 默认配置参数
    int port = 8080;
    int numThreads = std::thread::hardware_concurrency();
    std::string modelPath = "/Users/Tony/IdeaProjects/Monica/resources/common";
    size_t maxBodySize = 10 * 1024 * 1024; // 默认最大请求体大小为 10 MB

    // 定义命令行选项
    po::options_description desc("Allowed options");
    desc.add_options()
            ("help,h", "Display help message")
            ("http-port,p", po::value<int>(&port)->default_value(8080), "HTTP server port")
            ("num-threads,t", po::value<int>(&numThreads)->default_value(std::thread::hardware_concurrency()), "Number of worker threads")
            ("model-dir,m", po::value<std::string>(&modelPath)->default_value(modelPath), "Path to the model directory")
            ("max-body-size,b", po::value<size_t>(&maxBodySize)->default_value(maxBodySize), "Maximum HTTP body size in bytes");

    // 解析命令行参数
    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);

    if (vm.count("help")) {
        std::cout << desc << "\n";
        return 0;
    }

    net::io_context ioc{numThreads};
    tcp::endpoint endpoint{tcp::v4(), static_cast<unsigned short>(port)};
    server srv(ioc, endpoint, modelPath, maxBodySize);
    srv.run();

    std::vector<std::thread> threads;
    for (int i = 0; i < numThreads - 1; ++i) {
        threads.emplace_back([&ioc](){ ioc.run(); });
    }
    ioc.run();

    for (auto& t : threads)
        t.join();

    return 0;
}

该项目的地址:github.com/fengzhizi71...

该服务的使用也很简单

lua 复制代码
Tony-MacBook-Pro:build tony$ ./MonicaImageProcessHttpServer --help
Allowed options:
  -h [ --help ]                         Display help message
  -p [ --http-port ] arg (=8080)        HTTP server port
  -t [ --num-threads ] arg (=16)        Number of worker threads
  -m [ --model-dir ] arg (=/Users/Tony/IdeaProjects/Monica/resources/common)
                                        Path to the model directory
  -b [ --max-body-size ] arg (=10485760)
                                        Maximum HTTP body size in bytes

服务器启动:

css 复制代码
./MonicaImageProcessHttpServer --http-port 8080 --num-threads 4 --model-dir /Users/Tony/IdeaProjects/Monica/resources/common

该服务提供的接口包括:

  1. /health 服务器状态检测

  2. /api/sketchDrawing 提供生成素描画的服务

curl 调用的示例:

bash 复制代码
curl -X POST http://localhost:8080/api/sketchDrawing -H "Content-Type: image/jpeg" --data-binary "@/Users/Tony/xxx.png" --output output.jpg
  1. /api/faceDetect 提供人脸识别的服务

curl 调用的示例:

bash 复制代码
curl -X POST http://localhost:8080/api/faceDetect -H "Content-Type: image/jpeg" --data-binary "@/Users/Tony/xxx.png" --output output.jpg
  1. /api/faceLandMark 提供人脸检测的服务

curl 调用的示例:

bash 复制代码
curl -X POST http://localhost:8080/api/faceLandMark -H "Content-Type: image/jpeg" --data-binary "@/Users/Tony/xxx.png" --output output.jpg
  1. /api/faceSwap 提供人脸替换的服务

curl 调用的示例:

lua 复制代码
curl -X POST "http://localhost:8080/api/faceSwap" -H "Content-Type: multipart/form-data" -F "src=@/Users/Tony/src.jpg" -F "target=@/Users/Tony/target.jpg" --output output.jpg

在 Monica 中调用该算法服务,相比于原先还要写 jni 就简单太多了,由于都是 http 服务所以用 okhttp 封装一下即可。另外,需要在 Monica 中设置好算法服务的 base url,这样就可以正常使用算法服务了。

四. 总结

Monica 后续的重点是重构形状绘制模块,增加一些好玩的模型,以及优化软件的各种使用体验。

由于模型文件不用部署在本地之后,可以大大减少软件的体积,也方便未来可以不断添加新的模型。

Monica github 地址:github.com/fengzhizi71...

相关推荐
一只鱼^_1 分钟前
第十六届蓝桥杯大赛软件赛省赛 C/C++ 大学B组
c语言·c++·算法·贪心算法·蓝桥杯·深度优先·图搜索算法
说码解字2 分钟前
快速排序算法及其优化
算法
Lounger665 分钟前
107.二叉树的层序遍历II- 力扣(LeetCode)
python·算法·leetcode
IT猿手7 分钟前
动态多目标优化:基于可学习预测的动态多目标进化算法(DIP-DMOEA)求解CEC2018(DF1-DF14),提供MATLAB代码
学习·算法·matlab·动态多目标优化·动态多目标进化算法
竹下为生10 分钟前
LeetCode --- 444 周赛
算法·leetcode·职场和发展
明月看潮生37 分钟前
青少年编程与数学 02-016 Python数据结构与算法 14课题、动态规划
python·算法·青少年编程·动态规划·编程与数学
明月看潮生38 分钟前
青少年编程与数学 02-016 Python数据结构与算法 11课题、分治
python·算法·青少年编程·编程与数学
源客z1 小时前
SD + Contronet,扩散模型V1.5+约束条件后续优化:保存Canny边缘图,便于视觉理解——stable diffusion项目学习笔记
图像处理·算法·计算机视觉
freyazzr1 小时前
Leedcode刷题 | Day30_贪心算法04
数据结构·c++·算法·leetcode·贪心算法
IT猿手2 小时前
动态多目标进化算法:基于知识转移和维护功能的动态多目标进化算法(KTM-DMOEA)求解CEC2018(DF1-DF14)
算法·动态多目标进化·动态多目标进化算法·动态多目标测试·动态多目标