☰
淋巴细胞目标检测实战:从标注格式转换到YOLO训练避坑指南
2026/10/7 16:01:02 网站建设 项目流程

简介:一套面向医学影像算法工程师、病理辅助诊断研究人员及YOLO目标检测实践者的淋巴细胞检测数据集,聚焦病理切片中Lymphocyte的精准定位与计数,可直接支撑癌症病理分析、免疫微环境评估及免疫治疗研究。包内共含1152张医学图像,按训练580、验证290、测试282划分,配套医学专家校验的YOLO格式标注,覆盖细胞簇密集等真实场景,兼容YOLOv5/v7/v8/v12等主流框架,可扩展至细胞计数、密度分析。资源总文件数2000,以1152个txt标注与846个jpg图像为主,另有yaml配置和docx说明文档,压缩包67.68MB,结构清晰、开箱即用。目前已有60人学习,适合需要高质量医学标注数据开展模型训练、算法验证或教学演示的开发者与科研人员。

1. 淋巴细胞目标检测数据集.zip:想直接开箱训练前,先看清这三层问题

下载到一个叫「淋巴细胞目标检测数据集.zip」的压缩包,大多数人第一反应是解压后马上找个训练脚本跑起来。实际做过目标检测数据集工程的人都知道,这条路前两个小时大概率翻车:标注格式不认、标签路径写死、类别编号错位,半个下午就没了,模型还在空转。这个zip不是现成模型,而是训练淋巴细胞检测模型的原始素材。它通常包含细胞切片原图、人工标注框和类别说明,来源多为病理切片或血液涂片,标注格式常见为VOC XML、COCO JSON或YOLO TXT。能解决的问题是:把随意标注的细胞图片,变成可以让YOLO正常训练和评估的标准数据集。适合正在做医学图像目标检测的0基础纯小白,也适合被数据集格式反复坑过、想找一份可复现流程的从业者。先别急着训练,先看清标注格式、类别清单和数据切分方式,这套流程才算起步。

2. 解压后的第一件事:摸清标注格式、类别清单与数据组织方式

拿到压缩包之后,我一般不会急着把解压出来的东西塞进某个训练项目里,而是先建一个干净目录,完整解压,再用 tree 命令看一下全貌。训练脚本对路径极其敏感,你在哪个目录解压、图片和标注有没有分开存放,直接决定后面 dataset.yaml 里的 path 怎么写。医学图像标注数据集和自然图像数据集最大的差别是:原图往往很大,一张切片可能上亿像素,但压缩包里的图像可能只是切成块的缩略版本,这会影响你后续的检测粒度。

2.1 目录结构:images、annotations、labels 各自装什么

一个规范的淋巴细胞检测数据集,解压后通常长这样:

lymphocyte_dataset/ ├── images/ │ ├── train/ # 训练原图,常见 jpg/png,有的带 tif │ └── val/ ├── annotations/ │ ├── train/ # 原始标注,xml 或 json │ └── val/ ├── labels/ # 可能是空的,或者已经有人转好的 YOLO txt ├── classes.txt # 类别名,一行一个 └── README.txt # 数据说明,务必先读

注意,这个结构不是铁律。很多公开zip里根本没有 labels 目录,因为 labels 是转换后的产物,原始标注在 annotations 里。也有的zip只有 train 一个总目录,没有划分验证集,这类数据需要你自己按比例切。你需要先确认三件事:图片是什么格式、标注是什么格式、类别有几类。然后看一眼 classes.txt,里面如果写了多行名字,每一行对应一个类别。最怕的情况是 classes.txt 里写了三类,但 annotations 里出现了第四类名字,这种不一致会在训练时直接报class id out of range。

2.2 标注格式:VOC XML、COCO JSON 与 YOLO TXT 的读取差异

目标检测数据集的标注格式基本被三类把持。它们之间的核心差别是坐标系和存储方式:

格式后缀常见来源坐标表示
VOC XML.xmlLabelImg绝对像素 xmin/ymin/xmax/ymax
COCO JSON.jsonlabelme、官方 COCO绝对像素 x/y/width/height,也常见多边形 points
YOLO TXT.txtultralytics归一化 x_center/y_center/width/height

先学会读原始标注。用 Python 的 xml.etree.ElementTree 解析 VOC XML 是非常稳的做法:

import xml.etree.ElementTree as ET tree = ET.parse("annotations/train/cell_001.xml") root = tree.getroot() # 图片尺寸,后面转归一化坐标时必需 print(root.find("size/width").text, root.find("size/height").text) for obj in root.findall("object"): cls = obj.find("name").text # 类别名 bndbox = obj.find("bndbox") xmin = int(float(bndbox.find("xmin").text)) ymin = int(float(bndbox.find("ymin").text)) xmax = int(float(bndbox.find("xmax").text)) ymax = int(float(bndbox.find("ymax").text)) print(cls, xmin, ymin, xmax, ymax)

这段代码里的size/width和size/height是原图的宽高,不是标注框的宽高,转 YOLO 归一化时这两个值必须从根节点取。另一个细节是bndbox里的四个坐标在 XML 里都是字符串,参与计算前必须转成 int 或 float,不然乘除法会得到难以察觉的错误。还有一类标注是旋转框,XML 里带有rotated="1"这样的属性,标准 YOLO 格式不支持旋转框,遇到这种要先投影成外接矩形。

如果原始标注是 JSON,读取方式要看它是 COCO 风格还是 labelme 风格。labelme 风格的 JSON 里有一个shapes数组,每个元素包含label和points,points 可能是四点矩形,也可能是多边形。COCO 风格的 JSON 则是把图片和标注分开用 id 关联,读取起来更绕。最简单的方式是先用 json.load 打开,然后打印 keys,看清楚结构再动手,不要凭印象硬写解析器。

2.3 先可视化再调参:用 OpenCV 把标注框画到原图上检查

光看标注文件不能理解数据质量。我的习惯是随机抽 20 张训练图,把标注框原样画上去,然后逐张翻一遍。这一步能暴露大量问题:框偏移、类别标错、两个框重叠、有细胞没框住。用 OpenCV 做这个检查最快:

import cv2 import xml.etree.ElementTree as ET image_path = "images/train/cell_001.jpg" xml_path = "annotations/train/cell_001.xml" img = cv2.imread(image_path) root = ET.parse(xml_path).getroot() for obj in root.findall("object"): name = obj.find("name").text bndbox = obj.find("bndbox") xmin = int(float(bndbox.find("xmin").text)) ymin = int(float(bndbox.find("ymin").text)) xmax = int(float(bndbox.find("xmax").text)) ymax = int(float(bndbox.find("ymax").text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(0, ymin - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) cv2.imwrite("check_cell_001.jpg", img)

注意putText的坐标点,如果 ymin 太小,文字会被画出画布,所以用max(0, ymin - 6)保护一下。画出框后,把图片尺寸和框坐标对比,基本就能判断框是不是整体偏移。比如原图 3000 像素宽,但框全都集中在一个 600 像素的区域,那多半是标注时用缩放过的图片打的框,坐标却没有映射回原图尺寸。

3. 将标注转成 YOLO 格式:从 XML/JSON 到归一化 TXT 的完整脚本与四个边界坑

数据看完一遍后,最耗时的环节就是格式转换。很多人觉得转换只是一个脚本的事,实际上转换里的坑比训练本身还多。这一章给出一个能直接用的 XML 转 YOLO 脚本,以及一套边界处理逻辑。处理完这一轮,后面训练才会顺。

3.1 为什么不直接用原格式训练:ultralytics 只认 YOLO TXT

ultralytics 的 YOLO 训练接口设计为只读取 YOLO TXT 标注。原因不是它不支持 XML,而是数据加载时要走统一的快速路径:每行一个目标,类别 id 加四个归一化坐标,解析成本极低,也不需要额外依赖 XML 解析库。所以你得把原始标注转成这种 txt。转换时最核心的参数是类别清单的顺序,这个顺序会写死在 dataset.yaml 里,也决定最终模型的类别输出。先做一步统计:

# 看所有标注里的类别名出现次数,方便确定类别顺序 grep -oh "<name>.*</name>" annotations/train/*.xml | sort | uniq -c

上面这条命令适合 Linux 或 macOS,Windows 用户可以用 Python 统计。见到结果后,把出现次数多的类别放前面,不一定必须按字母序。比如淋巴细胞本身是目标,如果你手上有三类——淋巴细胞、粒细胞、单核细胞——那就直接按数据量从高到低排:lymphocyte、granulocyte、monocyte。这个顺序决定了模型输出的 class 0、1、2,后期不要轻易改,否则已经训练好的模型权重全部错位。

3.2 转换脚本:从 VOC XML 转 YOLO TXT 的完整实现

下面这段是完整可跑的转换脚本,建议单独存一个convert_xml_to_yolo.py,放在数据集根目录外执行:

import os import xml.etree.ElementTree as ET source_ann_dir = "lymphocyte_dataset/annotations/train" target_label_dir = "lymphocyte_dataset/labels/train" os.makedirs(target_label_dir, exist_ok=True) # 类别顺序必须和之后 dataset.yaml 中的 names 完全一致 class_list = ["lymphocyte", "granulocyte", "monocyte"] for xml_file in os.listdir(source_ann_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(source_ann_dir, xml_file)) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) out_lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_list: continue cls_id = class_list.index(name) 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) # 越界处理:很多标注框比图像大 1-2 像素 xmin = max(0, xmin) ymin = max(0, ymin) xmax = min(img_w - 1, xmax) ymax = min(img_h - 1, ymax) # 空框过滤:越界后宽或高必须大于 0 if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h out_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") if out_lines: txt_name = xml_file.replace(".xml", ".txt") with open(os.path.join(target_label_dir, txt_name), "w") as f: f.write("\n".join(out_lines))

逻辑说明:脚本遍历source_ann_dir下所有 XML,先从根节点拿到原图size/width和size/height,然后对每个 object 取类别名和矩形框。越界处理放在归一化之前,避免最终坐标出现负值或大于 1 的异常值。最后把空的、越界后宽度或高度为 0 的框过滤掉,输出为 YOLO 格式的一行。

参数说明:class_list的顺序是全局唯一的,后续 dataset.yaml 里names必须和这里完全一致。x_center:.6f这样的格式控制保留 6 位小数,对 640 尺寸的图来说精度已经足够,不用刻意加多。如果你手里的数据没有 val 目录,需要先手动切出 10% 到 20% 作为验证集,再跑这张脚本,标签路径要对应切换。

3.3 补充分支:从 JSON 多边形标注转矩形框

如果你的json不是矩形而是多边形,取外接矩形是最通用的做法:

import json import os json_file = "lymphocyte_dataset/annotations/train/cell_001.json" with open(json_file) as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] out_lines = [] for shape in data["shapes"]: label = shape["label"] points = shape["points"] # 多边形顶点列表 xs = [p[0] for p in points] ys = [p[1] for p in points] xmin = max(0, min(xs)) ymin = max(0, min(ys)) xmax = min(img_w - 1, max(xs)) ymax = min(img_h - 1, max(ys)) if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h # 这里先只写单个类别,多类别时按实际类名映射 id cls_id = 0 out_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") os.makedirs("lymphocyte_dataset/labels/train", exist_ok=True) with open("lymphocyte_dataset/labels/train/cell_001.txt", "w") as f: f.write("\n".join(out_lines))

逻辑说明:多边形细胞轮廓取外接矩形会用填充掉一些背景,但对绝大多数淋巴细胞检测场景来说够用。如果标注本身就是细胞核的精细轮廓,转矩形后边界会和核边界贴合,反而比手画的矩形更准。参数说明:这个脚本里img_w和img_h用的是 JSON 里的imageWidth,而不是图片实际读取尺寸。如果你发现 JSON 里没这个字段,一定要用cv2.imread拿到真正的宽高,否则坐标全偏。

3.4 转换时最容易翻车的四个边界点

第一,越界。标注框偶尔会出图像边界几个像素,不裁剪直接转出来的归一化坐标可能是负数或大于 1,ultralytics 训练时会警告但不报错,最后结果就是模型在边界处行为异常。第二,空框。裁剪后宽或高为 0 的框必须丢掉,不能保留。第三,类别顺序。如果你在转换时用了class_list.index(name),那class_list必须是一个无重复且按序的列表,不能直接用set()去构造,因为集合无序,两次运行顺序可能不同。第四,难例问题。有的数据集里把未确认的细胞单独标注成unsure或difficult,转换时通常直接跳过,但如果这类难例占比超过 15%,跳过会让模型丢失困难样本。我一般会先保留一个特殊类别,跑一轮看效果,再考虑是否合并到主类别。

4. 用 ultralytics 训练淋巴细胞检测模型:yolov8n 跑通最小实验的关键参数

数据转换干净了,就可以进入训练环节。这一章针对 0 基础纯小白,环境配置和命令直接可用。记住一个原则:先用最小的网络跑通整条链路,再回来调参。一上来就上 yolov11x 只会让机器冒烟,而且数据集不够大时收益很低。

4.1 数据集配置:dataset.yaml 的 path 与 names 必须对齐

ultralytics 不需要额外修改源码,它靠读取一个 yaml 文件来找数据和类别。对淋巴细胞检测数据集,我一般这样写:

# lymphocyte.yaml path: C:/work/lymphocyte_dataset # 数据集根目录的绝对路径 train: images/train val: images/val test: images/test nc: 3 names: 0: lymphocyte 1: granulocyte 2: monocyte

注意path这里填的是数据集根目录的绝对路径,不是 images 的路径。train和val是相对于根目录的路径,它们最终会拼接成C:/work/lymphocyte_dataset/images/train。如果path只写相对路径,训练时又换了当前目录,就会报Dataset not found。Windows 用户要注意反斜杠,yaml 里建议直接用正斜杠C:/work/...,避免转义符问题。

names的键从 0 开始,和转换脚本里的class_list一一对应。如果转换脚本里把淋巴细胞放在 0,这里也必须把 lymphocyte 放在 0,否则训练不报错,但预测出的框全部张冠李戴。

4.2 环境配置与训练命令:yolov8n 是 0 基础最快的起步组合

环境只需要 ultralytics 一个包:

pip install ultralytics

如果你的机器没有 NVIDIA 显卡,训练命令里要加device=cpu。CPU 训练 yolov8n 加上一个几千张图片的数据集,一个 epoch 可能要十几分钟,但至少能跑通流程。有 GPU 就直接用默认的 0 号卡:

yolo detect train \ model=yolov8n.pt \ data=lymphocyte.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ workers=4 \ project=lymphocyte_runs \ name=exp1

这条命令用的预训练权重是yolov8n.pt,模型会在训练开始时自动下载,如果下载失败,就去 ultralytics 的 release 页面手动下载丢到项目根目录。逻辑上选择 yolov8n 而不是 yolov11 是因为:参数量最小、显存占用最低、在自定义小数据集上不容易过拟合。yolov11 在参数上更强,但训练更慢,且对增强策略的默认值有差异,不适合第一轮摸底。

4.3 参数说明:imgsz、batch、epochs、mosaic 各自的边界

参数推荐初始值说明
imgsz640如果细胞尺寸很小,调到 1024 能提升小目标 recall,但显存成倍增加
batch16显存不足时降到 8,不要用 1,BN 会很不稳定
epochs100数据集小就训练到 200 或加 early stopping
workers4Windows 上超过 4 可能报 DataLoader 错误
mosaic1.0默认开启,但细胞数据建议关掉或降到 0.5

ultralytics 的增强参数可以写在 yaml 里,也可以直接作为命令行参数。对淋巴细胞这类密集小目标,我一般会在训练命令里加一句:

yolo detect train \ model=yolov8n.pt \ data=lymphocyte.yaml \ epochs=200 \ imgsz=1024 \ batch=16 \ mosaic=0.5 \ hsv_h=0.02 \ hsv_s=0.7 \ hsv_v=0.6

这里的mosaic=0.5表示每张训练图有 50% 概率触发马赛克拼接。mosaic 对自然场景目标检测强,但对细胞图像会切掉大量小细胞。hsv_s和hsv_v分别控制饱和度与亮度扰动,这对染色差异大的病理图很重要,默认值 0.7 和 0.6 可以保留,但色相扰动hsv_h不要给大,因为染色的色相本身有诊断意义,改太大会让模型学不到真实颜色分布。

4.4 训练完先看哪几张图

训练结束后,结果会写到lymphocyte_runs/exp1/目录。先不要急着跑预测,先打开这几张图:

ls lymphocyte_runs/exp1/

重点看results.png,它包含训练损失、验证损失、mAP50、mAP50-95 的曲线。如果训练损失一直降但验证损失在第 30 轮开始上涨,就是过拟合,需要回退到验证损失最低点的权重,对应best.pt。再看confusion_matrix.png,它显示每个类别被误判成什么。如果淋巴细胞这一行有大片色块落在背景列,说明漏检严重;如果落在粒细胞列,说明两类形态太接近,考虑合并类别。PR_curve.png是 precision-recall 曲线,mAP50 数值高不代表曲线形状好,要看曲线在 recall 高段是不是还有平台。

5. 淋巴细胞数据集训练中的 6 条避坑记录:从标注错位到小目标漏检

这一部分全部来自我做细胞检测的踩坑历史。每一条都是先描述现象,再解释原因,最后给解决方法。数值参数可以直接抄,但更推荐你根据自己数据的情况微调。

5.1 现象:训练 loss 降到很低,val mAP 却上不去

训练损失一路往下,val/mAP50 在 0.2 左右波动,退出来看验证集,发现模型把所有细胞都预测成淋巴细胞。原因:最常见的是类别不均衡。比如淋巴细胞框占比 90%,粒细胞和单核细胞加起来只有 10%,模型把所有目标都判成淋巴细胞也能把 loss 压得很低,验证集里那些低频率类别几乎全错。解决:先统计每个类别的框数量,用grep -o或 Python 数一遍。如果比例超过 5:1,把低频类别在训练集中做重复采样,或者干脆合并语义接近的类别。如果所有类别总数不到 1000 个框,先别调参,先去扩充数据。

5.2 现象:同一个细胞被标注了两次

可视化时看到两个几乎重叠的框,中心点差距不超过几个像素,预测时一个细胞出了两个框,NMS 之后还去不掉,因为置信度都很高。原因:可能是多人标注同一批图,或数据集作者把多套标注直接拼接,没有做去重。解决:写一个按 IoU 阈值合并重叠框的脚本,阈值设 0.6,遍历所有框,保留框中心更靠近细胞中心的那个。如果两个框类别不同,优先保留置信度或面积更大的类别。这个步骤要在转换 YOLO 之前做,最好在原始 XML/JSON 层面做,因为归一化后再合并坐标精度会损失。

5.3 现象:小淋巴细胞全部漏检

大细胞能框住,小淋巴细胞一个都框不住,precision 高但 recall 很低。原因:原图很大,直接缩放到 640 后,直径只有 8 像素的细胞被缩到 1 到 2 个像素,特征几乎丢失。解决:不要只调大 imgsz,正确做法是切图。把原图按 512 或 1024 的步长切成小块,每块之间重叠 64 像素,再基于切出来的小块做训练和推理,推理结果用 NMS 合并。切块大小根据细胞直径决定,保证目标至少占 20×20 像素。我在骨髓涂片数据上,imgsz=640 加切块的效果远好于直接 imgsz=1536,因为后者还受显存限制。

5.4 现象:验证集里混入了训练图,mAP 虚高到 0.99

训练一轮后在验证集上 mAP 达到 0.99,兴高采烈拿去测新切片,结果完全不能用。原因:数据集本身就存在重复图,或者是某个程序按前 80% 后 20% 硬切分时把一样的前后帧放到了两个集合。解决:用 md5 对图片去重。所有训练和验证图像都算一遍 md5,哈希相同就保留一份,再按去重后的列表划分数据集。这一步在医学时间序列数据里尤为重要,因为连续帧可能视觉相似但实际不同。

5.5 现象:验证集同源切片正常,换一批切片就崩

训练和验证是同一种染色方案的切片,效果不错,但拿到另一家医院或另一种染液制备的切片后,漏检和误检大幅增加。原因:染色差异、白平衡差异导致图像域偏移,模型学到的是这套数据的颜色分布,而不是细胞形态。解决:在训练时加大 HSV 扰动,把hsv_s设到 0.7 到 0.9,hsv_v设到 0.5 到 0.6,让模型适应颜色变化。更强的方法是在输入前做染色归一化,把所有图对齐到一张参考图的均值方差。染色归一化不是所有场景都需要,但如果跨中心泛化是刚需,它比增强更稳。

5.6 现象:数据增强过猛,模型学会识别假细胞

训练 loss 很低,但推理时把染色杂质、边界噪声也识别成淋巴细胞。原因:增强参数太激进,比如degrees=180、scale=2.0,模型看到大量被旋转或缩放的细胞形状,自然把不存在的伪影当成了真实形态。解决:细胞是有方向的,但旋转 180 度在生理上没意义,所以旋转角度限制在 30 度以内。缩放比例scale控制在 0.3 以内,translate控制在 0.1。我一般直接关掉mosaic,因为马赛克拼接会把一个细胞切成两半,让标注框内混进大量背景,干扰小目标学习。你把增强调回保守水平后,训练 loss 会略高,但验证集和真实场景的泛化会改善。

6. 最终验证:三个让模型在真实切片图像上可用的技巧

训练出了 best.pt,离真正能用还差一步。很多人直接拿整张 WSI 图跑 predict,得到一堆小框又合并不好,最后说模型不行。其实问题出在验证方式上。

6.1 用测试集做一次冻结权重评估

在模型交付前,先跑一次独立测试集评估,不用训练集和验证集:

yolo detect val \ model=lymphocyte_runs/exp1/weights/best.pt \ data=lymphocyte.yaml \ split=test

注意 yaml 里必须有test: images/test,否则 split 参数无效。评估完看 mAP50-95,而不是只看 mAP50,因为细胞检测对框的位置精度要求高,mAP50 允许框偏移很大,mAP50-95 才更接近真实可用度。

6.2 对整张 WSI 做滑窗推理,而不是直接 resize

真实切片原图可能上万像素,直接整图送入网络要么爆显存,要么被压缩到小目标全丢。正确做法是把原图切成小块,每块尺寸与训练时的 imgsz 保持一致,一般 1024 或 640。前后块留 10% 的重叠,推理完把所有框映射回原图坐标,再做一次全局 NMS。切块的步长不要超过块宽的一半,否则目标会横跨切割边界被截断。

6.3 TTA 和多尺度推理要不要开

ultralytics 的predict --augment会开启 TTA,包括水平翻转和多尺度推理。对细胞检测这种小目标场景,TTA 能提升 recall,但推理时间增加 2 到 3 倍,且可能出现同一个细胞被多个尺度框出,需要更宽容的 NMS。我的习惯是:离线处理一批切片时开 TTA,实时推理时关掉。

我自己第一次跑这类数据集,就是懒得可视化,结果在一个坐标错了一半的标注上跑了上百轮,三天后发现是标注问题。从那以后,我每次拿到新的目标检测数据集,永远先花半小时看数据,再决定要不要调参。这个习惯救了我很多次,希望帮到你。

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

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

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

立即咨询