简介压缩包内含Faster R-CNN血液细胞目标检测实战项目面向深度学习入门者、医学影像算法研究者以及课程与毕业设计开发者解决血液细胞形态多样、大小不一导致传统检测方法准确率不足的问题。包内共1565个文件包含777张血细胞图像、777个配套XML标注文件、5个预训练权重文件.pth、4个Python训练/推理脚本和2个pyc编译文件压缩包总大小约742.8MB。文件覆盖数据准备、模型训练、验证与推理完整链路可直接加载.pth权重进行检测实验也可通过脚本学习RPN区域提议网络如何快速生成候选区域、RoI全卷积网络如何完成分类与边界回归。目前已有256人学习下载说明其在同类项目中具备实际参考价值。利用这批数据与代码既能复现血涂片图像中细胞自动化检测流程也能基于锚点机制、ResNet等backbone进一步调优为后续医学影像分析课题提供基线。1. 血涂片里的目标检测Faster RCNN 凭什么在密集小目标上扛得住检验科每天要处理几百张血涂片镜下视野里挤着红细胞、淋巴细胞、中性粒细胞、血小板细胞直径往往只有十几个到几十个像素。人工分类计数既慢又容易疲劳而目标检测模型正好能替代这部分重复劳动。血液细胞目标检测这个方向本质上解决的是在极高密度、小尺度、边界模糊的医学图像里把每个细胞找出来并分好类的问题。这个任务第一个想到的模型往往是 Faster RCNN。作为两阶段检测器的代表它先用 RPN 生成候选框再对候选框逐个精细分类和回归天然适合小目标密集场景。YOLO 系列虽然快但在细胞重叠、边界不清的血涂片里漏检率会明显上升。本文从原理、数据、训练、排错一条线走下来适合正在做医学影像算法、细胞识别课题或者想用目标检测切入血液检验方向的工程师。2. 让候选框先找对细胞RPN 与 anchor 参数的血细胞配置2.1 两阶段检测的血细胞适用性为什么 YOLO 在这里会吃亏先看一个直观对比通用目标检测里的目标比如行人、车辆通常占图像的 10% 以上且边界清晰、类间差异大。血涂片则完全相反单张 512x512 的图像里可能有几十个红细胞每个红细胞只有 10~20 像素宽多个白细胞互相挤压血小板更是小到只有 5~8 像素。单阶段检测器YOLO、SSD在特征图上直接回归类别和位置正负样本比例悬殊时大量小目标对应的 anchor 会被背景淹没训练时难以被激活。Faster RCNN 的两阶段设计在这里有明显优势第一阶段 RPN 只做有没有细胞的粗筛把每张图的候选框从几万个压到几百个即使候选框有误差也先留着第二阶段 ROI 头再对每个候选框做细分类和位置修正。对小目标来说多一次精修就意味着多一次挽救机会。我在血细胞任务上对比过 Faster RCNN 和 YOLOv8相同迭代轮数下Faster RCNN 在白细胞和血小板上的 recall 高出 5~8 个点代价是推理速度慢一个数量级——但医学离线分析并不实时这个代价可接受。2.2 anchor 尺寸与宽高比按红细胞、白细胞、血小板的实际像素定参数Faster RCNN 的 anchor 机制决定了 RPN 能看见多大和多形状的目标。torchvision 里 fasterrcnn_resnet50_fpn_v2 默认使用的 anchor_generator 配置为anchor_generatordict( typeAnchorGenerator, sizes((32, 64, 128, 256, 512),) * 5, aspect_ratios((0.5, 1.0, 2.0),) * 5 )这套配置面向 COCO 的通用目标sizes 从 32 起步对血细胞来说明显偏大。血细胞在 512x512 输入下的实际像素尺寸大致如下细胞类型典型像素直径对应 anchor size红细胞10~258, 12, 20淋巴细胞15~3512, 20, 32中性粒细胞20~5020, 32, 48血小板5~124, 8, 12anchor 尺寸应该覆盖目标尺寸的范围并留余量我一般把 sizes 设为((4, 8, 12, 20, 32, 48, 64),) * 5每个尺度对应 FPN 的 P2~P6 层小 anchor 对应高层stride 小的特征图。宽高比方面红细胞和血小板近似圆形白细胞略椭圆所以 aspect_ratios 用((0.8, 1.0, 1.25),)比默认的(0.5, 1.0, 2.0)更合理。过大的宽高比会产生大量横竖长条 anchor和细胞形状完全无关白白增加 RPN 的计算量。修改 anchor 参数后RPN 的正样本数量会有明显变化。训练时可以先打印 RPN loss如果 rpn_classification_loss 在前 10 轮就降到 0.5 以下说明 anchor 设置合理如果一直在 1.0 以上波动优先检查 anchor 尺寸是否覆盖了目标的真实像素范围。2.3 在 512 输入下 RPN 的输出能力候选框数量与正负样本采样RPN 对每张特征图上的每个位置生成 k 个 anchor然后通过 pre_nms_top_n 和 post_nms_top_n 两个参数控制候选框数量。torchvision 默认的rpn_pre_nms_top_n_train2000、rpn_post_nms_top_n_train2000对血细胞场景偏大。因为血涂片里目标密集一张图有几十上百个细胞实际上候选框不需要那么多——保留太多反而让 ROI 头在大量低质量框上空转。我把训练时的 pre_nms_top_n 设为 1200post_nms_top_n 设为 600推理时改为 300 和 150。效果上训练速度提升约 30%mAP 几乎没有变化。原因在于血细胞虽然数量多但形状一致、分布均匀RPN 用少量高质量候选框就能覆盖绝大多数真值。GPU 显存紧张时这是最值得先动的参数。ROI 头正负样本采样的机制也要理解。每个 batch 里采样 512 个 ROI正负样本比例默认 1:1。实际训练中我发现如果某类细胞特别少比如嗜碱性粒细胞只占 1%1:1 的采样会让罕见类在每次迭代中反复出现模型过拟合到少数几个样本。这时可以把fg_bg_sampler.batch_size_per_image从 512 降到 256并适当调低正样本比例到 0.25让负样本更充裕、罕见类不会因为 oversample 而退化。3. 把血液细胞数据喂给模型从标注格式到数据集划分3.1 用公开数据集起步还是自建数据集成本对比血液细胞检测有公开数据集可用BCCD Dataset 是最常用的一个它包含白细胞分四类、红细胞和血小板的标注约 350 张血涂片图像格式是 PASCAL VOC XML。这个量级足够验证流程是否跑通但直接训练生产模型是不够的——350 张图对 Faster RCNN 来说样本太少尤其白细胞亚型的数量分布很不均衡。自建数据集的投入要大得多。一张 1000x1000 的血涂片视野标注一个细胞平均要 3~5 秒一张图里有几十个细胞加上重叠区域需要放大视图确认边界整张图标注下来要半小时。如果只做红细胞、白细胞、血小板三分类一个 2000 张图的标注项目大约需要 3~4 人周如果细分白细胞亚型需要检验科医师参与审核标注质量周期翻倍。我建议的路径是先用 BCCD 或类似的公开数据跑通训练和评估流程把精度基线立起来再按自己的应用场景补充标注数据。这样做的好处是能在标注开始前就确认模型方案可行避免投入大量标注后发现检测器压根不收敛。3.2 用标注工具产出 VOC 转 COCO 格式一次跑通的脚本目标检测常用标注工具有 LabelImg、LabelMe、X-AnyLabeling 等其中 LabelImg 直接输出 PASCAL VOC 格式对新手最友好。torchvision 的检测模型默认支持 COCO 格式数据集所以需要把 VOC XML 转成 COCO 的 JSON。下面这个脚本可以直接在项目里复用import xml.etree.ElementTree as ET import os, json, glob from PIL import Image def voc_to_coco(xml_dir, img_dir, out_json, categories): images, annotations [], [] ann_id 0 for img_id, xml_path in enumerate(glob.glob(os.path.join(xml_dir, *.xml))): root ET.parse(xml_path).getroot() filename os.path.basename(root.find(filename).text) img_path os.path.join(img_dir, filename) with Image.open(img_path) as im: w, h im.size images.append({ id: img_id, file_name: filename, width: w, height: h }) for obj in root.iter(object): name obj.find(name).text if name not in categories: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) annotations.append({ id: ann_id, image_id: img_id, category_id: categories[name], bbox: [xmin, ymin, xmax - xmin, ymax - ymin], area: (xmax - xmin) * (ymax - ymin), iscrowd: 0 }) ann_id 1 with open(out_json, w) as f: json.dump({ images: images, annotations: annotations, categories: [{id: v, name: k} for k, v in categories.items()] }, f) if __name__ __main__: voc_to_coco( xml_dirdata/Annotations, img_dirdata/JPEGImages, out_jsondata/annotations/train.json, categories{RBC: 1, WBC: 2, Platelets: 3} )这个脚本有几个容易踩坑的细节。VOC 的 bbox 是左上角和右下角两对坐标即[xmin, ymin, xmax, ymax]而 COCO 的 bbox 是[x, y, width, height]转换时宽度和高度必须用坐标相减不能直接抄。另一个坑是 VOC 里 filename 可能带相对路径比如images/001.jpg如果直接用os.path.join(img_dir, filename)会拼出错误的文件路径所以上面代码里先用os.path.basename取出纯文件名再拼接。3.3 数据增强的力度怎么定为什么按患者级别划分验证集血液细胞数据集的规模通常不够数据增强是必须的。torchvision 检测任务里常用的是随机水平翻转、随机缩放0.5~1.5 倍、随机亮度和对比度扰动。注意血涂片的染色程度在不同实验室甚至不同批次间都会有差异所以亮度和对比度扰动幅度可以适当加大这能让模型对染色差异更鲁棒。但有一个增强禁忌不要对颜色做太随机的 HSV 扰动。Hue 通道的随机偏移超过 20 度会产生诡异的颜色组合比如红细胞变成蓝色这不符合真实染色形态会让模型学到错误的颜色特征。真实场景中血涂片无非是偏紫、偏粉、偏红的差异把 hue 扰动限制在 ±10 度饱和度扰动限制在 0.85~1.15 倍区间内就够了。验证集的划分方式对 mAP 的真实性影响巨大。同一张血涂片的多个视野之间存在高度相似性如果只是随机按图像划分同一个患者的不同视野会同时出现在训练集和验证集里验证 mAP 虚高。正确做法是按标本或患者级别划分也就是一个患者的全部视野要么全进训练集要么全进验证集。具体实现可以用 scikit-learn 的GroupKFold把每个图像的 patient_id 作为分组依据保证同一组内的图像不会被切到两边。4. 用 PyTorch 微调 Faster RCNN训练脚本和 6 个必调参数4.1 模型构建torchvision 预训练权重、分类头替换和冻结策略torchvision 提供了现成的 Faster RCNN 实现基于 ResNet50-FPN 骨架代码量最少、最适合作为基线。关键是把最后的全连接分类头换掉因为预训练模型是在 80 类 COCO 上训练的我们的血细胞任务只需要红细胞、白细胞、血小板这三类。import torch import torchvision from torchvision.models.detection import FasterRCNN_ResNet50_FPN_V2_Weights model torchvision.models.detection.fasterrcnn_resnet50_fpn_v2( weightsFasterRCNN_ResNet50_FPN_V2_Weights.DEFAULT ) in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor torchvision.models.detection.faster_rcnn.FastRCNNPredictor( in_features, num_classes4 # 3 类血细胞 1 类背景 ) # 冻结 ResNet 的前三个 stage只微调 stage4、FPN 和 RPN/ROI 头 frozen_layers [backbone.body.layer1, backbone.body.layer2, backbone.body.layer3] for name, param in model.named_parameters(): if any(name.startswith(layer) for layer in frozen_layers): param.requires_grad False这里num_classes4一定不要写成 3检测头需要额外的背景类。冻结前三个 stage 的原因是这些层提取的是通用低级特征边缘、纹理、颜色块在 ImageNet 上已经学得足够好血细胞数据集太小微调这些层反而容易过拟合。只微调 stage4 和检测头可以让训练速度提升约 20%同时保住精度。如果你的显存只有 8GB 甚至更少可以考虑换成fasterrcnn_mobilenet_v3_large_fpn_v2。这个模型在血细胞任务上的 mAP 会比 ResNet50 低 2~3 个点但显存占用只有一半左右适合先跑通流程再看情况升级。4.2 训练循环collate_fn、损失回传与梯度裁剪torchvision 的检测模型训练有固定的数据格式要求。图像是[C, H, W]的 float tensor像素值范围 0~1目标是一个 dict包含boxesNx4 的[x0, y0, x1, y1]float tensor、labelsint64 tensor、image_id、area和iscrowd。dataset 返回的(image, target)是 Python 对象需要自定义 collate_fn 才能组成 batchdef collate_fn(batch): images [item[0].to(device) for item in batch] targets [{k: v.to(device) for k, v in item[1].items()} for item in batch] return images, targets def train_one_epoch(model, loader, optimizer): model.train() total_loss 0.0 for images, targets in loader: 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() return total_loss / len(loader)注意model(images, targets)在传入 targets 时返回的是一个 dict包含loss_classifier、loss_box_reg、loss_objectness、loss_rpn_box_reg四项。其中前两项是 ROI 头的分类和回归损失后两项是 RPN 的损失。训练时每轮打印这四个值很有用如果loss_rpn_box_reg远大于其他项说明 anchor 回归不稳定通常要检查 anchor 尺寸设置。梯度裁剪设成max_norm10.0是血细胞任务的经验值。医学图像里偶尔会有标注错误的极端样本一个 outlier 就能让梯度爆炸裁剪后训练过程稳定很多。优化器我推荐 SGDmomentum0.9weight_decay0.0005学习率从 0.005 起步每 5 个 epoch 乘以 0.1。AdamW 在小数据集上收敛快但最终精度往往不如调好学习率的 SGD。4.3 6 个必调参数的血细胞取值与调参顺序训练 Faster RCNN 血细胞检测时下面这 6 个参数的优先级最高按顺序调参数默认值血细胞取值调整依据anchor sizes(32, 64, ...)(4, 8, 12, 20, 32, 48, 64)按细胞实际像素尺寸设定先小后大rpn_pre_nms_top_n (train)20001200减少无效候选框提速rpn_post_nms_top_n (train)2000600与 pre 联动影响正样本数量rpn_nms_thresh0.70.7保持不变RPN 的 NMS 阈值batch_size_per_image512256缓解罕见类 oversamplelr0.0050.005如果 loss 震荡降到 0.002训练时观察验证 mAP 的变化如果 AP 在第 10 轮左右不再上升但 loss 还在降通常是过拟合了。优先检查数据增强强度是否过大、batch_size 是否太小、正则化权重是否需要增加。血细胞数据集小dropout 对检测头的作用不明显不如直接冻结更多 backbone 层来得有效。5. 血细胞检测避坑实录5 条让 mAP 虚高的隐性错误5.1 换一批染色涂片直接翻车域偏移与染色标准化现象在训练集同源的验证集上 mAP 有 0.9把模型部署到医院新采集的血涂片上AP 掉到 0.4白细胞漏检一大片。原因不同实验室的染色流程Wright 染色和 Giemsa 染色、染色时长、显微镜光源色温都会让同一细胞呈现完全不同的颜色分布。模型在训练集上把颜色特征学得太死换一个色彩分布就失效。解决数据增强里把颜色扰动范围加大亮度 ±20%、对比度 ±15%、饱和度 0.85~1.15同时加入不同源的涂片做测试集验证。如果条件允许用染色标准化算法如 Macenko 方法把输入图像统一到参考染色空间再送进模型域偏移问题会明显缓解。一条血的教训不要在同一个染色批次的数据上优化模型调参否则上线必然翻车。5.2 血小板和红细胞碎片混检小目标 anchor 与标注边界规则现象验证集上血小板的 AP 只有 0.3很多血小板被检测成红细胞碎片两者混在一起难以区分。原因血小板是红细胞直径的 1/3~1/4在 512 输入下只有 5~8 像素RPN 默认的 32px anchor 根本覆盖不到这么小的目标。而且红细胞碎片和血小板在形态、颜色上确实高度相似如果标注时把碎片也标成血小板模型会学到小颗粒就是血小板的错误规则。解决先把 anchor sizes 的下限扩到 4确保 RPN 能生成足够小的候选框。然后检查标注规则红细胞碎片应该单独作为负样本或标注为单独类别不要混进血小板。如果碎片过多可以对输入图像做 1.5 倍放大再推理代价是显存上升但小目标的 AP 通常能提升 2~3 个点。5.3 白细胞亚型分类错得离谱类别不平衡的样本层处理现象中性粒细胞和嗜酸性粒细胞分类准确率还行但嗜碱性粒细胞的 recall 只有 0.1模型几乎把所有嗜碱性都识别成了中性粒细胞。原因嗜碱性粒细胞在血涂片中的占比本来就只有 0.5%~2%数据集的类别严重不平衡。Faster RCNN 在 ROI 头里对类别做均匀采样时罕见类的样本每轮迭代只出现几次模型根本学不到区分特征。解决优先做数据层面的处理。收集更多罕见类的图像或者对包含罕见类的图像做过采样复制到多个 epoch 里。其次可以在采样器里对罕见类设置更高的采样权重让每轮迭代中罕见类出现的比例提高到 10% 以上。如果还是不行再用 focal loss 的形式改造 ROI 头的分类损失但要注意 focal loss 的 gamma 值对血细胞这种小样本任务很敏感从 1.0 开始调。5.4 RPN loss 不降标注框不贴合细胞膜的连锁反应现象训练了 5 个 epochrpn_classification_loss 一直在 1.0 以上不下降验证集的预测框全部偏移到细胞边缘外侧。原因血涂片里细胞互相挤压标注时如果为了省事框边界切割到邻近细胞或者框不贴细胞膜而沿着核的边缘画GT 框就和真实细胞区域有很大偏差。RPN 在计算 anchor 和 GT 的 IoU 正样本时这些偏移导致正样本 anchor 的标签也是错的模型学到的定位信号混乱。解决标注时把图像放大 3~5 倍沿着细胞膜的最外缘画框宁可框略大一点也别框到邻近细胞上。如果标注已经完成且发现普遍偏小可以用脚本统一外扩 10% 的边长后重新训练。grep 检查训练日志中的 rpn_classification_loss如果前 3 轮降到 0.5 以下说明标注质量没问题。5.5 推理时一个细胞被框两次NMS 参数修正和 class-wise NMS现象推理结果里同一个红细胞被两个 IoU 约 0.6 的框重复检测NMS 没有把它们合并。原因Faster RCNN 的 NMS 是跨类别执行的但血细胞类别间的形状高度相似模型对同一个细胞产生了两个分数相近的类别预测其中一个错误类别分数稍高把正确类别的框也压下去了。解决推理时调整roi_heads.nms_thresh从默认的 0.5 提高到 0.6让 IoU 在 0.6 附近的重复框被合并。另一个更细的方法是做 class-wise NMS即每个类别单独执行 NMS再合并结果这样同类别重叠框会被抑制不同类别的框不会互相误伤。实现时把模型的roi_heads.nms_thresh设为 -1 关闭内置 NMS在外部自己写循环按类别处理。6. 用混淆矩阵和小/中/大目标 AP 验证模型再对难例做补偿训练结束后不要急着看总 mAP先拆开看每个类别的 AP。用 pycocotools 评估是最直接的方式from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval coco_gt COCO(data/annotations/val.json) coco_dt coco_gt.loadRes(output/predictions.json) evaluator COCOeval(coco_gt, coco_dt, bbox) evaluator.params.imgIds [img[id] for img in coco_gt.imgs.values()] evaluator.params.maxDets [50, 100, 300] evaluator.evaluate() evaluator.accumulate() evaluator.summarize()summarize()会输出每个类别的 AP50、AP75 和按面积分级的 small/medium/large 结果。血细胞任务里绝大多数细胞属于 small 类别所以重点看 small 的 AP。如果 small AP 显著低于 medium说明 anchor 或输入分辨率需要调整。我的习惯是先跑混淆矩阵把红细胞误检成血小板的样本拉出来看图确认是否属于染色污染或碎片问题。对于混淆矩阵里错误集中的类别最后一步是难例补偿。如果血小板和碎片分不开对 RPN 输出的所有小目标额外训练一个二分类精修器如果某个白细胞亚型识别不稳定在 ROI 头之后再接一个针对该亚型的三分类网络。这类补偿会增加推理耗时但血细胞检测的应用场景里准确率优先于速度。我做过最有效的一次调整是在验证集上把 200 个误检样本逐张回放发现高置信度的假阳都集中在涂片边缘的染色沉淀上加上边缘区域抑制这一招AP 直接抬了 4 个点。这个经验我一直留着——每轮跑完多看看错误图比盲目调参有用得多希望帮到你。本文还有配套的精品资源点击获取