图像编辑器 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 常见算法的快速调参的能力

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

下面展示该模块的入口


展示该模块的入口.png

以及该模块的首页


该模块的首页.png

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

2.1 图像增强

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

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


图像增强.png

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


直方图均衡化.png
Gamma 变换.png

2.2 图像降噪

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

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


图像降噪.png

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


高斯滤波.png
高斯双边滤波.png
均值迁移滤波.png

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

2.3 形态学操作

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

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

加载原图.png

通过彩色图像分割进行二值化。


二值化.png

然后再进行形态学的闭操作。


闭操作.png

以及形态学的开操作。


开操作.png

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


准备轮廓分析.png

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


完成轮廓分析.png

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

2.4 模版匹配

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

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

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

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


模版匹配.png

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


加载目标图像和模版图像.png

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


模版匹配的结果.png

三. 功能的实现

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

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"

using namespace 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);
    } else if (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 (const auto& 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,124评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,453评论 3 404
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,386评论 0 368
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,136评论 1 301
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,142评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,593评论 1 315
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,958评论 3 429
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,944评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,477评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,512评论 3 346
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,639评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,227评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,971评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,397评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,550评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,203评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,713评论 2 366

推荐阅读更多精彩内容