跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++AI算法

OpenCV 形态学操作与乐谱线条提取

介绍 OpenCV 中的形态学操作,包括膨胀与侵蚀原理及结构元素构建。通过 C++ 实现从乐谱图像中提取水平与垂直线条的过程,涵盖灰度转换、自适应阈值处理、结构元素创建及形态学运算步骤,最终展示边缘提取与平滑处理结果。重点讲解了如何通过调整结构元素尺寸来定向提取特定方向的线条,并提供了完整的代码示例与参数调优建议。

全栈工匠发布于 2025/2/7更新于 2026/10/25105 浏览
OpenCV 形态学操作与乐谱线条提取

OpenCV 形态学操作与乐谱线条提取

目标

在本教程中,您将学习如何应用两个非常常见的形态算子(即膨胀和侵蚀),并创建自定义核,以便在水平轴和垂直轴上提取直线。为此,您将使用以下 OpenCV 函数:

  • cv::dilate (膨胀)
  • cv::erode (侵蚀)
  • cv::getStructuringElement (获取结构元素)
  • cv::morphologyEx (形态学变换)

在一个示例中,您的目标是从乐谱图像中提取音符之间的线条,从而分离出音符本身。

理论

形态学操作

形态学是一组图像处理操作,它们基于预定义的结构元素(也称为内核)处理图像。输出图像中每个像素的值基于输入图像中相应像素与其相邻像素的比较。通过选择内核的大小和形状,您可以构建对输入图像的特定形状敏感的形态操作。

两个最基本的形态学操作是扩张和侵蚀。膨胀将像素添加到图像中物体的边界上,而侵蚀则恰恰相反。添加或删除的像素量分别取决于用于处理图像的结构元素的大小和形状。通常,这两个操作遵循的规则如下:

膨胀 (Dilation):输出像素的值是结构元素大小和形状内所有像素的最大值。例如,在二进制图像中,如果输入图像中属于内核范围的任何像素设置为值 1,则输出图像的相应像素也将设置为 1。后者适用于任何类型的图像(例如灰度、BGR 等)。膨胀操作常用于填充物体内部的小孔洞或连接邻近的物体。

![图:二值图像上的膨胀效果演示]

侵蚀 (Erosion):反之亦然。输出像素的值是结构元素大小和形状范围内所有像素的最小值。请看下面的示例图:侵蚀操作常用于去除小噪点或断开两个相连的物体。

![图:二值图像上的侵蚀效果演示]

结构元素

如上所述,一般来说,在任何形态学操作中,用于探测输入图像的结构元素是最重要的部分。

结构元素是一个仅由 0 和 1 组成的矩阵,可以具有任意形状和大小。通常比正在处理的图像小得多,而值为 1 的像素定义邻域。结构元素的中心像素(称为原点)标识感兴趣的像素 - 正在处理的像素。

例如,下面演示了 7x7 大小的菱形结构元素。

![图:菱形结构元素及其起源]

结构元素可以具有许多常见的形状,例如线条、菱形、圆盘、周期线以及圆形和大小。通常,您选择与要在输入图像中处理/提取的对象具有相同大小和形状的结构元素。例如,要查找图像中的线条,请创建一个线性结构元素,稍后将看到。

对于乐谱提取任务,我们需要区分水平线和垂直线。因此,我们将创建长宽比极端的矩形结构元素:一个宽度大高度小的元素用于检测水平线,一个宽度小高度大的元素用于检测垂直线。

代码示例 C++

本教程代码如下所示。代码基于 OpenCV 4.x 版本编写,确保兼容性和稳定性。

#include <opencv2/opencv.hpp>
#include <iostream>

// 辅助函数:显示图像并等待按键关闭窗口
void show_wait_destroy(const char* winname, const cv::Mat& img) {
    cv::imshow(winname, img);
    int key = cv::waitKey(0);
    if (key == 'q' || key == 'Q') {
        return;
    }
    cv::destroyWindow(winname);
}

int main(int argc, char** argv) {
    // 1. 解析命令行参数加载图像
    std::string input_path = "music_sheet.png"; // 默认路径,实际使用时可替换为 argv[1]
    if (argc > 1) {
        input_path = argv[1];
    }

    cv::Mat src = cv::imread(input_path);
    if (src.empty()) {
        std::cout << "无法打开或找到图像!" << std::endl;
        std::cout << "Usage: " << argv[0] << " <image_path>" << std::endl;
        return -1;
    }

    // 显示源图像
    show_wait_destroy("Source", src);

    // 2. 将源图像转换为灰色(如果尚未转换为灰色)
    cv::Mat gray;
    if (src.channels() == 3) {
        cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
    } else {
        gray = src.clone();
    }
    show_wait_destroy("Gray", gray);

    // 3. 灰度到二进制图像
    // 在灰色 bitwise_not 应用 adaptiveThreshold,注意 ~ 符号
    // 这里使用自适应阈值来增强对比度,适合光照不均的乐谱
    cv::Mat bw;
    // 参数说明:src, dst, maxValue, adaptiveMethod, thresholdType, blockSize, C
    cv::adaptiveThreshold(~gray, bw, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C,
                          cv::THRESH_BINARY_INV, 15, -2);
    show_wait_destroy("Binary", bw);

    // 4. 创建将用于提取水平线和垂直线的图像副本
    cv::Mat horizontal = bw.clone();
    cv::Mat vertical = bw.clone();

    // 5. 提取水平线
    // 在水平轴上指定大小,通常为列数的 1/30 左右
    int horizontal_size = horizontal.cols / 30;
    // 创建用于通过形态操作提取水平线的结构元素
    // MORPH_RECT 表示矩形,(-1, -1) 表示自动计算
    cv::Mat horizontalStructure = cv::getStructuringElement(
        cv::MORPH_RECT, cv::Size(horizontal_size, 1));

    // 应用形态学操作:先腐蚀后膨胀(开运算)以去除噪声,或先膨胀后腐蚀(闭运算)
    // 此处为了提取线条,通常使用腐蚀去除非水平部分,再膨胀恢复
    cv::erode(horizontal, horizontal, horizontalStructure);
    cv::dilate(horizontal, horizontal, horizontalStructure);
    
    show_wait_destroy("Horizontal Lines", horizontal);

    // 6. 提取垂直线
    // 指定垂直轴上的大小,通常为行数的 1/30 左右
    int vertical_size = vertical.rows / 30;
    // 创建结构元素,用于通过形态操作提取垂直线
    cv::Mat verticalStructure = cv::getStructuringElement(
        cv::MORPH_RECT, cv::Size(1, vertical_size));

    // 应用形态学操作
    cv::erode(vertical, vertical, verticalStructure);
    cv::dilate(vertical, vertical, verticalStructure);
    
    show_wait_destroy("Vertical Lines", vertical);

    // 7. 细化边缘/结果优化
    // 正如你所看到的,我们快到了。但是,在这一点上,您会注意到音符的边缘有点粗糙。
    // 因此,我们需要优化边缘以获得更平滑的结果。
    // 逻辑步骤:
    // 1. 提取边
    // 2. 扩张(边缘)
    // 3. src.copyTo(平滑)
    // 4. 模糊平滑 IMG
    // 5. smooth.copyTo(src,edges)

    // 第 1 步:提取边
    cv::Mat edges;
    // 再次使用阈值处理垂直图像以获取清晰的边缘掩码
    cv::adaptiveThreshold(vertical, edges, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C,
                          cv::THRESH_BINARY, 3, -2);
    show_wait_destroy("Edges", edges);

    // 步骤 2:膨胀边缘
    cv::Mat kernel = cv::Mat::ones(2, 2, CV_8U);
    cv::dilate(edges, edges, kernel);
    show_wait_destroy("Dilated Edges", edges);

    // 步骤 3:复制原始垂直图像到平滑变量
    cv::Mat smooth;
    vertical.copyTo(smooth);

    // 步骤 4:模糊平滑图像
    cv::GaussianBlur(smooth, smooth, cv::Size(2, 2), 0);

    // 步骤 5:将平滑后的图像与边缘结合
    // 使用位运算保留边缘信息
    smooth &= edges;
    
    // 显示最终结果
    show_wait_destroy("Smoothed Final", smooth);

    return 0;
}

结果说明

从获取图像开始,以下是各阶段的详细处理流程及预期结果分析。

加载图像

首先使用 CommandLineParser 或直接读取文件路径加载图像。确保图像路径正确且格式支持(如 PNG, JPG)。

cv::Mat src = cv::imread(argv[1]);
if (src.empty()) {
    // 错误处理
}

![图:原始乐谱图像示例]

灰度转换

将源图像转换为灰色(如果尚未转换为灰色)。这减少了数据维度,便于后续的二值化处理。

cv::Mat gray;
if (src.channels() == 3) {
    cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
} else {
    gray = src.clone();
}

![图:灰度图像示例]

灰度到二进制图像

在灰色 bitwise_not 应用 adaptiveThreshold,注意 ~ 符号。自适应阈值能更好地处理光照不均匀的情况,这对于扫描的乐谱尤为重要。

cv::Mat bw;
cv::adaptiveThreshold(~gray, bw, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C,
                      cv::THRESH_BINARY_INV, 15, -2);

![图:二值化图像示例]

输出图像

现在我们准备应用形态学运算来提取水平线和垂直线,从而将音符与乐谱分开,但首先让我们初始化我们将为此目的使用的输出图像。

cv::Mat horizontal = bw.clone();
cv::Mat vertical = bw.clone();

结构元素设计

正如我们在理论中指定的那样,为了提取我们想要的对象,我们需要创建相应的结构元素。由于我们要提取水平线,因此用于此目的的相应结构元素将具有以下形状:长条形矩形。

在源代码中,这由以下代码片段表示:

int horizontal_size = horizontal.cols / 30;
cv::Mat horizontalStructure = cv::getStructuringElement(
    cv::MORPH_RECT, cv::Size(horizontal_size, 1));

应用形态学操作后,只有水平方向的线条会被保留,其他方向的噪声(如音符)会被腐蚀掉。

![图:提取的水平线示例]

这同样适用于具有相应结构元素的垂直线:

int vertical_size = vertical.rows / 30;
cv::Mat verticalStructure = cv::getStructuringElement(
    cv::MORPH_RECT, cv::Size(1, vertical_size));

![图:提取的垂直线示例]

细化边缘/结果优化

正如你所看到的,我们快到了。但是,在这一点上,您会注意到音符的边缘有点粗糙。因此,我们需要优化边缘以获得更平滑的结果。

  1. 提取边:通过阈值处理获得清晰的边缘掩码。
  2. 扩张(边缘):使用小核膨胀边缘,使其更连续。
  3. 平滑处理:利用高斯模糊减少锯齿。
  4. 融合:将平滑后的图像与边缘掩码结合。

最终结果显示了经过优化的乐谱线条,去除了大部分干扰噪声。

![图:平滑 - 最终结果示例]

常见问题与注意事项

  1. 结构元素尺寸调整:horizontal_size 和 vertical_size 的比例(如 /30)需要根据具体图像分辨率进行调整。如果线条提取不完整,可适当增大比例;如果误提取了音符,应减小比例。
  2. 阈值方法选择:ADAPTIVE_THRESH_GAUSSIAN_C 适用于光照不均的场景,若图像光照均匀,也可尝试 THRESH_BINARY 配合固定阈值。
  3. 性能优化:对于高分辨率图像,形态学操作可能较慢。建议先缩放图像进行处理,或使用并行计算库。
  4. 内存管理:确保及时释放中间变量(如 bw, gray),避免内存泄漏,特别是在循环处理多张图片时。

总结

本文详细介绍了 OpenCV 中形态学操作的基本原理及其在乐谱线条提取中的应用。通过合理设计结构元素并结合腐蚀、膨胀操作,可以有效分离图像中的几何特征。掌握这些基础技术有助于进一步开发复杂的文档分析和 OCR 系统。

目录

  1. OpenCV 形态学操作与乐谱线条提取
  2. 目标
  3. 理论
  4. 形态学操作
  5. 结构元素
  6. 代码示例 C++
  7. 结果说明
  8. 加载图像
  9. 灰度转换
  10. 灰度到二进制图像
  11. 输出图像
  12. 结构元素设计
  13. 细化边缘/结果优化
  14. 常见问题与注意事项
  15. 总结

更多推荐文章

查看全部
  • FPGA 嵌入式块存储器 RAM:原理与实现指南
  • Linux 系统编程:封装基础文件操作库与理解标准错误流 stderr
  • WebView 技术详解:原理、场景与性能安全挑战
  • OpenClaw 跨平台 AI 助手完全使用指南:从入门到进阶
  • CosyVoice 安装 openai-whisper 报错 ModuleNotFoundError: No module named'pkg_resources'
  • C++ 多态底层实现机制深度解析
  • Aspen 15.0 升级亮点:AI 建模与绿氢适配的流程模拟功能解析
  • 普通人成为黑客的十个基础学习步骤
  • 分布式文件系统 HDFS 核心概念详解:块、名称节点与数据节点
  • Selenium Web 自动化测试入门与实战指南
  • 知网 AIGC 检测原理与降重处理指南
  • GitHub Copilot Agent 模式实战指南与避坑建议
  • FPGA 视频 4 分屏系统架构详解
  • SpringAI 全栈开发 + RAG 检索增强实战
  • OpenClaw 架构原理与核心机制深度解析
  • OpenClaw 配置多 Agent 及多平台机器人(QQ/飞书)
  • 解析前端反爬日志:精准补全 Window 对象缺失属性
  • 华为 OD 机试 2025C 卷停车费用统计算法题
  • faster-whisper 技术解析:高效语音识别的工程实践
  • SpringBoot 集成 WebSSH 实现远程服务器连接

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online