☰
YOLO血细胞检测实战:红细胞、白细胞、血小板识别与计数全流程
2026/9/28 16:42:35 网站建设 项目流程

简介:本资源面向医学图像处理与深度学习入门者,提供一套基于YOLO模型的红细胞、白细胞、血小板识别与计数系统完整方案,可用于临床辅助诊断研究或课程设计实践。压缩包共约2000个文件,以1974个txt标注数据为主,辅以yaml配置、py源码、xml标注及css样式文件,整体约276.9MB,涵盖数据集、训练脚本与界面程序。项目包含UIProgram用户界面、TestFiles测试图片、models预训练模型、datasets数据集及save_data训练记录等模块,并集成wandb实验跟踪工具,便于调试与优化。已有85人学习下载,读者可获取可直接运行的Python源码、标注数据与模型文件,快速复现血细胞检测计数流程,理解YOLO在医学图像中的应用与工程组织方式。

1. 血涂片里的三行数字:为什么用 YOLO 做红细胞、白细胞、血小板识别与计数

一张外周血涂片,显微镜下几百个细胞,检验科医生要数出红细胞、白细胞、血小板三类目标的数量和比例。手工计数一个样本动辄十几分钟,还受疲劳和主观判断影响。用 YOLO 做红细胞、白细胞、血小板识别与计数,本质是把「找细胞」和「数细胞」这两件事交给一个目标检测模型:红细胞数量最多、白细胞有内核、血小板体积最小,三类目标在形态和尺度上差异极大,正好是检验检测模型能力的场景。这套系统适合医学图像方向的学生、做体外诊断设备的工程师,以及想找一个真实数据集练手 YOLO 全流程的人。Python 源代码加数据集的组合,意味着你能从数据标注一路跑到推理计数,而不是只调一个现成接口。下面把我实际做这类项目时的选型、训练、计数和踩坑过程拆开讲。

2. 数据准备与标注:三类细胞怎么标才不返工

2.1 红细胞、白细胞、血小板的数据集长什么样

血涂片数据集通常来自显微镜拍摄的染色图像,常见格式是每张图对应一个标注文件。三类目标的分布极不均衡:一张图里红细胞可能上百个,白细胞只有几个,血小板零散分布且和红细胞碎片、染色杂质容易混淆。我一般先统计每类目标的实例数,如果白细胞占比低于 2%,训练时就要考虑类别权重或者过采样,否则模型会把白细胞当成背景。

标注工具用 LabelImg 或 CVAT 都行,输出 YOLO 格式的 txt,每行是类别 中心x 中心y 宽 高,坐标全部归一化到 0 到 1。这里有个血泪经验:红细胞边界模糊,标注时容易把相邻两个红细胞框成一个,或者把重叠区域切错。我的做法是框尽量贴合细胞膜外缘,重叠细胞各自独立成框,宁可多标一个也不合并。血小板最难,直径可能只有十几个像素,标注前先把图像放大到 200% 以上再画框。

数据集划分按 8:1:1 分训练、验证、测试。注意同一张原始涂片不要同时出现在训练集和验证集里,否则验证指标虚高,这是很多人翻车的地方。划分脚本可以这样写:

import os import random import shutil # 原始图片和标注目录 img_dir = "raw/images" label_dir = "raw/labels" # 输出目录 out_root = "dataset" splits = {"train": 0.8, "val": 0.1, "test": 0.1} files = [f for f in os.listdir(img_dir) if f.endswith((".jpg", ".png"))] random.seed(42) # 固定随机种子,保证可复现 random.shuffle(files) n = len(files) n_train = int(n * splits["train"]) n_val = int(n * splits["val"]) assign = {} for i, f in enumerate(files): if i < n_train: assign[f] = "train" elif i < n_train + n_val: assign[f] = "val" else: assign[f] = "test" for f, split in assign.items(): # 复制图片 shutil.copy(os.path.join(img_dir, f), os.path.join(out_root, "images", split, f)) # 复制同名标注 stem = os.path.splitext(f)[0] src_lab = os.path.join(label_dir, stem + ".txt") if os.path.exists(src_lab): shutil.copy(src_lab, os.path.join(out_root, "labels", split, stem + ".txt")) print("划分完成:", {k: list(assign.values()).count(k) for k in splits})

这段脚本做了三件事:固定随机种子打乱文件、按比例分配 split、把图片和同名标注一起复制到对应目录。参数上random.seed(42)保证每次划分一致,方便复现;splits字典可以按数据集大小调整,小数据集可以把验证集比例提到 0.15。跑完后检查dataset/labels/train里每个 txt 是否和图片一一对应,缺标注的图片要单独处理,不能直接丢进去训练。

2.2 标注格式转换与数据增强的边界

如果你拿到的标注是 VOC 的 xml,需要转成 YOLO 的 txt。转换时注意 VOC 的坐标是左上角和右下角的绝对像素值,要除以图像宽高归一化,并且中心点坐标是(xmin+xmax)/2/width。转换脚本里最容易错的是类别名到类别 id 的映射,红细胞、白细胞、血小板三个名字必须和data.yaml里的names顺序完全一致,否则训练出来的类别全乱。

数据增强方面,血涂片图像不适合做水平翻转以外的几何变换。因为细胞形态有方向性,垂直翻转和大幅旋转会造出不存在的细胞朝向,模型学到的是伪特征。我一般只开水平翻转、轻微亮度和对比度扰动,HSV 的色调扰动要关掉,染色颜色是判断白细胞类型的重要线索,改色调等于破坏标签。Mosaic 增强可以用,但血小板太小,四图拼接后容易在边缘被裁掉,建议把 Mosaic 概率降到 0.3 以下。

注意:标注完成后一定要用可视化脚本把框画回原图检查一遍,尤其是血小板,漏标和错标在训练 loss 上不会明显报警,但会直接拉低召回。

3. YOLO 训练配置:从预训练权重到三类细胞收敛

3.1 环境搭建与预训练模型选择

环境用 Python 3.9 以上,PyTorch 装对应 CUDA 版本。YOLO 系列里我一般选 YOLOv8 或 YOLOv11 的检测模型,因为它们的 anchor-free 结构对小目标更友好,血小板这种十几个像素的目标用 v5 的 anchor 机制容易漏。预训练权重从官方发布页下载,不要用来路不明的权重,否则训练初期 loss 会异常震荡。

安装依赖和验证环境:

# 创建虚拟环境 python -m venv yolo_env source yolo_env/bin/activate # Windows 用 yolo_env\Scripts\activate # 安装 ultralytics 和基础依赖 pip install ultralytics opencv-python matplotlib # 验证 GPU 是否可用 python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"

如果输出True和显卡型号,说明环境通了。V100 这类卡跑 640 尺寸的 YOLO 训练,batch 可以开到 32 以上;消费级 8G 显存建议 batch 设 8 到 16,配合amp=True混合精度省显存。这里有个常见坑:torch.cuda.is_available()返回 False 但驱动明明装了,多半是 PyTorch 版本和 CUDA 版本不匹配,去 PyTorch 官网按命令重装,别自己猜版本号。

3.2 data.yaml 与训练参数怎么设

data.yaml是训练入口,三个关键字段:path指向数据集根目录,train/val/test指向划分后的图片目录,names是类别名列表。写成这样:

path: ./dataset train: images/train val: images/val test: images/test names: 0: RBC 1: WBC 2: PLT

类别顺序必须和标注时的类别 id 一致。RBC 是红细胞,WBC 是白细胞,PLT 是血小板,用英文缩写是为了避免中文路径在某些系统下的编码问题。

训练命令:

yolo detect train \ model=yolov8s.pt \ data=dataset/data.yaml \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ mosaic=0.3 \ hsv_h=0.0 \ hsv_s=0.3 \ hsv_v=0.3 \ fliplr=0.5 \ project=runs/blood \ name=exp1

参数逐个说:model=yolov8s.pt是预训练权重,s 版本在精度和速度间平衡,血小板多的话可以换 m 版本;epochs=150配合patience=30早停,验证集 30 轮不提升就停,省时间;imgsz=640是输入尺寸,血小板太小可以提到 1024,但显存和速度要重新评估;lr0=0.01初始学习率,lrf=0.01是最终学习率系数,余弦退火到初始的百分之一;mosaic=0.3降低拼接概率保护小目标;hsv_h=0.0关掉色调扰动,前面说过原因;fliplr=0.5水平翻转保留。

训练过程中重点看三个指标:metrics/mAP50看整体检测能力,metrics/mAP50-95看框的精度,以及每类的precision和recall。如果白细胞 recall 一直上不去,先查标注里白细胞是不是漏标,再考虑加类别权重。YOLO 的损失函数由框回归、置信度和分类三部分组成,训练日志里box_loss下降慢说明框不准,cls_loss高说明类别混淆,红细胞和血小板在低倍镜下颜色接近时容易混。

3.3 训练不收敛时的排查顺序

训练翻车的表现有好几种,按这个顺序查效率最高。第一,loss 从第一轮就是 nan,检查标注文件里有没有坐标超过 1 或者负数,归一化算错会直接让损失爆炸。第二,loss 下降但 mAP 不动,多半是学习率太大或者数据增强过猛,先把lr0降到 0.001 试。第三,某一类指标始终为 0,去验证集可视化里看这类目标有没有被预测出来,没有的话检查data.yaml的names顺序和标注 id 是否对得上。第四,BN 层崩溃,表现为 loss 突然飙升,这是 batch 太小或者学习率太高导致的,把 batch 调大或者加梯度裁剪。这些排查动作我一般做成一个 checklist,每次训练前过一遍,能省下大量重跑时间。

4. 计数逻辑与后处理:从检测框到三行数字

4.1 检测结果去重与计数规则

模型输出的是检测框,直接数框会重复计数,因为同一细胞可能被多个框命中。标准做法是用 NMS 非极大值抑制,YOLO 推理时已经内置,但 IoU 阈值要按细胞类型调。红细胞密集重叠,IoU 阈值设 0.5 容易把相邻细胞合并,我一般设 0.3 到 0.4,让重叠框保留更多。血小板小且分散,IoU 可以设 0.5。白细胞数量少,阈值影响不大。

推理和计数的代码:

from ultralytics import YOLO import cv2 model = YOLO("runs/blood/exp1/weights/best.pt") # 推理单张图,conf 是置信度阈值,iou 是 NMS 阈值 results = model.predict( source="test_sample.jpg", conf=0.25, iou=0.35, imgsz=640, save=True ) # 统计每类数量 counts = {"RBC": 0, "WBC": 0, "PLT": 0} names = results[0].names # {0: 'RBC', 1: 'WBC', 2: 'PLT'} for box in results[0].boxes: cls_id = int(box.cls[0]) cls_name = names[cls_id] counts[cls_name] += 1 print("红细胞:", counts["RBC"]) print("白细胞:", counts["WBC"]) print("血小板:", counts["PLT"]) # 计算白细胞比例,临床常看这个 total_wbc_rbc = counts["WBC"] + counts["RBC"] if total_wbc_rbc > 0: ratio = counts["WBC"] / total_wbc_rbc * 100 print(f"白细胞占比: {ratio:.2f}%")

conf=0.25是置信度阈值,低于这个值的框直接丢,血小板可以降到 0.15 提高召回,但会引入假阳性,需要看具体场景取舍。iou=0.35控制 NMS 合并程度,前面解释过。计数逻辑就是遍历boxes,按类别累加。白细胞占比是临床参考指标,正常范围在千分之一到百分之一量级,如果算出来偏差大,先回头查检测精度。

4.2 把计数结果做成可复现的批量脚本

单张推理只能验证流程,实际用要批量跑整个测试集并输出汇总表。批量脚本要处理图片读取失败、空检测结果、类别缺失这些边界情况:

import os import csv from ultralytics import YOLO model = YOLO("runs/blood/exp1/weights/best.pt") test_dir = "dataset/images/test" out_csv = "counts_result.csv" rows = [] for fname in sorted(os.listdir(test_dir)): if not fname.lower().endswith((".jpg", ".png")): continue img_path = os.path.join(test_dir, fname) results = model.predict(source=img_path, conf=0.25, iou=0.35, verbose=False) counts = {"RBC": 0, "WBC": 0, "PLT": 0} if len(results) > 0: names = results[0].names for box in results[0].boxes: counts[names[int(box.cls[0])]] += 1 rows.append([fname, counts["RBC"], counts["WBC"], counts["PLT"]]) with open(out_csv, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["image", "RBC", "WBC", "PLT"]) writer.writerows(rows) print(f"共处理 {len(rows)} 张,结果写入 {out_csv}")

verbose=False关掉每张图的日志,批量跑的时候屏幕干净。results为空说明这张图没检测到任何目标,计数保持 0,不要报错中断。输出 CSV 方便后续用 pandas 做统计和可视化。跑完拿几张图人工核对计数,如果某张图红细胞数明显偏少,去看是不是 NMS 阈值把重叠细胞合并了。

提示:批量推理前先用model.val()在测试集上跑一遍,拿到每类的 mAP 和混淆矩阵,确认模型没有把红细胞系统性误判成血小板,再进入计数环节。

5. 避坑与常见问题:血细胞检测里最容易翻车的五件事

现象一:训练 mAP 很高,实际计数却差很多。原因通常是验证集和测试集来自同一张涂片的不同区域,模型记住了这张涂片的染色风格,换一张涂片就失效。解决方法是划分数据时按涂片来源分组,同一张涂片的图只进一个 split,测试集必须包含训练时没见过的涂片。

现象二:血小板几乎检测不到。原因是血小板在 640 输入尺寸下只有几个像素,特征在 backbone 下采样后基本丢失。解决办法是把imgsz提到 1024 或 1280,或者在模型里加 P2 小目标检测层。如果显存不够,可以切图推理,把大图裁成小块分别检测再合并计数。

现象三:白细胞被识别成红细胞。低倍镜下白细胞内核不明显,颜色和红细胞接近。先确认标注时白细胞框是否准确,再看训练时hsv_h是不是被误开了,色调扰动会让模型分不清两类颜色。把hsv_h设为 0,并适当提高白细胞的类别权重。

现象四:同一细胞被数了两次。NMS 的 IoU 阈值设太高,重叠框没被抑制。红细胞密集区域把iou降到 0.3,同时检查推理时有没有重复调用模型。如果用了切图推理,块与块之间的重叠区域要做去重,否则边界上的细胞会被两块各数一次。

现象五:训练到一半 loss 突然爆炸。常见原因是学习率太高或者 batch 太小导致 BN 统计量失稳。先把lr0减半,batch加到显存允许的最大值,还不行就加warmup_epochs让前几轮学习率慢慢升。如果用的是预训练权重,确认权重和模型结构匹配,不匹配的权重加载会静默失败,训练从随机初始化开始,loss 自然难看。

6. 把计数误差压下去:一个可落地的验证习惯

模型训完、计数脚本跑通,不代表这套系统能用。我最后一定会做一件事:拿一批人工计数的样本做对照,算每类细胞的计数误差。具体做法是选 20 到 30 张测试图,让标注人员逐图数出红细胞、白细胞、血小板真实数量,然后和模型输出做对比,算平均绝对误差和相对误差。红细胞基数大,相对误差控制在 5% 以内算可用;白细胞数量少,差一个就是百分之十几,所以看绝对误差更合理;血小板最难,相对误差 15% 以内我已经能接受。

对照表长这样:

图片真实 RBC预测 RBC真实 WBC预测 WBC真实 PLT预测 PLT
001.jpg186179442319
002.jpg210205331721
003.jpg154160543126

从表里能看出误差分布:红细胞偶尔多算或少算几个,多半是重叠区域 NMS 的取舍;白细胞基本准;血小板波动最大,漏检和误检都有。针对血小板,我会单独把conf从 0.25 降到 0.15 再跑一遍,看召回提升多少、假阳性增加多少,取一个平衡点。这个调参过程没有捷径,就是拿数据说话。

还有一个习惯:每次改完参数重新训练,都在runs/blood下新建一个exp目录,不要覆盖旧结果。YOLO 默认会递增命名,但手动改配置时容易覆盖。保留每次实验的权重和日志,后面对比不同参数的效果时才有依据。我吃过这个亏,一次把lr0从 0.01 改到 0.001 重新训练,结果把之前效果好的权重覆盖了,只能重跑一遍。

如果要把这套系统往产品方向推,下一步是量化模型和推理加速,把 PyTorch 权重转成 ONNX 或 TensorRT,在边缘设备上跑实时计数。但那是另一个话题了,先把检测精度和计数逻辑做扎实,后面的优化才有意义。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询