简介:面向医学影像目标检测需求的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~40 | WC=40, WW=350~400 |
| 肾脏实质(增强) | 120~200 | WC=100, WW=400 |
| 草酸钙结石 | 400~800 | WC=300, WW=1200~1500 |
| 尿酸结石 | 200~400 | WC=300, WW=1200~1500 |
下面是最小可用的DICOM转PNG脚本,核心是裁窗后固定映射到0~255,不随图像自身分布漂移。
import pydicom import numpy as np import cv2 def dcm_to_windowed_png(dcm_path, out_path, wc=40, ww=400): 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标签有没有写值,有就用,没有就按解剖部位定。肾脏、结石双类别检测常见两条路:一是腹部窗和结石窗各出一套图,分别训练两个模型再融合;二是用中间窗(WC=50, WW=600~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_area=4): 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~1,YOLOv5训练时会按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卡用8,16GB以上用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_h | 0.01 | 0 | CT灰度图没有色调概念 |
| hsv_s | 0.7 | 0 | 饱和度增强会扭曲HU信息 |
| hsv_v | 0.4 | 0 | 亮度抖动过大会破坏软组织对比 |
| fliplr | 0.5 | 0.5 | 水平翻转对CT可行,解剖左右对称 |
| scale | 0.5 | 0.3 | 过大尺度变化会让小结石直接消失 |
| mosaic | 1.0 | 0 | 小目标场景建议关闭,理由见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以上;结石是稀疏小目标,AP@0.5如果低于0.7,就值得怀疑标注有漏框或前后切片的窗宽窗位没有统一。
评估用官方val.py:
python 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, size=16): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) resized = cv2.resize(gray, (size + 1, size), interpolation=cv2.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")size=16时哈希为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但标注里没有对应框的切片单独抽出来,对照放宽窗宽后的原图人工复核。如果这些假阳性位置确实有高密度影,那不是模型错误而是漏标,把这类切片补标后加入训练集,比调任何超参数都更有效。
本文还有配套的精品资源,点击获取