
简介这份资源面向医学影像检测与深度学习实践者提供一套已标注的乳房X光检查数据集可直接用于YOLOv8目标检测模型的训练与验证。数据整合自RSNA、MINI-DDSM-PNG-16、MIAS、INBREAST四个公开来源共900张图像统一缩放至416×416覆盖不同设备与拍摄条件下的乳腺病灶区域适合入门级到中级的检测任务练手与算法对比实验。压缩包共1908个文件以1000个png图像和901个txt标注文件为主体另含4个pt预训练权重yolov8l、yolov8m、yolov8n、yolov8x、2个cache缓存与1个yaml配置文件整体约496.76MB训练集与测试集划分清晰开箱即可接入YOLOv8训练流程。目前已有764人学习下载读者可借此省去数据搜集与标注成本快速复现检测基线、调整模型结构并观察不同规模预训练权重在医学影像上的表现差异。1. 从一张钼靶片说起YOLOv8标记乳房X光检查数据集到底在做什么乳房X光检查也就是常说的钼靶是乳腺疾病筛查里最普遍的影像手段之一。一张标准的钼靶片动辄 3000×4000 像素灰阶 12 到 16 位病灶在整幅图里可能只占几百个像素——微钙化点甚至只有十几个像素。让放射科医生一张张看疲劳和漏检是绕不开的问题。把 YOLOv8 用在这类影像上做病灶检测是这两年很实际的一条落地路径而整件事的起点不是模型是数据集怎么把原始钼靶影像标成 YOLOv8 能吃的格式怎么处理超大分辨率怎么在样本极少的情况下不把模型训崩。这篇笔记面向的是手里已经有一批钼靶影像、想用 YOLOv8 跑通检测的工程师和做毕设的同学。我会按「数据从哪来 → 怎么转成 YOLO 格式 → 怎么配训练参数 → 坑在哪 → 怎么验证」的顺序讲中间给可直接抄的脚本和参数。需要先说明乳房X光检查数据集属于医学影像公开可用的规模远不如 COCO标注成本高、类别极不平衡所以后面很多取舍都是围绕「小样本 超大图」这两个约束展开的而不是照搬通用目标检测那套。2. 乳房X光检查数据集的来源、格式与标注口径2.1 公开数据集与自采数据的取舍做这个方向第一步是确定数据来源。常见做法有三类一是用公开的钼靶数据集这类数据通常以 DICOM 或 16 位 PNG/TIFF 形式发布附带病灶级别的标注文件CSV 或 XML但标注往往是图像级或粗略的框需要自己二次整理二是医院或体检机构合作拿到的脱敏数据质量高但量少且标注口径要自己定三是拿已有的检测数据集做迁移预训练再用少量钼靶数据微调。我一般会先确认三件事影像是不是原始位图有没有被压缩成 8 位 JPEG压缩过的微钙化基本就废了、标注是图像级还是框级、类别体系是什么。钼靶检测里最常见的类别划分是「肿块mass」和「钙化calcification」两类有的还会细分良性/恶性。类别一旦定下来后面所有脚本和配置都围绕它走中途改类别等于重标。注意如果拿到的标注只有图像级标签这张图有没有病灶是不能直接喂给 YOLOv8 检测头的必须先转成框。没有框的情况下要么补标要么退一步先做分类任务别硬上检测。2.2 从 DICOM/原始影像到可视图像的预处理钼靶原始数据常见是 DICOM像素值范围大、位深高直接读进来是没法看的。预处理的核心是把窗宽窗位调好把 16 位压到 8 位同时尽量保留病灶对比度。下面这段是常见的读取与归一化流程import pydicom import numpy as np import cv2 def dicom_to_png(dcm_path, out_path): ds pydicom.dcmread(dcm_path) img ds.pixel_array.astype(np.float32) # 用百分位裁剪避免极端值把对比度拉死 low, high np.percentile(img, [1, 99]) img np.clip(img, low, high) img (img - low) / (high - low 1e-6) * 255.0 img img.astype(np.uint8) # 钼靶常见左右朝向不一致统一成一种朝向 if getattr(ds, Laterality, L) R: img cv2.flip(img, 1) cv2.imwrite(out_path, img) return img.shape逻辑说明pixel_array拿到原始像素百分位裁剪是为了压掉金属标记、边缘伪影这类极端亮暗值否则归一化后病灶区域会被压成一片灰。Laterality判断左右侧并翻转是为了让所有影像朝向一致——钼靶左右乳的摆位方向相反不统一的话模型会学到「位置」这种伪特征。参数上百分位取 1/99 是常用起点如果病灶偏暗可以改成 0.5/99.5具体看你的数据分布建议先抽 20 张画直方图确认。2.3 标注工具与框的粒度框级标注常用 labelme 或 CVAT。labelme 的好处是能直接导出 YOLO 格式坏处是超大图在浏览器里缩放卡顿。我的习惯是先把钼靶图降采样到 1024 或 1280 长边再标标完记录缩放比例最后把框坐标按比例还原回原图尺寸。这样标注体验好也不会因为浏览器渲染丢精度。标注口径要提前和医生对齐肿块框是紧贴边界还是留一点余量、钙化是框单个点还是框一片区域。钙化尤其麻烦散在的微钙化点如果每个都框一张图可能几十个框模型很难学常见做法是把同一区域的钙化合并成一个大框或者只标最密集的那一簇。这个口径不统一后面 mAP 再高也没意义。3. 把钼靶标注转成 YOLOv8 格式脚本、目录结构与四个边界坑3.1 YOLO 格式的坐标规则与目录约定YOLOv8 检测的标签是每行class_id x_center y_center width height全部归一化到 0~1。目录结构官方约定是images/train、images/val、labels/train、labels/val平行放置再配一个data.yaml指向这些路径和类别名。钼靶数据因为图大、样本少划分时不能随机切要按病人切——同一个病人的左右乳、不同体位如果分到 train 和 val 两边验证集分数会虚高这是医学影像里最典型的泄漏。import os, random, shutil from pathlib import Path def split_by_patient(records, val_ratio0.2, seed42): # records: [(img_path, label_path, patient_id), ...] patients sorted({r[2] for r in records}) random.Random(seed).shuffle(patients) n_val max(1, int(len(patients) * val_ratio)) val_pat set(patients[:n_val]) train, val [], [] for r in records: (val if r[2] in val_pat else train).append(r) return train, val def copy_split(pairs, img_dir, lbl_dir): Path(img_dir).mkdir(parentsTrue, exist_okTrue) Path(lbl_dir).mkdir(parentsTrue, exist_okTrue) for img, lbl in pairs: shutil.copy(img, os.path.join(img_dir, os.path.basename(img))) shutil.copy(lbl, os.path.join(lbl_dir, os.path.basename(lbl)))逻辑说明split_by_patient先按病人 ID 去重再打乱保证同一病人的所有影像只出现在一边。val_ratio0.2是小数据集的常用值如果病人总数不到 20建议改成 0.25 甚至做交叉验证。seed固定是为了可复现换种子前先确认划分稳定。3.2 从 labelme JSON 批量转 YOLO txtlabelme 导出的 JSON 里shapes是框列表imageWidth/imageHeight是标注时图像的尺寸。如果标注用的是降采样图要乘回缩放比。下面脚本同时处理这两件事import json, os from pathlib import Path CLASS_MAP {mass: 0, calcification: 1} def labelme_to_yolo(json_path, out_txt, scale1.0): data json.load(open(json_path, encodingutf-8)) w, h data[imageWidth], data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue (x1, y1), (x2, y2) shape[points] # 还原到原图尺寸 x1, x2 sorted([x1 * scale, x2 * scale]) y1, y2 sorted([y1 * scale, y2 * scale]) # 裁剪到图像范围内防止越界 x1, x2 max(0, x1), min(w * scale, x2) y1, y2 max(0, y1), min(h * scale, y2) cx (x1 x2) / 2 / (w * scale) cy (y1 y2) / 2 / (h * scale) bw (x2 - x1) / (w * scale) bh (y2 - y1) / (h * scale) lines.append(f{CLASS_MAP[label]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) Path(out_txt).write_text(\n.join(lines), encodingutf-8) return len(lines)逻辑说明scale是标注图相对原图的缩放比比如原图 4000 宽、标注图 1000 宽scale 就是 4.0。sorted保证左上右下顺序labelme 有时点选顺序是反的。裁剪到边界是必须的钼靶边缘常有胸壁侧被裁掉的情况框越界会让训练时报归一化坐标超出 0~1 的警告。CLASS_MAP里没列的标签直接跳过避免把「乳头」「标记」这类辅助标注混进检测类别。3.3 四个边界坑越界、空标签、类别失衡、超大图第一个坑是坐标越界。钼靶图边缘有黑色背景和标记标注时容易框到图外转出来 cx 或 bw 大于 1YOLOv8 训练时会直接报错或静默丢弃。解决就是在转换脚本里做 clip并在转换后跑一遍校验。第二个坑是空标签文件。有些图确实没有病灶转出来是空 txt。YOLOv8 默认会把空标签当负样本这是好事但如果空标签占比超过 30%模型会偏向「什么都别检」。我的做法是保留一部分空样本但控制在 10%~15%。第三个坑是类别失衡。钙化样本通常远多于肿块或者反过来。直接训会让模型偏向多数类。常见做法是在data.yaml之外训练时用cls损失加权或者对少数类做复制增强。YOLOv8 本身没有直接的类别权重参数得靠数据层面平衡。第四个坑是超大图。4000 像素的图直接进网络显存扛不住YOLOv8 默认imgsz640会把病灶缩到几乎看不见。钼靶检测的常见折中是imgsz1024或1280配合切片推理把大图切成带重叠的小块分别检测再合并。切片会带来重复框需要 NMS 合并这块在最后一章展开。4. YOLOv8 训练参数怎么设从 imgsz 到数据增强的取舍4.1 环境与最小可跑命令环境上CPU 版本能跑通但训练慢到没法用钼靶这种大图建议至少一张 8G 以上显存的卡。装好 ultralytics 后最小训练命令是yolo detect train \ datadata.yaml \ modelyolov8s.pt \ imgsz1024 \ epochs100 \ batch4 \ patience20 \ projectruns/mammo \ nameexp1参数说明modelyolov8s.pt是权衡速度和精度的起点数据量少于 500 张时用yolov8n更容易收敛数据多再上m。imgsz1024是钼靶的常用值再大显存吃不消。batch4配合 1024 输入在 8G 卡上比较稳显存够可以加到 8。patience20是早停小数据集上过拟合来得快别设太大。4.2 数据增强哪些能用哪些会帮倒忙YOLOv8 默认开了一堆增强包括 mosaic、mixup、随机翻转、HSV 抖动。钼靶数据里HSV 抖动基本没用影像本身是灰阶mosaic 会把四张图拼一起对超大图切片场景反而可能有用但对整图训练会破坏解剖结构。我的配置是关掉 HSV、降低 mixup保留水平翻转和轻微缩放# 在训练命令里用参数覆盖或写进自定义 cfg hsv_h: 0.0 hsv_s: 0.0 hsv_v: 0.0 mixup: 0.0 mosaic: 0.5 fliplr: 0.5 scale: 0.2 degrees: 5.0逻辑说明hsv_*全关是因为灰阶影像没有色相饱和度可言。mixup0是因为两张钼靶叠一起会产生不存在的解剖结构医学场景里这是有害的。mosaic0.5保留一半概率能提升小目标召回但别全开。degrees5是轻微旋转钼靶摆位本身有角度差异小角度旋转合理大角度会引入不真实的解剖关系。4.3 学习率、优化器与损失权重YOLOv8 默认用 SGDlr00.01。小数据集上我一般换成 AdamWlr00.001收敛更稳。损失里 box、cls、dfl 三项类别失衡时可以把cls的权重调高但 YOLOv8 命令行不直接暴露这个参数得改模型配置或自定义 trainer。更省事的做法还是在数据层面做平衡。验证指标上钼靶检测别只看 mAP50要看 mAP50-95 和每个类别的 AP。钙化这种小目标mAP50 可能还行mAP50-95 会掉得厉害这个差距本身就是信息——说明框的定位精度不够可能需要提高 imgsz 或改锚点。5. 训练钼靶检测模型时最容易翻车的几个地方5.1 验证集分数虚高实际一塌糊涂现象训练日志里 mAP 一路涨到 0.9拿新数据一测惨不忍睹。原因划分时没按病人切同一病人的影像同时进了 train 和 val模型记住了这个病人的特征。解决回到 3.1 的split_by_patient按病人 ID 划分划分后人工抽查确认没有病人跨集。5.2 训练一开始 loss 就是 nan现象第一个 epoch 就 nan。原因标签里有越界坐标或宽高为 0 的框归一化后出现非法值。解决转换后跑一遍校验脚本检查每行五个值是否都在合法范围、宽高是否大于 0。下面这段可以批量查def check_labels(label_dir): bad [] for f in Path(label_dir).glob(*.txt): for i, line in enumerate(f.read_text().splitlines()): if not line.strip(): continue parts line.split() if len(parts) ! 5: bad.append((f.name, i, 字段数不对)); continue _, cx, cy, w, h map(float, parts) if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): bad.append((f.name, i, 坐标越界)) return bad5.3 显存爆了batch 降到 1 还是爆现象CUDA out of memory。原因imgsz太大或者开了 mosaic 后单张图实际拼成 2×2 的大图。解决先把imgsz降到 640 确认能跑再逐步往上加同时把mosaic关掉试一次确认是不是增强导致的。钼靶整图训练显存紧张时切片训练是更根本的解法。5.4 模型只检大肿块钙化全漏现象肿块 AP 还行钙化 AP 接近 0。原因钙化目标太小1024 输入下可能只剩几个像素加上正样本少。解决提高imgsz、对含钙化的图做切片、在数据层面过采样含钙化样本。单纯调损失权重效果有限根子还是在分辨率和样本量。5.5 推理时同一病灶出好几个框现象一张图同一个位置叠了好几个框。原因用了切片推理但没做跨块 NMS或者 NMS 的 iou 阈值设太高。解决切片推理后把所有块的框汇总统一做一次 NMSiou设 0.5 左右如果病灶本身细长可以适当降到 0.4。6. 切片推理与结果验证让大图上的小病灶真正被检出来钼靶整图直接推理小病灶的召回很难看切片推理是绕不开的一步。思路是把原图按固定窗口和重叠切块每块单独送进模型再把所有框映射回原图坐标做 NMS。重叠是为了避免病灶正好落在切缝上被切两半。import numpy as np from ultralytics import YOLO def sliced_infer(model, img, tile1024, overlap128, conf0.25, iou0.5): h, w img.shape[:2] step tile - overlap boxes [] for y in range(0, max(h - overlap, 1), step): for x in range(0, max(w - overlap, 1), step): patch img[y:ytile, x:xtile] if patch.shape[0] 32 or patch.shape[1] 32: continue r model.predict(patch, confconf, verboseFalse)[0] for b in r.boxes: x1, y1, x2, y2 b.xyxy[0].tolist() boxes.append([x1 x, y1 y, x2 x, y2 y, float(b.conf), int(b.cls)]) return nms(boxes, iou) def nms(boxes, iou_thr): if not boxes: return [] boxes sorted(boxes, keylambda b: b[4], reverseTrue) keep [] while boxes: best boxes.pop(0) keep.append(best) boxes [b for b in boxes if iou(best, b) iou_thr] return keep def iou(a, b): xx1, yy1 max(a[0], b[0]), max(a[1], b[1]) xx2, yy2 min(a[2], b[2]), min(a[3], b[3]) inter max(0, xx2 - xx1) * max(0, yy2 - yy1) area_a (a[2]-a[0]) * (a[3]-a[1]) area_b (b[2]-b[0]) * (b[3]-b[1]) return inter / (area_a area_b - inter 1e-6)逻辑说明tile1024和overlap128是起点重叠太小病灶会被切断太大推理变慢。step是滑动步长。每个 patch 的框坐标加上x, y偏移映射回原图。最后统一 NMSiou_thr0.5是通用值病灶密集时可以降到 0.4。这段是纯 Python 实现慢但好懂生产环境可以换成 torchvision 的nms。验证方法上我习惯做两件事一是把预测框画回原图人工看几张重点看有没有漏掉的小钙化二是按病灶尺寸分桶统计召回比如小于 50 像素的、50 到 200 的、大于 200 的分桶看才能知道模型到底在哪一档失效。只看总体 mAP 会掩盖小目标的糟糕表现。最后说个我自己的习惯钼靶数据每次重新标注或调整类别后我都会先跑一遍标签校验脚本再抽 10 张图把框画出来肉眼过一遍确认没问题才开训。这个动作花不了十分钟但省下的是一次几小时的无效训练。医学影像的数据集没有后悔药标注错了模型只会忠实地学错。希望帮到你。本文还有配套的精品资源点击获取