☰
光学音乐识别数据集全攻略:从选型到标注转换与训练避坑
2026/10/1 23:33:07 网站建设 项目流程

简介:面向光学音乐识别(OMR)研究者的数据集集合,整合了多种风格与历史时期的乐谱图像及标注信息,适用于算法训练、模型评估与预处理流程开发。包内附带 omrdatasettools 工具链,包含图像生成、数据集下载、边界框与符号标注处理等功能,可复现 Homus、DeepScores、Muscima++ 等公开数据集样本,帮助用户快速建立标准化实验流程。资源共 85 个文件,以 PNG 示例图像、Python 脚本、Markdown 文档及配置文件为主,压缩包仅 6.23MB,轻量但功能完整。Python 脚本覆盖生成器、下载器、测试与打包发布流程,PNG 样例直观展示各类乐谱图像效果,适合希望在本地快速搭建 OMR 数据管线的中高级研究者。目前已有 149 人学习浏览。这一集合不仅提供多类数据集入口,还包含完整的项目文档、变更记录、行为准则与 PyPI 发布脚本,从数据处理到工具分发均有覆盖,是开展 OMR 研究与复现实验的实用基础资源。

1. 光学音乐识别数据集集合:先弄清楚这个领域卡在哪

光学音乐识别(Optical Music Recognition,OMR)数据集集合,是把一批用于训练和评估 OMR 系统的乐谱图像数据集集中整理、统一描述的资源。OMR 要解决的核心问题是让机器看懂扫描版或拍摄版的乐谱,像 OCR 之于文字一样,把五线谱、音符、强弱记号、歌词这些视觉元素转换成可检索、可播放、可编辑的结构化内容(MEI、MusicXML 这类格式)。这项技术在数字化馆藏乐谱、音乐教学、古籍乐谱整理等场景里都有实际需求。但 OMR 和 OCR 最大的不同在于:乐谱是二维符号系统,音符的时值由符头形状和符干方向决定,音高由五线谱上的位置决定,同一个符号脱离上下文可能根本没有意义,所以数据集的质量和标注深度直接决定模型上限。

对从业人员来说,这份数据集集合的价值在于替你做了一次领域筛选:不用再分散去各家论文的附录里翻数据集地址,也不用纠结某个数据集到底是单页还是整本、是印刷体还是手写体、标注是像素级还是符号级。适合谁用呢?一类是做乐谱数字化系统的算法工程师,另一类是音乐信息检索方向的研究生,还有一类是想做小规模验证但不想从零扫描标注的独立开发者。接下来的内容,会按数据集的类型、格式、转换方法、评估口径、复用技巧一路拆开讲,每个环节我都会给出可以直接上手的做法。

2. OMR 数据集选型:单页到整本、印刷到手写,到底怎么挑

2.1 先做分类:OMR 数据集不完全是一回事

OMR 数据集从任务形态上可以粗暴分成四类:单页图像分类/检测、序列转录、语义分割、整本乐谱文档级处理。所谓“数据集集合”(Collection)这类项目,常见做法是把这些数据集做成一张索引表,附上图像格式、标注格式、声部数量、是否含歌词、版权状态等字段。实际使用时,从这张表里挑数据集的顺序应该是:先看任务——你是做检测还是识别,再看图像——是印刷还是手写、是单声部还是多声部,最后看标注——是否有像素级标注,还是只有符号级序列。

以我处理过的乐谱数据集为例,PrIMuS 是印刷体单声部乐谱的序列转录数据集,标注是语义化的符号序列;MUSCIMA++ 是手写体乐谱的图形/符号级数据集,带像素级掩码和符号边界框,适合做语义分割或目标检测;Capitan 是整本书级别的数据集,包含完整的乐谱页面和对应的语义标注;DoReMi 则偏向倾斜校正和版面分析,不适合做端到端识别。

如果读者对乐谱种类的差异没概念,最容易踩的坑是拿着印刷体模型去直接推手写体。印刷乐谱的符头形状、连线曲率、符干长度都相对规整;手写乐谱带有个人笔迹变化,同一个作者的同一页都能出现符干长短不一致、符头大小浮动,更不要提墨迹深浅。所以第一层选型逻辑是:你要解决的问题里,输入图像是扫描件还是相机拍摄件,是 19 世纪雕版印刷还是电脑制版后的打印稿,这些直接决定你该用哪个子集。

2.2 标注格式带来的兼容性成本

OMR 数据集的标注格式五花八门,常见的有 PAGE XML、MusicXML、MEI、语义字符串序列,以及针对目标检测的边界框 JSON。PAGE XML 主要用在文档分析层面,描述版面区域、五线谱区域、谱线位置;MEI 是音乐编码标准,适合描述音乐内容本身;MusicXML 更适合交换到打谱软件里回放。至于 PrIMuS 这类数据集,标注通常是一串语义记号字符串——比如“note-whole_G3”,把五线谱位置、时值、音名全部编码成一个符号,模型在训练时把它当成序列生成任务来做。

这意味着什么?意味着你选定数据集后,第一件要做的事不是搭模型,而是统一标注口径。做检测任务,需要的是符号边界框和类别;做语义分割,需要的是像素级掩码;做序列转录,需要的是图像到符号序列的对齐。如果你只有一个计算平台和有限的标注预算,我的建议是优先选带像素级标注的数据集(比如 MUSCIMA++),因为它的标注可以向下兼容成边界框,也可以聚合语义信息生成序列,灵活性最高。反过来,如果只有序列标注,想反推回边界框就非常困难,基本等于重新标注。

2.3 数据集规模与训练实用性的折算

OMR 数据集和 CV 里的 ImageNet 相比,体量小一个量级。通常一个数据集只有几百页到几千页图像,每页的符号数量几十到几百不等。以 PrIMuS 为例,单页训练样本数量在万级,但实际分到每个类别后,低频符号(比如装饰音、踏板记号)可能只出现几十次。所以做 OMR 时,训练一开始就要带上数据增强策略:随机裁剪、透视扰动、加入噪声和墨迹污渍模拟,这些操作能有效避免模型在小数据集上过拟合。

另外一个实际问题是数据集格式的混乱:有的给的是 PNG 原始扫描图,有的给的是 PDF 页面,有的给的是 TIFF 多页文件。我的做法是先把所有图像统一成 PNG,分辨率统一到适合模型输入的尺度(比如 800×600 或按比例缩放),再按数据集来源分别建目录,保留一份 manifest 文件记录原始路径和标注路径,避免后续把来源搞混。这一层整理工作看起来很琐碎,但后面做交叉验证和跨数据集评估时,它的价值会成倍释放。

3. 把数据集转成模型输入:三种落地方案与标注对齐细节

3.1 方案一:目标检测——从 PIC 标注到 YOLO 格式

如果你选的是带符号级边界框的数据集,直接转 YOLO 格式是最快能跑通的做法。以 MUSCIMA++ 为例,它的标注里包含符号类别、边界框和轮廓点集,但 YOLO 只需要归一化的中心点坐标和宽高。需要注意:MUSCIMA++ 的边界框是图形对象的像素范围,有些对象(如符干)是细长条,直接转成水平矩形框会包含大量背景,影响正样本质量。常见做法是先取外接矩形,再在训练时用较大的 IoU 阈值或者改用旋转框目标检测,但旋转框并不是所有框架都方便落地,我一般会先试水平框加上足够强的数据增强,如果 mAP 上不去再换旋转框方案。

# 用 Python 把 PAGE XML 里的边界框转成 YOLO 格式 import xml.etree.ElementTree as ET import os xml_dir = 'path/to/page_xml' out_dir = 'path/to/yolo_labels' os.makedirs(out_dir, exist_ok=True) class_map = {'notehead-filled': 0, 'notehead-empty': 1, 'stem': 2, 'beam': 3} for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() image_width = int(root.find('.//{http://schema.primaresearch.org/PAGE/gui/v1}ImageWidth').text) image_height = int(root.find('.//{http://schema.primaresearch.org/PAGE/gui/v1}ImageHeight').text) with open(os.path.join(out_dir, xml_file.replace('.xml', '.txt')), 'w') as f: for coords in root.iter('{http://schema.primaresearch.org/PAGE/gui/v1}Coords'): points = coords.attrib['points'].split() xs = [int(p.split(',')[0]) for p in points] ys = [int(p.split(',')[1]) for p in points] x_min, x_max, y_min, y_max = min(xs), max(xs), min(ys), max(ys) x_center = (x_min + x_max) / 2 / image_width y_center = (y_min + y_max) / 2 / image_height width = (x_max - x_min) / image_width height = (y_max - y_min) / image_height f.write(f"{class_map[coords.attrib.get('label', 'notehead-filled')]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")

这段代码的作用是把 PAGE XML 里存放的轮廓点集强制转换成 YOLO 需要的归一化矩形框。核心逻辑是先取所有点的最小和最大 x/y,求出外接矩形,再除以图像宽高完成归一化。参数说明里值得关注的是 class_map 字典,类别的映射顺序必须和模型训练配置文件保持一致,否则会出现类别错位——这是初学者最常犯的错:XML 里类别名是字符串,而模型用的是整数索引,对不上时训练不会报错,但推理结果会张冠李戴。另外,PAGE XML 的命名空间前缀在解析时必须写全,用花括号括起来,否则找不到元素。

3.2 方案二:语义分割——一次性干完谱线检测和符号分离

如果你要做的工作包含谱线移除或者版面分析,那就得走语义分割路线。MUSCIMA++ 提供像素级掩码,每个音符头、符干、符杠、连音线都有自己的掩码区域。这里常见的做法是把多类像素掩码合并成几个大类:背景、谱线、音符符号、文本、其他。合并的理由很现实——有些类别(比如谱线)在模型看来并不需要单独成类,把它和背景分离开反而会让损失函数更稳定。

语义分割方案的输出直接用于后处理:比如先分割出谱线掩码,然后在原图上做减法得到去谱线的乐谱图像;或者把音符掩码送到关联算法里,生成符号实例。相比检测方案,语义分割对重叠符号(比如符头压着连线)更鲁棒,因为像素级标注天然能处理遮挡。缺点也很明显:手工修正掩码的成本极高,跨数据集时类别定义可能不一致,比如同一个“符头”,在一个数据集里被拆成“实心符头”和“空心符头”,另一个数据集里统称“符头”。合并类别时一定要做可视化校验,不能只对着类别名合并——我遇到过两个数据集的“文本”含义完全不同的情况,一个是歌词,一个是页眉页码,直接合并会把训练集搞脏。

# 语义分割模型训练时的损失函数选择:以 U-Net 为例 import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, smooth=1e-6): super().__init__() self.smooth = smooth self.bce = nn.BCEWithLogitsLoss() def forward(self, logits, targets): probs = torch.sigmoid(logits) intersection = (probs * targets).sum(dim=(2, 3)) union = probs.sum(dim=(2, 3)) + targets.sum(dim=(2, 3)) dice = (2.0 * intersection + self.smooth) / (union + self.smooth) bce = self.bce(logits, targets) return bce + (1 - dice.mean())

写这个自定义损失是因为 OMR 分割场景里前景像素占比通常远低于背景,纯 BCE 会让模型偏向把一切预测为背景,Dice 项则放大了前景区域的梯度贡献。smooth 参数建议保持默认值,设置太小在首次训练时可能出现除零问题。注意这里 target 是 float 类型的掩码张量,训练时需要同步做归一化和 resize,掩码的插值方式建议用 nearest,避免类别边界被平滑得模糊不清。

3.3 方案三:序列转录——把乐谱当成语言翻译来做

如果目标是直接输出音乐编码(比如 MEI 或语义字符串),就属于图像到序列的转录任务。以 PrIMuS 为代表的序列转录数据集把每一页乐谱图像和对应的符号序列配对,模型结构一般是 CNN 编码器加 Transformer 或 GRU 解码器。这里有一个关键细节:序列标注的顺序不是从左到右简单排列,而是按音乐逻辑顺序排的。例如一个和弦,三个音可能共享一个符干,序列里呈现的顺序和空间顺序不一定一致。所以训练时需要格外注意数据加载器是否每次保持相同的“视图顺序”。

我的做法是把数据集里的原始序列原样使用,不按图像坐标重排,因为重排会破坏音乐语义。实际转换时还需要做一步图像预处理:把原始扫描图先二值化,再裁剪出五线谱区域,否则模型会把扫描件上的污渍当成噪声学进去。

# 从 PrIMuS 语义序列构建字典并生成训练样本 from collections import Counter train_sequences = [] with open('primus_train.txt', 'r') as f: for line in f: image_path, sequence = line.strip().split('\t') train_sequences.append((image_path, sequence.split())) # 统计符号频率,用于构建词表 symbol_counter = Counter() for _, seq in train_sequences: symbol_counter.update(seq) min_freq = 2 vocab = [s for s, c in symbol_counter.items() if c >= min_freq] vocab = ['<pad>', '<sos>', '<eos>', '<unk>'] + vocab symbol2idx = {s: i for i, s in enumerate(vocab)} idx2symbol = {i: s for s, i in symbol2idx.items()}

这里的 min_freq 参数控制在 2 到 5 之间比较合适。设得太低会把低频噪声符号带进词表,增加解码难度;设得太高又会把真实但少见的音乐记号(比如花舌、震音)直接变成<unk>,导致模型永远学不会它们。序列结束符<eos>必须加,否则解码器不知道什么时候停。构建好词表后,需要确认所有训练序列里没有超出 max_length 的样本,我的经验是 PrIMuS 这类数据集里单页最长序列在 300 个左右,把 max_length 设成 400 留出余量,避免训练到一半截断报错。

4. 训练与评估口径:三个指标、两种分割方式,结果才不会失真

4.1 数据划分:整页切割还是随机切块

OMR 模型的评估结果很容易受数据划分方式影响,尤其是目标检测和语义分割任务。我的建议是先用整页图像的划分方式,把每一页完整归入训练集或测试集,不做跨页切块。理由很简单:同一页内的版面风格、墨迹浓度、扫描噪声高度一致,如果训练和测试都包含同一页的不同区域,模型相当于提前看到了“考试环境”,指标会虚高。随机切块更适合做数据增强或者局部符号识别,不适合做最终性能评估。

测试集的比例建议占 20%~25%,同时保证测试集里包含低频符号类别的出现次数不低于阈值。如果某个符号类别在测试集里只出现一两次,模型识别它达到 100% 或者 0% 都没有统计意义——这个在论文里很容易被 “bar-level accuracy” 这种聚合指标掩盖,实际工程里不可信。

4.2 三个核心指标:符号级、音节级、序列级

OMR 常用的评估指标分三个层级:符号级准确率只算单独符号分类对不对;音节级(或小节级)准确率要求同一个小节内所有符号全部正确才算对;序列级准确率要求整页输出和标注完全一致才算对。工程上我通常报告前三者中的符号级和音节级,因为序列级接近不可能达到——受限于行对齐和五线谱分页,哪怕一个音符的时值点错,整页就归零。

符号级准确率对小错误宽容,适合调参阶段对比模型结构;音节级准确率更贴近实用场景,毕竟音乐上的“错音”通常以小节为单位被人感知。除了准确率,还需要关注未识别率(把符号漏掉的比例)和误识别率(把背景当成符号的比例),这两个指标分别对应召回率和精确率的反面。真正上线做数字化时,误识别率更麻烦——漏识别还可以靠人工补,误识别会被系统直接写进结构化文件,后续修改成本高得多。

# 计算符号级准确率与混淆矩阵 python - <<'EOF' from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_true = ['note-G3', 'note-A3', 'rest', 'note-G3'] y_pred = ['note-G3', 'note-A4', 'rest', 'note-G3'] # 符号级准确率 acc = np.mean([t == p for t, p in zip(y_true, y_pred)]) print(f"Symbol-level accuracy: {acc:.3f}") # 混淆矩阵便于观察高频错误对 report = classification_report(y_true, y_pred, zero_division=0) print(report) EOF

这脚本适合在训练过程中快速验证输出格式。有一点要提醒:符号级评估必须按“语义等价”做对齐,比如note-G3和note-G3_quarter如果只是时值表示方式不同,要归一化后再对比,否则你会看到大量其实是正确的预测被判错。深度学习中经常出现模型学会了区分时值、音高、八度,但标注文件里把某个休止符时值漏标了——这种“真值错误”在 OMR 数据集里非常多,评估时要留出人工抽检环节。

4.3 跨数据集验证:真正衡量泛化能力

如果只在单一数据集的测试集上跑出 90% 以上的准确率,先不要高兴太早。OMR 模型的泛化瓶颈通常出现在换数据源时:训练用的是合成乐谱渲染图,测试用的是真实扫描古籍,准确率可能直接掉 20 个百分点。我的做法是至少准备两套评测数据:一套是源数据集留出的测试集,评估模型在分布内表现;另一套是外部数据集(即使只有几十页),评估跨域表现。

这步在标题所述的数据集集合项目中尤其重要——正因为集合了多个来源的数据集,才能做真正的跨数据集验证。具体实现时,我在训练过程中每三个 epoch 就跑一次外部验证集的符号级准确率,并记录到训练日志里。如果外部准确率在停止上升后显著下降,说明模型开始记忆内部噪声,就回退到最优 checkpoint,同时增强数据扰动强度。这套做法能抵消小数据集带来的运气成分,判断模型是否真的学到了可迁移的乐谱结构特征。

5. 数据集使用避坑:格式、缺标注、不均衡的五个教训

5.1 标注错位:XML 里的坐标单位和图像实际像素不一致

这个坑在 OMR 数据集里非常普遍。现象是训练出来的检测模型在推理时,所有边界框都偏左上方,错位幅度固定。原因大多是 PAGE XML 里记录的坐标是在降采样后的缩略图上标注的,或者是 DPI 信息没有写进 XML,而代码默认按原始尺寸换算。解决方法是转换格式前先用一张已知图像手工核对坐标点——在原图上画出标注框,和真值做肉眼比对。尤其是从数据集集合索引里下载的样本,因为来源不统一,很容易混入不同 DPI 的标注批次,建议每个子数据集独立写一个坐标校验脚本,而不是统一处理。

5.2 类别不均衡:音符头远多于装饰音,训练损失被主类吃光

现象是模型把所有符号都预测成音符头,准确率居然还有 80%。原因是主类(符头、符干)占训练样本八成以上,损失函数被主类支配。解决思路不是单纯改损失权重,而是先做类别频率统计,把低频类别(倚音、琶音、踏板标记)挑出来做复制粘贴增强——把低频符号区域裁剪出来,随机贴在空白区域或同页其他位置。需要注意粘帖区域不能覆盖其他真实符号,否则会造成新的标注错误。

5.3 太老的扫描件带有背景伪影,模型把斑点学成特征

现象是模型在干净图像上表现好,一到稍带霉斑、墨渍的古籍扫描件上就疯狂输出假符号。原因是训练时用了过于干净的预处理管线,或者二值化阈值固定,导致背景伪影被当作前景参与训练。解决方法是训练集中混入带噪声的负样本,并且用阈值自适应二值化,而不是全局固定阈值。这条在古籍乐谱数字化项目里尤其重要——真实馆藏扫描件什么噪声都有,数据集集合里那些看起来漂亮的页面并不能代表真实上线环境。

5.4 序列转录的标注顺序和图像上符号顺序不一致

现象是模型输出的序列长度正确,但音节级准确率极低,单看符号级准确率又正常。原因是序列转录数据集的标注顺序遵循音乐逻辑(先从左到右,再从上到下),而模型注意力输出倾向于按图像空间顺序。这类问题没有完全自动的解法,只能在数据预处理时保持原始序列顺序不变,同时在后处理阶段将预测序列重新排序后再计算正确率。评估时要明确说明用了哪种对齐方式,否则和别人论文里的数字没有可比性。

5.5 版权与使用边界:数据集集合里的数据未必全部可以商用

这个坑不在技术,但比技术更致命。现象是模型开发顺利上线前被法务叫停。原因是部分数据集来自图书馆数字化项目或者受版权保护的出版物,使用条款只允许学术研究。解决方法是选型阶段就阅读每个数据集的许可证,单独记录是否允许商用、是否必须注明来源、是否允许修改后分发。整理成一张清单放在工程文档里,而不是散落在各处。数据集集合项目一般会标注这些信息,但有时也会漏,必须自己核对原始站点。

6. 进阶:拿你自己的扫描件扩展数据集,迁移与清洗一条龙

当你已经能在现有数据集集合上跑通训练和评估,下一件值得做的事就是用自己的扫描件把模型逼到真实场景。这里给一个可复制的扩展流程:第一步,把扫描件按页切割并命名;第二步,用现有的检测模型做预标注,生成粗略边界框;第三步,人工抽检和修正错误框;第四步,把修正后的框作为伪标注,加入训练集做一轮微调;第五步,重复一两次直到模型在误检率上不再有明显下降。

具体到操作,一定要把预标注置信度阈值设低而不是设高。阈值设成 0.3 会多出一堆误检框,但至少不会漏掉真实符号;你需要在修正阶段把误检的框删掉,这个操作比手工画新框快得多。微调时的学习率建议比从头训练小一个量级,比如原训练用 1e-3,微调用 1e-4,并且只解冻解码器和最后的卷积层,避免破坏已经学好的底层视觉特征。如果发现微调后在新数据上识别率反而下降,立刻回退到微调前模型,再用数据增强强度减半的方式重试。

# 预标注过滤与合并脚本:控制伪标注质量 import json raw_preds = json.load(open('raw_predictions.json')) filtered = [] max_overlap = 0.4 # 与高置信度框重叠超过此值则丢弃 high_conf = [b for b in raw_preds if b['confidence'] > 0.7] low_conf = [b for b in raw_preds if b['confidence'] <= 0.7] def iou(a, b): x1 = max(a['x'], b['x']); y1 = max(a['y'], b['y']) x2 = min(a['x'] + a['w'], b['x'] + b['w']) y2 = min(a['y'] + a['h'], b['y'] + b['h']) inter = max(0, x2 - x1) * max(0, y2 - y1) union = a['w'] * a['h'] + b['w'] * b['h'] - inter return inter / union if union > 0 else 0 for box in low_conf: overlap_flag = any(iou(box, ref) > max_overlap for ref in high_conf) if not overlap_flag: filtered.append(box) all_boxes = high_conf + filtered

脚本的逻辑是保留置信度高于 0.7 的检测结果,对低置信度结果用 IoU 过滤,避免与高置信度框重叠的重复框污染训练标签。max_overlap 参数是核心调节项:设太大会把一些确实存在的、与邻近符号重叠的低置信度符号误删,设太小又会让重复框大量保留。我在不同扫描质量上试下来 0.4 是个折中值,如果你手里的扫描件更干净可以放宽到 0.5,更脏则收紧到 0.3。最终得到的就是一份可用于微调的伪标注集,再配合人工抽检修正,就能在几天内把模型逐步迁移到你的目标场景里。

最后说一个我自己的习惯:不管新做的伪标注效果看着多顺,我始终会在每个数据集来源上保留一个单独的验证集,绝对不混合训练。原因很简单,一旦混合训练,模型性能的波动会让人分不清是数据变好还是模型变好。数据集的整理工作要像实验记录本一样,每天发生的改动都能追溯回具体文件名。 OMR 这种小数据领域,数据整理和模型调优几乎同等重要。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询