2026/10/16 3:23:23

LIDC肺结节CT数据集处理:DICOM与XML对齐提取ROI全指南

LIDC肺结节CT数据集处理:DICOM与XML对齐提取ROI全指南 简介一套专门用于LIDC-IDRI肺结节CT数据集的工具包面向医学影像研究人员、算法工程师与相关专业学生帮助解决从原始CT扫描图像和多位医生XML标注中提取、转换并整理肺结节病灶信息的难点。资源包共24个文件压缩后约182KB以8个MATLAB脚本为核心覆盖注解矩阵与掩模图像、真实图像的互相转换XML文件解析像素映射重构以及大规模注解数据拆分等环节另配4个Perl脚本、3个Shell脚本用于批量检索DICOM影像、读取图像几何参数、修正文件路径等预处理操作6个txt文档及许可说明、使用手册则对工具原理和调用方式作了交代。目前已有2947人学习下载。借助这套工具用户无需从零编写数据转换代码即可快速获得肺结节掩模、Ground Truth等规范化数据直接支撑结节检测、分割、特征提取和深度学习模型训练既能减少重复劳动也便于在统一数据格式下复现和对比实验结果是入门与进阶肺结节影像分析的实用资源。1. lidc 肺结节 CT 数据集工具包先搞懂原始影像再谈模型很多人下载 lidc 肺结节 CT 数据集时都会卡在同一个位置手里一堆 DICOM 文件标注全在 XML 里两套数据看起来毫无关联。某开发者第一次用这份工具包时光是把 XML 里的轮廓坐标对上 CT 层面就折腾了两天最后发现坑在坐标系翻转和层厚过滤上。这份资源就是把“原始 CT 影像 医生标注”变成“能直接喂给检测或分割模型的结节切片”的完整链路覆盖 DICOM 读取、XML 解析、ROI 切图、批量导出和标签提取。适合正在做肺结节检测、分类或分割的从业者和研究生尤其是不想从零造轮子、只想快速拿到干净数据的人。先讲数据结构再逐步复现最后是踩过的坑。2. 认识数据集结构DICOM 影像与 XML 标注的对应关系2.1 一个病例的目录长什么样数据集按病例组织每个病例一个文件夹DICOM 文件和 XML 标注文件平铺在一起。一个病例里可能同时出现平扫和增强、薄层和厚层等多套序列全部混在同一目录下。LIDC/ ├── case_0001/ │ ├── 000001.dcm │ ├── 000002.dcm │ ├── ... │ ├── 000286.dcm │ └── annotation.xml └── case_0002/ ├── 000001.dcm ├── ... └── annotation.xml这里第一个坑文件名不能作为排序依据。部分序列文件名是纯流水号跨序列还有重号按名字排序会导致层面错乱。正确做法是用 DICOM 头里的 SeriesInstanceUID 区分序列再用 ImagePositionPatient 的 z 分量排序。为什么一个病例会出现多套序列因为同一患者做检查时会扫平扫和增强重建算法还可能输出薄层与厚层两组图像。XML 标注只针对其中某一个序列通常是层厚最薄的那套。所以读数据的第一步不是直接读像素而是先识别出正确的序列否则后面所有匹配都会错位。2.2 关键 DICOM 字段与读取DICOM 读取不能只看像素阵列有几个字段决定了后续提取是否可靠。字段Tag作用Modality(0008,0060)确认是否为 CT 序列SeriesInstanceUID(0020,000E)区分不同采集序列SliceThickness(0018,0050)层厚决定 z 方向分辨率ImagePositionPatient(0020,0032)每层切片的物理 z 坐标RescaleSlope / RescaleIntercept(0028,1053) / (0028,1052)存储值到 HU 的线性变换系数读取并组织成有序序列的代码import pydicom from pathlib import Path def read_ct_series(series_dir, thickness_max1.5): # 读取目录下所有 dcm 文件 files list(Path(series_dir).glob(*.dcm)) dcms [pydicom.dcmread(str(f)) for f in files] # 只保留 CT 序列且包含像素数据的文件 ct_slices [ d for d in dcms if d.get(Modality) CT and PixelData in d ] # 按层厚过滤掉厚层序列 ct_slices [ d for d in ct_slices if float(d.SliceThickness) thickness_max ] # 按物理 z 位置排序而不是按文件名 ct_slices.sort(keylambda d: float(d.ImagePositionPatient[2])) return ct_slices逻辑说明先按 Modality 过滤掉定位像或其他非 CT 序列再用 SliceThickness 只保留薄层最后用 ImagePositionPatient 的 z 值做空间排序。返回的列表就是按人体头尾方向排列的连续切片。参数说明thickness_max 默认 1.5mm这是提取肺结节的经验阈值。小于 3mm 的结节在 1.5mm 层厚下至少能覆盖两层更厚的层会因体积平均效应抹掉小目标。如果数据集里只有厚层序列可以放宽到 3mm但后续 ROI 质量会明显下降。另外如果目录里有嵌套子目录glob(.dcm) 只匹配当前层要改成 rglob(.dcm)。实际处理时我一般还会打印每个序列的 SeriesInstanceUID 和 SliceThickness发现一个病例里有多套薄层序列时再结合 SeriesDescription 判断哪一套是对齐标注的。这个判断宁可人工确认不要盲信名字。2.3 XML 标注的层级结构XML 文件承载所有医生标注结构是固定的四层嵌套readingSession 是某位医生的完整读取会话里面包含多个 unblindedReadNodule每个结节又包含多个 roi每个 roi 对应 CT 的一个层面roi 下面才是边缘坐标 edgeMap。readingSession unblindedReadNodule noduleTypenodule/noduleType malignancy4/malignancy roi imageZposition-124.2/imageZposition edgeMap x210/xy245/y x215/xy240/y /edgeMap /roi /unblindedReadNodule /readingSessionnoduleType 有三种取值nodule 是直径大于等于 3mm 的结节带完整边缘less than 3 mm 是小于 3mm 的小结节只有中心标记没有轮廓non-nodule 是非结节做检测时一般不取。malignancy 字段只有部分 nodule 有取值 1 到 55 恶性程度最高可以当软标签用。imageZposition 是 XML 和 DICOM 之间的桥梁。它是物理坐标单位毫米和 DICOM 里 ImagePositionPatient[2] 同属一个坐标系。“找到该层面”在代码里就是找 z 值最接近的那张切片。需要注意多位医生会各自生成一个 readingSession同一个结节可能重复出现在不同 session 里。这不是脏数据而是设计如此。提取时如果不做去重就会把同一结节的多次标注当成多个样本后面第 5 章会专门讲这个坑。2.4 用 z 坐标把两套数据对齐匹配层面的函数只需要几行import numpy as np def find_layer_index(dcms, target_z): # 取所有切片的物理 z 坐标 z_vals [float(d.ImagePositionPatient[2]) for d in dcms] # 找和 target_z 最接近的切片索引 return int(np.argmin(np.abs(np.array(z_vals) - target_z)))逻辑说明把目标 z 与所有切片的 z 做差取绝对值最小值的索引就是要找的层面。参数 target_z 来自 XML 里的 imageZposition。这里有个前提XML 的 z 必须和所选 DICOM 序列是同一套采集。如果 XML 基于薄层标注而你拿了厚层序列去匹配层间隔不一样匹配到的层面可能偏出半个层厚以上。所以我在读取函数里就把序列锁定成薄层 CT匹配前不再重复筛选序列。到这一步两套数据的对应就打通了DICOM 提供体素和物理位置XML 提供结节的轮廓和语义标签两者通过 z 坐标绑定在一起。接下来就可以做真正的提取工作。3. 提取肺结节从 XML 标注到 ROI 切片3.1 解析结节信息先写一个解析函数把 XML 里的结节、轮廓、恶性程度全部读出来。import xml.etree.ElementTree as ET def parse_nodules(xml_path): root ET.parse(str(xml_path)).getroot() nodules [] for session in root.iter(readingSession): for nodule in session.iter(unblindedReadNodule): nod_type nodule.findtext(noduleType) # 默认只要大于等于 3mm 的结节 if nod_type ! nodule: continue rois [] for roi in nodule.iter(roi): z float(roi.findtext(imageZposition)) xs [int(x.text) for x in roi.iter(x)] ys [int(y.text) for y in roi.iter(y)] rois.append({ z: z, xs: xs, ys: ys, center: (sum(xs) // len(xs), sum(ys) // len(ys)) }) malignancy nodule.findtext(malignancy) nodules.append({ type: nod_type, malignancy: int(malignancy) if malignancy else None, rois: rois }) return nodules逻辑说明遍历所有医生的 readingSession只保留 nodule 类型。每个 roi 代表这个结节在某一层的轮廓xs 和 ys 是成对的像素坐标序列。center 取轮廓点均值后面做定位和去重时会用到。malignancy 可能缺失所以做了 None 处理。参数说明如果想连小于 3mm 的结节也一并提取把过滤条件改成 if nod_type in (nodule, less than 3 mm)。但这类小结节没有 roi 边缘只有中心点实际提取不出有意义的切片我默认把它滤掉。输出结构的字段如下键类型说明typestrnodule / non-nodule / less than 3 mmmalignancyint / None恶性程度 1-5roislist每层一个 roi含 z、xs、ys、center解析完 XML 后建议先打印一个病例的结节数量和每个结节的层面数。如果所有结节都只有一层 roi说明这个结节贴在一个薄片上提取时要格外小心中心层选择。如果某个结节有 5 层以上 roi那是个大结节提取时随便取中心层都不容易出错。3.2 生成掩膜并裁剪包围盒拿到轮廓点之后用 OpenCV 的 fillPoly 生成二值掩膜再按掩膜的外接矩形裁剪。import cv2 def make_mask(shape, roi): # 生成全零掩膜 mask np.zeros(shape, dtypenp.uint8) # 把轮廓点组装成 (N, 1, 2) 格式 pts np.stack([roi[xs], roi[ys]], axis-1).astype(np.int32) # 填充闭合多边形 cv2.fillPoly(mask, [pts], 1) return mask def crop_roi(volume, mask, margin8): # 找到掩膜非零区域 ys, xs np.where(mask 0) if len(xs) 0: return None # 外扩 margin 像素并限制在边界内 y0 max(int(ys.min()) - margin, 0) y1 min(int(ys.max()) margin, volume.shape[1]) x0 max(int(xs.min()) - margin, 0) x1 min(int(xs.max()) margin, volume.shape[2]) # 沿 z 轴整段裁剪三维体 return volume[:, y0:y1, x0:x1]逻辑说明make_mask 把轮廓填充成 0/1 掩膜crop_roi 根据掩膜非零区域计算包围盒并外扩 margin然后沿着 z 轴整段裁剪。这样切出来的 ROI 是一个三维小块中心层是结节本体上下层是邻近背景对训练分割模型更友好。参数说明margin 单位是像素不是毫米。如果希望所有病例的物理范围一致要用 PixelSpacing 换算margin_px int(margin_mm / float(spacing))。比如 margin_mm 给 5mmPixelSpacing 是 0.69mmmargin_px 就是 7 像素。另外裁剪前建议确认 mask 和 volume 的坐标系一致否则会出现轮廓错位具体见第 5 章。每个结节的中心层不需要手动指定代码用 rois 列表的中间元素自动定位。边界情况是当 rois 长度为偶数时取下标 len(rois)//2 会偏向列表后半段对 z 方向影响不大因为层面通常连续分布偏移一层不影响裁剪结果。3.3 窗宽窗位转换CT 值动态范围达上千 HU而肺结节的软组织对比集中在一个窄区间。直接做全局归一化弱小病灶会被淹掉。常规做法是窗宽窗位截断。def hu_to_uint8(slice_hu, window_width1500, window_level-500): # 计算窗口上下界 lower window_level - window_width / 2 upper window_level window_width / 2 # 截断到窗内 clipped np.clip(slice_hu, lower, upper) # 线性映射到 0-255 out (clipped - lower) / (upper - lower) * 255 return out.astype(np.uint8)逻辑说明先把 HU 值截断到 [lower, upper]再线性映射到 [0, 255]。低于窗位下限的全部变黑高于上限的变白中间段保留对比度。参数说明window_width1500、window_level-500 是肺窗的常用组合适合看肺实质和结节。如果是纵隔窗改 window_width400、window_level40。提取肺结节一律用肺窗纵隔窗下结节边缘会糊。做 3D 网络训练时这套转换要固化成一个函数避免每个病例各自归一化造成分布漂移。这里有个常见误用有人先对整张 CT 做窗宽窗位再裁剪导致裁剪出的 ROI 数值范围被全局极值带偏。正确做法是先裁剪出 HU 体数据再逐层套用同一套窗宽窗位参数转换。这样每个 ROI 的对比度标准一致模型输入分布也稳定。3.4 导出切片与体积数据最后把三维 ROI 切成 2D 图或者保存成 npy 体积。from pathlib import Path def export_slices(roi_volume, save_dir, prefix): save_dir Path(save_dir) save_dir.mkdir(parentsTrue, exist_okTrue) # 逐层转成 uint8 并保存 PNG for i in range(roi_volume.shape[0]): img hu_to_uint8(roi_volume[i]) png cv2.imencode(.png, img)[1].tobytes() (save_dir / f{prefix}_slice{i:02d}.png).write_bytes(png) # 同时保留原始 HU 体积 np.save(save_dir / f{prefix}.npy, roi_volume)逻辑说明roi_volume 的 shape 是 (z, y, x)逐层转 uint8 写 PNG同时保留原始 HU 体积为 npy。PNG 给 2D 网络训练和人工 QCnpy 给 3D 网络训练。参数说明prefix 建议带上病例名和结节序号例如 case_0001_nod_00方便回溯到原始标注。如果要压缩体积可以只保存中心三到五层但模型需要上下文时反而吃亏我一般全层保留。把 3.1 到 3.4 串起来的单例流程可以这样组织def process_single_case(dcm_dir, xml_path, out_dir): dcms read_ct_series(dcm_dir) volume np.stack([d.pixel_array for d in dcms], axis0) nodules parse_nodules(xml_path) for n_idx, nodule in enumerate(nodules): if not nodule[rois]: continue center_roi nodule[rois][len(nodule[rois]) // 2] layer find_layer_index(dcms, center_roi[z]) mask make_mask(volume.shape[1:], center_roi) roi_volume crop_roi(volume, mask) if roi_volume is None: continue prefix fcase_{Path(dcm_dir).name}_nod{n_idx:02d} export_slices(roi_volume, out_dir, prefix)逻辑说明这个函数把所有步骤串成一条线输入是一个病例目录和对应 XML输出是 PNG 切片和 npy 文件。跑通这一个函数之后批量就是套循环或进程池的事。参数说明volume 用 np.stack 把 DICOM 列表转成 (z, y, x) 数组dtype 是 uint16后续在 hu_to_uint8 里做浮点运算。如果机器内存紧张可以先只读中心层附近的切片不要一次载入整套序列。4. 批量处理与参数设定把单例脚本跑成可用数据集4.1 先建索引表再动手大规模处理前先扫目录生成索引这一步能提前发现坏数据比跑一半报错再回头查高效得多。def build_index(root_dir): root_dir Path(root_dir) index [] for case_dir in sorted(root_dir.iterdir()): if not case_dir.is_dir(): continue xmls list(case_dir.glob(*.xml)) dcms list(case_dir.glob(*.dcm)) # 正常病例应该恰好一个 xml 且至少一个 dcm if len(xmls) 1 and len(dcms) 0: index.append({ case: case_dir.name, xml: xmls[0], dcm_dir: case_dir }) else: print(f跳过 {case_dir.name}: xml{len(xmls)} dcm{len(dcms)}) return index逻辑说明索引表记录了病例名、XML 路径和 DICOM 目录同时统计文件数量。xml 数不等于 1 或 dcm 数为 0 的病例直接打标跳过。参数说明如果数据集的目录层级不是“一键直达”的把 glob(.dcm) 改成 rglob(.dcm)但要小心一个病例嵌套多个子序列时会把多套序列混进来。更稳妥的做法是在索引表里记录 SeriesInstanceUID后续按 UID 锁定目标序列。索引表还有一个作用它能直观反映数据集规模。把结果存成 CSV 或 pickle后面换机器跑或者复现时就不需要重新扫描一遍目录了。我一般会在这一步同时记录每个病例的 XML 文件大小标注文件小于 1KB 的基本是空标注或坏文件直接标记待人工检查。4.2 批量提取的进程池写法单例跑通后批量就是套一个进程池的事。核心是让每个病例的处理函数独立互相不共享状态。from concurrent.futures import ProcessPoolExecutor def process_one_case(entry): try: dcms read_ct_series(entry[dcm_dir]) # 重组成三维体数据 volume np.stack([d.pixel_array for d in dcms], axis0) nodules parse_nodules(entry[xml]) results [] for n_idx, nodule in enumerate(nodules): if not nodule[rois]: continue center_roi nodule[rois][len(nodule[rois]) // 2] layer find_layer_index(dcms, center_roi[z]) mask make_mask(volume.shape[1:], center_roi) roi_volume crop_roi(volume, mask) if roi_volume is None: continue prefix f{entry[case]}_nod{n_idx:02d} export_slices(roi_volume, entry[out_dir], prefix) results.append({ case: entry[case], nodule: n_idx, malignancy: nodule[malignancy], roi_shape: roi_volume.shape, layer: layer }) return results except Exception as e: # 单个病例失败不影响整体 print(f处理 {entry[case]} 出错: {e}) return [] with ProcessPoolExecutor(max_workers4) as pool: all_results list(pool.map(process_one_case, index))逻辑说明每个病例读一次序列、重建一次 3D 体然后逐结节取中心层面、生成掩膜、裁剪、导出。异常被捕获后就地跳过返回空列表不拖垮整个批次。参数说明max_workers 要按内存调。一个薄层病例的 3D 体积在 float 下大约 300-500MB4 个进程同时跑峰值约 2GB16GB 内存的机器用 8 进程问题不大8GB 内存就老实开 2。跑全量前务必先用单例模式跑三个病例。把 process_one_case 直接在一个 index 元素上执行看输出效果和报错没问题再上进程池。省去“跑了半小时才发现路径写错”的后悔药时间。4.3 参数集中管理与调参建议批量前把参数集中成配置块比散落在代码里好维护。参数推荐值说明thickness_max1.5mm薄层阈值控制 z 向分辨率margin8-12px包围盒外扩像素window_width1500肺窗窗宽window_level-500肺窗窗位min_area20 px滤掉面积过小的掩膜噪声nodule_typenodule只提取大于等于 3mm 的结节margin 的取舍是个平衡外扩太小边缘被截断外扩太大背景占比过高模型会学到过多无关结构。我一般先对几个病例画叠加图看效果再定。min_area 用来挡掉因轮廓异常产生的碎片 mask面积小于 20 像素的直接丢弃这个值针对的是本数据集的像素间距范围换数据集要重新标定。输出格式上2D 模型吃 PNG 序列3D 模型吃 npy 体积。如果磁盘紧张npy 可以转成 npz 只保留中心三到五层但前提是模型的感受野能接受。我自己做分割时都保留全层切片边缘层对边界回归有帮助。5. 避坑指南坐标翻转、HU 转换与多医生标注那些坑这一章是血泪经验五个坑按高频程度排序。每一条都是“现象、原因、解决”三段结构。5.1 提取的 ROI 全黑或全白现象切出来的切片要么黑成一片要么亮成一片完全看不到肺纹理。原因CT 的 PixelData 里存的并不是 HU 值。不少设备存的是未标定的重建值需要按 RescaleSlope 和 RescaleIntercept 做线性变换。直接用原始值做图像范围不对映射到 0-255 时全部落在同一端。解决取到 pixel_array 后立即转 HU。slope float(d.RescaleSlope) intercept float(d.RescaleIntercept) # 存储值转成 HU hu d.pixel_array * slope intercept这段代码写在读取函数里后续所有处理都基于 hu 而不是原始像素。注意 slope 和 intercept 是每个 series 级别的 tag不是每个文件都不同按整个序列读一次赋值即可。5.2 mask 和结节明显错位现象画出来的轮廓落在结节的对侧或斜向镜像位置看起来像标注错了。原因坐标系原点不一致。XML 里的 x、y 在部分工具链中按左下角原点记录而 numpy 数组的 (row, col) 按左上角原点。两者只差一个 y 方向翻转视觉上就是上下颠倒。解决先抽一个病例画叠加图验证。确认错位后做一次行方向翻转mask mask[::-1, :]这一步看起来简单但每换一个数据源都要重新验证一次。我习惯在批量前先跑五个病例逐个眼睛比对掩膜和 CT 轮廓确认无误再放开全量。换脚本重写解析逻辑后也要重跑这个验证别以为以前对过就永远对。5.3 提取的结节呈细长条现象同一个结节切出来只有一两层有信号其他层几乎是空的整体呈细长条。原因层面匹配不准。XML 的 imageZposition 和 DICOM 的 ImagePositionPatient[2] 本来可以精确对齐但如果你用了厚层序列去匹配层间距大坐标落在两层中间时argmin 会把目标拉到相邻层实际切面偏离结节中心。解决锁定薄层序列后再匹配并且做偏差校验。target_z center_roi[z] layer find_layer_index(dcms, target_z) z_error abs(float(dcms[layer].ImagePositionPatient[2]) - target_z) # 偏差超过半个层厚时报警 if z_error float(dcms[layer].SliceThickness) / 2: print(f警告: {target_z} 匹配偏差 {z_error:.2f}mm)逻辑说明匹配偏差大于半个层厚说明 XML 和 DICOM 可能不是同一套序列直接报警比安静产出坏数据更有用。参数说明层厚小于 1mm 时半个层厚是 0.5mm容差收紧到 1mm 也不亏。5.4 同一结节被重复提取现象输出列表里有两个 ROI 的坐标几乎一样形状几乎一样只是来源 session 不同。原因标注由多位读片医生独立完成同一结节在多个 readingSession 里都会出现。如果解析时不区分 session同一个结节会被重复计入。解决三种策略按需选。第一只取第一个 readingSession 的结果简单但有召回损失第二按结节重心聚类去重重心距离小于某阈值视为同一结节第三做成多标签聚合把多位医生的标注都保留下来训练时做融合目标。我一般用重心聚类def dedup_nodules(nodules, max_dist_px12): kept [] for nod in nodules: if not nod[rois]: continue # 取中心层面重心 cx nod[rois][len(nod[rois]) // 2][center][0] cy nod[rois][len(nod[rois]) // 2][center][1] # 与已保留结节去重 if any(abs(cx - kc[0]) max_dist_px and abs(cy - kc[1]) max_dist_px for kc in kept): continue kept.append(nod) kept[-1][center_overall] (cx, cy) return kept逻辑说明用中心层面的重心做去重距离阈值按像素算。参数说明max_dist_px 要根据结节大小调整结节越小阈值越小。12px 在 0.69mm 像素间距下约等于 8mm 空间距离对 3mm 以上的结节是安全上限。5.5 一批病例提取结果为空现象某个病例跑完输出里没有 ROI也没有报错。原因有的是正常对照病例本来就没有结节有的则是 XML 解析路径有问题解析出来的结节列表为空。两种情况混在一起不排查会误判数据集质量。解决统计每个病例的结节数分布。正常对照病例占比高是符合预期的但如果连标注明确的病例都为空检查 XML 里 noduleType 是否为 nodule 而非 Nodule 或其他大小写变体。另外确认 XML 文件是 UTF-8 编码某些编辑器保存成 GBK 会让 ElementTree 直接抛编码错误。此时在 build_index 阶段打印 XML 文件头几个字节能提前发现编码问题。我还会在批量结束后把 all_results 汇总成一张 CSV统计每个病例的结节数、平均层面数、ROI 尺寸分布。任何异常都能在这张表里暴露出来而不是等模型训练崩了才回头查数据。6. 进阶验证对准、提取标签并做增强6.1 叠加验证mask 与 CT 是否真的对上批量产出后别急着训练先做叠加验证。OpenCV 把掩膜轮廓画到窗宽窗位处理后的图像上人工扫一眼就能判断对齐。def overlay_mask(slice_hu, mask, window_width1500, window_level-500): # 转成可视化图像 img hu_to_uint8(slice_hu, window_width, window_level) img_bgr cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # 提取掩膜轮廓 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 画出红框标注 cv2.drawContours(img_bgr, contours, -1, (0, 0, 255), 2) return img_bgr逻辑说明把二值掩膜转成轮廓画到 BGR 图像上红框即标注边界。参数说明如果发现轮廓系统性偏移回到第 5.2 节的翻转处理。这一步应当纳入每次批量提取的固定流程而不是出问题才做。6.2 提取标签并做随机增强malignancy 字段可以转成二分类标签配合简单增强扩充样本量。def malignancy_to_label(mal): # 1-2 归为良性3-5 归为恶性 if mal is None: return None return 1 if mal 3 else 0 def augment_slice(img, seedNone): # 90 度整数倍旋转加水平翻转 rng np.random.default_rng(seed) k int(rng.integers(0, 4)) img np.rot90(img, k) if rng.random() 0.5: img img[:, ::-1] return img逻辑说明旋转 90 度的整数倍加水平翻转不引入插值噪声。参数说明如果要模拟亮度扰动可以在 HU 域加上一个整体偏移后再做窗宽窗位转换比在 uint8 空间加值更接近真实扫描差异。增强只在训练集上做验证集保持原图。从那以后我每次拿到新的医学影像数据集都会强制先跑三件事建索引、转 HU、叠加验证。这三步没问题才敢放开批量提取。希望帮到你。本文还有配套的精品资源点击获取