Bootstrap

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

一. 图像编辑器 Monica

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

39dfd09eb26f4941b923290e13720cb4.jpeg
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 常见算法的快速调参的能力

在之前的相关系列文章中,曾介绍过该模块完成了二值化、边缘检测、轮廓分析等功能。这次更新了一些新的功能包括: 图像增强、图像降噪、形态学操作、模版匹配。

下面展示该模块的入口f72319ffc6f7258759c1f0b6c7ded9b6.jpeg

以及该模块的首页27064fb7b8eb73dfde9f22cefcdd698e.jpeg

目前,该模块只规划了上述的功能,并已全部实现。短期内暂时不会增加新功能。

2.1 图像增强

该模块提供的图像增强算法,其实之前在 Monica 中早已实现了,我只是把他们从首页移动到这里。这些图像增强算法包括:直方图均衡化、clahe、gamma 变换、Laplace 锐化、USM 锐化、自动色彩均衡。

下图展示的的是进入图像增加的页面,并加载了某个图像的原图。6de93b22b22f965385876d8291268fe5.jpeg

下图分别展示的是原图经过直方图均衡化后的效果和原图经过 gamma 变换后的效果。9b270217a18d71ddfff224795325aa15.jpeg

df5f360955d7ea743b467fa965eb6d99.jpeg
Gamma 变换.png

2.2 图像降噪

该模块提供的图像降噪算法都是通过图像滤波实现的。目前支持高斯滤波、中值滤波、高斯双边滤波、均值迁移滤波。

下图展示的的是进入图像降噪的页面,并加载了某个图像的原图。446bb5bb8486417320e27ebc06ffa28a.jpeg

下图分别展示的是原图经过高斯滤波、高斯双边滤波、均值迁移滤波后的效果。e5fe8214d4470029e5b7cdb055e73937.jpeg

7a1db77f9451e64f8b10ffc5e2e5c19c.jpeg
高斯双边滤波.png
5392a3c5f8f71232615dcf8c871d0f18.jpeg
均值迁移滤波.png

上述功能的实现,分别是调用 OpenCV 对应的图像滤波函数。

2.3 形态学操作

形态学操作是一种基于形状的图像处理技术,通过使用结构元素对图像进行处理,从而提取图像中的形状、大小等信息。

下面用之前的例子,展示一下如何使用形态学操作。首先,加载一张包含多个苹果的图片。

22a1b63c2be9b1075bce383243bf3278.jpeg
加载原图.png

通过彩色图像分割进行二值化。0c8364c875bfcf45ff0e75c0ca9014ba.jpeg

然后再进行形态学的闭操作。2c169bc96bdf154b7336ad44dd459126.jpeg

以及形态学的开操作。0492ceb906db78d9d7c2da997a830e58.jpeg

这些形态学的操作,有助于对轮廓的进一步分析。da8b074b849581b9bfe1bb087a32300d.jpeg

通过轮廓分析之后,我们在原图中可以找到比较明显的苹果。9b611349d826482f3152a17632a4fc00.jpeg

形态学操作的是为了帮助提取图像中主要的信息。

2.4 模版匹配

模板匹配是一种经典的图像处理技术,用于在一幅图像中查找与另一幅模板图像最匹配的部分。

然而 OpenCV 提供的模版匹配函数有一些局限性,例如当模板图像与目标图像之间存在旋转角度差异,或者模板图像与目标图像的尺寸比例不同时,匹配效果都会变差。另外,在默认情况下,模板匹配函数会返回与模板最相似的一个匹配区域,如果需要支持多目标的匹配则需要通过一定的策略来实现。

这里,Monica 实现了一个支持模版匹配的算法,并且支持旋转,用于多角度、多尺度、多目标的模板匹配。

下面,给出简单的演示,先进入支持模版匹配功能的页面。8b590e76c1628d6aebeabbe7ccae93e8.jpeg

加载一张连连看的图片,选取其中一个作为模版图像,并调整一些参数。74637687cdafb86062da60e3a6e46f97.jpeg

最后执行模版匹配,很快能看到匹配的结果。33aa262b00e70e811369de3e34cc216f.jpeg

三. 功能的实现

其他的功能都比较简单,这里只介绍一下模版匹配的实现。由于模版匹配速度很慢,这是使用并行化的模版匹配,以及能够支持多目标的匹配。

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);
};
#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

;