
简介这是一份面向机器人行业目标检测与计算机视觉开发者的笔记note单类别数据集专门用于识别和定位图像中的笔记区域可支撑文档处理、OCR系统开发、智能教育工具及YOLO系列模型训练等场景。资源包共878个文件其中438张JPEG图片与438个YOLO格式txt标注文件一一对应另附1个yaml配置文件和1个docx说明文档分别用于定义数据集参数和提供资源说明压缩包整体约30.98MB结构清晰。标注均采用精确的边界框与类别标签可直接导入YOLOv5等主流框架样本来源涵盖日常文档与视频截图等多场景有助于提升模型在不同环境下的泛化能力与鲁棒性。目前已有91人学习下载适合需要轻量级目标检测基准数据的算法工程师、研究人员及高校学生快速上手也可作为机器人行业识别任务的验证集。1. 从视频帧里抠出438张“笔记”这个数据集到底值不值得用一张课堂白板照片、一页文档局部、一段教学视频的某一帧——这些画面里都藏着同一个目标笔记。任务并不需要识别笔记里写了什么而是把笔记所在的区域框出来。这份“笔记数据集.zip”打包的正是这么一件看起来单调但很实用的事438张JPEG原图每张只有一个类别“note”标注文件按YOLO格式给出边界框和类别id。数据一部分来自文档扫描另一部分来自mp4视频抽帧文件名里还留着IMG_1589_mp4-0这样的痕迹。对于机器人行业目标检测数据集来说这种“一张图框一个目标”的单类任务往往比千类大杂烩更容易先跑到线上可用状态。这个数据集的现实价值在于它足够干净可以直接用来做yolov5或yolov8训练自己的数据集全流程验证它也足够有代表性能暴露视频抽帧数据里连续帧重复、背景光照变化、框边界不贴合等真实问题。适合三类人刚接触目标检测的学生、做文档自动化工具的工程师以及在机器人项目里需要快速做一个“找笔记区域”演示的开发者。如果你手头正好也有一个解压后散着一堆图片和同名txt的zip下面这套处理链路可以直接搬走。2. 拆开“笔记数据集.zip”YOLO标签格式与数据清单拿到zip先不要急着训练。一个能用来训练YOLO的数据集本质上由三样东西组成原始图像、与图像同名的txt标签文件以及描述类别的data.yaml。很多刚上手的人拿到数据集第一件事就是解压然后打开文件夹看图片这其实只完成了10%的工作。我一般会先跑一段脚本把整个zip的结构、图片数量、标签是否缺失、类别是否单一全部检查一遍再决定要不要继续训练。2.1 zip包里的真实布局图片、标注和命名痕迹解压后你会看到类似这样的文件对IMG_1589_mp4-0_jpg.rf.9b098d201005036fc1b4d6c0ae0030ef.jpg IMG_1589_mp4-0_jpg.rf.9b098d201005036fc1b4d6c0ae0030ef.txt IMG_1589_mp4-1_jpg.rf.35d13bade0cb076dfab7752e5ebbfa60.jpg IMG_1589_mp4-1_jpg.rf.35d13bade0cb076dfab7752e5ebbfa60.txt其中.rf.后跟一串哈希是Roboflow平台导出的痕迹mp4-0、mp4-1说明原图来自同一段MP4视频的连续两帧。这种命名对人工看很不友好但对程序非常友好因为图片和标签只需要去掉后缀就能一一对应。下面这个表格把命名拆开解释命名片段含义IMG_1589原始图片或视频的采集批次编号mp4-0从名为 IMG_1589.mp4 的视频中抽取的第0帧jpg导出时统一转成的JPEG格式rf.9b09...Roboflow导出时自动追加的唯一哈希标识用一段Python把目录和样本数统计清楚import os data_dir 笔记数据集 image_exts {.jpg, .jpeg, .png} label_exts {.txt} imgs [] lbls [] for root, _, files in os.walk(data_dir): for f in files: ext os.path.splitext(f)[1].lower() if ext in image_exts: imgs.append(os.path.join(root, f)) elif ext in label_exts: lbls.append(os.path.join(root, f)) else: print(忽略非标注文件:, f) print(图片总数:, len(imgs)) print(标签总数:, len(lbls)) missing [os.path.basename(p)[:-4] for p in imgs if not os.path.exists(os.path.splitext(p)[0] .txt)] print(缺少标签的图片数量:, len(missing)) print(样例文件:, imgs[:3])这段脚本做的事很简单递归遍历目录分别收集图片和txt标签再动态检查图片是否有同名标签。os.path.splitext(p)[0]会去掉最后的扩展名然后补上.txt来判断是否存在。如果输出的“缺少标签数量”大于0说明数据不完整要么重新导出要么把那部分图片直接丢弃否则训练时日志里会堆满WARNING: label(s) not found。2.2 标签里的五个数字从归一化坐标还原真实像素框打开任意一个txt内容通常是每行五个数字例如0 0.687500 0.325156 0.581250 0.649688含义是这张图中有一个目标类别id为0对应names里的 note目标框中心的x在图片宽度的68.75%处中心的y在图片高度的32.52%处框宽占整图宽度的58.13%框高占整图高度的64.97%。注意这是中心点加宽高的表示方式不是左上角坐标。训练时YOLO会直接读取这些归一化数值所以不要换成像素值存进txt。要验证标注质量最直接的办法是把它画回图上。下面这段代码把YOLO格式转成PIL的xyxy并画出矩形from PIL import Image, ImageDraw def draw_yolo_bbox(img_path, label_path, output_path): img Image.open(img_path).convert(RGB) W, H img.size # 原图宽高单位像素 draw ImageDraw.Draw(img) with open(label_path, r) as f: for line in f.readlines(): line line.strip() if not line: continue parts line.split() if len(parts) ! 5: continue cls_id, cx, cy, bw, bh map(float, parts) # 归一化 - 像素坐标 x1 (cx - bw / 2) * W y1 (cy - bh / 2) * H x2 (cx bw / 2) * W y2 (cy bh / 2) * H draw.rectangle([x1, y1, x2, y2], outlinered, width3) draw.text((x1, y1 - 10), str(int(cls_id)), fillred) img.save(output_path) draw_yolo_bbox(IMG_1589_mp4-0_jpg.rf.9b09.jpg, IMG_1589_mp4-0_jpg.rf.9b09.txt, visual_check.jpg)逻辑也很直观bw / 2 * W得到的是目标框一半宽度对应的像素数用中心点去加减它就得到矩形框的左上角和右下角坐标。如果画出来的框比真实笔记大好几圈大概率是标注时把边缘留白也算进去了如果框的中心整体漂移就要检查标注工具导出时是否做过旋转或裁剪增强。2.3 单类数据集的 data.yaml一个类别也值得写完整YOLOv8训练时需要数据描述文件最常见写法path: ../笔记数据集 # 数据集根目录 train: images/train val: images/valid test: images/test names: 0: notepath是数据集根目录的相对路径或绝对路径train、val、test指向三个子目录。如果你的zip里没有预先按train/valid/test分包只是图片全在一个文件夹就要手动按比例切分。切分脚本我习惯用train_test_split同时保证训练、验证、测试三部分文件名不重合import os import shutil from sklearn.model_selection import train_test_split image_files [f for f in os.listdir(all_images) if f.endswith(.jpg)] train_imgs, tmp_imgs train_test_split(image_files, test_size0.3, random_state42) valid_imgs, test_imgs train_test_split(tmp_imgs, test_size0.5, random_state42) for split, files in [(train, train_imgs), (valid, valid_imgs), (test, test_imgs)]: os.makedirs(fimages/{split}, exist_okTrue) os.makedirs(flabels/{split}, exist_okTrue) for f in files: shutil.copy(fall_images/{f}, fimages/{split}/{f}) label f.replace(.jpg, .txt) if os.path.exists(fall_labels/{label}): shutil.copy(fall_labels/{label}, flabels/{split}/{label}) print(f训练集 {len(train_imgs)}验证集 {len(valid_imgs)}测试集 {len(test_imgs)})参数说明test_size0.3表示先留出30%给验证加测试之后从这30%里对半分最终得到70%、15%、15%的分配。random_state42固定随机种子保证每次切分结果一致。特别注意labels目录也要有对应的train/valid/test子目录YOLO不会自动帮你建。还有一个容易踩的坑单类数据集最容易把负样本全部扔掉。笔记检测在机器人工作区里经常要处理桌面上没有笔记只有咖啡杯的场景所以如果原始zip里全是含目标的图像我建议训练时额外保留一些完全没目标的空标签图片否则推理时误检率会很难看。3. 用YOLOv8把这个数据集训成“笔记检测器”的完整过程第2章已经把数据和标签理顺了接下来进入训练。当前社区主流是YOLOv8但你仍然可以用YOLOv5的命令直接跑因为数据集格式完全兼容。这里我用YOLOv8演示因为它的配置方式对单类数据集更友好训练日志里也直接给出mAP50和mAP50-95。3.1 选YOLOv8n而不是更大模型的原因438张图像、单类别这是典型的小数据集。此时用YOLOv8x或者YOLOv5x大概率在训练集上过拟合验证集mAP反而低。我一般先选YOLOv8n参数量只有3.2M一张RTX 3060上batch16也能轻松跑。相比大模型YOLOv8n的迭代速度快得多改一次增强策略几十分钟就能看到结果。等模型基线稳定后如果mAP50低于0.9再考虑换成YOLOv8s或把输入分辨率提上去。注意笔记目标在图像中的尺度差异很大视频帧里笔记可能占一半画面文档扫描图里又是细长条。YOLOv8的anchor-free head对尺度变化不敏感这是选它的另一个理由。3.2 目录结构和 data.yaml 的最终形态无论你是从zip里自己整理还是用Roboflow直接导出最终目录必须长这样notes_dataset/ images/ train/ (约306张) IMG_1589_mp4-0_jpg.rf.9b09.jpg valid/ (约66张) test/ (约66张) labels/ train/ IMG_1589_mp4-0_jpg.rf.9b09.txt valid/ test/ data.yamldata.yaml就用2.3节的写法把path改成实际路径。训练命令yolo detect train \ modelyolov8n.pt \ datanotes_dataset/data.yaml \ imgsz640 \ batch16 \ epochs100 \ patience20 \ projectnotes \ nameexp_note \ pretrainedTrue参数说明modelyolov8n.pt使用COCO预训练权重做迁移学习而不是从零开始。小数据集从零训练效果很差COCO前景知识能帮忙提取边缘和纹理特征。imgsz640输入分辨率。如果笔记在画面里面积偏小可以提到960但训练时间接近翻倍。batch16根据显存调整。8G显存建议降到812G以上可以16或32。patience20连续20轮验证集指标不提升就早停防止无效训练浪费时间。pretrainedTrue显式加载预训练权重。训练过程中需要盯两个指标train/box_loss在缓慢下降val/mAP50在上升。如果loss震荡不降第一件事检查data.yaml的path是否写对第二件事看labels是否和图片一一对应第三件事降低初始学习率。用Python脚本训练时参数写法更直白from ultralytics import YOLO model YOLO(yolov8n.pt) model.train( datanotes_dataset/data.yaml, imgsz640, batch16, epochs100, lr00.005, # 默认0.01小数据集保守一点 lrf0.1, # 最终学习率降到初始值的10% cos_lrTrue, patience20, workers4, seed42 )这里lr00.005是初始学习率小数据集上梯度波动大调低一点能避免loss前期发散lrf0.1配合cos_lrTrue让学习率按余弦曲线平滑下降。workers4是数据加载线程数Windows下如果报DataLoader错误把它改成0。3.3 训练后先做一次快速验证训练完不要急着导模型。先跑yolo detect val \ modelnotes/exp_note/weights/best.pt \ datanotes_dataset/data.yaml \ splittest \ imgsz640输出结果里最值得关注的是mAP50和mAP50-95。对于边界框非常规整的单类目标mAP50应该在0.85以上才算合格我见过用这份数据训练半小时就跑到0.92的。如果只有0.5左右大概率是标签框和内容错位或者训练时用了错误的data.yaml。快速验证时可以把推理置信度默认的0.25调高到0.5看模型在“高置信度”条件下的表现更容易发现目标太小导致漏检的问题。3.4 单类数据集训练参数速查表参数小数据集(200~1000)中等数据集(1000~5000)modelyolov8n / yolov8syolov8mimgsz640640~960batch8~3216~64epochs80~150100~200lr00.003~0.010.01patience20~3020~40增强策略mosaic 翻转 轻微光照可以关闭mosaic这张表是我自己训练时常用的起点不是硬性标准。数据集直接从zip解压后图片尺寸可能不是640的整数倍YOLO会自动做letterbox填充所以不需要手动resize。如果你习惯用YOLOv5命令变成python train.py --data data.yaml --weights yolov5n.pt本质完全一样即yolov5训练自己的数据集同样需要这套目录结构。4. 视频帧数据集的隐藏坑连续重复帧与单一生境文件名里的mp4-0、mp4-1暴露了数据来源——从MP4视频里按帧导出的图片。这类数据集有个隐蔽问题相邻帧之间重叠度极高。如果训练集里同时出现第0帧和第1帧相当于同一个笔记被复制了两份进训练集验证集里也很可能混入它的孪生兄弟导致mAP虚高。这对机器人行业目标检测数据集来说是致命伤因为机器人视觉要面对真实连续视频流而不是静态图片库。4.1 用感知哈希把重复帧踢出数据集去重不能直接用像素差判断因为视频编码压缩会产生噪点。我一般用dhash差异哈希计算每张图的指纹再根据汉明距离去重。from PIL import Image import numpy as np def dhash(img_path, hash_size8): img Image.open(img_path).convert(L) img img.resize((hash_size 1, hash_size)) pixels np.asarray(img, dtypenp.int32) diff pixels[:, 1:] pixels[:, :-1] # 比较相邻像素亮度 return diff.flatten().astype(np.uint8) def hamming(a, b): return np.count_nonzero(a ! b) # 对同一视频下所有帧做两两比较 frames [IMG_1589_mp4-0_jpg.rf.xxx.jpg, IMG_1589_mp4-1_jpg.rf.yyy.jpg] hashes [dhash(f) for f in frames] if hamming(hashes[0], hashes[1]) 5: print(这两帧高度相似保留其中一帧即可)逻辑说明hash_size8会把图片压缩成9x8像素然后逐行比较左右相邻两个像素的亮度大小得到64位哈希。两张相似图片的汉明距离很小。阈值5表示最多有5个位置的亮度关系不同对摄像头视频帧来说小于5基本就是重复帧。注意不要直接用MSE因为视频编码在暗部产生的随机噪声会让MSE虚高而哈希只关心相对亮度关系。如果你的视频是静止画面加轻微抖动阈值可以放宽到10。4.2 数据增强弥补场景单一Albumentations的bbox同步变换去掉重复帧后438张可能缩水到300张左右。此时需要用增强把“拍摄角度、光照、遮挡”这些真实变化模拟出来。Albumentations 是目标检测场景里比torchvision更好用的增强库因为它会自动同步变换边界框。import albumentations as A train_transform A.Compose([ A.RandomBrightnessContrast(p0.5), A.HueSaturationValue(p0.3), A.Affine(scale(0.8, 1.2), translate_percent(-0.1, 0.1), rotate(-15, 15), p0.8), A.RandomSizedBBoxSafeCrop(height640, width640, p0.5), A.Mosaic(p0.3), ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels]))参数说明bbox_params里的formatyolo表示输入输出的边界框是YOLO格式的 cxcywhAlbumentations 内部会自动转成像素坐标做变换返回时再转回去。RandomSizedBBoxSafeCrop会保证裁剪区域包含图像中所有边界框不会把正样本裁掉。Mosaic是把4张图拼成一张相当于在batch维度做了额外混合能显著缓解单类数据量不足的问题但如果原始图中笔记大小非常一致Mosaic反而会让模型学到拼接痕迹需要观察训练loss曲线来判断。生成增强后的图像并同步标签有一个前提训练逻辑里要用同样的增强管道同时处理图像和边界框。albumentations 输入时给一个image和一个bboxes列表即可image cv2.imread(img_path) boxes [0.6875, 0.3251, 0.5812, 0.6496] # 归一化 yolo bbox transformed train_transform(imageimage, bboxes[boxes], class_labels[0]) transformed_image transformed[image] transformed_boxes transformed[bboxes] # 自动保持 yolo 格式验证阶段绝对不能加任何随机增强只需要letterbox否则mAP统计出来的指标会严重失真。4.3 用初版模型做半自动标注第4章最后一条技巧是把已经训练好的模型用来扩展数据集。当你拿到新的视频或新的文档图片时先用best.pt跑一遍推理把高置信度的预测框直接转成YOLO标签人工只需要修正那些置信度在0.3到0.6之间的边界框。from ultralytics import YOLO model YOLO(notes/exp_note/weights/best.pt) results model.predict(new_video_frame.jpg, conf0.4, save_txtFalse) for r in results: boxes r.boxes.xyxyn.cpu().numpy() # xyxy 归一化坐标 class_ids r.boxes.cls.cpu().numpy().astype(int) with open(new_video_frame.txt, w) as f: for box, cls in zip(boxes, class_ids): x1, y1, x2, y2 box cx (x1 x2) / 2 cy (y1 y2) / 2 w x2 - x1 h y2 - y1 f.write(f{cls} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)说明xyxyn输出已经是归一化坐标直接换算成中心宽高写回txt即可。conf0.4是推理置信度阈值推荐先定0.4既覆盖大部分正样本又不会引入太多误检。人工修正的重点是框的边界是否贴合笔记边缘而不是类别对不对——单类任务里类别永远只有一个。把预测结果混合人工修正后的标签再训练一轮通常能再把mAP50提升3到5个点。5. 验证与部署把模型吞回视频流看真实帧率最后不讲虚的而是把训练好的模型放回到真实视频帧里验证。你追求的不应该是测试集上多0.01的mAP而是摄像头画面里稳定框住笔记。这里有三个具体技巧。5.1 用 PR 曲线确认误检来源yolo detect val会在输出目录生成PR_curve.png和F1_curve.png。单类别模型没有类别混淆但PR曲线能反映置信度阈值设置是否合理。如果P-R曲线整体靠近右上角说明任何置信度下精度和召回都稳定。此时看F1曲线找到F1最大值对应的置信度这就是部署时的推荐阈值。我处理这份数据时最佳conf通常在0.35附近因为笔记边缘在照片里对比度偏低置信度很难超过0.7。把conf设得太高会出现漏检设得太低会出现桌面纹理误检。5.2 导出 ONNX 并写一个最小推理脚本部署到机器人场景通常不能依赖Ultralytics那套Python环境。导出ONNX是通用做法yolo export modelnotes/exp_note/weights/best.pt formatonnx dynamicTrue imgsz640然后用onnxruntime推理import onnxruntime as ort import numpy as np import cv2 sess ort.InferenceSession(best.onnx) input_name sess.get_inputs()[0].name img cv2.imread(test_frame.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) resized cv2.resize(img_rgb, (640, 640)) input_tensor np.transpose(resized.astype(np.float32) / 255.0, (2, 0, 1))[None] outputs sess.run(None, {input_name: input_tensor})这里有个容易被忽略的参数resized是letterbox后的图但你没有记录原始图到letterbox图的缩放比例和填充偏移。直接画框会错位。常见做法是手动记录缩放系数scale min(640 / img.shape[1], 640 / img.shape[0]) pad_w (640 - int(img.shape[1] * scale)) // 2 pad_h (640 - int(img.shape[0] * scale)) // 2 # 预测结果归一化 - 原图坐标 x1 (pred_x1 * 640 - pad_w) / scale y1 (pred_y1 * 640 - pad_h) / scale把输出里的归一化框先乘640再减去padding最后除以scale就能映射回原始图像。这个反向映射每次部署都要写建议封装成一个函数。5.3 一个验证小工具把模型跑回原视频既然数据来自mp4帧验证也应回到mp4。写一个循环逐帧读取视频对每一帧推理把检测结果画上输出带框的视频。这是最直观判断模型是否过拟合的方式如果视频第50帧框得很好第51帧突然丢失说明模型对光照突变敏感需要补充更多不同时段、不同角度的样本。我还会在画面角落实时显示FPS。笔记目标通常不大YOLOv8n在CPU上能跑到10FPS左右机器人配GPU的话可以到100FPS以上。发现FPS太低先检查imgsz640是否与训练时一致把它降到480能让速度提升约30%但mAP会掉2到3个点。这个取舍就看你的机器人是直行到目标前慢慢对准还是要求高速运动中瞬间捕捉。本文还有配套的精品资源点击获取