一. 图像编辑器 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
该服务提供的接口包括:
-
/health 服务器状态检测
-
/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
- /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
- /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
- /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...