
简介面向医学影像目标检测需求的YOLOv5格式CT肾脏、结石检测数据集适合需要训练YOLOv5等目标检测模型的开发者可直接用作目标检测数据集无需额外处理。数据集包含结石和肾脏两个类别图像分辨率统一为640×640按YOLOv5文件夹组织训练集325张图片与对应txt标签验证集135张图片与对应txt标签标注清晰完整。包内附带可视化Python脚本随机传入一张图片即可自动绘制边界框并保存结果便于快速核验标注效果。资源共923个文件其中460张jpg图像、461个txt标注文件另含1个py脚本和1个png示例压缩包大小16.9MB轻量易用。目前已有132人学习适合医学影像目标检测入门及YOLOv5流程实践。1. CT图像上的肾脏与结石检测数据准备决定了模型上限如果只把YOLOv5当成一个喂图出框的工具CT场景会立刻浇一盆冷水CT影像不是相机拍的JPG而是DICOM格式灰度跨越上千个Hounsfield单位直接转8位图会丢细节。肾脏在平扫CT里是边界模糊的中等灰度大目标结石往往只有几个到十几个像素两类目标在同一张图中天然失衡。标题里的2类别、训练集、验证集看着简单落地时却要处理窗宽窗位、按患者划分数据、标签格式转换和数据去重。下面以构建一份可用的CT肾脏、结石检测YOLOv5数据集为主线从DICOM预处理讲到训练参数与校验脚本适合正在做医学影像检测或准备用yolov5训练自己数据集的工程师做过自然图像检测但没碰过医疗数据的同学也能从中摸清边界。2. 从DICOM到YOLOv5能识别的CT图像窗宽窗位与8位转换2.1 CT图像的Hounsfield取值为什么不能直接缩放CT的本质是X射线衰减系数映射标准单位是HU范围从空气的-1024到致密骨骼的3000以上。肾脏实质平扫大约在30~40HU注射造影剂后可达120~200HU结石成分不同密度差异很大草酸钙结石常在400~800HU尿酸结石在200~400HU。如果按整张图的全局最小最大值做线性缩放软组织会被压成几个灰度级小结石直接淹没在背景里。正确做法是窗宽窗位裁剪窗位Window Center决定显示中心窗宽Window Width决定保留的灰度范围超出范围的部分全部截断。CT阅片的腹部窗一般取窗位40、窗宽350~400能看清肾脏轮廓结石窗取窗位300~400、窗宽1200~1500能把结石从软组织里分出来。数据集采用哪个窗取决于标注时用的显示条件训练和推理必须保持一致这也是CT数据集和自然图像数据集最本质的区别。目标/成分HU范围常用窗位/窗宽肾脏实质平扫30~40WC40, WW350~400肾脏实质增强120~200WC100, WW400草酸钙结石400~800WC300, WW1200~1500尿酸结石200~400WC300, WW1200~1500下面是最小可用的DICOM转PNG脚本核心是裁窗后固定映射到0~255不随图像自身分布漂移。import pydicom import numpy as np import cv2 def dcm_to_windowed_png(dcm_path, out_path, wc40, ww400): ds pydicom.dcmread(dcm_path) arr ds.pixel_array.astype(np.float32) # 有些CT厂商存的是RAW值需要换算成HU if RescaleSlope in ds and RescaleIntercept in ds: arr arr * float(ds.RescaleSlope) float(ds.RescaleIntercept) lower wc - ww / 2.0 upper wc ww / 2.0 arr np.clip(arr, lower, upper) # 固定映射避免每张图独立归一化导致序列亮度抖动 img ((arr - lower) / (upper - lower) * 255.0).astype(np.uint8) # YOLOv5按RGB三通道读图灰度图复制到三通道 img_bgr cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) cv2.imwrite(out_path, img_bgr)这段代码先读pixel_array再做HU换算关键在裁窗之后不能再用np.min/max做归一化。如果每张切片各自归一到0~255整个序列的亮度会在层面之间漂移模型学到的就不是密度信息而是扫描噪声。输出用PNG而不是JPG避免有损压缩破坏裁窗后的灰度层次这是医学图像做检测的一条底线。参数选择上我一般先看DICOM头里的WindowCenter和WindowWidth标签有没有写值有就用没有就按解剖部位定。肾脏、结石双类别检测常见两条路一是腹部窗和结石窗各出一套图分别训练两个模型再融合二是用中间窗WC50, WW600~800一张图同时保留软组织和结石对比度。后一种方案对YOLOv5更省事代价是小结石的对比度比专用结石窗略弱实际效果要跑一版验证才知道。2.2 切片筛选验证集里不能出现训练集同一序列的相邻层一份腹部CT平扫通常有几百层层厚1~5mm不等。YOLOv5吃的是单张二维图像直接把全部层入数据集有两个问题一是相邻切片高度相似数据冗余严重二是如果随意把同一个患者的部分层放进训练集、部分放进验证集验证指标会虚高。这里的原则是按患者划分而不是按切片划分。筛选层面时推荐只保留有标注框的阳性切片再加入少量无目标的负样本切片作为hard negative。结石检测场景里负样本能让模型学会拒绝肾实质内的血管钙化等高密度伪影。层厚的选择直接决定小结石能否被检出1mm或1.5mm重建层中结石才不会被部分容积效应抹平5mm层厚上小于3mm的结石基本只剩一个模糊亮点。同一切片序号内若有重复导出的版本只保留一份这也是后面要做图像去重的原因之一。3. 训练集与验证集的目录结构、标签格式与一致性校验3.1 YOLOv5标准目录结构与按患者划分脚本YOLOv5对数据集的目录约定是images和labels平级train与val各自成目录文件名严格一一对应。下面的结构可以直接套用ct-kidney-stone/ ├── images/ │ ├── train/ │ │ ├── ct_001_042.png │ │ └── ... │ └── val/ │ └── ct_101_018.png ├── labels/ │ ├── train/ │ │ ├── ct_001_042.txt │ │ └── ... │ └── val/ │ └── ct_101_018.txt └── dataset.yaml文件名里建议带上患者ID和切片序号比如ct_001_042表示1号患者第42层排错时能立刻定位来源也便于用正则做按患者划分。划分比例常见做法是8:2或9:1医学样本少时9:1也合理前提仍然是验证集患者不能出现在训练集。import os, random from collections import defaultdict img_root images/all patient_files defaultdict(list) for f in sorted(os.listdir(img_root)): pid f.split(_)[1] # 文件名形如 ct_001_042.png patient_files[pid].append(f) patients list(patient_files.keys()) random.seed(42) random.shuffle(patients) split int(len(patients) * 0.8) train_patients set(patients[:split]) val_patients set(patients[split:]) for pid, files in patient_files.items(): for f in files: src_img os.path.join(img_root, f) dst_img os.path.join(images/train if pid in train_patients else images/val, f) os.rename(src_img, dst_img) txt f[:-4] .txt src_txt os.path.join(labels/all, txt) dst_txt os.path.join(labels/train if pid in train_patients else labels/val, txt) os.rename(src_txt, dst_txt)脚本按患者ID聚合文件shuffle后整体切分再逐个移动图像和同名txt。注意先收集完患者列表再切分不能在遍历文件时边读边切否则同一患者的层会被拆到两边。划分完成后必须复查验证集里任意一个患者ID都不应出现在训练集文件名中这个检查要写进数据发布脚本里。提示同一患者上下相邻CT层虽然文件名不同内容却几乎相同整体移动是按患者划分的关键。一旦序列泄漏进验证集mAP会比真实泛化能力虚高后续调参全部失真。3.2 标注掩码转YOLO格式从分割拿到边界框医学影像标注工具ITK-SNAP、3D Slicer最常用的是画掩码直接得到分割图而YOLOv5要的是边界框文本。实际项目中不需要重新画框从掩码提取外接矩形即可。同一切片里肾脏和结石共用一个txt文件类别编号空间建议固定为0肾脏、1结石与后面的dataset.yaml保持一致。import cv2 def mask_to_yolo_txt(mask_path, txt_path, class_id, img_w, img_h, min_area4): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 连通域分析避免一个掩码里多个离散目标被合并或漏掉 num, labels, stats, _ cv2.connectedComponentsWithStats(mask) lines [] for i in range(1, num): x, y, w, h stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], \ stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] if w * h min_area: continue # 过滤掩码边缘的零散噪点 cx (x w / 2) / img_w cy (y h / 2) / img_h nw, nh w / img_w, h / img_h lines.append(f{class_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))这里用connectedComponentsWithStats而不是findContours是为了处理同一掩码里存在多个不相连目标的情况比如一张图上左右双肾各有一个掩码区域时不会丢框。坐标全部除以图像宽高归一化到0~1YOLOv5训练时会按img_size做letterbox缩放只要坐标在0~1之间就不会错位。min_area过滤掉面积只有几个像素的假框这类假框常出现在掩码边缘的孤立噪点上不清理会在验证时持续拉低mAP。3.3 标签与图像的一致性检查脚本数据集发布前至少跑一遍三项检查每个图像都有同名txt每个txt里坐标都在[0,1]区间两个类别的标注在训练集和验证集都有分布。下面是精简版脚本。import os def verify_pair(img_dir, lbl_dir): imgs set(f[:-4] for f in os.listdir(img_dir) if f.endswith(.png)) lbls set(f[:-4] for f in os.listdir(lbl_dir) if f.endswith(.txt)) assert not (imgs - lbls), f缺标签: {list(imgs - lbls)[:5]} assert not (lbls - imgs), f缺图像: {list(lbls - imgs)[:5]} for f in os.listdir(lbl_dir): with open(os.path.join(lbl_dir, f)) as fp: for line in fp: parts line.split() assert len(parts) 5, f{f} 行格式错误: {line} cls int(parts[0]) assert cls in (0, 1), f{f} 类别编号错误: {cls} vals list(map(float, parts[1:])) assert all(0 v 1 for v in vals), f{f} 坐标越界: {line.strip()} verify_pair(images/train, labels/train) verify_pair(images/val, labels/val)这段脚本应该在每次新增数据后都跑而不是只在发布时跑。CT数据常有多批次标注不同标注员对结石边界的判定标准有差异格式错误往往发生在批量转换脚本写错文件名映射的时候靠肉眼查几百个txt是查不出来的。检查通过后再进入训练环节能省下大量排错时间。4. 用YOLOv5训练自己CT数据集的参数与验证方法4.1 dataset.yaml与最小训练命令数据目录就绪后第一步是写dataset.yaml路径建议写绝对路径避免训练和验证时工作目录不一致导致找不到数据。train: /data/ct-kidney-stone/images/train val: /data/ct-kidney-stone/images/val nc: 2 names: 0: kidney 1: stone训练命令用YOLOv5官方仓库即可最小可用形式如下python train.py \ --img 640 \ --batch 16 \ --epochs 200 \ --data dataset.yaml \ --weights yolov5s.pt \ --hyp hyp.scratch-low.yaml \ --no-mosaic参数说明--img 640是默认值对CT双类别场景偏小显存允许时优先提到1280对比一版--batch按显存决定8GB卡用816GB以上用16--weights建议用coco预训练权重而不是随机初始化CT和自然图像差异虽大但浅层的边缘和纹理特征仍然可迁移--hyp选择hyp.scratch-low.yaml它是官方低增强配置比默认的hyp.scratch.yaml更适合样本量小的医学数据--no-mosaic表示全程不启用mosaic增强YOLOv5默认在每轮训练的最后10个epoch自动关闭mosaic帮助收敛但CT小目标场景下mosaic把四张图拼接后随机缩放小结石经常在拼接边界被裁掉或缩到消失直接关掉更省心。如果显存允许我一般先把--img提到1280训练一版再回退到640看指标差距。结石这类小目标在640输入下可能只占十几个像素模型下采样32倍后特征图上不到一个点1280输入能显著改善召回代价是训练时间和显存翻几倍。两份实验对比后再决定最终部署用的分辨率。4.2 医学灰度图下的YOLOv5超参数调整YOLOv5默认超参数面向自然图像设计直接套到CT上会有一批增强是负收益。以hyp.scratch-low.yaml为基线重点调整以下几项超参数scratch-low默认值建议值调整理由hsv_h0.010CT灰度图没有色调概念hsv_s0.70饱和度增强会扭曲HU信息hsv_v0.40亮度抖动过大会破坏软组织对比fliplr0.50.5水平翻转对CT可行解剖左右对称scale0.50.3过大尺度变化会让小结石直接消失mosaic1.00小目标场景建议关闭理由见4.1修改方式是在hyp.scratch-low.yaml里直接改值或复制一份自己的yaml再传给--hyp。需要特别注意的是CT图像已经做过窗宽窗位裁切任何色彩空间类的增强都是在改动标注者当时的显示条件模型学到的特征会被无意义地扰动。flipud垂直翻转不建议开腹部扫描的上下方向有解剖学意义翻转过后的肾脏形态和位置关系不自然。超参数之外anchor也值得看一眼。YOLOv5训练启动时会自动执行autoanchor检查根据数据集的真实框尺寸重算anchors启动日志里会输出Optimal anchors一行确认替换结果即可。当输入尺寸从640提到1280后anchor会自动适配到新的尺度分布一般不手动干预。对小目标而言输入分辨率才是信息量的上限anchor只是匹配策略花太多时间调anchor不如先提升分辨率。4.3 验证集评估命令与mAP的解读方式训练完成后验证指标的反应和自然图像检测不一样。肾脏是大目标、每张切片出现频率高AP通常轻松到0.95以上结石是稀疏小目标AP0.5如果低于0.7就值得怀疑标注有漏框或前后切片的窗宽窗位没有统一。评估用官方val.pypython val.py \ --weights runs/train/exp/weights/best.pt \ --data dataset.yaml \ --img 1280 \ --conf 0.001 \ --iou 0.5注意val.py默认置信度阈值就是0.001保持这个值才能看到小目标的真实召回。如果为了PR图好看把阈值调到0.25低置信度的小目标全被过滤得到的mAP只代表高置信度子集不能作为发布指标。结果里重点看stone类的recall和PR曲线拐点。拐点靠前说明模型对小结石的置信度整体偏低首要任务是提高输入分辨率或补标漏框而不是无脑加数据。另一个容易忽略的维度是假阳性类型结石检测的假阳性大概率来自肾盂钙化、血管壁钙化这类高密度结构它们在HU上和真结石几乎无法区分。如果模型把肾窦内的高密度点全报到stone类说明训练数据里缺同密度但不是结石的反例要在负样本切片里补充这类结构而不是调低置信度阈值硬压。5. 训练集图像去重与标签分布检查两个硬性技巧5.1 用dHash对训练集和验证集做去重CT序列面临的重复有两种一种是完全相同的文件来自批量导出失误另一种是相邻层近重复像素几乎一致。第一种用md5就能查第二种要用感知哈希。dHash实现简单对医学灰度图足够稳定import cv2 import numpy as np def dhash(img, size16): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (size 1, size), interpolationcv2.INTER_AREA) diff resized[:, 1:] resized[:, :-1] return np.packbits(diff.ravel()) def hamming(a, b): return bin(int(np.bitwise_xor(a, b))).count(1)size16时哈希为256位汉明距离小于等于8视为近重复。对所有图像两两比较是O(n^2)CT数据集几千张还能接受更大时先按患者分组再在组内比较。需要明确处理策略优先保留验证集里的图像从训练集里删除与验证集近重复的样本训练集内部的重度重复层每隔N层保留一层即可既能去冗余又不丢结石出现的连续性上下文。5.2 标签分布统计与错标回看训练前用一条命令统计两个类别的标注框数量分布awk {count[$1]} END {for (c in count) print c, count[c]} labels/train/*.txt只看数字不够我会把标注框画回图像上做一轮快速巡检肾脏的大框有没有把肾门血管包进来结石框是否全部落在肾实质的高密度区同一结石在连续两层上的框位置是否跳变。这一步通常能发现两成以上的标注问题是发布前性价比最高的检查。工具上可以用CVAT人工复查也可以直接写脚本批量输出带框图。对采用宽窗单图方案的数据终检的终极技巧是用训练好的模型在验证集上做一次推理把所有预测为stone但标注里没有对应框的切片单独抽出来对照放宽窗宽后的原图人工复核。如果这些假阳性位置确实有高密度影那不是模型错误而是漏标把这类切片补标后加入训练集比调任何超参数都更有效。本文还有配套的精品资源点击获取