ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从VOC到YOLO:细胞活性检测数据集训练与格式转换实战

从VOC到YOLO:细胞活性检测数据集训练与格式转换实战 简介细胞活性检测数据集面向计算机视觉目标检测任务收录了一千二百九十八张真实细胞图像每张图像均带Pascal VOC格式的xml标注与YOLO格式的txt标注标签类别为死细胞和活细胞两种适用于医学图像中细胞状态识别、活性评估等研究场景也可作为目标检测算法验证的公共数据集。所有标注由labelImg工具按矩形框规则绘制总计三万一千三百三十二个真实标注框其中死细胞框两万四千三百一十五个、活细胞框七千零一十七个标注过程严格统一保证框位置准确合理数据集不附带模型精度承诺只提供可靠标注便于使用者专注算法开发。压缩包共两千个文件主要包含一千二百九十八个xml标注文件与七百零二个txt标注文件整体大小约三十二点四五兆字节文件按统一编号组织图像与标注一一对应解压后可直接用于YOLO、SSD、Faster R-CNN等主流框架训练。目前已有约一百九十七人学习/浏览适合计算机视觉初学者、生物医学图像分析工程师及相关专业学生借助该数据集可快速熟悉检测标注格式转换、训练参数调整与模型评估流程为细胞活性检测项目提供完整的数据基础。1. 细胞活性检测数据集1298张图能撑起什么样的活细胞检测项目拿到“细胞活性检测数据集1298张2类标签VOCYOLO格式.zip”这个名字第一反应应该是这包数据是给我训练目标检测模型的现成弹药不用再从培养皿照片里徒手画框了。细胞活性检测在生物实验室里通常意味着区分活细胞和死细胞人工数数又慢又容易看花眼用目标检测模型批量数、按时数才是自动化的价值。1298张图像、2个类别标签、VOC和YOLO双格式——这意味着无论你用 ultralytics 那套 yolo 命令还是老一点的 Faster R-CNN 训练流程都能直接喂进去。但反直觉的是这1298张如果处理不好踩格式坑的时间比省下的标注时间还长。2. 先拆开数据包两个类别是什么、VOC和YOLO标签差在哪2.1 打开压缩包先确认类别定义活/死这两类怎么对齐别急着训练。第一步永远是解压后找类别定义文件常见的是 classes.txt、labels.txt 或 .names 结尾的 Darknet 文件。细胞活性检测里的“2类”最典型的是活细胞与死细胞台盼蓝染色后活细胞不着色、死细胞呈蓝色也可能是正常态与凋亡态、或者某种特定形态的两类。名字具体叫什么以压缩包内文件为准。用一条命令先把所有 XML 里的类别名统计出来比手工翻文件快得多find . -name *.xml | head -50 | xargs -I {} grep -o name.*/name {} | sed s/[^]*//g | sort | uniq -c逻辑说明先用 find 定位到前 50 个 XML再逐个抓出name标签的内容最后用 sort、uniq -c 做频次统计。参数说明head -50控制了只检查 50 个文件你要是想扫全量把它删掉直接跑。这一步的目的是搞清楚类 0、类 1 到底对应哪个字符串因为后文 data.yaml 里的 names 顺序必须和这里一致。顺序搞反模型会把活细胞识别成死细胞后续所有指标都是假象。拿到类别名之后再确认图片与标签的数量是否对得上find . -name *.xml | wc -l find . -name *.jpg -o -name *.png -o -name *.tif | wc -l数量差太多说明有孤儿标注或缺失标注。1298 张图的体量不算大这一步几十秒就能做完别省。2.2 VOC格式的XML绝对像素坐标和结构VOC 格式的核心是一个 XML 文件对应一张图片。里面几个关键字段filename写原图文件名size给出图像的宽、高和通道数object里放每个目标的类别名和bndbox边界框。边界框坐标是绝对像素值xmin/ymin是左上角xmax/ymax是右下角。一段典型的 VOC XML 结构长这样annotation filenamecell_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namelive/name bndbox xmin120/xmin ymin80/ymin xmax180/xmax ymax140/ymax /bndbox /object /annotation逻辑说明这个annotation根节点下挂了两块信息一块描述图像本身一块描述目标。size里的 width、height 是归一化时的基准也是后面转换脚本里最容易出错的地方。参数说明name是类别字符串和你之前统计的结果必须一致bndbox四个值都是像素不是比例。VOC 格式的优点是人眼可读、方便用 labelImg 等工具二次编辑缺点是文件数量多而且绝大多数训练框架不直接吃 XML需要你自己写解析器。如果你打算走 ultralytics YOLO 路线最终大概率要转成 txt。2.3 YOLO格式的txt归一化坐标YOLO 格式把每个目标写成一行五列第一列是类别编号整数后面四列分别是中心点 x、中心点 y、宽度 w、高度 h。关键区别是这四个值都除以了图像的宽高归一化到 0~1 之间没有绝对像素概念。一个 1920x1080 的图像里坐标换算出来大概是0 0.489583 0.506481 0.062500 0.041667 1 0.510417 0.703704 0.055208 0.033333逻辑说明第一行的类 0 目标中心点 x 乘 1920 约等于 940中心点 y 乘 1080 约等于 547宽高乘以对应边的像素数就是实际框的大小。参数说明类别编号必须和类别定义文件里的顺序严格对应比如 classes.txt 第 0 行是 live那么这里出现的 0 就代表 live。相比 VOCYOLO 格式文件体积小、读取快、和 ultralytics 等框架天然兼容。缺点是数字不直观错了很难一眼看出来必须靠可视化验证才能发现坐标偏移。2.4 首检清单图片-标签配对和可疑标注解压完先跑一遍统计脚本确认类别分布和标签完整性。我一般会看三件事每个类别的实例数量、有没有空标签文件、有没有 txt 找不到对应图片。import os from collections import Counter txt_dir labels # 按实际路径改 img_dir images # 按实际路径改 cnt Counter() bad_files [] for fn in os.listdir(txt_dir): if not fn.endswith(.txt): continue path os.path.join(txt_dir, fn) with open(path) as f: lines [line for line in f.readlines() if line.strip()] for line in lines: cls int(line.split()[0]) cnt[cls] 1 base os.path.splitext(fn)[0] # 常见扩展名轮询一遍注意 tif 也要照顾到 found any( os.path.exists(os.path.join(img_dir, base ext)) for ext in (.jpg, .jpeg, .png, .bmp, .tif) ) if not found: bad_files.append(fn) print(类别分布:, dict(cnt)) print(缺少对应图片的txt数量:, len(bad_files)) print(样例:, bad_files[:10])逻辑说明脚本遍历 labels 目录统计每个类别的目标数同时检查同名图片是否存在。参数说明扩展名列表里我加了 tif细胞显微图像经常用 tiff 保存只看 jpg 会误报大量缺失。如果类别分布严重偏向某一个类先记下来第 5 章会讲怎么处理。3. 用Ultralytics YOLO训练细胞检测目录、yaml与参数3.1 最省事的目录结构ultralytics 的 YOLO 训练默认按 images 和 labels 两个顶层目录组织数据。images 放图片labels 放同名 txt两者各自再分 train、val、test 子目录。如果压缩包里已经整理好的嘲笑直接跳过这步但如果是散装的 VOC 结构需要先手动建目录mkdir -p data_cell/{images/{train,val,test},labels/{train,val,test}}逻辑说明这一个命令同时创建了 images 和 labels 下的训练集、验证集、测试集目录是给后续数据划分打底。参数说明目录名字不要随便改ultralytics 在 yaml 里引用的是相对路径train、val 这种命名是约定俗成改了容易出错。划分数据时要把同视野、同一培养皿的多张图尽量放到同一个集合里。最省事的做法是按文件名随机分但对于细胞数据同一个培养皿的连续帧高度相似随机分会让验证集虚高。我的做法是先用文件名前缀做分组再按组划分。import os, random, shutil files [f[:-4] for f in os.listdir(labels) if f.endswith(.txt)] random.seed(42) random.shuffle(files) n_train int(len(files) * 0.7) n_val int(len(files) * 0.2) for i, base in enumerate(files): img_candidates [fimages/{base}{ext} for ext in (.jpg, .jpeg, .png, .tif)] img_path next((p for p in img_candidates if os.path.exists(p)), None) lb_path flabels/{base}.txt if img_path is None or not os.path.exists(lb_path): continue if i n_train: target data_cell/images/train target_lb data_cell/labels/train elif i n_train n_val: target data_cell/images/val target_lb data_cell/labels/val else: target data_cell/images/test target_lb data_cell/labels/test shutil.copy(img_path, target) shutil.copy(lb_path, target_lb)逻辑说明先把所有 txt 文件名读进来按 7:2:1 随机分到 train、val、test。代码里特意用了候选扩展名轮询避免因为图片是 .tif 就漏掉。参数说明random.seed(42)保证每次划分结果可复现方便你在后续调参时对比同样数据下的模型表现。3.2 data.yaml配置与names顺序训练前要写一个 data.yaml。这个文件告诉 ultralytics 数据在哪、类别有几类、每类叫什么path: /absolute/path/to/data_cell train: images/train val: images/val test: images/test names: 0: live 1: dead逻辑说明path是数据集的绝对路径train/val/test 都是相对 path 的相对路径。names 里的键值对定义了类别编号和类别名的映射。参数说明names 的顺序必须和 txt 里的类别编号完全一致。如果你的 classes.txt 里类 0 是 dead、类 1 是 live那 yaml 里也要写成0: dead, 1: live不能按照自己的习惯乱排。改 yaml 只影响显示名字不会改变 txt 里的数字含义。3.3 训练命令、关键参数与损失函数的读法环境配置按常规 ultralytics 流程走先装匹配 CUDA 版本的 PyTorch再pip install ultralytics。如果显卡驱动和 PyTorch 版本对不上训练时大概率会静默落到 CPU 上一个 epoch 跑到天荒地老。训练命令yolo train modelyolo11n.pt datadata_cell.yaml epochs80 imgsz640 batch16 patience15 device0逻辑说明modelyolo11n.pt是加载预训练权重初始化数据量小的时候从 COCO 预训练权重起步比随机初始化收敛快得多。data指向刚写好的 yaml 文件。参数说明epochs80是最大训练轮数imgsz640是输入分辨率batch16是按显存调整的patience15表示连续 15 个 epoch 验证集指标没提升就提前停止。device0指定第一块 GPUCPU 训练把这里改成devicecpu但速度会慢几十倍。训练时日志里会出现 box_loss、cls_loss、dfl_loss 这三条损失曲线。box_loss 是边界框回归误差cls_loss 是分类误差dfl_loss 是分布焦点损失负责边界框边缘质量。如果你看到 cls_loss 明显下降但 box_loss 长期横盘说明类别学得差不多了但框总是套不准细胞边界。这种情况一般不是参数问题而是标注本身的框太粗糙回头检查标注比继续调参更有效。有人问我要不要换 transformer 目标检测在 1298 张这种体量下我的结论是别换YOLO 系列在中小规模数据集上的稳定性好得多。3.4 细胞显微图的增强设置ultralytics 默认开启了一批数据增强但对细胞染色图要收紧。细胞图像大多来自固定染色方案背景颜色和细胞形态有强规律增强过猛反而破坏特征。训练时按需覆盖默认值yolo train modelyolo11n.pt datadata_cell.yaml epochs80 imgsz640 batch16 patience15 device0 hsv_h0.01 hsv_s0.2 hsv_v0.2 flipud0.0逻辑说明hsv_h0.01把色调扰动压到很低避免染色颜色失真hsv_s0.2和hsv_v0.2允许适度的饱和度和亮度变化模拟不同曝光下的成像差异flipud0.0关掉上下翻转。细胞在培养皿里本来有重力方向上下翻转可能把沉积方向反转让模型学到错误先验。参数说明这些值不是万能如果你的图像来源复杂、拍摄角度分散flipud0.5也可以接受。另外注意细胞相对整个视野通常属于中小目标。如果 640 分辨率下细胞只有十几个像素考虑把 imgsz 提到 960 或 1280或者做切图训练。提升 imgsz 会成倍增加显存占用batch 要跟着降。4. 手工转换VOC到YOLO格式脚本、归一化和边界坑4.1 从XML转TXT一个能直接跑的转换脚本即使压缩包里已经有 YOLO 格式我也常建议把 VOC 当源头自己再转一遍。原因很简单转换过程顺带做一次标注清洗把越界框、倒置坐标、空目标全部过滤掉比直接拿现成 txt 盲训要稳。import xml.etree.ElementTree as ET import os classes {live: 0, dead: 1} # 以实际 classes.txt 为准 def convert_xml(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) if size is None: return False width int(size.find(width).text) height int(size.find(height).text) if width 0 or height 0: return False lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in classes: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 坐标倒置时自动交换这是很多标注工具导出的常见病 if xmax xmin: xmin, xmax xmax, xmin if ymax ymin: ymin, ymax ymax, ymin x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height # 防止归一化后出现负数或大于1 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) box_w max(0.0, min(1.0, box_w)) box_h max(0.0, min(1.0, box_h)) if box_w 1e-6 or box_h 1e-6: continue lines.append(f{classes[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) if not lines: return False base os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base .txt), w) as f: f.write(\n.join(lines)) return True逻辑说明先读 XML 的 size 拿到真实宽高再遍历每个 object。坐标从绝对像素值转成归一化中心点和宽高最后写一行一条 txt。参数说明classes字典的映射关系来自第一步统计的类别定义如果 XML 里出现字典外的类别名会被直接跳过这样能过滤掉一些脏标注。clip操作把越界值拉回 0~1是为了防止后续训练出现 NaN。4.2 转换中的边界坑转换脚本看着简单实际跑起来有几个高频坑。第一个是 XML 里 xmin 比 xmax 大、ymin 比 ymax 大。这常见于从某些标注工具或半自动标注流程导出的数据原因一言难尽处理方式就是脚本里的交换逻辑先把左右互换、上下互换再做计算。第二个是 size 字段缺失或宽高为 0。有些标注工具导出时没写 size脚本会直接 return False说明这个 XML 本身不完整。此时需要用 PIL 打开原图补读尺寸from PIL import Image img Image.open(img_path) width, height img.size逻辑说明当 XML 里的 size 不可信时直接从原图拿真实宽高。参数说明这一步要保证 img_path 和 XML 的 filename 对应否则宽高错位转出来的框全部变形。第三个坑是类别编号错位。VOC 的 name 是字符串YOLO 是整数映射关系错一位整个数据集的语义就全乱了。解决方式是把 2.1 那步统计出来的类别名和 classes 字典核对一遍再决定是改字典还是改 txt。第四个坑是归一化出来的宽高为负数。如果坐标本来就倒置且没有交换w 和 h 就会是负值训练时直接报错或 loss 异常。脚本里的 clip 和微小框过滤就是兜底。4.3 反向转换YOLO转VOC在什么时候用得上反向转换的场景少一些但如果拿到的是 txt 格式而你要用只支持 VOC 的老框架或工具就得转回去。核心逻辑是把归一化值还原成像素坐标w, h 1920, 1080 # 必须是这张图的实际宽高 cx, cy, bw, bh map(float, txt_line.split()[1:]) xmin int((cx - bw / 2) * w) ymin int((cy - bh / 2) * h) xmax int((cx bw / 2) * w) ymax int((cy bh / 2) * h)逻辑说明先还原中心点的像素位置再根据宽高算出左上角和右下角。参数说明w、h 必须是原图的真实尺寸不能用训练时 resize 后的尺寸。如果你在训练时用了 letterbox 或 pad必须用原始图像的宽高去还原否则框会整体偏移。4.4 转换完成后必须做的可视化验证这一节是我个人的血泪经验强烈建议不要跳过。转完格式后程序上看都是合法数字但框到底贴不贴细胞只有画出来看了才知道。import cv2 import os img_dir images txt_dir output_labels draw_dir visualize os.makedirs(draw_dir, exist_okTrue) for fn in os.listdir(txt_dir): if not fn.endswith(.txt): continue base os.path.splitext(fn)[0] img_path os.path.join(img_dir, base .jpg) if not os.path.exists(img_path): continue img cv2.imread(img_path) height, width img.shape[:2] with open(os.path.join(txt_dir, fn)) as f: for line in f.readlines(): parts line.split() if len(parts) 5: continue cls int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) x1 int((cx - bw / 2) * width) y1 int((cy - bh / 2) * height) x2 int((cx bw / 2) * width) y2 int((cy bh / 2) * height) color (0, 0, 255) if cls 1 else (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(cls), (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(os.path.join(draw_dir, base .jpg), img)逻辑说明把 txt 里的归一化坐标还原成像素框画到原图上输出到 visualize 目录。参数说明颜色区分了类 0 和类 1出现大量红色框套在绿色细胞上就是类别编号映射错了框明显超出图像边界就是宽高读错了。随机挑 30 张画完扫一眼转换脚本的问题基本都能暴露。5. 细胞检测训练翻车避坑记5个高频问题与处理步骤5.1 类别极度不平衡少数类被模型直接无视现象训练出来的模型对多数类检测得很积极对少数类几乎不报少数类 precision 和 recall 都很低。原因1298 张图里可能一类细胞占了 80% 以上的标注框另一类只在少数视野中出现。损失函数被多数类主导模型学到的是“全部预测成多数类也能拿到低损失”。解决先统计每个类别的实例数看看是不是超过 5:1。如果差距很大我一般会先对少数类做简单的复制增强把少的那类图片重复放进训练集或者用更强的仿射增强只作用在少数类样本上。1298 张的体量小复制不会明显拖慢训练。还有一种做法是给损失函数加类别权重ultralytics 里可以自己构造 weights 传给训练但操作上比复制繁琐效果未必更好。5.2 训练突然出现NaN loss断在中途现象前几个 epoch 正常到第十几个 epoch 时 loss 变成 nan训练中断或者一开始就是 nan。原因最常见的元凶是 txt 标签里混进了非法数字比如坐标值无穷大、负数、或者根本不是数字的字符串。另一个常见原因是学习率太高小数据集配合大 lr 很容易让训练发散。解决先用脚本检查所有 txt 文件找出非数字和明显超出 0~1 范围的行。确认标签没问题后把初始学习率调低到 lr00.001 或 0.0005yolo train modelyolo11n.pt datadata_cell.yaml epochs80 batch16 device0 lr00.001逻辑说明lr0 是初始学习率下调后训练过程会更平稳。参数说明如果标签和 lr 都没问题还出 nan检查一下是不是 batch 太小导致梯度方差过大batch4 时很容易震荡提到 8 或 16 再试。5.3 推理时把培养皿边缘、碎片误检成细胞现象模型在训练集上表现很好一到现场拍的培养皿照片就把皿边缘、气泡、碎片检测成一个类别。原因训练集里没有包含足够多的“负样本”图像。活/死细胞检测只有两个正类别背景没有被显式建模模型看到形状类似的边缘结构就会乱报。解决收集一些完全没有细胞目标或者只有皿底杂质的图像放到训练集里对应 txt 文件写成空文件。ultralytics 支持训练集中出现无目标的图片它们会被当作负样本参与训练。另外一个直接有效的手段是提高推理时的置信度阈值yolo predict modelruns/detect/train/weights/best.pt sourcetest_images conf0.5逻辑说明conf0.5把低于 0.5 置信度的检测全部过滤掉边缘碎片的置信度通常不高提阈值能有效压掉误检。参数说明阈值不是越高越好提到 0.6 以上可能会把一些小而模糊的细胞也滤掉需要按验证集表现来调。5.4 VOC转YOLO后框全部跑偏宽高明显不对现象按可视化脚本画出来的框要么整体偏移要么宽高比原标注大一圈看起来完全没有贴合细胞边缘。原因转换脚本里读的宽高不是图像实际分辨率。有的 XML 里 size 字段写了但和图片实际尺寸不一致有的项目随便拿了一个固定值做归一化转出来的 YOLO 格式在别的分辨率下就会错位。解决用 4.1 的脚本逻辑以 XML size 为准但如果发现 size 可疑直接用 PIL 读原图尺寸覆盖。更常见的是训练阶段 imgsz 改变了输入分辨率而你在反向转换时用错了宽高。记住一条原则归一化用的分母永远是原始图像的分辨率不是任何预处理后的分辨率。5.5 数据划分泄露验证集指标虚高实拍一塌糊涂现象训练时 mAP50 刷到很高验证曲线也很漂亮但拿到新培养皿上一测漏检严重感觉模型像没见过真实场景。原因同一视野的连续帧或同一培养皿的多张图被同时分到训练集和验证集。模型在训练时已经“背过”相似的图像纹理验证时自然表现好但换个培养皿就露馅。解决划分数据时按采集来源分组而不是完全随机。如果文件名里看不出分组信息至少把同一个前缀的图片放在同一个集合里。对 1298 张这个体量我的建议是留一组完整培养皿图像做测试让验证集和训练集完全不重叠。6. 收尾在验证与迁移让细胞活性模型从能跑到真能用6.1 先看四类指标别被单点mAP迷惑训练结束后ultralytics 会自动输出 mAP50、mAP50-95、precision、recall 四项结果。mAP50 衡量的是框与真值重叠度超过 50% 就算检测中适合快速判断模型有没有学到东西mAP50-95 更严格对细胞这种中小目标数值会比 mAP50 低不少这是正常的。真正要重视的是 precision 和 recall 的平衡。如果你做的是细胞计数precision 低意味着把杂质当细胞数进去recall 低意味着漏掉真的细胞两者都会直接搞坏计数量。我一般会把 precision-recall 曲线拉出来找到曲线的拐点再按那个点的置信度阈值去做推理。6.2 调置信度与NMS契合计数场景推理时把阈值调好比换模型更见效yolo predict modelruns/detect/train/weights/best.pt sourcetest_images conf0.35 iou0.5逻辑说明conf控制置信度下限iou控制 NMS 的 IoU 阈值决定了两个重叠框是否被合并。参数说明计数场景里我一般先用 conf0.25 跑一遍看误检数量再逐步调到 0.45 左右配合 0.5 的 NMS。重叠严重的细胞团块区域可以试着把 iou 降到 0.4减少被 NMS 误删的相邻框。6.3 迁移到你自己的细胞类型上如果你要检测的细胞类型和这个数据集的类别不完全一致别从零开始。最稳妥的做法是把这个数据集训练出来的 best.pt 当作预训练权重再做两阶段微调yolo train modelruns/detect/train/weights/best.pt datamy_cell.yaml epochs50 freeze10 lr00.0005 patience15逻辑说明freeze10冻结前 10 层 backbone让模型先只更新检测头跑完这一阶段后再去掉 freeze用更小的学习率解冻所有层继续微调。参数说明lr00.0005比从头训练低一个量级防止在已有特征上过度震荡。我的习惯是先看冻结阶段的验证损失能不能降下来再解冻微调两步都比从 COCO 初始化收敛快得多。做这类带标注数据集的项目我最后的经验是1298 张图的规模完全值得用但前提是类别对应、格式清洗、可视化验证一步都不能省。我自己因为跳过可视化验证把一次坐标翻转的标注带到了训练里白跑一周才翻车回来。现在拿到任何数据集我都坚持先看类别分布、再转格式、再画框检查最后才写 yaml。希望帮到你。本文还有配套的精品资源点击获取
返回列表