一. 图像编辑器 Monica
Monica 是一款跨平台的桌面图像编辑软件(早期是为了验证一些算法而产生的)。
screenshot.png
其技术栈如下:
-
Kotlin 编写 UI(Kotlin Compose Desktop 作为 UI 框架)
-
基于 mvvm 模式,依赖注入使用 koin,编译使用 JDK 17
-
部分算法使用 Kotlin 实现
-
其余的算法使用 OpenCV C++ 来实现,Kotlin 通过 jni 来调用。
-
Monica 所使用的模型,主要使用 ONNXRuntime 进行部署和推理
-
其余少部分模型使用 OpenCV DNN 进行部署和推理。
-
本地的算法库使用 C++ 17 编译
Monica 目前还处于开发阶段,当前版本的可以参见 github 地址:https://github.com/fengzhizi715/Monica
这两个月由于工作比较繁忙,我只把 CV 算法快速调参的模块做了完善。
二. 实验性的功能------提供 CV 常见算法的快速调参的能力
在之前的相关系列文章中,曾介绍过该模块完成了二值化、边缘检测、轮廓分析等功能。这次更新了一些新的功能包括: 图像增强、图像降噪、形态学操作、模版匹配。
下面展示该模块的入口
以及该模块的首页
目前,该模块只规划了上述的功能,并已全部实现。短期内暂时不会增加新功能。
2.1 图像增强
该模块提供的图像增强算法,其实之前在 Monica 中早已实现了,我只是把他们从首页移动到这里。这些图像增强算法包括:直方图均衡化、clahe、gamma 变换、Laplace 锐化、USM 锐化、自动色彩均衡。
下图展示的的是进入图像增加的页面,并加载了某个图像的原图。
下图分别展示的是原图经过直方图均衡化后的效果和原图经过 gamma 变换后的效果。
Gamma 变换.png
2.2 图像降噪
该模块提供的图像降噪算法都是通过图像滤波实现的。目前支持高斯滤波、中值滤波、高斯双边滤波、均值迁移滤波。
下图展示的的是进入图像降噪的页面,并加载了某个图像的原图。
下图分别展示的是原图经过高斯滤波、高斯双边滤波、均值迁移滤波后的效果。
高斯双边滤波.png 均值迁移滤波.png
上述功能的实现,分别是调用 OpenCV 对应的图像滤波函数。
2.3 形态学操作
形态学操作是一种基于形状的图像处理技术,通过使用结构元素对图像进行处理,从而提取图像中的形状、大小等信息。
下面用之前的例子,展示一下如何使用形态学操作。首先,加载一张包含多个苹果的图片。
加载原图.png
通过彩色图像分割进行二值化。
然后再进行形态学的闭操作。
以及形态学的开操作。
这些形态学的操作,有助于对轮廓的进一步分析。
通过轮廓分析之后,我们在原图中可以找到比较明显的苹果。
形态学操作的是为了帮助提取图像中主要的信息。
2.4 模版匹配
模板匹配是一种经典的图像处理技术,用于在一幅图像中查找与另一幅模板图像最匹配的部分。
然而 OpenCV 提供的模版匹配函数有一些局限性,例如当模板图像与目标图像之间存在旋转角度差异,或者模板图像与目标图像的尺寸比例不同时,匹配效果都会变差。另外,在默认情况下,模板匹配函数会返回与模板最相似的一个匹配区域,如果需要支持多目标的匹配则需要通过一定的策略来实现。
这里,Monica 实现了一个支持模版匹配的算法,并且支持旋转,用于多角度、多尺度、多目标的模板匹配。
下面,给出简单的演示,先进入支持模版匹配功能的页面。
加载一张连连看的图片,选取其中一个作为模版图像,并调整一些参数。
最后执行模版匹配,很快能看到匹配的结果。
三. 功能的实现
其他的功能都比较简单,这里只介绍一下模版匹配的实现。由于模版匹配速度很慢,这是使用并行化的模版匹配,以及能够支持多目标的匹配。
go
class MatchTemplate {
public:
Mat templateMatching(Mat image, Mat templateImage, int matchType,
double angleStart, double angleEnd, double angleStep,
double scaleStart, double scaleEnd, double scaleStep,
double matchTemplateThreshold, float scoreThreshold, float nmsThreshold);
private:
// 使用 Canny 边缘检测
Mat computeCanny(const cv::Mat& image, double threshold1, double threshold2);
// 处理单个角度和尺度
static void processAngleScale(const cv::Mat& inputEdges, const cv::Mat& templateEdges, double angle, double scale,
double threshold, std::mutex& resultMutex, std::vector<cv::Rect>& results, std::vector<float>& scores);
// 并行化的模板匹配
void parallelTemplateMatching(const cv::Mat& inputEdges, const cv::Mat& templateEdges,
double angleStart, double angleEnd, double angleStep,
double scaleStart, double scaleEnd, double scaleStep,
double threshold, std::vector<cv::Rect>& matches, std::vector<float>& scores);
// 使用 OpenCV 的 NMS
void applyNMS(const std::vector<cv::Rect>& boxes, const std::vector<float>& scores, std::vector<cv::Rect>& finalBoxes,
float scoreThreshold, float nmsThreshold);
};
go
#include "../../include/matchTemplate/MatchTemplate.h"
usingnamespace cv::dnn;
// 使用 Canny 边缘检测
cv::Mat MatchTemplate::computeCanny(const cv::Mat& image, double threshold1 = 50, double threshold2 = 150) {
cv::Mat edges;
cv::Canny(image, edges, threshold1, threshold2);
CV_Assert(edges.type() == CV_8U); // 确保输出为单通道图像
return edges;
}
// 处理单个角度和尺度
void MatchTemplate::processAngleScale(const cv::Mat& inputEdges, const cv::Mat& templateEdges, double angle, double scale,
double threshold, std::mutex& resultMutex, std::vector<cv::Rect>& results, std::vector<float>& scores) {
// 旋转模板
cv::Point2f center(templateEdges.cols / 2.0f, templateEdges.rows / 2.0f);
cv::Mat rotationMatrix = cv::getRotationMatrix2D(center, angle, 1.0);
cv::Mat rotatedTemplate;
cv::warpAffine(templateEdges, rotatedTemplate, rotationMatrix, templateEdges.size(), cv::INTER_LINEAR);
// 缩放模板
cv::Mat scaledTemplate;
cv::resize(rotatedTemplate, scaledTemplate, cv::Size(), scale, scale);
// 检查模板有效性
if (scaledTemplate.empty() || scaledTemplate.cols < 1 || scaledTemplate.rows < 1) {
return; // 跳过无效模板
}
// 检查模板与输入图像尺寸
if (scaledTemplate.cols > inputEdges.cols || scaledTemplate.rows > inputEdges.rows) {
return; // 跳过尺寸不匹配的模板
}
// 边缘模板匹配
cv::Mat result;
try {
cv::matchTemplate(inputEdges, scaledTemplate, result, cv::TM_CCOEFF_NORMED);
} catch (const cv::Exception& e) {
std::cerr << "Error in matchTemplate: " << e.what() << std::endl;
return;
}
// 记录满足阈值的匹配结果
for (int y = 0; y < result.rows; ++y) {
for (int x = 0; x < result.cols; ++x) {
float matchScore = result.at<float>(y, x);
if (matchScore >= threshold) {
std::lock_guard<std::mutex> lock(resultMutex);
results.emplace_back(cv::Rect(x, y, scaledTemplate.cols, scaledTemplate.rows));
scores.push_back(matchScore);
}
}
}
}
// 并行化的模板匹配
void MatchTemplate::parallelTemplateMatching(const cv::Mat& inputEdges, const cv::Mat& templateEdges,
double angleStart, double angleEnd, double angleStep,
double scaleStart, double scaleEnd, double scaleStep,
double threshold, std::vector<cv::Rect>& matches, std::vector<float>& scores) {
std::mutex resultMutex;
std::vector<std::future<void>> futures;
for (double angle = angleStart; angle <= angleEnd; angle += angleStep) {
for (double scale = scaleStart; scale <= scaleEnd; scale += scaleStep) {
futures.emplace_back(std::async(std::launch::async, &MatchTemplate::processAngleScale,
std::ref(inputEdges), std::ref(templateEdges),
angle, scale, threshold, std::ref(resultMutex),
std::ref(matches), std::ref(scores)));
}
}
// 等待所有线程完成
for (auto& future : futures) {
future.get();
}
}
// 使用 OpenCV 的 NMS
void MatchTemplate::applyNMS(const std::vector<cv::Rect>& boxes, const std::vector<float>& scores,
std::vector<cv::Rect>& finalBoxes,
float scoreThreshold, float nmsThreshold) {
if (boxes.empty() || scores.empty()) {
return; // 避免空输入导致的崩溃
}
std::vector<int> indices;
NMSBoxes(boxes, scores, scoreThreshold, nmsThreshold, indices);
for (int idx : indices) {
finalBoxes.push_back(boxes[idx]);
}
}
Mat MatchTemplate::templateMatching(Mat image, Mat templateImage, int matchType,
double angleStart, double angleEnd, double angleStep,
double scaleStart, double scaleEnd, double scaleStep,
double matchTemplateThreshold, float scoreThreshold, float nmsThreshold) {
// 绘制最终结果
cv::Mat resultImage = image.clone();
if (matchType == 1) { // 灰度匹配
cvtColor(image, image, COLOR_BGR2GRAY);
cvtColor(templateImage, templateImage, COLOR_BGR2GRAY);
} elseif (matchType == 2) { // 边缘匹配
// 计算图像和模板的 Canny 边缘
image = computeCanny(image, 50, 150);
templateImage = computeCanny(templateImage, 50, 150);
}
vector<Rect> matches;
vector<float> scores;
// 并行模板匹配
parallelTemplateMatching(image, templateImage, angleStart, angleEnd, angleStep, scaleStart, scaleEnd, scaleStep, matchTemplateThreshold, matches, scores);
// 使用 OpenCV 的 NMS 过滤结果
vector<Rect> finalMatches;
applyNMS(matches, scores, finalMatches, scoreThreshold, nmsThreshold);
for (constauto& match : finalMatches) {
rectangle(resultImage, match, cv::Scalar(0, 0, 255), 2);
}
return resultImage;
}
四. 总结
Monica 对 CV 算法快速调参的模块算是基本完成,暂时告一段落。这一模块的后续规划,主要看 2025 年工作的忙碌程度。
Monica 后续的重点是将其现在使用的部分模型,部署到云端以及软件层面 UI 和性能优化等。
Monica github 地址:https://github.com/fengzhizi715/Monica