
简介面向计算机视觉与生物医学图像分析场景的细胞活性检测数据集覆盖“死亡”和“存活”两类目标适合用于药物筛选、细胞活性分析、生物样本质量评估等任务的检测模型训练帮助研究者省去图像采集与人工标注的重复工作。压缩包共2000个文件主要包含VOC格式的xml标注文件与YOLO格式的txt标注文件对应每张图像的目标框位置整体大小约为32.45MB可直接接入常见深度学习框架。数据集标注规则统一采用矩形框精细标注总计31332个目标框其中死亡类约24315个、存活类约7017个类别区分清晰可用于模型训练、验证与算法对比。目前已有197人浏览学习对需要高质量细胞样本的深度学习初学者或生物医学算法开发者具有实用参考价值。1. 细胞活性检测数据集1298张图、31332个框先看清Dead和Live的边界细胞活性检测落到算法上就是目标检测在一张明场或荧光图里把死细胞Dead和活细胞Live分别框出来。这份数据集提供了1298张jpg、1298个VOC格式xml、1298个YOLO格式txt合计31332个标注框其中Dead 24315个、Live 7017个。跟很多公开医学数据集比它的总量不算大但胜在标注格式干净、两类划分明确适合用来训练细胞活性计数、存活率分析这类垂直检测模型。适合谁做生物图像AI的算法工程师、想快速搭一个细胞自动计数demo的研究生以及刚入门目标检测、想拿真实数据练手的开发者。先泼一盆冷水Dead框是Live框的3.5倍类别不平衡非常明显直接训练大概率Live召回率很难看这个坑后面专门讲。2. 数据集格式拆解VOC和YOLO两种标签是怎么对齐的拿到压缩包后第一件事不是解压就跑训练而是先搞清楚三种文件之间的关系。很多新手拿到同时带xml和txt的文件夹下意识认为它们是同一份标注的两种格式可以直接喂给任意框架——这句话只对了一半YOLO训练确实吃txt但VOC的xml和YOLO的txt在坐标定义、文件名匹配逻辑上都有细节错一处整个训练就翻车。2.1 文件构成jpg、xml、txt 三者之间的对应关系解压后看到的文件类似图片VOC标注YOLO标注xyxr_cell_362.jpgxyxr_cell_362.xmlxyxr_cell_362.txtxyxr_cell_315.jpgxyxr_cell_315.xmlxyxr_cell_315.txtxyxr_cell_741.jpgxyxr_cell_741.xmlxyxr_cell_741.txt三者文件名完全一致只有后缀不同。这种命名规则直接决定了后续写脚本时可以用Path.stem把三者关联起来。需要注意文件名的数字不是按时间或序号排的不要指望它表示顺序批量处理时务必按实际文件名遍历不要用range(0,1298)去拼路径。还有一个容易误解的点这里的txt不是训练用的图片路径列表而是YOLO格式的标注文件每行一条目标信息。摘要里特别说明“不包含分割路径的txt文件”意思就是压缩包里没有train.txt/val.txt那种记录图片路径的文本只有每张图对应的yolo标签txt。这点搞错后面配置data.yaml时会一头雾水。提示如果你的训练框架要求提供图片路径列表需要自己用find或Python遍历生成数据集本身不负责这件事。2.2 VOC格式的xml里到底存了什么随便打开一个xml内容结构大致是annotation foldercells/folder filenamexyxr_cell_362.jpg/filename size width1920/width height1080/height depth3/depth /size object nameDead/name bndbox xmin120/xmin ymin80/ymin xmax210/xmax ymax190/ymax /bndbox /object object nameLive/name bndbox xmin330/xmin ymin100/ymin xmax420/xmax ymax200/ymax /bndbox /object /annotation这个是labelImg默认输出的VOC格式。每个object里有name值是Dead或Livebndbox里是像素绝对坐标。这里最容易踩的坑是如果用脚本解析xml一定要兼容bndbox字段大小写有的软件会写成box或BndBox。这份数据用的是标准VOC直接按bndbox读就行。解析的时候我一般写一个小函数把xml转成统一的列表结构import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) objs [] for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) objs.append({name: name, bbox: [xmin, ymin, xmax, ymax]}) return img_w, img_h, objs逻辑说明这个函数读取xml中的图片宽高和所有目标框返回结构化数据。参数说明xml_path是xml的绝对路径或相对路径返回的objs是字典列表每个字典包含类别名和像素坐标框。注意int()转换必须做因为xml读出来是字符串。如果你的xml里缺了size后续坐标归一化就完全没有分母这也是常见问题。2.3 YOLO格式的txt坐标是怎么换算的YOLO标注和VOC最大的区别是VOC是绝对像素坐标YOLO是归一化后的相对坐标具体格式是class_id x_center y_center width height其中x_center (xmin xmax) / 2 / img_w。注意这里的width和height是目标框的归一化宽高而不是图片的宽高。初学者最容易犯迷糊的就是把txt的前两个数当成左上角坐标实际上YOLO存的是中心点坐标。用一个脚本把VOC转成YOLOdef voc_to_yolo(xml_path, txt_path, class_namesNone): img_w, img_h, objs parse_voc(xml_path) if class_names is None: class_names [Dead, Live] lines [] for obj in objs: name obj[name] if name not in class_names: continue # 忽略未定义类别 cls_id class_names.index(name) xmin, ymin, xmax, ymax obj[bbox] dw 1.0 / img_w dh 1.0 / img_h x_center (xmin xmax) / 2.0 * dw y_center (ymin ymax) / 2.0 * dh w (xmax - xmin) * dw h (ymax - ymin) * dh lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))参数说明class_names定义了类别到id的映射这里按数据集给定的顺序Dead0、Live1dw、dh是宽高的倒数用乘法效率更高。注意坐标保留6位小数就够训练时精度损失可以忽略。如果你直接拿labelImg导出的txt它已经是这个格式了不需要再转但如果你的txt是从其他地方拼来的务必检查x_center是否在0~1范围内。这里有个“后悔药”经验如果下载到的txt是归一化坐标但你不小心又除了一次图片宽高框会小得离谱训练时loss直接飞掉。我一般拿到数据后先抽样打印几个txt五行再和xml比对确认转换没有重复归一化。2.4 类别不平衡Dead框是Live的3.5倍训练要注意什么按摘要给的统计Dead框24315个、Live框7017个比例大约是3.46:1。这个不平衡不算极端但足以让普通交叉熵模型偏向预测Dead。想象一个场景画面里死细胞多、活细胞少模型学会把大部分物体都判成Dead总体loss下降得挺快但Live类的召回率可能连60%都不到。检测场景下处理不平衡有几种常见做法。第一是在损失函数里给Live类更高的权重比如使用focal loss的gamma参数第二是在数据层面做类别平衡采样比如Live样本的图片多重复几遍第三是干脆把它当作“少数类召回率优先”的任务用PR曲线来选置信度阈值而不是只看mAP。这个数据集本身没有提供类别权重文件需要你自己在训练配置里处理。做一个小统计脚本用来验证你手上的副本是否和摘要一致import os import xml.etree.ElementTree as ET xml_dir path/to/xmls count {Dead: 0, Live: 0} for fn in os.listdir(xml_dir): if not fn.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, fn)) for obj in tree.getroot().findall(object): name obj.find(name).text if name in count: count[name] 1 print(count)这段代码遍历所有xml累加每个类别的目标数量。如果跑出来的结果和摘要差异很大说明文件被改过或者有损坏得回头重新下载。别小看这一步我之前在另一个数据集上发现“声称1万张实际只有8000张有标签”因为源数据里有空xml训练到最后才发现。这份资源同时保留VOC和YOLO两种格式也是这个原因VOC格式易于人工检查、可读性强YOLO格式是训练器直接要的。如果你换了训练框架比如用Detectron2或MMDetection可以从xml重新导出对应格式而不用从yolo txt倒推像素坐标。底层数据是同一套只是投影成不同视图而已。3. 训练自己的细胞活性检测模型从目录整理到yaml配置把VOC和YOLO标签都放在同一层不是不能训练但Ultralytics的YOLO训练器要求的目录结构是images/train、images/val、labels/train、labels/val。如果不整理训练时它只会去默认路径找图片找不到就直接报错。所以第一步是规整目录。3.1 把数据集目录整理成YOLO惯例结构下面这段bash脚本会把原始文件按比例划分到训练集和验证集。假设原始文件都在raw/下且已有对应的txt标签。mkdir -p dataset/images/train dataset/images/val dataset/labels/train dataset/labels/val python - PY import os, random, shutil raw_dir raw train_ratio 0.85 random.seed(42) files [f for f in os.listdir(raw_dir) if f.endswith(.jpg)] random.shuffle(files) split int(len(files) * train_ratio) train_files files[:split] val_files files[split:] for part, flist in [(train, train_files), (val, val_files)]: for f in flist: stem os.path.splitext(f)[0] shutil.copy(os.path.join(raw_dir, f), fdataset/images/{part}/{f}) shutil.copy(os.path.join(raw_dir, stem .txt), fdataset/labels/{part}/{stem}.txt) print(ftrain: {len(train_files)}, val: {len(val_files)}) PY逻辑说明先按train_ratio切分图片列表再同步复制同名txt到labels目录。这里用random.seed(42)保证每次划分结果一致方便复现。参数说明train_ratio0.85表示85%用于训练剩下的15%用于验证如果你的图片量少可以放宽到0.9如果追求更严格的验证0.8也行。注意只复制了txt没有复制xml因为YOLO训练不需要VOC格式xml保留在原始目录当作备份。这里有个细节文件列表要基于jpg而不是txt因为不是每张图都有对应txt虽然这份数据集理论上都有但万一有空标签直接基于jpg列表能自动跳过缺标签的图避免训练报错。3.2 写data.yaml并做第一次验证YOLO训练需要一个data.yaml描述数据集路径和类别信息内容如下path: /absolute/path/to/dataset train: images/train val: images/val names: 0: Dead 1: Live参数说明path是数据集根目录的绝对路径不要用相对路径因为Ultralytics在切换工作目录时相对路径容易失效train和val是相对path的子目录names是一个字典key是类别id必须和txt里的第一个数字对应——也就是Dead必须挂0Live必须挂1反了的话模型会学错。注意names不要写成列表[Dead,Live]虽然Ultralytics也能解析但字典形式更明确避免类别id和顺序混淆。第一次训练前建议先做一次“空转”验证确认标签能被正确加载yolo detect train datadata.yaml modelyolov8n.pt epochs1 imgsz640跑一个epoch看日志里有没有出现“Scanning labels”并且数字正常。如果显示0 labels scanned说明目录没放对如果扫描的labels数量明显大于图片数说明txt被重复使用或路径混乱。这一步看起来多此一举其实能省下半小时的排查时间。3.3 用Ultralytics YOLOv8训练几个关键参数正式训练我一般用以下命令yolo detect train \ datadata.yaml \ modelyolov8m.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/cell_detect参数说明modelyolov8m.pt是中等规模的预训练权重比nano精度高比large显存友好epochs100对于1300张图的数据集来说足够配合patience20早停loss不降就自动停imgsz640是默认输入尺寸如果你的细胞密集且偏小可以试imgsz1280后面会讲。batch16是显存不够时的妥协值A100上可以推到32或64。project指定输出目录方便管理和回溯。如果跑不动就把model换成yolov8n.pt或者把imgsz降到512。但要注意imgsz降太多小细胞很容易被下采样抹掉Dead和Live之间的纹理差异就丢了所以不建议低于512。3.4 验证集划分随机划分还是按文件划分上面用的纯随机划分有个隐患同一张图的内容可能来自同一次实验相邻视野的细胞状态高度相似随机划分后这些相似样本同时出现在train和val里验证结果会虚高。更严谨的做法是按“实验批次”或“文件前缀”划分。比如文件名里的数值看起来像某个批次编号如果同一批次的图片在语义上应该一起划入训练或验证就不能打散。怎么判断我一般会先画一个文件名序号分布图看序号是否有聚类结构然后按前缀切分比如把xyxr_cell_300-400范围划到val其他划到train。不过这份资源没有附带实验分组信息所以如果只是做模型能力验证纯随机也能接受但如果你要拿去发论文、给出可信的泛化指标最好按时间或批次划分。我自己的习惯是同时保留两套划分一套随机划分用于快速迭代一套按序号聚类划分用于最终评估避免因划分方式不同得到虚高的数字。4. 训练中的常见问题排查标签错位、类别错乱、loss不降训练目标检测玄学问题比写业务代码多但绝大多数翻车点都集中在数据和配置。这里把我在这个数据集上实际遇到过的四类问题列出来每一条都按“现象→原因→解决”写清楚照着排查可以少熬夜。4.1 现象训练时提示“no labels in …”或“0 labels scanned”原因目录结构不对或者标签文件放错了位置。常见的是把原本的txt当成了路径列表文件或者把labels目录建成了label大小写不一致。Ultralytics的扫描逻辑是找{dataset_path}/labels/{train|val}/{stem}.txt多一个字符都匹配不上。解决重新按照3.1的目录结构整理然后检查一个案例dataset/labels/train/xyxr_cell_362.txt必须存在里面至少有一行。如果txt是空文件属于“空标签”训练时会忽略掉对应图片。压缩包里应该没有空文件但你自己扩展数据时很容易产生空标签需要单独清理。4.2 现象class loss一直不降比如连续30个epoch停在2.5附近原因类别不平衡加上细胞特征差异不明显模型收敛困难。先确认不是loss曲线本身震荡而是真的一直不降。另一个可能原因是学习率设置不对lr0给的太大0.1或太小0.001。解决先调学习率用默认0.01跑15个epoch看趋势不降再考虑类别权重。这个数据集Dead和Live外观接近建议用yolov8m而不是nanonano的特征表达力不够。如果还是不行可以试试开启fraction0.5随机用50%数据跑排除个别难样本拖累。4.3 现象验证时mAP为0但训练loss正常下降原因data.yaml里的names顺序和txt里的class_id对不上。比如你把Live放在0Dead放在1但txt第一列0依然代表Dead模型输出的类别名就错位了。这种情况loss照样能降因为模型只是把类别名当作两个抽象类但mAP按names含义计算自然对不上。解决打开一个txt文件看第一列数字再对照data.yaml第0个名字是Dead还是Live。这份数据集的规范是Dead0、Live1。另一个原因是你用了预训练模型但预训练权重的类别数不是2而你在yaml里只写了2类Ultralytics会自动改最后一层但如果加载不当输出层的类别映射也会错。换用yolov8m.pt并确保data.yaml正确一般能解决。4.4 现象按网上的教程脚本跑不通报各种import错误原因网上很多转换脚本是为COCO、VOC等标准数据集写的内部硬编码了文件夹名称、类别列表甚至图片后缀。直接套用到这个数据集当然会报模块找不到、文件不存在。解决不要迷信现成脚本理解数据格式后用上面2.2和2.3的小函数自己拼转换流程。我一般把parse_voc和voc_to_yolo放在单独一个data_utils.py任何数据集来了都先跑一遍能适配大部分VOC标注。如果脚本需要读取classes.txt而这个数据集没提供你自己写一个[Dead,Live]就行不要指望资源里自带。5. 标注质量检查用两个脚本把脏数据挡在训练之前训练跑崩了可以调参但标注本身错了调参是救不回来的。所谓垃圾进垃圾出就是这个道理。在花几个GPU小时跑训练之前先花十分钟做标注质量检查是我一直坚持的习惯。这一章给三个最实用的脚本统计分布、可视化抽查、坐标越界检查。5.1 统计每个类别的框数和图片数刚才2.4已经写过一个简化统计这里给一个更完整的版本同时输出图片数、带标签图片数、每个类别框数和每个类别对应的图片数。核心代码如下import os import xml.etree.ElementTree as ET from collections import defaultdict xml_dir xml # 按实际路径改 class_names [Dead, Live] img_count 0 valid_img_count 0 box_count defaultdict(int) img_with_class defaultdict(int) for fn in os.listdir(xml_dir): if not fn.endswith(.xml): continue img_count 1 tree ET.parse(os.path.join(xml_dir, fn)) objs tree.getroot().findall(object) if objs: valid_img_count 1 for obj in objs: name obj.find(name).text if name in class_names: box_count[name] 1 img_with_class[name] 1 print(图片文件数:, img_count) print(含标注图片数:, valid_img_count) print(框总数:, sum(box_count.values())) for name in class_names: print(f{name}: {box_count[name]} 框, 出现在 {img_with_class[name]} 张图中)逻辑说明box_count统计所有框img_with_class统计哪些类别出现在多少张图中。参数说明xml_dir要指向存放xml的目录不要写错。输出后与摘要核对图片1298、Dead 24315、Live 7017。如果数字对不上说明你下载的文件不完整或被别人改过赶紧重新下载。5.2 可视化检查标注框统计只能发现数量不对发现不了“框错位置”的问题。细胞图像里死细胞和活细胞经常黏在一起标注员手抖一下框中心就偏了。可视化抽查是唯一可靠的方法。下面脚本会把xml里的框画在图片上并输出为check目录下的jpgimport os import cv2 import xml.etree.ElementTree as ET img_dir images xml_dir xml out_dir check os.makedirs(out_dir, exist_okTrue) color_map {Dead: (0, 0, 255), Live: (0, 255, 0)} for fn in os.listdir(xml_dir): if not fn.endswith(.xml): continue stem fn[:-4] img_path os.path.join(img_dir, stem .jpg) img cv2.imread(img_path) if img is None: continue tree ET.parse(os.path.join(xml_dir, fn)) for obj in tree.getroot().findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) color color_map.get(name, (255, 255, 255)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) cv2.putText(img, name, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(os.path.join(out_dir, stem .jpg), img) print(可视化图片已输出到, out_dir)参数说明红色框代表Dead绿色框代表Livecv2.putText字体大小0.5适合1080P图如果图片更大就调大。跑完后人工随机抽看50张左右重点关注两类细胞交界处是否有框套错、有没有一个大框把多个细胞圈在一起、有没有漏标。这些在数量统计里看不出来必须靠眼睛。5.3 检查坐标越界和空标签坐标越界在手动标注时很少出现但经过脚本转换后非常容易出现比如把归一化坐标用在了非归一化图片上。越界框会让YOLO训练时loss出现NaN或异常尖峰。下面脚本能一次性扫描所有txt的合法性import os txt_dir labels bad_files [] for fn in os.listdir(txt_dir): if not fn.endswith(.txt): continue with open(os.path.join(txt_dir, fn)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: bad_files.append((fn, 字段数量错误)) continue cls, xc, yc, w, h parts xc, yc, w, h map(float, (xc, yc, w, h)) if not (0 w 1 and 0 h 1 and 0 xc 1 and 0 yc 1): bad_files.append((fn, 坐标越界)) break if w 0 or h 0: bad_files.append((fn, 零尺寸框)) break if bad_files: for path, reason in bad_files: print(path, reason) else: print(全部标签坐标合法)逻辑说明逐行读取txt检查是否5个字段、归一化坐标是否在0~1、框宽高是否非零。参数说明cls是类别id应该为0或1如果出现2或-1说明类别映射错了。对于YOLOx_center width/2理论上应该1但边界框贴着图片边缘时因为浮点精度可能到1.000001所以这里用0~1的宽松判断避免误报。三个脚本配合使用基本能把数据集的脏东西筛出来。等到跑训练时再出问题至少可以先排除标注质量这个因素。6. 进阶技巧针对细胞小目标的两步优化细胞检测最头疼的问题是小目标。一张1920×1080的图细胞直径可能只有二三十像素imgsz640缩放后目标只有10像素见方YOLO的骨干网络下采样32倍后特征图上一个目标可能就剩不到一个像素。所以直接训练效果一般。我的解法分两步第一步把imgsz提到1280如果显存够让细胞在输入图上保留更多信息第二步做“切patch”训练把大图切成512×512的patch相当于数据集扩充同时目标相对尺寸变大。方案imgsz显存占用小目标召回率建议普通640低一般快速验证选型高分辨率1280高约16G明显提升有A100/T4可尝试切patch640训练patch 512中最好配合高分辨率更稳切patch的脚本不细写但关键参数是overlap64防止细胞正好被切在边界上。切完记得同步切标签否则标签坐标全部失效。这两步能明显提升Live这种少样本类别的召回率。如果要验证效果建议分别在imgsz640和imgsz1280下训练50个epoch对比平均精度不要只看了loss就下结论。从那以后我每跑一个数据集都会先做第5章的检查再做imgsz对比最后才谈调参。流程走顺了细胞活性检测这类任务从数据到能出指标两三天足够。希望帮到你。本文还有配套的精品资源点击获取