图像编辑器 Monica 之 CV 常见算法的快速调参

一. 图像编辑器 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

相关推荐
汉克老师1 小时前
GESP2024年3月认证C++六级( 第三部分编程题(1)游戏)
c++·学习·算法·游戏·动态规划·gesp6级
闻缺陷则喜何志丹1 小时前
【C++图论】2685. 统计完全连通分量的数量|1769
c++·算法·力扣·图论·数量·完全·连通分量
利刃大大2 小时前
【二叉树深搜】二叉搜索树中第K小的元素 && 二叉树的所有路径
c++·算法·二叉树·深度优先·dfs
CaptainDrake2 小时前
力扣 Hot 100 题解 (js版)更新ing
javascript·算法·leetcode
一缕叶2 小时前
洛谷P9420 [蓝桥杯 2023 国 B] 子 2023 / 双子数
算法·蓝桥杯
甜甜向上呀2 小时前
【数据结构】空间复杂度
数据结构·算法
Great Bruce Young3 小时前
GPS信号生成:C/A码序列生成【MATLAB实现】
算法·matlab·自动驾驶·信息与通信·信号处理
Mryan20053 小时前
LeetCode | 不同路径
数据结构·c++·算法·leetcode
qy发大财3 小时前
验证二叉搜索树(力扣98)
数据结构·算法·leetcode·职场和发展
人类群星闪耀时4 小时前
用深度学习优化供应链管理:让算法成为商业决策的引擎
人工智能·深度学习·算法