YOLOv8血细胞检测实战:数据集划分与训练避坑指南
简介面向血细胞自动检测任务的YOLO目标检测数据集包含1000张真实场景医学显微图像标注对象覆盖红细胞、白细胞与血小板适用于医学影像分析及相关课程项目。资源包以VOC格式xml、COCO格式json和YOLO格式txt三种标签同步存放满足不同检测框架的输入要求同时内置数据集划分脚本可快速生成训练集、验证集和测试集并附有YOLO环境搭建与训练教程兼顾入门与进阶使用。整体文件总数2000个主要文件类型包括xml标签、txt标签、Python脚本、配置说明及HTML教程文档压缩包大小约29.34MB内容组织清晰便于按需提取。目前已有190人学习下载适合需要真实医学血细胞数据开展目标检测实验或完成相关课题的研究者与学生。1. 红白细胞血小板检测1000 张图的数据集到底能训练成什么样拿到一个 YOLO 红白细胞血小板检测数据集1000 张血涂片显微图像带 VOC、COCO、YOLO 三份标签和划分脚本第一反应通常是这么点图够不够训但实际做过几次血细胞检测项目的人会告诉你1000 张图不是不能做而是要把功夫下在数据格式统一、划分脚本的严谨性和训练参数的选择上。这个标题里最值钱的部分不是那 1000 张图而是三套早有想好后路的标注格式和配套脚本。本篇按我实际跑这类项目的顺序展开先看三份标签怎么读怎么转再讲划分脚本怎么用不翻车然后走一遍 YOLOv8 的训练流程最后把那些让人半夜爬起来重跑的坑一个个点名。2. 一张细胞图三份标签VOC、COCO、YOLO 三种格式的差异与互转2.1 为什么一个数据集要存三份标注而不是只用一种格式做血细胞检测这类目标检测项目标注格式的混用是绕不过去的。LabelImg 默认导出 VOC 的 XML很多显微镜标注工具生成 COCO 的 JSON开源仓库和比赛数据大多用 YOLO 的 TXT。三份格式并存的惯例本质上是在减少使用方的迁移成本新来的协作者手头只有 COCO 工具链就用 JSON只想用 YOLO 系列直接训就用 TXT做公共数据集发布则倾向于给 VOC 格式。常见做法是先把三份标签在项目目录里分开存好例如Annotations放 VOC XMLlabels_coco放 COCO JSONlabels_yolo放 YOLO TXT。图像保持同一份只让标签目录各自和图像目录建立对应关系。这个结构在后续做数据增强、模型对比实验、接入不同训练框架时能省一大笔转换时间。2.2 三种标注格式的坐标体系与关键字段对比三种格式的本质差异只有一个边界框的坐标表达方式。VOC 格式是 XML 文件核心字段是bndbox下的xmin、ymin、xmax、ymax使用图像像素坐标左上角为原点。COCO 格式是 JSON 文件annotations 里每一条记录包含bbox: [x, y, width, height]同样是像素坐标但和 VOC 不同记的是左上角点和宽高。YOLO 格式是 TXT 文件每行一个目标class_id x_center y_center width height四个数值全部对图像宽高归一化到 0~1。这三套体系之间互转最核心的一步是拿到每张图片的真实宽高。标注阶段如果已经 resize 过图片转换时必须以“标注时用的原始图像尺寸”为准否则转出来的框全偏。参数对照表如下值得贴到你的标注规范里格式存储形态坐标含义是否归一化文件名对应VOCXMLxmin, ymin, xmax, ymax否与图片同名扩展名 .xmlCOCOJSONx, y, width, height否根目录一个 annotations JSON内含 image_id 关联YOLOTXTclass, x_center, y_center, width, height是与图片同名扩展名 .txt2.3 用 Python 把 YOLO 标签转成 VOC 标签我一般会在做训练前写一个最小转换脚本把现有标签统一成自己习惯的中间格式。最常见的场景是把 YOLO 格式转成 VOC因为 LabelImg 等工具体验好回看错误标注方便。下面的脚本基于 XML 生成最简 VOC 文件并保留 category 映射。import os import cv2 import xml.etree.ElementTree as ET from xml.dom import minidom IMG_DIR images # 原始图片目录 YOLO_DIR labels_yolo # YOLO txt 标签目录 OUT_DIR Annotations # 输出的 VOC xml 目录 CLASSES [RBC, WBC, Platelet] # 按训练时类别顺序 def yolo2voc(txt_path, img_w, img_h): boxes [] with open(txt_path, r, encodingutf-8) as f: for line in f.readlines(): line line.strip() if not line: continue parts line.split() cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) xmin int((cx - bw / 2) * img_w) ymin int((cy - bh / 2) * img_h) xmax int((cx bw / 2) * img_w) ymax int((cy bh / 2) * img_h) boxes.append((cls_id, xmin, ymin, xmax, ymax)) return boxes def make_voc_xml(img_name, img_w, img_h, boxes): root ET.Element(annotation) ET.SubElement(root, filename).text img_name size ET.SubElement(root, size) ET.SubElement(size, width).text str(img_w) ET.SubElement(size, height).text str(img_h) ET.SubElement(size, depth).text 3 for cls_id, xmin, ymin, xmax, ymax in boxes: obj ET.SubElement(root, object) ET.SubElement(obj, name).text CLASSES[cls_id] bnd ET.SubElement(obj, bndbox) ET.SubElement(bnd, xmin).text str(xmin) ET.SubElement(bnd, ymin).text str(ymin) ET.SubElement(bnd, xmax).text str(xmax) ET.SubElement(bnd, ymax).text str(ymax) return ET.tostring(root, encodingunicode) os.makedirs(OUT_DIR, exist_okTrue) for img_name in os.listdir(IMG_DIR): if not img_name.lower().endswith((.jpg, .jpeg, .png)): continue stem os.path.splitext(img_name)[0] txt_path os.path.join(YOLO_DIR, stem .txt) if not os.path.exists(txt_path): # 图片无对应标签直接跳过 continue img cv2.imread(os.path.join(IMG_DIR, img_name)) img_h, img_w img.shape[:2] boxes yolo2voc(txt_path, img_w, img_h) xml_str make_voc_xml(img_name, img_w, img_h, boxes) with open(os.path.join(OUT_DIR, stem .xml), w, encodingutf-8) as f: f.write(minidom.parseString(xml_str).toprettyxml(indent )) print(转换完成输出目录:, OUT_DIR)逻辑说明YOLO 的cx cy bw bh都是 0~1 归一化值转回像素坐标先算出左上角xmin (cx - bw/2) * img_w再算右下角xmax (cx bw/2) * img_w。读取图片尺寸用 OpenCV避免去解析标注文件里可能缺失的宽高数据。类别顺序必须和未来训练用的data.yaml完全一致否则转过去的类别名对不上。参数说明IMG_DIR、YOLO_DIR、OUT_DIR三个路径按实际目录结构调整CLASSES列表的顺序就是训练时类别 ID 的顺序。这个脚本只处理单层目录如果你的图像按 train/val/test 分子目录存放需要先拼接出完整图片路径。实际跑之前先抽一张图验证 XML 里xmax没超出图片宽度。2.4 反方向转换VOC 转 COCO 时最容易错的 image_id 关联从 VOC 转 COCO 时最容易翻车的不是坐标计算而是 JSON 里的image_id和annotation的关联关系。COCO 要求每张图片有一个全局唯一的 id每个标注框属于某张图片还必须带上category_id而且categories数组里的id可以不是连续的但训练代码会按这个 id 来映射类别。常见做法是先扫描所有 XML 给每张图片分配递增 id再遍历每个 XML 里的目标生成 annotations。如果你只在本地训练而不需要提交到 COCO eval可以把 category 的 id 设为 0、1、2 这样连续值省得后面还要改。但如果你要拿 COCO 的cocoEval做评测注意 COCO 官方实现里 background 类别 id 通常不占 0需要你设置成从 1 开始。很多血细胞模型在原数据集上 mAP 高、换到 COCO 评测就崩就是 category_id 没对齐。3. 划分脚本怎么用图片与标签的绑定和分层划分是关键3.1 划分前先检查文件名绑定关系数据集里最常见的隐患是图片有 1000 张标签却少了几十个。很多人拿到数据集后直接跑网上的划分脚本训练到一半发现验证集和训练集重叠或者某些图片根本没有标签文件。如果标题里这套数据集自带划分脚本第一步应该是先核对文件名映射。我习惯在划分前跑一个绑定检查脚本把图片文件名、对应标签文件名、标注框数量一次性拉出来看差异。import os IMG_DIR images YOLO_DIR labels_yolo imgs sorted(os.listdir(IMG_DIR)) txts sorted(os.listdir(YOLO_DIR)) img_stems set(os.path.splitext(f)[0] for f in imgs) txt_stems set(os.path.splitext(f)[0] for f in txts) missing_txt img_stems - txt_stems missing_img txt_stems - img_stems print(图片总数:, len(imgs)) print(标签总数:, len(txts)) print(有图无标签:, len(missing_txt), missing_txt if len(missing_txt) 10 else ) print(有标签无图:, len(missing_img)) zero_box_txts [] for t in txts: path os.path.join(YOLO_DIR, t) with open(path, r, encodingutf-8) as f: if len(f.read().strip()) 0: zero_box_txts.append(t) print(空标签文件数量:, len(zero_box_txts))逻辑说明前三组差集先检查文件层面的对应关系。空标签文件容易被忽略它们在训练时不会报错但会输出大量空白梯度对模型收敛影响极小却会在数据划分时埋下隐患。如果zero_box_txts数量不少我建议直接把这些图片从划分列表里剔除而不是保留空标签。参数说明检查结果会直接决定后面划分脚本的策略。如果丢失的标签超过 5%先补标注或者删图片不要硬着头皮训如果只有个位数可以把它们合并进验证集因为验证集不需要反向传播少量未标注背景对评估影响不大。3.2 分层划分血细胞数据集的典型类别不平衡问题血涂片里白细胞数量远少于红细胞和血小板1000 张图里可能只有 200 张图含有白细胞。如果划分脚本用纯随机划分运气不好时验证集里白细胞样本几乎为 0训练时模型对白细胞的召回率看起来还行一换数据集就翻车。这个时候应该用分层划分让训练集和验证集都保持相近的类别分布比例。划分逻辑参考如下脚本它按“图片类别分布向量”做分层抽样用哈希映射而不是随机数保证多次运行结果一致。import hashlib import os import shutil from collections import Counter IMG_DIR images YOLO_DIR labels_yolo OUT_TRAIN split/train OUT_VAL split/val OUT_TEST split/test VAL_RATIO 0.2 # 验证集比例 TEST_RATIO 0.1 # 测试集比例 def image_class_vector(yolo_path): class_counts Counter() with open(yolo_path, r, encodingutf-8) as f: for line in f: line line.strip() if line: class_counts[int(line.split()[0])] 1 return tuple(class_counts[i] for i in range(3)) # 3 个类别 samples [] # [(img_name, class_vec), ...] for img_name in sorted(os.listdir(IMG_DIR)): stem os.path.splitext(img_name)[0] txt_path os.path.join(YOLO_DIR, stem .txt) if not os.path.exists(txt_path): continue vec image_class_vector(txt_path) if sum(vec) 0: continue samples.append((img_name, vec)) # 按类别分布向量做稳定 hash近似完成分层划分 train_bucket, val_bucket, test_bucket [], [], [] for img_name, vec in samples: h int(hashlib.md5((img_name - str(vec)).encode()).hexdigest(), 16) r h % 100 if r VAL_RATIO * 100: val_bucket.append(img_name) elif r (VAL_RATIO TEST_RATIO) * 100: test_bucket.append(img_name) else: train_bucket.append(img_name) for bucket, out_dir, label in [ (train_bucket, OUT_TRAIN, train), (val_bucket, OUT_VAL, val), (test_bucket, OUT_TEST, test), ]: os.makedirs(os.path.join(out_dir, images), exist_okTrue) os.makedirs(os.path.join(out_dir, labels), exist_okTrue) for img_name in bucket: stem os.path.splitext(img_name)[0] shutil.copy( os.path.join(IMG_DIR, img_name), os.path.join(out_dir, images, img_name), ) shutil.copy( os.path.join(YOLO_DIR, stem .txt), os.path.join(out_dir, labels, stem .txt), ) print(f{label} 数量: {len(bucket)})逻辑说明image_class_vector把一张图片的类别计数转成向量做哈希时向量参与计算这样每张图片落入哪个集合由它所属的类别分布直接决定而不是纯独立随机。如果某张图只有一个白细胞、两张图各有十个血小板它们会因为向量不同而被分到不同桶里从而让集合之间的类别比例更接近。参数说明VAL_RATIO和TEST_RATIO按项目需求调整医学图像检测我建议测试集留 10%~20%因为最终评估要覆盖不同涂片染色差异。哈希划分的优点是结果可复现不需要设随机种子缺点是无法保证精确比例如果你想严格控制每个类别在每个集合的数量需要改成按类别逐类抽样的脚本但血细胞检测场景里分布近似已经足够。3.3 数据增强是 1000 张图的基础上限所在1000 张显微镜图像本质是小样本训练。模型能不能收敛一半看数据增强有没有把空间换起来。显微镜血涂片图像和自然图像有差异染色剂的色偏、镜头边缘暗角、目标尺度变化大都决定了增强策略不能直接照搬 YOLO 默认值。我给血细胞图片常用的增强组合是mosaic 增强开启因为能模拟多个视野拼接HSV 色域增强时把 saturation 和 hue 范围调小避免改变红细胞颜色让人眼都难分辨水平翻转和垂直翻转同时开启因为细胞方向没有语义旋转角度微调 15 度以内超过 45 度会导致多目标重叠严重标签重叠反而让模型困惑。YOLOv8 的增强参数在训练命令里直接控制例如hsv_h0.015 hsv_s0.5 hsv_v0.4 degrees10 fliplr0.5 flipud0.5。刚开始不要一条命令把增强拉满先在默认参数上训练一轮观察 loss 曲线如果过拟合迹象明显再逐步上调 degrees 和 fliplr。翻车案例里最多的是为了防过拟合把 mosaic 开到最大结果小目标血小板在拼接边界被切碎损失函数不降反升。4. 训练教程拆解从 Anaconda 环境到 YOLOv8 训练自己的血细胞模型4.1 环境配置yolo v8 anaconda 环境配置要求很多做过 YOLOv8 训练的人会告诉你环境配置其实是整个流程里最玄学的一步。血细胞检测这类项目通常不需要多机多卡一个 conda 环境就够用。创建环境并安装核心依赖的命令如下conda create -n yolo_env python3.10 -y conda activate yolo_env pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics逻辑说明PyTorch 与 CUDA 的版本匹配是环境配置里最容易踩的坑。--index-url指定 CUDA 11.8 对应的 PyTorch 轮子包地址如果你的显卡驱动只支持 CUDA 12把cu118换成cu121或cu124。装完后在 Python 里执行import torch; torch.cuda.is_available()返回 True 再继续这一步可以省掉后面所有内存报错类的排查。参数说明Python 版本选 3.10 是因为该版本与当前主流 ultralytics 包兼容性最好3.12 也能跑但会遇到个别依赖编译报错。ultralytics这个包已经内含 YOLOv8 训练、验证、导出的全部功能不需要再手动安装yolo命令行工具。4.2 数据集组织data.yaml 里写了什么训练前有个容易被忽略的细节无论标签是 VOC、COCO 还是 YOLO 格式ultralytics 框架要求的目录结构都固定是images/train、images/val、labels/train、labels/val且 labels 目录下必须是 YOLO 格式的 TXT。也就是说如果你手里是 VOC 或 COCO 标签得先转换成 YOLO 格式再组织目录。data.yaml是训练时唯一需要你手写的数据配置文件path: /home/user/blood_cell_dataset train: images/train val: images/val test: images/test names: 0: RBC 1: WBC 2: Platelet逻辑说明path建议写绝对路径因为训练脚本的工作目录可能和数据集不在同一层级相对路径最容易出现“找不到图片”的报错。train和val指向的是相对path的目录路径框架会自动拼接成/home/user/blood_cell_dataset/images/train。names的键值对必须和 YOLO 标签里每行的第一个数字对应顺序错了模型类别名就全乱了。参数说明test字段可以留空最后评估时单独指定names里类别名称不要用中文某些依赖 matplotlib 绘图的组件对中文编码支持不好会在画混淆矩阵时报错。4.3 训练命令与关键参数imgsz、batch、epochs 怎么定训练命令是这一步的核心参数本身不复杂难点在于知道每个参数对血细胞检测项目的具体影响。yolo detect train \ datablood_cell.yaml \ modelyolov8n.pt \ imgsz640 \ batch16 \ epochs100 \ patience20 \ hsv_h0.015 hsv_s0.5 hsv_v0.4 degrees10 fliplr0.5 \ projectblood_cell_runs \ nameexp1 \ seed42逻辑说明第一行指定数据和模型。yolov8n.pt是 nano 版本预训练权重参数量最小血细胞检测只需要区分三个类别目标本身形态特征明显nano 足够不必一上来就用 large。imgsz640是训练时缩放边长血涂片图像里血小板可能只有十几个像素如果显存允许imgsz1280会对小目标检测有明显提升但训练时间和显存占用大约翻四倍。patience20是早停参数连续 20 个 epoch val loss 不下降就停止训练防止无效等待。参数说明batch16要根据显存调整如果训练时报 CUDA out of memory先减半到 8再不行就 4。seed42固定随机种子保证多次训练结果可对比。这类三类别检测任务训练过程中最值得盯的是cls_loss因为白细胞类别数量少损失值下降曲线会比box_loss慢这个现象是正常的负数不要误以为模型没有在学习。4.4 训练后看什么损失函数曲线与置信度门限调整训练完成后项目目录下会生成results.png、weights/best.pt和weights/last.pt。best.pt不是最后一次权重而是验证集上综合指标最好的一版后续推理和部署全部用best.pt。损失函数方面YOLOv8 的损失由三部分组成box_loss边界框回归损失、cls_loss类别分类损失、dfl_loss分布焦点损失。血细胞检测里dfl_loss通常降得最快因为它处理的是边框的精细回归红细胞这种边界模糊的目标会让它比自然图像数据集下降得更慢。如果看到dfl_loss曲线在 60 个 epoch 后开始上涨说明模型过拟合到训练集的细胞边界纹理上了此时应该提前停止或者加大增强。置信度门限在实际使用中比训练超参更影响感受。训练默认置信度是 0.25但血细胞检测中白细胞类别少、模型对它信心不足时会被过滤掉。推理时用conf0.1可以找回一部分漏检代价是背景误检增多conf0.5则过滤掉几乎所有背景脏点但血小板这类小目标可能大量丢失。这个参数没有标准答案我的惯例是先在验证集上跑几个档位0.15、0.25、0.4 各跑一遍对比混淆矩阵再决定。5. 血细胞检测最容易踩的 6 个坑小样本、标注和评估陷阱5.1 现象训练集 mAP 很高验证集却全碎框原因数据划分时图片与标签没有绑定或者划分前没有去重导致同一张图既出现在训练集又出现在验证集。显微镜图像连续视野之间非常相似如果复制粘贴过时更容易造成数据泄漏。解决重新用第三章节的绑定检查脚本核对images与labels是否一一对应确认划分脚本没有把同一 basename 分配到两个集合。泄漏严重时只能重新划分重训没有后悔药。5.2 现象血小板目标一个都检测不出来验证集上漏检率极高原因血小板在 640×640 分辨率下往往只有 10~20 像素属于小目标。YOLOv8 默认下采样 32 倍小目标在深层特征图里只剩一两个像素特征信息基本丢失。解决优先把imgsz提到 1280 重训一版其次开启safer参数提升小目标聚焦能力再者检查增强配置里scale0.5是否把血小板缩得比原图更小。这三步做完仍不行考虑用 YOLO 的 P2 层定制模型但工程成本较高一般临床场景下 1280 分辨率已经够用。5.3 现象白细胞和红细胞重叠区域被模型重复框出一个目标两个框原因标注阶段标框边缘没有对齐相邻细胞边界粘连导致标签本身有重叠或者训练时nms_iou阈值太低重叠程度较高的两个预测框没有被合并。解决先检查原始 YOLO 标注框的 IoU 分布如果大量框之间有超过 30% 的重叠大概率是标注质量问题需要重新检查部分标签。训练侧在验证时设置iou0.45到0.6之间测试选择漏检最少的档位。5.4 现象训练损失正常收敛但推理时背景区域出现大量误检原因1000 张图里纯背景样本不足模型没有见过足够的空气区域或者推理时conf门限设置过低背景里的染色杂质被当成血小板识别。解决在测试集中抽一批不含目标的背景图将其标注为空标签并加入训练集让模型学习负样本。推理侧的临时止血是调高conf到 0.4 以上但根本解法还是补充负样本。5.5 现象训练过程中内存不断增长最终进程被系统杀掉原因batch值过高workers数设置过大加上数据加载线程在预处理图像时占满内存显微镜原图如果分辨率特别大在imgsz640下依然需要先加载原图再缩放瞬间内存峰值很高。解决查看原图尺寸如果普遍超过 2000×2000先离线缩放保存一份训练用副本再做增强。训练参数上把workers4 batch8并限制cacheFalse避免一次性缓存全部增强图。5.6 现象训练结果在本地测试集表现不错换一台电脑推理就明显变差原因图像预处理差异。显微镜设备更换后图像颜色分布变化明显染色制片标准化不严格时尤其严重。YOLO 训练时的色彩增强幅度如果太小模型对颜色偏移完全没有鲁棒性。解决训练时把hsv_h提升到 0.02、hsv_s提升到 0.7模拟不同染色剂带来的色偏。跨设备验证时先用灰度化测试一版如果灰度图仍能保持 80% 以上的精度说明模型学的是形态特征泛化能力基本可靠。6. 验证与部署收尾不看 mAP 看混淆矩阵导出 ONNX 上边缘设备训练结束后的第一件事不是看 mAP而是生成测试集上的混淆矩阵。mAP 是一个聚合指标白细胞样本少时即使全检错mAP 也可能很高因为它被 RBC 这个大类拉起来了。混淆矩阵能按行看出每个类别的漏检和误检模式如果 WBC 一行的对角线值低说明白细胞被系统性漏检就该回到数据增强和损失函数权重上找问题如果某两列之间有稳定误检比如 Platelet 被分到 RBC那就得检查标注的尺寸分布和形态特征。验证命令本身很简单yolo detect val \ datablood_cell.yaml \ modelruns/exp1/weights/best.pt \ conf0.25 \ iou0.5这个命令会在测试集上重新推理一遍输出 mAP50、mAP50-95 和混淆矩阵图。跑完后重点看两项mAP50-95 是否小于 mAP50 的八成差距过大表示边界框定位精度不够通常是因为原图分辨率太低或者标注框边缘不准确。部署到边缘设备时血细胞检测常见的平台是 RK3588 这类带有 NPU 的板卡。流程是先导出 ONNX再转成对应 NPU 的格式。导出命令如下yolo export modelruns/exp1/weights/best.pt formatonnx opset12 simplifyTrue导出的 ONNX 文件如果要在 RK3588 上跑还需要再做一次 RKNN 格式转换和量化。量化时优先选择i8混合量化1000 张图里挑选约 200 张代表不同染色模式的图片做校准集。直接训练用的图片做校准会得到偏乐观的量化精度最好留一批没参与训练的图。量化后验证一次 mAP 和单帧耗时如果 mAP 掉幅超过 5%可以保留fp16精度的 RKNN 模型代价是推理时间多 30%~50%。边缘设备上的conf门限建议比服务器端调高 0.05 到 0.1因为 NPU 量化后的特征图数值浮动会让低置信度框更不稳定。这是我做血细胞检测项目的一贯收尾流程训练完提取混淆矩阵、导出 ONNX、量化到边缘设备上一轮一轮验证。跑了几个类似项目后我越来越认同一个判断1000 张图的医学图像数据集制约模型上限的不是模型结构而是数据划分的严谨程度和增强策略的克制程度。这套方法在血细胞方向验证过在类似的颗粒物检测、显微图像分析项目里也完全可以直接复现希望帮到你。本文还有配套的精品资源点击获取