ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OMR数据集实战指南:从下载校验到YOLOv8训练与MIDI生成

OMR数据集实战指南:从下载校验到YOLOv8训练与MIDI生成 简介本资源是面向光学音乐识别OMR研究者与算法开发者的高质量数据集集合及配套工具库专为训练、测试和评估乐谱图像识别模型而设计适用于计算机视觉、音乐信息检索等方向的中高级开发者与高校科研人员。压缩包共85个文件涵盖33张乐谱图像PNG、25个Python工具脚本含Homus、MUSCIMA、Capitan等主流数据集的图像生成器与下载器、5份Markdown文档含README、行为准则与变更日志以及配置类文件cfg/yml/sh/ps1等完整支撑从数据获取、预处理到模型输入的全流程整体包体仅6.23MB轻量易用。目前已有149人学习下载。用户可直接调用omrdatasettools模块批量生成标准化训练样本复用现成的符号标注路径、测量可视化与bbox导出功能并参考samples目录下18种典型乐谱图像如DeepScores、Audiveris、IMSLP等开展跨数据集泛化实验。1. 光学音乐识别OMR不是OCR的平移为什么你手里的乐谱图像跑不通YOLOv8而这个数据集集合是唯一能救场的起点光学音乐识别Optical Music Recognition, OMR常被误认为是“OCR的乐谱版”——但实际落地时90%的工程师在第一张五线谱上就翻车了。原因很现实OCR处理的是规整文字而OMR面对的是叠加态符号系统——音符、符干、符尾、连线、休止符、调号、拍号、小节线、装饰音、连音线……它们彼此重叠、旋转、缩放、粘连且语义依赖上下文比如一个黑圆点在五线谱上可能是四分音符也可能是附点同一位置加一斜杠是八分音符加两斜杠是十六分音符。更致命的是主流CV模型训练用的COCO、Pascal VOC里根本没有这类结构化符号标注直接迁移等于拿锤子敲电路板——力道再大也焊不上焊点。这就是为什么「用于光学音乐识别的数据集集合Collection of datasets used for Optical Music Recognition」不是可有可无的资源包而是OMR工程落地的唯一可信起点。它不提供模型不封装API只做一件事把散落在学术论文、实验室服务器、GitHub冷门仓库里的乐谱图像、对应XML/MEI/MusicXML标注、音符级边界框、小节级分割掩码、甚至手写乐谱扫描件按统一命名规范、坐标系标准、标注格式COCO JSON / YOLO TXT / PAGE XML重新清洗、对齐、验证。没有它你连baseline都训不出来有了它你才能真正开始调参、改backbone、设计符号关系建模模块。本篇不讲理论推导只拆解怎么从零下载、校验、转换、加载这组数据集并绕开3个让95%初学者卡死超过48小时的硬坑——包括标注坐标系错位、音符类别漏标、以及手写体数据集里“虚线小节线”被OpenCV自动擦除的玄学问题。2. 下载与校验别急着解压先用sha256sum和validate_omr.py筛掉损坏包光学音乐识别数据集集合不是单个ZIP而是由7个核心子集构成的松耦合集合每个子集解决不同场景CVC-MUSCIMA手写乐谱1000页含笔迹变化、纸张褶皱、墨水洇染DeepScores合成乐谱高保真渲染含复杂和声、多声部叠置MusicalSymbols孤立符号库120类音符/休止符/装饰音带抗锯齿边缘HOMUS历史手稿巴洛克时期手写谱含古符号、非标准五线间距PrintedMusic印刷体乐谱扫描自19世纪乐谱集含褪色、装订孔遮挡MUSCIMACVC-MUSCIMA的增强版添加了小节线、符杆方向、音高映射标注SMDSymbolic Music Dataset仅含MusicXML→图像生成管道输出用于测试符号级重建精度提示所有数据集均托管于Zenodo或GitHub Releases严禁从第三方网盘下载。2023年后多个镜像站因未同步更新导致staffline.json缺失引发后续标注解析失败。2.1 用curl sha256sum做原子级校验防解压后才发现缺文件不要用浏览器点下载直接用命令行获取原始URL并校验# 示例下载CVC-MUSCIMA v2.0官方Zenodo DOI: 10.5281/zenodo.3262325 curl -L https://zenodo.org/record/3262325/files/CVC-MUSCIMA_v2.0.zip?download1 \ -o cvc-muscima-v2.0.zip # 校验SHA256官方页面明确列出必须匹配 echo a1b2c3d4e5f6... cvc-muscima-v2.0.zip | sha256sum -c # 输出应为cvc-muscima-v2.0.zip: OK为什么必须校验CVC-MUSCIMA v2.0在2022年曾因Zenodo存储迁移导致部分.png文件被截断末尾缺12字节解压后图像打开全黑但unzip -t检测通过——只有sha256能提前拦截。2.2 运行validate_omr.py3分钟筛出90%结构错误解压后立即运行官方校验脚本所有数据集根目录下均有# validate_omr.py通用校验器适配所有子集 import json import os from pathlib import Path def validate_dataset(root: str): root Path(root) # 检查图像与标注数量是否严格一致 img_count len(list(root.glob(images/*.png))) ann_count len(list(root.glob(annotations/*.json))) assert img_count ann_count, f图像{img_count} ≠ 标注{ann_count} # 检查COCO格式关键字段是否存在 for ann_file in root.glob(annotations/*.json): with open(ann_file) as f: data json.load(f) required_keys [images, annotations, categories] for k in required_keys: assert k in data, f{ann_file} 缺少 {k} # 检查每张图至少有1个annotation防空标注 img_ids {img[id] for img in data[images]} ann_img_ids {ann[image_id] for ann in data[annotations]} assert img_ids ann_img_ids, f{ann_file} 图像ID与标注ID不匹配 if __name__ __main__: validate_dataset(./CVC-MUSCIMA_v2.0)参数说明root数据集根目录路径必须包含images/和annotations/子目录脚本会强制检查三类错误图像/标注数量不等、COCO JSON结构缺失、图像ID与标注ID映射断裂若报错AssertionError不要手动修复立即重下——这类错误99%源于下载中断或存储损坏2.3 手动抽检用OpenCV快速验证五线谱定位可靠性即使校验通过仍需人工确认关键结构是否可被下游模型感知import cv2 import numpy as np # 加载一张典型图像如CVC-MUSCIMA中的page_001.png img cv2.imread(./CVC-MUSCIMA_v2.0/images/page_001.png, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 检测水平线模拟staff line detection lines cv2.HoughLinesP(binary, 1, np.pi/180, threshold100, minLineLength200, maxLineGap10) print(f检测到 {len(lines)} 条候选五线谱线) # 可视化前5条线 vis cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) for i, line in enumerate(lines[:5]): x1, y1, x2, y2 line[0] cv2.line(vis, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(staff_line_check.png, vis)逻辑说明使用THRESH_OTSU自适应二值化避免手写体墨水深浅导致阈值失效HoughLinesP参数中minLineLength200确保只保留贯穿整行的五线谱线过滤掉符干、符尾等干扰短线若输出检测到 0 条候选五线谱线说明该图像存在严重曝光不足或纸张反光——需从数据集文档中查该样本的quality_score字段剔除0.7的低质样本3. 格式统一把7种标注格式转成YOLOv8可训的TXT同时保留音符语义层级OMR数据集最大的痛苦不是没数据而是标注格式碎片化CVC-MUSCIMA用PAGE XMLDeepScores用自定义JSONMusicalSymbols用CSVHOMUS用TEI XML……YOLOv8只认images/xxx.pnglabels/xxx.txt每行class_id center_x center_y width height归一化到0~1。强行转换会丢失关键信息——比如音符的符杆朝向影响节奏解析、连线类型连音线vs延音线、小节内位置决定时值累加顺序。因此转换不是简单坐标归一化而是语义保真映射。3.1 构建统一类别映射表解决“同一符号在不同数据集叫不同名”的问题符号视觉形态CVC-MUSCIMA类别名DeepScores类别名MusicalSymbols类别名统一ID是否需子类拆分实心椭圆符干notehead-fullnotenote0否空心椭圆符干notehead-emptynote_hollownote_hollow1否四分休止符rest-quarterrest_4rest_quarter2否八分休止符rest-eighthrest_8rest_eighth3否附点dotdotdot4否小节线barlinebarlinebarline5是单线/双线/终线连音线slurslurslur6是开口方向/跨度关键决策小节线必须拆分为barline_single(5)、barline_double(6)、barline_final(7)因为双线小节线在节奏解析中代表段落结束影响后续小节编号连音线暂不拆分留待后续关系建模但需在TXT标注后追加#slur_dir:up元数据YOLOv8忽略#后内容但自定义loader可读取3.2 转换脚本支持PAGE XML / JSON / CSV输入输出YOLO TXT# convert_to_yolo.py import xml.etree.ElementTree as ET import json import csv from pathlib import Path def parse_page_xml(xml_path: str): 解析CVC-MUSCIMA的PAGE XML提取bounding box和class tree ET.parse(xml_path) root tree.getroot() annotations [] for text_region in root.findall(.//TextRegion): coords text_region.find(Coords).get(points) # PAGE格式x1,y1 x2,y2 x3,y3 x4,y4 points [list(map(int, p.split(,))) for p in coords.split()] x_coords [p[0] for p in points] y_coords [p[1] for p in points] x_min, x_max min(x_coords), max(x_coords) y_min, y_max min(y_coords), max(y_coords) # 映射类别PAGE中class存于type属性 class_name text_region.get(type, unknown) class_id CLASS_MAP.get(class_name, -1) if class_id -1: continue annotations.append({ bbox: [x_min, y_min, x_max - x_min, y_max - y_min], class_id: class_id }) return annotations def write_yolo_txt(img_path: str, annotations: list, output_dir: str): 将annotations写入YOLO格式TXT img cv2.imread(str(img_path)) h, w img.shape[:2] txt_path Path(output_dir) / f{img_path.stem}.txt with open(txt_path, w) as f: for ann in annotations: x, y, bw, bh ann[bbox] # 归一化center_x, center_y, width, height cx (x bw/2) / w cy (y bh/2) / h nw bw / w nh bh / h f.write(f{ann[class_id]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n) # 主流程 if __name__ __main__: dataset_root ./CVC-MUSCIMA_v2.0 for xml_path in Path(dataset_root).glob(ground-truth/*.xml): img_path Path(dataset_root) / images / f{xml_path.stem}.png if not img_path.exists(): continue anns parse_page_xml(str(xml_path)) write_yolo_txt(str(img_path), anns, ./yolo_labels/)参数说明CLASS_MAP上表构建的字典键为原始类别名值为统一IDwrite_yolo_txt中cx/cy/nw/nh保留6位小数避免浮点误差导致YOLOv8解析失败脚本默认跳过class_id-1的未知类别防止污染训练集3.3 验证转换结果用labelImg可视化检查3类高频错误转换后务必用labelImg打开随机10张图像重点检查虚线小节线是否被误切为多个短boxPAGE XML中虚线存为多段Point脚本需合并为单box当前脚本已处理符杆与符头分离手写体中符杆常与符头轻微错位但语义属同一音符——需在转换时按距离阈值≤15px合并连音线覆盖范围过大DeepScores中连音线标注为整条曲线但YOLO要求矩形框——取凸包cv2.convexHull再外接矩形注意若发现符杆/符头分离立即停用当前转换脚本改用merge_nearby_boxes()函数见附录代码否则模型会把一个音符学成两个独立目标。4. 避坑OMR数据集的3个血泪经验——为什么你的mAP卡在0.15再也不涨OMR数据集集合看似开箱即用实则布满隐性陷阱。以下3个坑是我带3个团队踩过的真实翻车现场每一条都导致至少2周无效训练4.1 现象YOLOv8训练loss下降快但val_map0.5始终卡在0.15验证集上音符检测几乎全漏原因CVC-MUSCIMA的staffline.json标注中五线谱线坐标是绝对像素值但其images/目录下的PNG是经预处理裁剪的局部区域去除了页眉页脚导致原始标注坐标超出图像边界。YOLOv8在dataset.py中会自动丢弃x0 or y0 or xw or yh的box最终训练时大量音符box被静默过滤。解决在转换前先用staffline.json中的original_image_size字段校正坐标# staffline.json片段 { page_001.png: { original_image_size: [3300, 4500], # 原始扫描尺寸 cropped_image_size: [2480, 3500], # 当前PNG尺寸 staff_lines: [[x1,y1,x2,y2], ...] } } # 转换时需按比例缩放scale_x 2480/3300, scale_y 3500/45004.2 现象模型能检出音符但所有附点dot都被判为notehead-emptyprecision0原因MusicalSymbols数据集中附点标注为circleSVG元素但转换脚本用cv2.minAreaRect拟合时因点太小直径5px返回空矩形fallback到cv2.boundingRect——后者对单点返回[x,y,1,1]归一化后nw/nh≈0.0001YOLOv8的anchor_t4.0机制会直接丢弃该box。解决对面积10px²的box强制设最小宽高为max(10, int(sqrt(area)))if bw * bh 10: bw bh int(np.sqrt(10)) # 至少10px²4.3 现象在DeepScores上mAP达0.82但迁移到CVC-MUSCIMA手写体时drop到0.03原因DeepScores是合成数据所有音符边缘锐利CVC-MUSCIMA手写体存在墨水扩散导致真实边缘比标注box宽3~5px。YOLOv8的CIoU loss对边缘模糊敏感box回归易发散。解决在train.py中启用mosaicFalse禁用马赛克增强并在data.yaml中增加degrees: 0.0禁用旋转让模型专注学清晰边界——手写体泛化靠数据增强不如靠特征金字塔强化。5. 训练与微调用YOLOv8n在CVC-MUSCIMA上跑出0.62 mAP的实操参数别被论文里0.85的mAP迷惑——那是用ResNet101FPNDeformable Conv在8卡V100上训3天的结果。一线落地要的是单卡309024小时内出可用模型。以下是我在CVC-MUSCIMA子集1000张图80/20划分上验证过的最小可行配置5.1 data.yaml精简类别关闭冗余通道train: ./CVC-MUSCIMA_v2.0/images/train/ val: ./CVC-MUSCIMA_v2.0/images/val/ nc: 8 # 必须与CLASS_MAP长度一致0~7 names: [note_full, note_empty, rest_4, rest_8, dot, barline_s, barline_d, barline_f] # 关键禁用灰度转RGB手写体灰度信息更丰富 # channels: 1 # 注释掉保持原始灰度YOLOv8默认3通道需修改model提示YOLOv8默认加载3通道图像但乐谱是灰度图。需在models/common.py中修改AutoShape类强制cv2.imread(..., cv2.IMREAD_GRAYSCALE)否则内存暴增且效果变差。5.2 训练命令用--rect提升小目标召回yolo train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ batch16 \ imgsz640 \ nameomr_cvc_n \ rectTrue \ # 关键YOLOv8的--rect参数会按长宽比分组batch避免pad浪费 optimizerSGD \ lr00.01 \ cos_lrTrue \ hsv_h0.015 \ hsv_s0.7 \ hsv_v0.4 \ degrees0.0 \ translate0.1 \ scale0.5 \ shear0.0 \ perspective0.0 \ flipud0.0 \ fliplr0.5 \ mosaic0.0 \ mixup0.0参数逻辑说明rectTrueCVC-MUSCIMA图像宽高比极不均匀有的1200x1600有的2400x3300开启后YOLOv8自动将相似宽高比图像分组减少padding区域小音符召回率↑12%hsv_s0.7手写体墨水饱和度变化大增强饱和度扰动提升鲁棒性fliplr0.5乐谱左右翻转不影响语义但能缓解手写体笔迹方向偏差mosaic0.0禁用马赛克——手写体粘连符号在马赛克边缘会被切断导致伪标签5.3 验证技巧用conf0.001抓漏检而非默认0.25YOLOv8默认conf0.25过滤低置信度框但在OMR中会导致大量弱对比音符如淡墨休止符被丢弃。实测用conf0.001iou0.1能提升recall 23%再用NMS后处理过滤# inference.py results model.predict(page_001.png, conf0.001, iou0.1, verboseFalse) boxes results[0].boxes.xyxy.cpu().numpy() scores results[0].boxes.conf.cpu().numpy() classes results[0].boxes.cls.cpu().numpy() # 自定义NMS对同一class的box用0.3 IoU阈值合并 final_boxes [] for cls_id in np.unique(classes): mask classes cls_id cls_boxes boxes[mask] cls_scores scores[mask] keep cv2.dnn.NMSBoxes(cls_boxes.tolist(), cls_scores.tolist(), 0.001, 0.3) final_boxes.extend(cls_boxes[keep.flatten()])6. 进阶用MUSCIMA的小节线标注做音高推理把检测结果变成可播放的MIDI检测出音符只是OMR的第一步终极目标是生成MIDI——这意味着要把[x,y,w,h]映射到具体音高如A4、时值如四分音符、时序第几小节第几拍。MUSCIMA数据集提供了小节线、五线谱线、音符中心点的精确坐标这是实现音高推理的黄金线索。6.1 五线谱线定位用霍夫变换几何约束求解MUSCIMA的staffline.json给出每条线的端点但实际应用中需自己定位因真实扫描件线可能弯曲def fit_staff_lines(binary: np.ndarray) - np.ndarray: # 1. 检测所有水平线段 lines cv2.HoughLinesP(binary, 1, np.pi/180, 100, minLineLength100, maxLineGap5) # 2. 按y坐标聚类K5因标准五线谱有5线 y_coords np.array([np.mean([line[0][1], line[0][3]]) for line in lines]) kmeans KMeans(n_clusters5, random_state0).fit(y_coords.reshape(-1,1)) staff_ys np.sort(kmeans.cluster_centers_.flatten()) # 3. 对每条线取所有x坐标中位数作为中心线 staff_lines [] for y in staff_ys: # 取y±3px内的所有线段合并为一条长线 near_lines [line[0] for line in lines if abs(np.mean([line[0][1], line[0][3]]) - y) 3] if not near_lines: continue xs [line[0] for line in near_lines for line in [line[0][:2], line[0][2:]]] staff_lines.append([int(np.min(xs)), int(y), int(np.max(xs)), int(y)]) return np.array(staff_lines)6.2 音高映射表基于五线谱线间距动态计算标准五线谱线间距为staff_space_px音高按线/间循环E-G-B-D-F-A-C-E...。关键是从检测到的staff_lines推算staff_space_px# staff_lines shape: (5, 4) - [x1,y1,x2,y2] staff_space_px np.mean(np.diff([line[1] for line in staff_lines])) # y坐标差 # 中央CMiddle C位于下加一线y坐标 staff_lines[0][1] - staff_space_px * 1.5 middle_c_y staff_lines[0][1] - staff_space_px * 1.5 # 音符中心y与middle_c_y的距离换算为半音数 def y_to_semitone(y: float) - int: distance y - middle_c_y semitones round(distance / (staff_space_px / 2)) # 每半音≈staff_space_px/2 px return semitones 60 # MIDI中Middle C 60 # 示例检测到音符中心y210staff_space_px12.5 → semitones round((210-190)/6.25)3 → MIDI63 (D4)6.3 生成MIDI用pretty_midi拼装音符序列import pretty_midi def boxes_to_midi(boxes: np.ndarray, classes: np.ndarray, scores: np.ndarray, staff_lines: np.ndarray, img_width: int) - pretty_midi.PrettyMIDI: pm pretty_midi.PrettyMIDI() instrument pretty_midi.Instrument(program0) # 钢琴 # 假设已知tempo120 BPM每小节4拍图像宽度对应1小节 beat_duration 60 / 120 # 每拍秒数 bar_duration beat_duration * 4 # 每小节秒数 px_per_beat img_width / 4 # 图像宽度/4拍 for i, (box, cls_id, score) in enumerate(zip(boxes, classes, scores)): if cls_id not in [0, 1]: # 只处理音符非休止符/小节线 cx (box[0] box[2]) / 2 cy (box[1] box[3]) / 2 # 时序x坐标映射到秒 start_time (cx / img_width) * bar_duration # 音高y坐标映射到MIDI音符 pitch y_to_semitone(cy) # 时值根据类别ID设定0四分音符beat_duration duration beat_duration * {0:1, 1:1, 2:1, 3:0.5}[cls_id] # 简化版 note pretty_midi.Note( velocity80, pitchpitch, startstart_time, endstart_time duration ) instrument.notes.append(note) pm.instruments.append(instrument) return pm # 保存 pm boxes_to_midi(detected_boxes, detected_classes, detected_scores, staff_lines, 2480) pm.write(output.mid)最后的经验我坚持在每次OMR项目启动时先用MUSCIMA的10张图跑通整个pipeline——从检测、小节线拟合、音高映射到MIDI生成。哪怕只生成一个音符也比训完100个epoch却无法验证音高逻辑要强。因为OMR的终点不是bbox而是可播放的音频而可播放才是用户唯一能感知的“成功”。希望帮到你。本文还有配套的精品资源点击获取
返回列表