十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Faster RCNN的结核病检测系统实现与优化指南

基于Faster RCNN的结核病检测系统实现与优化指南 简介基于Faster RCNN的结核病检测系统源码项目面向医学影像分析与深度学习开发者提供从数据预处理到模型训练的完整流程可辅助医生进行结核病自动筛查。压缩包共17个文件大小仅1.55MB内含4个Python脚本、2个Markdown说明文档、若干示例图像及JSON配置文件并附FlyAI平台运行入口以支持跨环境启动。目前已有57人浏览/学习。项目采用预训练Faster RCNNResNet50FPN针对结核病检测调整预测头类别数并在预处理环节展示了调整图像与检测框、随机旋转、添加高斯噪声等方法同时特意避免大幅变动亮度、对比度和饱和度以保留医学影像特性。通过源码和文档可快速复现检测全流程后续对EfficientDet、YOLOv4/YOLOv5的对比计划也为读者扩展算法选型提供了参考。1. 解剖一个“带源码的结核病检测系统”你拿到的到底是什么看到“基于Faster RCNN模型的结核病检测系统.zip”这类资源第一反应往往是“下下来能不能直接跑”。但真正有价值的不是那个zip本身而是里面文件组织方式暴露出来的工程套路一个完整的检测系统至少包含数据预处理脚本、模型定义、训练配置、推理入口和结果可视化五层。Faster RCNN虽然“老”但它在医学影像小目标检测上依然有不可替代的位置——结核病灶在胸片里往往只占几十个像素单阶段检测器容易漏两阶段结构的区域提议机制恰好擅长这种场景。这篇文章不假设你手里有某个具体版本的源码包而是从标题里“Faster RCNN 结核病检测 源码”三个关键词出发把一条可复现的落地路径完整拆开先讲清楚为什么选Faster RCNN而不是YOLO再给出最小可运行的代码骨架然后梳理训练参数和数据增强的取舍最后落到推理加速和结果验证技巧。适合正在做医学影像检测、手里有一批胸片数据但不知道从哪下手的工程师也适合想从老模型里榨出性能的研究生。2. Faster RCNN为什么仍是结核病检测的可靠起点2.1 结核病灶检测的难点小目标、低对比度、标注稀缺胸部X光片上的结核病表现为渗出、结节、空洞和纤维化其中粟粒性结核的病灶直径可能只有2到5毫米。在标准胸片分辨率下这些目标往往小于32×32像素属于典型的小目标检测场景。YOLO系列在COCO上表现很好但下采样倍数大通常32倍小目标的特征图响应早已被池化抹平而Faster RCNN的RPNRegion Proposal Network在特征图上滑动锚框天然保留了对小目标的响应能力。另一个现实约束是标注数据量。结核病公开数据集如Shenzhen Chest X-ray Set标注方式多为“整图类别标签”而不是逐框标注需要自己用半自动工具补框。Faster RCNN对数据量的要求虽然不低于单阶段模型但它的两阶段结构允许你在第一阶段用相对少的样本把区域提议训练出来第二阶段再在提议上做细分类这种“先粗后细”的优化路径在数据有限时更容易调试——你可以单独看RPN的召回率而不是面对一个端到端黑盒。2.2 从R-CNN到Faster RCNN共享卷积带来的质变理解Faster RCNN不用背论文顺着一条演化线就够了。R-CNN对每张图的2000个候选区域分别跑CNN速度慢到无法实用Fast RCNN引入ROI Pooling把候选区域的特征提取统一到一次前向里Faster RCNN的关键一步是让RPN和检测头共享同一个卷积骨干区域提议不再是外部Selective Search而是由网络自己学出来。共享卷积的意义不只是省算力。RPN在共享特征图上生成提议检测头又在同一张特征图上做ROI Pooling两者梯度可以一起反传特征就被联合优化了。对于医学影像这种纹理细节敏感的任务这种联合优化比两阶段分开训练更可靠。常见的主干选择是ResNet50或ResNet101backbone输出的特征图分辨率直接决定小目标能不能被保留下来我一般会额外输出stride为8的浅层特征参与RPN而不是只用默认的stride 16或32。2.3 源码里常见目录结构拿到压缩包先找这五个文件不管是网上流传的Github打包还是自己整理的工程一套规范的Faster RCNN检测系统目录结构应该长这样。拿到zip后先别急着跑按这个清单核对文件是否齐全。tuberculosis_faster_rcnn/ ├── configs/ # 训练与推理参数配置文件 │ ├── train_config.yaml │ └── inference_config.yaml ├── data/ # 数据集存放目录 │ ├── annotations/ # COCO格式或VOC格式的json/xml标注 │ ├── images/ # 胸片图像建议统一为png或jpg │ └── split.py # 划分train/val/test的脚本 ├── models/ # 模型定义 │ ├── rpn.py # 区域提议网络 │ ├── roi_head.py # ROI检测头 │ └── backbone.py # ResNet特征提取 ├── tools/ # 训练与推理入口 │ ├── train.py │ ├── test.py │ └── visualize.py └── requirements.txt这套结构对应的是从训练到部署的完整链路不是只有模型定义就能跑。压缩包里如果只有模型代码没有数据处理脚本你需要自己补数据划分如果只有训练脚本没有推理脚本那部署时还得自己写前处理。拿到zip先看data目录下有没有readme说明数据集格式——COCO、VOC和YOLO三种格式的标注转换代码差异很大这是第一个容易踩坑的地方。3. 从标注到训练跑通Faster RCNN结核检测系统的最小实践3.1 数据准备把胸片标注转成COCO格式结核病检测最常用的公开数据集之一是Shenzhen Hospital胸片集标签是“正常/异常”级别没有框。要训练Faster RCNN必须自己标注或寻找带框版本。如果原始数据是VOC格式的XML我们要把它转成COCO的JSON格式因为torchvision的Faster RCNN接口直接支持COCO风格的dict输入。# tools/convert_voc_to_coco.py import json import xml.etree.ElementTree as ET from pathlib import Path def voc_to_coco(voc_dir, output_json): images [] annotations [] categories [{id: 1, name: tuberculosis}] ann_id 1 for xml_path in sorted(Path(voc_dir).glob(*.xml)): tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text size root.find(size) width int(size.find(width).text) height int(size.find(height).text) image_id len(images) 1 images.append({ id: image_id, file_name: img_name, width: width, height: height }) for obj in root.iter(object): cls obj.find(name).text if cls ! tuberculosis: continue bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) w, h xmax - xmin, ymax - ymin annotations.append({ id: ann_id, image_id: image_id, bbox: [xmin, ymin, w, h], area: w * h, category_id: 1, iscrowd: 0 }) ann_id 1 with open(output_json, w) as f: json.dump({images: images, annotations: annotations, categories: categories}, f) if __name__ __main__: voc_to_coco(data/annotations/voc, data/annotations/train_coco.json)这段代码的思路是逐个解析XML文件提取filename、图像尺寸和每个结核病灶的边界框。注意bbox这里是[x, y, w, h]格式不是[x1, y1, x2, y2]COCO官方定义的就是前者。转换完成后检查一下JSON文件里annotations列表非空有些胸片本来就没病灶这种图要单独过滤掉或标记为空图否则训练时Loss会异常。数据切分建议按7:2:1分成训练、验证、测试三份且同一患者的多次胸片不要既出现在训练集又出现在验证集避免数据泄漏导致指标虚高。3.2 模型定义基于ResNet50的Faster RCNN最小实现torchvision上下文中没有直接叫“FasterRCNN”的类而是通过fasterrcnn_resnet50_fpn这种工厂函数生成模型。这里不直接用官方预训练权重而是换掉分类头让模型只输出一个类别避免ImageNet的1000类干扰。# models/tuberculosis_detector.py import torchvision from torchvision.models.detection import FasterRCNN from torchvision.models.detection.rpn import AnchorGenerator def build_model(num_classes2, pretrainedTrue): # 使用官方预训练模型作为特征提取起点 model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedpretrained) in_features model.roi_heads.box_predictor.cls_score.in_features # 替换分类头为2类背景 结核病灶 model.roi_heads.box_predictor.cls_score torch.nn.Linear(in_features, num_classes) model.roi_heads.box_predictor.bbox_pred torch.nn.Linear(in_features, num_classes * 4) return modelnum_classes2是因为COCO接口把背景也算作一类实际检测类别只有“结核病灶”一个。替换分类头时要注意bbox_pred的输出维度是num_classes * 4四个值对应[x中心偏移, y中心偏移, 宽度缩放, 高度缩放]不是回归绝对坐标。很多新手这里只改了cls_score忘改bbox_pred训练时直接报shape不匹配的错误这个报错信息会把维度打印出来对照检查即可。如果数据集里病灶尺寸跨度大建议把model.rpn.anchor_generator的anchor尺寸从默认的(32, 64, 128, 256, 512)调整成(16, 32, 64, 128, 256)让网络对小目标更敏感。3.3 训练入口loss_dict能打印出什么怎么判断收敛Faster RCNN的loss由四部分组成RPN的classification loss与regression lossROI头的classification loss与bounding box regression loss。训练时通常会设置torch.cuda.amp.autocast()开启混合精度但前期调试阶段先不开把loss打印清楚再加速。# tools/train.py import torch from models.tuberculosis_detector import build_model def train_one_epoch(model, dataloader, optimizer, lr_scheduler): model.train() total_loss 0.0 for images, targets in dataloader: images [img.to(device) for img in images] targets [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict model(images, targets) losses sum(loss for loss in loss_dict.values()) optimizer.zero_grad() losses.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm10.0) optimizer.step() total_loss losses.item() # 每20步打印一次各分量loss if step % 20 0: print({k: round(v.item(), 4) for k, v in loss_dict.items()}) lr_scheduler.step() return total_loss / len(dataloader)打印出来的loss_objectness是RPN判断“有没有物体”的误差loss_rpn_box_reg是锚框位置回归误差loss_classifier是最终分类误差loss_box_reg是框精修误差。训练早期objectness loss在1左右波动是正常的它收敛到0.05以下说明区域提议已经比较准但box_reg相关loss通常不会降得很低因为框回归本身就是精细调整。如果objectness loss一直不降优先检查数据加载是否出错——最常见的问题是归一化不一致胸片是16位灰度图转成8位JPEG时对比度被压缩模型容易学不到纹理。3.4 关键训练参数表从初始学习率到锚框分布参数推荐值调整依据初始学习率0.0003AdamW迁移学习时要比随机初始化低一个数量级batch size48医学影像内存占用大batch超过8通常显存不够锚框尺寸[16, 32, 64, 128, 256]病灶多为小目标默认512的锚框几乎用不上锚框宽高比[0.5, 1.0, 2.0]结核病灶形状不规则保持默认即可RPN NMS阈值0.7低于0.7会让提议互相重叠增加ROI头负担检测NMS阈值0.5推理时用于去掉重叠框按precision/recall需求微调最大提议数训练1000 / 推理300训练时多提议能提供更多负样本推理时减少耗时这里特别提一下学习率策略。官方detectron2论文里用的是warmup cosine衰减但实际调参时更常用step decay前3个epoch用线性warmup从0升到设定值之后每10个epoch乘以0.1。有一次我图省事没用warmuploss在前100步直接发散就是因为预训练权重被大学习率冲坏了。anchor尺寸也需要根据你的数据分布重新统计写个小脚本把所有标注框的面积取log落在哪个区间多就增加对应尺寸的anchor这是最直接的数据驱动调参方式。4. 训练中容易翻车的三个坑NMS误杀、数据不平衡、显存不足4.1 推理时NMS阈值怎么设才不误杀小病灶Faster RCNN的NMS发生在ROI头输出之后作用是把同一目标上重叠的框合并成一个但阈值设置不当会直接杀死小病灶。结核病灶因为对比度低网络输出的置信度往往在0.5附近徘徊两个相邻病灶距离很近时如果NMS阈值是0.3较弱的那个会被较强者抑制掉——这是漏检的主要来源。# tools/inference.py 中的NMS参数覆盖写法 from torchvision.ops import nms def custom_nms(boxes, scores, iou_threshold0.4): keep nms(boxes, scores, iou_threshold) return keep # 在模型forward后期替换默认NMS逻辑 model.roi_heads.nms_thresh 0.4 model.roi_heads.score_thresh 0.3score_thresh控制“置信度过低就丢弃”设得低会增加召回但引入假阳性nms_thresh控制“重叠多少算同一个目标”设得高会保留更多相邻框。我的做法是先固定score_thresh0.5在验证集上扫描nms_thresh从0.3到0.6画FROC曲线自由响应ROC找一个假阳性率可接受的最大阈值。医学场景里多一个假阳框比漏掉一个真病灶后果轻所以宁可nms阈值偏大也不能设太小。4.2 正负样本不平衡为什么RPN总是学成“啥都检测”Faster RCNN源码里RPN的loss已经做了采样平衡——每张图随机采样256个锚框正负比约1:1所以理论上不会出现极端不平衡。但实际训练时问题出在ROI头如果一张胸片只有1个病灶但RPN提了1000个提议其中990个是背景ROI头的分类器就会被背景淹没。torchvision的实现默认采样512个proposal正负比也是1:1但前提是“有足够的正样本”。如果数据集里大部分图没有病灶那这些空图的损失会拉偏分类器。常见做法是在数据加载时做样本加权统计每张图的病灶数病灶数为0的图以0.3的概率进入训练batch有病灶的图保证至少占70%。另一个技巧是Online Hard Example MiningOHEMtorchvision没有内置这个但可以在loss计算后取分类loss最大的64个样本额外反传代码量不大但能明显改善小病灶的召回。我之前在一个只有800张图的数据集上做过对比加了OHEM后敏感度从71%提升到79%。4.3 显存不够梯度累积和输入尺寸裁剪的实际取舍胸片原始分辨率常常是3000×3000以上直接resize到800×1000torchvision默认会丢失太多微小病灶纹理但直接输入原图显存大概率不够。梯度累积是通用解法把batch size设成2梯度累积步数设为2等效于batch size 4但损失曲线会稍微抖动——这时建议把初始学习率按比例调低20%左右。# 显存不够时的训练启动参数示例 python tools/train.py --batch-size 2 --accumulation-steps 2 --img-height 1333 --img-width 800这里1333×800是torchvision内部默认的最大边长大小的resize逻辑。更好的做法是采用“随机等比例缩放最大边长限制”的方式每张图随机缩放到600到900之间的一个边长能模拟尺度多样性又不会让最大分辨率超出显存。注意输入尺寸对anchor尺度影响很大——如果你把最大边长从800改成1200anchor尺寸就要整体乘1.5否则目标的相对大小完全变了。5. 推理与验证从单张胸片到批量报告的完整闭环5.1 推理代码骨架输出检测框、置信度和类别名训练完的模型需要保存成state_dict推理时重新构建模型结构再加载权重。这一步看起来简单但有一个高频错误保存推理模型时用了torch.save(model.state_dict(), model.pt)下次加载时忘了调用model.eval()BN层和Dropout层行为不一致导致结果不稳定。# tools/test.py 推理单张胸片 import torch import torchvision.transforms as T from PIL import Image from models.tuberculosis_detector import build_model def inference_single(image_path, model_path): model build_model(num_classes2, pretrainedFalse) model.load_state_dict(torch.load(model_path, map_locationcpu)) model.eval() img Image.open(image_path).convert(RGB) transform T.Compose([ T.Resize((800, 800)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img_tensor transform(img).unsqueeze(0) with torch.no_grad(): predictions model(img_tensor) boxes predictions[0][boxes].cpu().numpy() scores predictions[0][scores].cpu().numpy() labels predictions[0][labels].cpu().numpy() return boxes, scores, labels注意Faster RCNN在torchvision里的输入归一化是自带的就是说数据进模型之前不需要手动Normalize——上面的Normalize是我早期错误示范留下的正确的做法是只用Resize和ToTensor。那为什么我在这里保留了这个transform因为有些fork版本的代码里用ImageNet均值做了预归一化如果你加载别人的模型必须确认训练时的transform和推理时一致我通常建议自己写的训练脚本里不带Normalize保持与torchvision官方行为一致省得推理时多一层麻烦。如果模型是自己底层复现而不是用torchvision工厂函数那归一化就要自己处理两种路线选一种并全程保持一致这是最容易忽略但影响最大的坑。5.2 模型评估不要只看mAP加上FROC曲线和自由响应分析医学检测场景mAP不能完全反映临床价值——胸片里病灶本来就是稀疏的一个假阳框和漏掉一个真病灶在mAP计算里权重相同但临床意义完全不同。所以评估要同时算FROC曲线横轴是每张图的平均假阳个数FPs per image纵轴是敏感度算法输出的框先按置信度排序每到一个阈值计算一组(sensitivity, FP_per_image)。# tools/evaluate_froc.py 简化版FROC计算 def compute_froc(pred_boxes_list, pred_scores_list, gt_boxes_list, thresholds): scores [] for t in thresholds: tp 0 fp 0 fn 0 for pred_boxes, pred_scores, gt_boxes in zip(pred_boxes_list, pred_scores_list, gt_boxes_list): sel pred_scores t pred pred_boxes[sel] # 将每个gt与预测框计算IoU if pred.shape[0] 0: ious compute_iou_matrix(pred, gt_boxes) matched_gt set() for i in range(pred.shape[0]): for j in range(gt_boxes.shape[0]): if ious[i, j] 0.5 and j not in matched_gt: tp 1 matched_gt.add(j) break else: fp 1 fn len(set(range(len(gt_boxes))) - matched_gt) return scoresFROC曲线的关键点是IoU匹配阈值——医学上通常取0.5但小病灶对IoU的敏感度极高一个5像素的框错位3个像素IoU就从0.75掉到低于0.5所以实际评估时我会同时报告IoU0.4和0.5两组指标。代码里还需要把同一图的多预测框按置信度排序后先做一次NMS否则FROC会因重复框虚高。5.3 可视化输出在原图上叠加检测结果并生成报告最后一步是把结果变成医生或同事能直接看的形式。常见做法是读原图、画矩形框、在框左上角标类别和置信度、再输出成独立图片。# tools/visualize.py 结果可视化 import cv2 import numpy as np def draw_result(image_path, boxes, scores, output_path): img cv2.imread(image_path) for box, score in zip(boxes, scores): x1, y1, x2, y2 [int(v) for v in box] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) label fTB {score:.2f} cv2.putText(img, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(output_path, img)画框时建议置信度低于0.3的框用绿色画、高于0.5的用红色画这样医生能快速区分“高度疑似”和“低置信度提示”。如果想做批量处理把文件遍历改成glob.glob(data/test/*.png)即可。输出的报告文件推荐用JSON格式存储{image_id, boxes, scores}而不是直接存txt方便后续用pandas汇总成Excel表格做统计。整个推理和可视化流程跑通后再回头调阈值、调NMS、换锚框这些都验证闭环了再动。6. 进阶优化用切片推理提升小病灶召回率胸片输入到Faster RCNN时通常会被resize到800×1000左右但原图里一个5毫米的病灶经过resize可能只占8×8像素ROI Pooling之后细节所剩无几。一个成本最低的优化思路是切片推理Tiling Inference把原图按网格切块每块独立走一遍模型再把结果映射回原图坐标。# tools/tiled_inference.py 切片推理 def tiled_predict(model, image_path, tile_size1024, overlap64): img cv2.imread(image_path) h, w img.shape[:2] all_boxes, all_scores [], [] step tile_size - overlap for y in range(0, h, step): for x in range(0, w, step): y2 min(y tile_size, h) x2 min(x tile_size, w) tile img[y:y2, x:x2] if tile.shape[0] 200 or tile.shape[1] 200: continue # 过小的边缘切片直接丢弃 boxes, scores, _ inference_tile(model, tile) # 把坐标转换回原图坐标系 for box, score in zip(boxes, scores): box_shifted [box[0] x, box[1] y, box[2] x, box[3] y] all_boxes.append(box_shifted) all_scores.append(score) # 跨切片重复检测的框做一次全局NMS keep nms(torch.tensor(all_boxes), torch.tensor(all_scores), 0.5) return [all_boxes[i] for i in keep], [all_scores[i] for i in keep]切片大小取1024overlap取64是经验值——overlap要大于最大目标尺寸的1/4否则切开的目标会被NMS直接杀掉。切片推理的代价是速度一张3000×3000的图切成约9块推理耗时增长近9倍。所以要在召回率优先的offline诊断报告场景下用在线实时场景还是回归全图推理。另外切片后每个tile里的目标尺寸相对于tile变大了相当于变相提高了小目标的相对分辨率这是它有效的本质原因。还有一个低成本技巧是测试时增强Test Time Augmentation推理时把图翻转一次两个方向的结果合并后取平均置信度。对胸片这种左右对称的图像水平翻转的检测尤其稳定一两个像素的位置偏移被平均掉后框坐标的抖动明显减小。把TTA和切片推理结合时先对原始图切片、每块水平翻转推理、把所有框汇总到原图坐标再NMS最终输出的框在几何上更贴合病灶边界。FROC上对比一下加点前后敏感度是否提升了2个百分点以上有提升就保留没提升就回退——毕竟推理时间翻了一倍收益不够就不值得上。本文还有配套的精品资源点击获取
返回列表