资讯动态

光学音乐识别(OMR)数据集构建与工程实践

发布时间:2026/9/16 6:06:59 来源:尧图企业网站定制
简介面向光学音乐识别OMR研究的数据集集合提供多种风格、时期和作曲家的乐谱图像用于训练与评估音符检测、符号分类等识别模型适合计算机视觉与音乐信息检索领域的开发者使用。压缩包共85个文件涵盖png图像、py脚本、md文档、csv数据表、xml配置等类型其中png为各数据集样例图py为图像生成与下载工具md为说明文档整体大小6.23MB目录结构清晰。已有149人学习可用于快速了解OMR数据集生态。内含Homus、Capitan、Audiveris、Muscima等多个公开数据集的生成与下载工具并提供README、代码行为准则、变更日志等开源标准文件方便了解项目背景与使用规范。从数据获取、预处理到标注可视化均有配套脚本覆盖可帮助研究者快速搭建OMR实验流程。1. 光学音乐识别数据集先解决“喂什么”再谈模型怎么改光学音乐识别Optical Music Recognition, OMR在文档分析领域里是一个相当“挑食”的方向。同样是图像识别车牌、票据的公开数据集都够你打样好几轮而乐谱识别上的公开数据集集合却小得多而且分散在不同研究组、不同格式和不同许可之下。很多团队把模型从论文里复现出来之后第一轮评测就崩了问题往往不是网络结构而是训练数据里的乐谱风格、扫描噪声和标注粒度跟真实场景对不上。下面按数据集合这一层展开先盘点能用的公开集合再给出自己造数据、切分、增强和接进现有管线的方法。适合两类人一类在做乐谱数字化另一类打算把OMR作为OCR的延伸方向去评估可行性。2. OMR公开数据集选型印刷、手写、合成三类怎么权衡光学音乐识别的公开数据集不像通用OCR那样有一个“ImageNet”它更接近一批小规模、强特化的实验集合。按图像来源来分绝大多数能碰到的OMR数据集可以归成三类扫描的印刷乐谱、手写乐谱、渲染器生成的合成乐谱。印刷扫描件接近真实数字化环境但标注时需要人工校对手写数据样本量小笔画风格成为主要变化源合成数据可以按需批量生成但仿真程度决定迁移效果。理解这三类的差别比背一个数据集的名字更重要。2.1 三类公开集合的定位与命名印刷乐谱方向的公开集合通常会提供整页或整行图像以及对应的语义真值比如MEI或MusicXML。手写数据集更常见的是符号级别的裁剪图配套分类或分割标签用于训练音符识别头而不是端到端模型。合成数据则多来自MuseScore、LilyPond等排版引擎的渲染结果带完整排版信息适合做预训练和切分模型。实际使用时它们不是互斥的常见做法是先合成大量数据预训练再用少量扫描数据微调。类别常见公开集合举例图像形式常见标注粒度优先解决的问题印刷单声部PrIMuS、DeepScores等整页/整行位图符号序列、边界框端到端序列识别、页到语义手写乐谱Homus、MUSCIMA等裁剪符号/手写行符号级分割、关系符号分类、笔迹鲁棒合成数据各类项目自渲染可控制PNG自动生成框与序列预训练、数据增强、切分提示上面这些集合的侧重点不同引用前先看原文的描述不要默认它们都带页面级边界框。拿到一份数据集后我一般会先做一个快速统计避免被目录里的文件数量误导。假设目录里每个子集一个文件夹可以用下面这段脚本列出图像和标注文件的规模from pathlib import Path ds_root Path(./omr_datasets) for sub in sorted(ds_root.iterdir()): images list(sub.rglob(*.png)) list(sub.rglob(*.jpg)) anns list(sub.rglob(*.mei)) list(sub.rglob(*.musicxml)) jsonl list(sub.rglob(*.jsonl)) print(f{sub.name:20s} images{len(images):4d} mei{len(anns):3d} jsonl{len(jsonl):3d})代码里的rglob会递归匹配所有子目录mei和musicxml通常表示语义真值jsonl可能是检测框或序列标注。如果images和anns数量差很多不要急着下结论先看是不是一个PDF拆出多张图或者MEI文件合并了多页。2.2 选型前先回答三个问题第一个问题是模态。你手头的待识别材料是扫描的旧书还是软件截图的乐谱如果偏向前者直接用合成数据训练会出现明显的噪声差异因为纸面扫描件里有页码透印、纸张纹理和装订阴影。第二个问题是标注深度。只做五线谱检测页面级框够了要做音符序列识别就必须有与图形对齐的音乐语义否则模型学到的是“音符形状”而不是“音高节奏”。第三个问题是许可。公开数据集里有一部分是研究用途允许训练但明确禁止再分发如果你的产品要商用需要把这些集合和自建数据分开管理并记录来源。这三个问题直接决定后续的工程路径。比如为了控制成本可以只用公开的印刷扫描集做验证用合成数据做训练主体但如果目标材料全是手写这种组合就会失真。反过来如果你只需要验证一个音符分类网络那么公开的手写符号裁剪集就足够不需要去碰整页标注。2.3 数据清单模板和许可核查每次拿到数据我会建一个简单的CSV清单记录数据集的名称、子目录、图像后缀、真值后缀、许可证关键词、是否允许商用、是否需要署名。最后四项可以用一段脚本粗扫for f in $(find ./datasets -name LICENSE -o -name COPYING -o -name README*); do echo $f grep -i -E commercial|redistrib|cc-by|cc-by-nc|research only $f | head -5 done这个命令把常见的许可关键字从文档里捞出来避免人工翻几百个README。注意grep只能辅助如果关键字缺失要去源头页面确认。许可问题比模型精度更严重因为数据集合一旦用错后面整个模型都不能上线。3. 自建光学音乐识别数据集用渲染器和合成噪声扩充数据集合当公开数据集覆盖不到你的目标风格时最常见的方法是自建合成数据而不是去硬标几万张图。这里的“自建”不是手动画框而是让乐谱排版引擎生成图像同时从源文件里导出真值。你只要控制少数几个参数就能得到一套分布可控、标注相对一致的试验数据用来验证网络结构或做预训练都够用。3.1 为什么程序渲染优于手工标注乐谱图像和普通文档有一个明显差异图形元素密集、重叠规则复杂手写标注边界框很容易漏掉带符干跨行、连音线跨页的符号。而一份MusicXML或MEI文件里已经包含了音符的时值、音高、临时的升降号还知道这些符号在谱表上的布局参数。从这类源文件去渲染图像等于让同一个“真值”同时出现在两个模态里省掉了人工对齐的过程。渲染器的选择上LilyPond是常见的开源渲染器输出质量高、命令行接口清晰MuseScore也能从命令行导出胜在支持MusicXML导入导出。工程上通常把两者都试一下选择跟目标扫描件相近的字体和间距。3.2 用LilyPond渲染PNG的最小命令假设你手上有一个sample.ly文件想生成训练图像最基础命令是lilypond -dbackendgs -dresolution300 --png -o output/sample sample.ly参数说明-dbackendgs把排版结果交给Ghostscript输出避免直接生成PDF再转图片时出现字体替换-dresolution300是扫描数字化常用的DPI太低会丢失连音线的细线--png直接输出PNG-o output/sample控制输出文件前缀后面会得到output/sample.png和可能有output/sample.pdf。如果要对多个文件批量操作我会套一个Python循环from pathlib import Path import subprocess def render_ly_to_png(ly_path: Path, out_dir: Path, dpi: int 300): out_prefix out_dir / ly_path.stem cmd [ lilypond, f-dresolution{dpi}, --png, -o, str(out_prefix), str(ly_path) ] subprocess.run(cmd, checkTrue, capture_outputTrue) return out_prefix.with_suffix(.png)subprocess.run(..., checkTrue)会在渲染失败时直接抛异常避免后续用残缺图片训练capture_outputTrue把日志收进来方便排错。3.3 把整页切成谱表行并保留真值渲染出来的PNG通常是整页直接训练会浪费大量背景和页边距常见的做法是先切出一个个谱表行。切分依据是五线谱形成的横向密集区域可以用水平投影做也就是把每一行的像素值加起来找到连续高亮的行区间import cv2 import numpy as np def find_staff_rows(img_path: str, row_threshold: float 0.02): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) _, bin_img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) row_sum bin_img.sum(axis1) peak row_sum.max() mask (row_sum peak * row_threshold).astype(np.uint8) diff np.diff(np.r_[0, mask, 0]) starts np.where(diff 1)[0] ends np.where(diff -1)[0] return [(int(s), int(e)) for s, e in zip(starts, ends)]这段代码先做Otsu二值化使五线谱线变成白色再对行求和五线谱区域的投影值会明显高于空白最后用差分找到连续段。row_threshold设成0.02只在峰值很高时切块如果你的图像有标题或歌词可能需要调小这个值。切完后训练用的真值不能只保留整页需要按切出的行区间去裁剪源文件对应的范围。如果标注是JSONL里的边界框就根据框中心的y坐标分到最近的同名行区间。3.4 给合成图叠加扫描噪声合成的干净PNG直接训练往往会过拟合因为真实扫描件有灰度变化和噪点。一个低成本做法是在图像上叠加噪声层真值保持不变噪声项常用值作用高斯模糊核大小12像素模拟轻微失焦亮度偏移±20模拟扫描仪曝光差异椒盐噪声密度0.00010.001模拟纸面杂质透视扰动0.5%1%模拟装订扭曲对应的OpenCV实现import cv2 import numpy as np import random def simulate_scan(img: np.ndarray, blur1, brightness20, noise0.0005) - np.ndarray: if blur: img cv2.GaussianBlur(img, (blur * 2 1, blur * 2 1), 0) img cv2.add(img, random.randint(-brightness, brightness)) mask np.random.random(img.shape[:2]) noise img[mask] 0 return img亮度偏移通过cv2.add完成避免uint8溢出噪声直接置黑模拟纸点。这里没有对边界框做变换因为噪声和亮度不会改变符号位置这正是比旋转、透视更稳的训练增强方式。如果需要旋转边界框就必须同步变换否则模型学到的检测框会整体偏移。4. 数据集划分、增强与质量校验让OMR数据可训练数据集合再大划分不对也会把评估结果做得很虚。乐谱和普通图片有一点不同同一首作品的多个页面之间音符排列、谱号、调号高度相似如果训练集和验证集各放同一作品的不同页模型很可能只是记住了这份作品的排版而不是学会了识别规则。因此划分维度的第一选择是“作品ID”或“作曲家曲目标题”而不是简单按文件序号。4.1 按作品划分而不是按页划分先假设每一条JSONL记录里都有一个work_id字段标识它来自哪首作品。用字典把相同work_id的所有图像归到一起再按作品列表随机划分import json from pathlib import Path from collections import defaultdict import random def group_by_work(ann_dir: Path): groups defaultdict(list) for f in ann_dir.glob(*.jsonl): with open(f, r, encodingutf-8) as fh: for line in fh: rec json.loads(line) groups[rec[work_id]].append(rec[image]) return groups def split_works(groups, val_ratio0.2, seed42): works list(groups.keys()) random.Random(seed).shuffle(works) split_at int(len(works) * (1 - val_ratio)) train_keys, val_keys works[:split_at], works[split_at:] return train_keys, val_keys返回的是作品ID列表之后训练时再根据ID去取对应的图像而不是把图像文件直接复制一份。这样做可以保证同一个作品只出现在一个集合里评估出的指标不会虚高。val_ratio设0.2还是0.1取决于样本总数作品数少于10时尽量用交叉验证。4.2 OMR增强参数表与边界框同步变换做完划分后下一步是增强。OMR增强和普通OCR类似但要防止破坏五线谱的平行关系。下面这组参数适合以300 DPI扫描件为主的乐谱图像增强操作参数范围注意事项随机旋转±2°超过3°会导致五线谱行切分变难透视扭曲0.0030.007需同步重算边界框游程噪声细线/斑点密度0.0005模拟破损扫描高斯模糊σ0.30.8保留线宽亮度/对比度0.91.1应围绕原图亮度均值波动和3.4的噪声叠加不同这里的旋转、透视会改动符号位置所以必须同步变换边界框。实现时用一个函数同时接收图像和框列表返回变换后的两组数据import cv2 def rotate_with_boxes(img, boxes, angle): h, w img.shape[:2] M cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) img_rot cv2.warpAffine(img, M, (w, h), flagscv2.INTER_LINEAR) new_boxes [] for x, y, bw, bh in boxes: new_boxes.append(rotate_box(x, y, bw, bh, M)) return img_rot, new_boxes这里的rotate_box用四个角点经过矩阵变换后再取外接矩形实现。如果图省事只转图像不转框最终训练到的检测框会整体偏移后处理阶段很难拉回来。4.3 用文件名和标注对齐做自洽检查数据质量校验不是可选项。最常遇到的问题是图像文件名和标注文件名不匹配或者一个JSONL里出现了不存在的图像这些都会让训练在DataLoader层崩溃。可以用下面的脚本扫一遍def check_ann_image_consistency(ann_dir: Path, img_dir: Path): ann_names set() for f in ann_dir.glob(*.jsonl): with open(f, encodingutf-8) as fh: for line in fh: rec json.loads(line) ann_names.add((f.stem, rec[image])) real_images {p.stem: p for p in img_dir.rglob(*.png)} missing [name for name in ann_names if name not in real_images] orphan [p for p in img_dir.rglob(*.png) if p.stem not in ann_names] return missing, orphan这个检查按文件名的stem比较返回两边的差集。注意glob(*.jsonl)只读当前目录如果标注按作品分目录需要先递归收集所有JSONLrglob(*.png)则会把子目录都扫描进来。缺失项多于几十条时先对齐文件再训练不要靠DataLoader的异常输出来排查。4.4 用前景像素占比排查样本分布偏差除了文件数量我还会统计每张图的平均前景像素占比也就是二值化后白色像素的行均值。如果训练集和验证集的前景占比差超过0.3说明采集偏了可能是扫描亮度不一致也可能是切分时把页边距带进来了。这个指标不用写进loss但它能提醒你增强参数是否改变得太猛。5. 把数据集接到识别管线最小转格式与评估指标数据集合做完清洗最终要喂给网络。OMR领域里常见的一级任务拆法是“检测识别”检测模块给出符号边界框识别模块把裁剪图映射成符号序列。为了快速验证一个数据集合是否可用我习惯先把它转成COCO格式跑一个最小检测器再用符号序列的编辑距离卡一个基线。5.1 把标注导出成COCO格式COCO格式把图像元信息和标注对象分开检测器都很吃这一套。下面这个函数把JSONL里的边界框转成COCO的annotation元素def jsonl_to_coco(jsonl_path, img_dir, out_path): dataset {images: [], annotations: [], categories: [{id: 1, name: music_symbol}]} img_id 0 ann_id 0 with open(jsonl_path, encodingutf-8) as fh: for line in fh: rec json.loads(line) img_path img_dir / rec[image] h, w cv2.imread(str(img_path)).shape[:2] dataset[images].append({id: img_id, file_name: rec[image], width: w, height: h}) for box, label in zip(rec[boxes], rec[labels]): dataset[annotations].append({ id: ann_id, image_id: img_id, category_id: 1, bbox: [box[0], box[1], box[2] - box[0], box[3] - box[1]], area: (box[2] - box[0]) * (box[3] - box[1]), iscrowd: 0, category_label: label }) ann_id 1 img_id 1 with open(out_path, w, encodingutf-8) as fh: json.dump(dataset, fh)这里假设boxes已经存成[x1, y1, x2, y2]并在转换为COCO时减掉左上角得到宽高。为了保留标签我在annotation里额外加了一个category_label字段虽然COCO官方不要求但对后续分析识别错误很有用。5.2 用编辑距离评估识别结果检测之后再接一个识别头我们希望比较预测序列和真值序列的变化。编辑距离比ACC更能容忍对齐偏移def edit_distance(a, b): dp [[0] * (len(b) 1) for _ in range(len(a) 1)] for i in range(len(a) 1): dp[i][0] i for j in range(len(b) 1): dp[0][j] j for i in range(1, len(a) 1): for j in range(1, len(b) 1): cost 0 if a[i - 1] b[j - 1] else 1 dp[i][j] min(dp[i - 1][j] 1, dp[i][j - 1] 1, dp[i - 1][j - 1] cost) return dp[-1][-1]动态规划是经典实现符号序列较短时足够快如果序列上千可以换用带相位限制的Wagner-Fischer。在数据集验证阶段我会预先算出一组随机预测结果的编辑距离作为下限之后任何模型只要在同一划分上不低于这个分数说明模型还没有学到该数据里的基本符号。这个分数可以写进数据迭代的CI检查里每次变更标注后自动跑一遍低于底线的提交直接拦下来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价