简介一套面向医学影像算法工程师、病理辅助诊断研究人员及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/ymaxCOCO JSON.jsonlabelme、官方 COCO绝对像素 x/y/width/height也常见多边形 pointsYOLO 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 里带有rotated1这样的属性标准 YOLO 格式不支持旋转框遇到这种要先投影成外接矩形。如果原始标注是 JSON读取方式要看它是 COCO 风格还是 labelme 风格。labelme 风格的 JSON 里有一个shapes数组每个元素包含label和pointspoints 可能是四点矩形也可能是多边形。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 TXTultralytics 的 YOLO 训练接口设计为只读取 YOLO TXT 标注。原因不是它不支持 XML而是数据加载时要走统一的快速路径每行一个目标类别 id 加四个归一化坐标解析成本极低也不需要额外依赖 XML 解析库。所以你得把原始标注转成这种 txt。转换时最核心的参数是类别清单的顺序这个顺序会写死在 dataset.yaml 里也决定最终模型的类别输出。先做一步统计# 看所有标注里的类别名出现次数方便确定类别顺序 grep -oh name.*/name annotations/train/*.xml | sort | uniq -c上面这条命令适合 Linux 或 macOSWindows 用户可以用 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_okTrue) # 类别顺序必须和之后 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_okTrue) 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 转换时最容易翻车的四个边界点第一越界。标注框偶尔会出图像边界几个像素不裁剪直接转出来的归一化坐标可能是负数或大于 1ultralytics 训练时会警告但不报错最后结果就是模型在边界处行为异常。第二空框。裁剪后宽或高为 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 显卡训练命令里要加devicecpu。CPU 训练 yolov8n 加上一个几千张图片的数据集一个 epoch 可能要十几分钟但至少能跑通流程。有 GPU 就直接用默认的 0 号卡yolo detect train \ modelyolov8n.pt \ datalymphocyte.yaml \ epochs100 \ imgsz640 \ batch16 \ device0 \ workers4 \ projectlymphocyte_runs \ nameexp1这条命令用的预训练权重是yolov8n.pt模型会在训练开始时自动下载如果下载失败就去 ultralytics 的 release 页面手动下载丢到项目根目录。逻辑上选择 yolov8n 而不是 yolov11 是因为参数量最小、显存占用最低、在自定义小数据集上不容易过拟合。yolov11 在参数上更强但训练更慢且对增强策略的默认值有差异不适合第一轮摸底。4.3 参数说明imgsz、batch、epochs、mosaic 各自的边界参数推荐初始值说明imgsz640如果细胞尺寸很小调到 1024 能提升小目标 recall但显存成倍增加batch16显存不足时降到 8不要用 1BN 会很不稳定epochs100数据集小就训练到 200 或加 early stoppingworkers4Windows 上超过 4 可能报 DataLoader 错误mosaic1.0默认开启但细胞数据建议关掉或降到 0.5ultralytics 的增强参数可以写在 yaml 里也可以直接作为命令行参数。对淋巴细胞这类密集小目标我一般会在训练命令里加一句yolo detect train \ modelyolov8n.pt \ datalymphocyte.yaml \ epochs200 \ imgsz1024 \ batch16 \ mosaic0.5 \ hsv_h0.02 \ hsv_s0.7 \ hsv_v0.6这里的mosaic0.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 像素。我在骨髓涂片数据上imgsz640 加切块的效果远好于直接 imgsz1536因为后者还受显存限制。5.4 现象验证集里混入了训练图mAP 虚高到 0.99训练一轮后在验证集上 mAP 达到 0.99兴高采烈拿去测新切片结果完全不能用。原因数据集本身就存在重复图或者是某个程序按前 80% 后 20% 硬切分时把一样的前后帧放到了两个集合。解决用 md5 对图片去重。所有训练和验证图像都算一遍 md5哈希相同就保留一份再按去重后的列表划分数据集。这一步在医学时间序列数据里尤为重要因为连续帧可能视觉相似但实际不同。5.5 现象验证集同源切片正常换一批切片就崩训练和验证是同一种染色方案的切片效果不错但拿到另一家医院或另一种染液制备的切片后漏检和误检大幅增加。原因染色差异、白平衡差异导致图像域偏移模型学到的是这套数据的颜色分布而不是细胞形态。解决在训练时加大 HSV 扰动把hsv_s设到 0.7 到 0.9hsv_v设到 0.5 到 0.6让模型适应颜色变化。更强的方法是在输入前做染色归一化把所有图对齐到一张参考图的均值方差。染色归一化不是所有场景都需要但如果跨中心泛化是刚需它比增强更稳。5.6 现象数据增强过猛模型学会识别假细胞训练 loss 很低但推理时把染色杂质、边界噪声也识别成淋巴细胞。原因增强参数太激进比如degrees180、scale2.0模型看到大量被旋转或缩放的细胞形状自然把不存在的伪影当成了真实形态。解决细胞是有方向的但旋转 180 度在生理上没意义所以旋转角度限制在 30 度以内。缩放比例scale控制在 0.3 以内translate控制在 0.1。我一般直接关掉mosaic因为马赛克拼接会把一个细胞切成两半让标注框内混进大量背景干扰小目标学习。你把增强调回保守水平后训练 loss 会略高但验证集和真实场景的泛化会改善。6. 最终验证三个让模型在真实切片图像上可用的技巧训练出了 best.pt离真正能用还差一步。很多人直接拿整张 WSI 图跑 predict得到一堆小框又合并不好最后说模型不行。其实问题出在验证方式上。6.1 用测试集做一次冻结权重评估在模型交付前先跑一次独立测试集评估不用训练集和验证集yolo detect val \ modellymphocyte_runs/exp1/weights/best.pt \ datalymphocyte.yaml \ splittest注意 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实时推理时关掉。我自己第一次跑这类数据集就是懒得可视化结果在一个坐标错了一半的标注上跑了上百轮三天后发现是标注问题。从那以后我每次拿到新的目标检测数据集永远先花半小时看数据再决定要不要调参。这个习惯救了我很多次希望帮到你。本文还有配套的精品资源点击获取