多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

肝癌影像AI诊断实战:从DICOM到模型训练的医疗影像处理指南

肝癌影像AI诊断实战:从DICOM到模型训练的医疗影像处理指南 简介肝癌影像AI诊断项目代码包面向医疗影像研究与深度学习开发者尤其是相关课题的学生或算法工程师提供一套基于Python的肝癌医学影像分析与诊断流程。环境基于Anaconda Python 3.6依赖TensorFlow 1.8、OpenCV、SimpleITK、scikit-image等常见库涵盖数据预处理、数据集管理、模型定义、训练与评估等环节能在Windows/Linux x64系统上运行适合用于复现实验、学习医学图像预处理与模型训练。压缩包共7个文件主要包括4个Python脚本涉及数据预处理、模型定义、训练与推断、2个文本说明文件含环境配置与标签说明和1个Markdown文档整体体积仅8KB轻量精炼。目前已有36人学习下载适合作为入门与复现参考。读者可借此快速搭建肝癌影像AI诊断的代码框架理解从医学影像读取、预处理到模型训练与评估的完整链路为开展相关研究或调优提供参考。1. 拿到“肝癌影像AI诊断.zip”先别急着解压从同事、导师或某个项目分享链接拿到一个“大数据医疗-肝癌影像AI诊断.zip”第一反应通常是双击解压然后看里面有什么。这个包里大概率装着肝癌CT影像、病灶标注、模型代码、推理脚本运气好还有一份 README。它能把“医疗影像 AI”从概念变成一组可以打开的文件不需要自己从零收集肝癌CT也不用先把医学影像论文读三个月找到正确的打开方式就能跑通“图像输入 → 模型输出 → 诊断参考”的完整链路。特别适合正在做大数据毕业设计、医学图像处理课程项目或者想评估本地机器能否跑医疗AI的工程师。不过先泼一盆冷水真正让你翻车的通常不是模型结构而是 DICOM 读不出来、数据划分捣鬼以及 zip 包本身的问题。2. 解开 zip 之前先看清这个包的真实结构拿到压缩包先别急着双击解压。医疗影像项目的 zip 和普通软件压缩包不一样里面有大量 DICOM 文件、JSON 标注、模型权重文件又多又大一旦中间有个文件损坏解压到一半报错你连问题出在哪都看不出来。我一般会先列一下包内清单确认三件事目录长什么样、有没有说明文档、模型文件和数据文件是否完整。这一步花了不到一分钟却能省下后面一整天的排错时间。2.1 用 unzip 和 Python zipfile 先列清单别盲解在 Linux 或 macOS 上直接用 unzip 的列表模式看包内容不解压unzip -l 大数据医疗-肝癌影像AI诊断.zip | head -50这个命令会打印出压缩包内每个文件的路径、原始大小和压缩后大小。加上head -50是防止文件太多刷屏。重点看 filename 前缀是否有data/、code/、model/、docs/这样的顶层目录是否有一个README.md或requirements.txt在根目录。如果所有文件都堆在根目录下没有分层说明打包的人没太在意结构后面找文件要耐心一点。Windows 没有原生命令行支持 unzip 完整参数的话用 Python 的 zipfile 模块效果一样还能顺便把压缩方式打出来import zipfile zf zipfile.ZipFile(大数据医疗-肝癌影像AI诊断.zip) for info in zf.infolist(): print(f{info.filename} {info.file_size} {info.compress_type})compress_type是 0 表示 store不压缩8 表示 deflate普通压缩。医疗影像 PNG 和 DICOM 文件如果大量是 store 存储zip 体积会比想象中大很多这是正常的不是因为包坏了。先列清单还有一个好处你能看到模型权重文件名是.pth、.h5还是.onnx这会直接决定后面推理脚本用哪个框架去加载。2.2 伪加密和损坏包解压失败的两种典型形态列完清单再解压最常见的翻车现场有两个。第一个是“伪加密”。现象是unzip能正常列目录但一解压就提示输入密码输入空密码或者随便试几个都不行。原因是 zip 文件头里有个 general purpose bit 标记了“本文件已加密”但数据本身并没有被真正加密或者密码就是空字符串。这种情况常见于某些工具打包时勾错了加密选项或者文件经过网盘在线转换后加密标记被写坏。处理办法是把这个加密标记位清掉然后用新 zip 重新导出import zipfile zf zipfile.ZipFile(大数据医疗-肝癌影像AI诊断.zip) with zipfile.ZipFile(fixed.zip, w) as out: for info in zf.infolist(): info.flag_bits ~0x1 # 清除bit 0加密标记 data zf.read(info.filename) out.writestr(info, data)这段代码的逻辑是先逐个读取压缩包内文件信息把flag_bits里的第 0 位值为 1清零再写入新压缩包。fixed.zip解压时就不会再要求输入密码。注意zf.read()在文件真的加密时可能仍然报错所以操作前先用 2.1 节的代码确认能读出文件大小至少确认不是暴力加密。第二种更常见的是“解压到一半 CRC 报错”。现象是解压到某个文件时报crc error或unexpected end of data然后解压中断。原因是 zip 包在下载、网盘同步或 U 盘拷贝过程中被截断文件本身不完整。解决办法是先验证整个包的完整性再决定重新下载还是放弃unzip -t 大数据医疗-肝癌影像AI诊断.zip-t参数会测试整个压缩包的完整性输出每个文件是否 OK。如果显示bad CRC或者packed data corrupt基本可以判定是传输导致的问题。遇到这种情况不要反复重新解压浪费时间换下载渠道重新获取一次才是正路。之前还遇到过一种特殊情况包在本地解压完全正常但复制到服务器上后 CRC 报错后来发现是内存卡有坏块换一块盘复制就好了。下表是常见的解压报错关键词对照方便排查报错关键词可能原因处理方式password required / 需要密码伪加密或真加密先试空密码再用脚本清加密位CRC error / central directory corrupt文件不完整、下载截断用unzip -t复核重新获取文件unexpected end of data文件被截断检查磁盘剩余空间重新下载No such file or directory包内路径含特殊字符用 Python zipfile 打印文件名看是否乱码完成这一步之后包是完好的结构也清楚了下一步才是真正碰影像数据和模型。3. 跑通肝癌 CT 检测的最小流程从 DICOM 到第一张预测图拿到项目包之后推荐的工作顺序是先跑通一次推理再回头研究训练。原因很简单推理链路短涉及的文件少能快速验证“数据和模型是否匹配”。如果一上来就想重新训练等花几个小时发现数据读取就有问题心态会很受影响。这一章的目标就是用包里已有的数据让模型对一张 CT 切片给出预测结果。3.1 影像数据入口DICOM 读取与窗宽窗位调整肝癌影像诊断的数据几乎逃不开 DICOM 格式。这种格式不只是“一张图”它还带着病人信息、扫描参数、像素间距甚至像素值都带有物理意义。CT 图像的像素单位是 HU亨斯菲尔德单位空气约是 -1000水是 0人体软组织和病灶集中在 -100 到 200 之间。深度学习模型直接读原始 HU 值范围太宽一般会先做窗宽窗位调整把关心的灰度范围映射到 0 到 255 之间。肝脏 CT 最常看的窗口是窗位 40、窗宽 150能把肝实质和低密度病灶区分开。用 pydicom 读一个 DICOM 文件并转成 8 位灰度图代码如下import pydicom import numpy as np ds pydicom.dcmread(data/raw/case001/slice_100.dcm) pixels ds.pixel_array.astype(np.float32) # 部分设备存了RescaleSlope/RescaleIntercept需要先转成真实HU值 if hasattr(ds, RescaleSlope) and hasattr(ds, RescaleIntercept): pixels pixels * float(ds.RescaleSlope) float(ds.RescaleIntercept) window_center 40.0 # 窗位 window_width 150.0 # 窗宽 lower window_center - window_width / 2.0 upper window_center window_width / 2.0 pixels np.clip((pixels - lower) / (upper - lower), 0, 1) img_8bit (pixels * 255).astype(np.uint8)逻辑说明ds.pixel_array拿到的是设备原始像素值没有直接的物理意义RescaleSlope和RescaleIntercept是 DICOM 标准里的线性变换参数代码里先判断有没有这个属性有就先把像素值换算成 HU。np.clip把低于窗位下界的值截断为 0、高于上界的截断为 1这样肝脏、血管和病灶的对比度才会拉出来。参数说明窗宽越小对比度越强但超出范围的软组织会变成纯黑或纯白窗位决定灰度范围的中间点。实务中如果发现肝脏区域整体偏亮或偏暗先调窗位不要急着改模型。3.2 组织数据集肿瘤/非肿瘤样本目录与标注文件项目包里常见的数据组织方式有两种。第一种是直接放原始 DICOM 文件夹加一个 CSV 标注CSV 里每一行表示“哪个病人的哪张切片是阳性还是阴性”。第二种是已经切成 PNG 样本集按positive/和negative/分好目录。如果是第一种你需要先自行转换成模型方便读取的目录结构。常见做法是写一个转换脚本把 DICOM 转成 PNG 并按标签归档import os import csv import cv2 import pydicom import numpy as np def dcm_to_png(src, window_center40, window_width150): ds pydicom.dcmread(src) pixels ds.pixel_array.astype(np.float32) if hasattr(ds, RescaleSlope) and hasattr(ds, RescaleIntercept): pixels pixels * float(ds.RescaleSlope) float(ds.RescaleIntercept) lower window_center - window_width / 2.0 upper window_center window_width / 2.0 pixels np.clip((pixels - lower) / (upper - lower), 0, 1) return (pixels * 255).astype(np.uint8) with open(labels.csv) as f: rows list(csv.DictReader(f)) for row in rows: src fdata/raw/{row[case_id]}/{row[slice_id]}.dcm if not os.path.exists(src): continue img dcm_to_png(src) label_dir pos if int(row[label]) 1 else neg out_dir fdata/png/{label_dir} os.makedirs(out_dir, exist_okTrue) out_path f{out_dir}/{row[case_id]}_{row[slice_id]}.png cv2.imwrite(out_path, img)这段代码做的事情是从 CSV 标注文件逐行读取记录拼出 DICOM 文件路径执行窗口变换之后用 OpenCV 写入 PNG。文件名保留case_id和slice_id是为了后面做数据划分时能追溯到病人层级这一点在第 4 章会展开说明。参数上window_center和window_width放到函数参数里而不是写死是因为不同项目的数据来源可能不同有的用腹窗窗位 50、窗宽 350效果更好预留参数位方便你多试几组。3.3 第一次推理加载预训练模型跑通单张切片转换出 PNG 之后可以尝试加载 zip 里自带的预训练模型对一张切片做推理。这里以 PyTorch 为例因为目前医疗影像的 2D 分类/检测模型仓库大多数基于 PyTorch 生态import torch from torchvision import transforms from PIL import Image checkpoint torch.load(model/checkpoint.pth, map_locationcpu) model checkpoint # 有些项目直接存了模型对象有的存state_dict需要先实例化模型结构 model.eval() trans transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) img Image.open(data/png/pos/case001_slice_100.png).convert(RGB) x trans(img).unsqueeze(0) # 增加batch维度 with torch.no_grad(): logit model(x) prob torch.sigmoid(logit).item() print(f异常概率: {prob:.4f}) # 大于0.5判为阳性逻辑说明torch.load加载的权重如果直接就是完整模型可以直接调用如果只有state_dict就需要先用模型类实例化再load_state_dict具体以 zip 包里的推理脚本为准。transforms.Resize((224, 224))是把不同尺寸的 CT 切片统一缩放到网络输入尺寸Normalize用的参数是 ImageNet 预训练权重常用的均值和标准差因为很多项目是在 ImageNet 预训练基础上微调的归一化方式必须对齐训练时的设置。参数说明unsqueeze(0)把单张图变成[1, 通道, 高, 宽]的张量sigmoid把 logit 压缩到 0 到 1 之间得到阳性概率。如果输出结果始终在 0.5 附近先怀疑预处理与训练时不匹配不要急着怀疑模型坏了。4. 训练自己的肝癌检测模型数据划分与四个关键参数推理跑通只算完成了“能用”距离“这个方案值不值得投入”还差一个属于自己的模型验证。z包的训练代码可能给你留了训练脚本但直接拿过来跑通常不是一个好主意。你需要先理解训练环节的四个关键点数据量认知、数据划分、类别平衡和早停策略。4.1 从“大数据”到“够用数据”医疗影像项目的真实数据量“大数据医疗”这个词很容易让人以为要处理 TB 级数据、上 Spark 集群。实际接触下来一个肝细胞癌影像 AI 诊断项目的核心数据量往往比想象中小得多。几百个病例、每个病例几十到几百张切片整理成 PNG 之后可能就几万张图总大小不超过几个 GB。这个规模完全可以在单张 GPU 上训练连分布式训练都不需要。大数据医疗在这个场景下的含义更接近“数据治理”批量处理 DICOM、清洗损坏文件、组织标注、按层级划分数据而不是海量流式计算。如果 zip 包里只有几百张切片也不用觉得做不了诊断模型。医疗影像 AI 的常见做法是使用在 ImageNet 上预训练过的模型做迁移学习用一个小的影像数据集微调最后一两层就足以取得不错效果。真正决定项目成败的不是数据量而是标注质量和数据划分是否严谨。如果 zip 包的压缩率异常高检查一下是不是 DICOM 被强行转成了 JPGJPG 有损压缩对病灶边界和纹理信息有破坏用在训练上会降低模型上限。4.2 数据划分与增强按病例划分 8:1:1医疗影像训练里最容易犯的错误是按切片维度划分训练集、验证集、测试集。同一个病人的连续切片高度相似如果一部分在训练集、一部分在测试集模型相当于提前见过答案。正确做法是必须保证同一个病人的所有切片只出现在同一个集合里。按 case_id 划分的代码如下import os from sklearn.model_selection import train_test_split sample_files os.listdir(data/png/pos) case_ids list(set(f.split(_)[0] for f in sample_files)) train_cases, tmp_cases train_test_split(case_ids, test_size0.2, random_state42) val_cases, test_cases train_test_split(tmp_cases, test_size0.5, random_state42) print(ftrain病例数: {len(train_cases)}, fval病例数: {len(val_cases)}, ftest病例数: {len(test_cases)})逻辑说明sample_files是正样本目录下的 PNG 文件名每个文件名以case_id_slice_id.png命名所以用split(_)[0]能取到病人编号。先对 case_id 做切分再把剩下的临时集合切成验证集和测试集最终得到 8:1:1 的病人级划分。参数说明test_size0.2表示第一轮切 20% 出来做后续拆分的原料random_state42固定随机种子保证每次执行结果一致。这是可复现实验的基本要求。划分完以后做数据增强。医疗影像增强和自然图像略有不同翻转、小角度旋转、轻微对比度扰动是安全的但不要做随机裁剪或者大尺度缩放因为病灶的位置和大小是有临床意义的过度增强会让模型学到不真实的形态。from torchvision import transforms train_trans transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])这里先放大到 256 再旋转、裁回 224是为了避免旋转时四周出现黑边。ColorJitter的亮度与对比度扰动幅度控制在 0.1既能模拟不同扫描设备的灰度差异又不会破坏病灶本身的形态。4.3 训练脚本的四个关键参数学习率、batch size、类别权重、早停模型结构如果 zip 包里已经给了直接沿用如果没有常见做法是用 ResNet18 或 ResNet50 预训练权重微调。训练逻辑的核心参数有四个学习率、batch size、类别权重和早停耐心值。下面是一段可以直接改造的训练循环骨架import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision.models import resnet18 model resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, 1) # 二分类输出 optimizer torch.optim.Adam(model.parameters(), lr1e-4) pos_weight torch.tensor([3.0]) # 正负样本比约1:3时使用 criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) best_val_loss float(inf) patience 0 for epoch in range(50): model.train() for images, labels in train_loader: preds model(images).squeeze(1) loss criterion(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() val_loss evaluate(model, val_loader) # 验证损失 if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pth) patience 0 else: patience 1 if patience 10: print(f早停于 epoch {epoch}) break参数说明lr1e-4是迁移学习的常用起点比从头训练小一个数量级如果发现损失下降太慢可以提到3e-4但注意观察验证集是否反而变差。pos_weight的作用是给阳性样本更高的损失权重正负比 1:3 时设为 3.0 是一个合理的初值先跑一轮再看混淆矩阵微调。patience10表示验证损失连续 10 个 epoch 不下降就停止训练这是省时间和防过拟合最有效的策略。BCEWithLogitsLoss内部自带 sigmoid 操作所以模型输出层不加激活函数否则会在计算损失时重复计算导致梯度异常。5. 肝癌影像 AI 诊断的常见坑与排查5 条踩坑记录这个方向的项目看似简单实际上处处是坑。下面这 5 条是我自己在类似项目里踩过的、或者帮别人排查时反复遇到的问题每一条都是“现象 → 原因 → 解决”的顺序希望能帮你少走弯路。5.1 数据泄露同一个病人的切片被同时拆进训练集和验证集现象训练时验证集 AUC 高达 0.95 以上模型表现得近乎完美一换到新数据集或者实际部署性能掉到 0.7 左右完全不是同一个水平。原因项目包的初始代码里直接对所有 PNG 文件做了随机 train/val 划分同一个 case 的切片同时出现在训练集和验证集中模型记忆的是病人特征而不是病灶特征。解决严格按 4.2 节的 case_id 划分并且测试集划分完之后不要参与任何调参过程。验证方法很简单打印验证集和训练集的 case_id 集合确认交集为空。5.2 窗宽窗位不统一模型学成了“亮度识别器”现象在数据源 A 上训练出来的模型换到数据源 B 的影像上预测结果明显变差尤其是肝实质区域的假阳性增多。原因不同CT设备的重建算法不同同一个病人的肝脏在两家医院的影像上灰度分布差异很大。如果训练时没有统一窗宽窗位模型学到的可能是“这个亮度范围是病灶”而不是“这个形态和密度是病灶”。解决所有训练和推理数据统一走同一个窗口变换并且把窗宽窗位参数固化到预处理函数里。训练时可以只用窗位 40、窗宽 150也可以在数据增强里随机微调窗位±15增强模型对不同扫描协议的鲁棒性。5.3 类别不均时只看 accuracy虚高的“正常”预测现象模型在测试集上的 accuracy 达到 90%看起来很漂亮但一看混淆矩阵发现绝大多数阳性样本都被判成了阴性模型实际上是在“蒙”大多数类别。原因肝癌阳性切片在数据集中占比往往只有 10% 到 20%模型全预测阴性就能拿到 80% 以上的 accuracy优化器也倾向于走这条捷径。解决训练指标不以 accuracy 为准改用 AUC、敏感度、特异度损失函数里加pos_weight让少数类样本的错误付出更大代价。报告里同时给出敏感度和特异度其中敏感度不漏诊是医疗场景最值得关注的指标。5.4 zip 包里的模型权重解压报 CRC文件不完整现象解压时某个.pth文件报 CRC error强行忽略后继续解压训练或推理时torch.load报unexpected end of data甚至直接段错误退出。原因压缩包在下载或拷贝过程中文件被截断模型权重文件在 zip 内部就是不完整的。解决先用unzip -t测试整个包确认哪些文件损坏如果只有模型文件损坏尝试单独重新下载该文件如果整个包都损坏换一个网络渠道或让发送方重新打包。不要尝试用修复工具硬补深度学习模型权重文件差一个字节都可能加载不出来即便加载成功推理结果也不可信这是玄学不值得赌。5.5 标注与切片错位框和病灶对不上现象在 png 上把标注框画出来发现框的位置总偏几个像素或者某些阳性切片的标注框画到了明显正常的肝组织上。原因DICOM 转 PNG 时发生过翻转或者裁剪但标注 JSON 里的坐标没有跟着变换另一种可能是标注的 slice_id 和 DICOM 的 InstanceNumber 对应关系存在偏差导致张冠李戴。解决转换脚本里把标注坐标一并做相同的几何变换同时做一个可视化检查工具把标注框叠加到切片上随机抽查几十张。这个检查做一遍只需要几分钟但能避免模型在一个标注与图像错位的数据集上白白训练好几天。6. 把“能跑的模型”做成“能交付的方案”验证指标与汇报技巧模型训练完成不代表事情结束。医疗影像 AI 项目的交付对象往往是临床医生、导师或评审专家他们关心的不是损失函数曲线而是“这个模型能不能用、凭什么信它”。所以最后这一步是把技术结果转化成可以验证、可以汇报的方案。6.1 用 AUC 代替“猜对几个”推荐用 AUC 作为首要评估指标因为它不依赖具体阈值能反映模型把阳性排在阴性前面的能力。用 sklearn 一次性算出 AUC 和最优阈值from sklearn.metrics import roc_auc_score, roc_curve y_prob model_outputs # 模型在测试集上输出的阳性概率 y_true test_labels # 真实标签 auc roc_auc_score(y_true, y_prob) fpr, tpr, thresholds roc_curve(y_true, y_prob) # 约登指数选阈值最大化 敏感度特异度-1 j tpr - fpr best_threshold thresholds[j.argmax()] print(fAUC: {auc:.3f}, 最佳阈值: {best_threshold:.3f})逻辑说明roc_curve返回的是不同阈值下的假阳性率和真阳性率j.argmax()找到让约登指数最大的位置再取对应的阈值。这个阈值用于后续推理脚本里的判定标准比拍脑袋定 0.5 更合理。6.2 交付报告看什么三张图和一张表交付时我会准备三样东西ROC 曲线图、按阈值分类的混淆矩阵、以及几张 Grad-CAM 热力图。热力图能直观展示模型关注的是不是病灶区域对建立信任非常有帮助。常用做法是用pytorch-grad-cam库对最后一层卷积特征做加权激活映射叠加到原图上输出。汇报用的核心指标表指标含义参考值AUC阳性与阴性的排序能力0.85 以上敏感度阳性样本被查出的比例不低于 0.90特异度阴性样本被正确排除的比例0.70 到 0.90阳性预测值阳性结果的可信度与数据患病率相关一个实用的交付技巧把整个项目整理成固定的目录结构训练脚本、推理脚本、预处理脚本分开再写一个run.sh从数据转换开始一步步执行到输出指标表格。这样别人拿到打好的 zip 包以后不需要口头解释太多就能自己复现结果。我自己吃过亏本科毕设那次按切片随机划分数据集验证集 AUC 0.97答辩现场换了新数据直接掉到 0.72被评委一眼看穿是数据划分出了问题。后来养成了一个习惯——凡是医疗影像项目拿到 zip 第一件事就是重写数据划分按病人分完再谈训练。希望帮到你。本文还有配套的精品资源点击获取
返回列表