资讯详情 YOLOv26结合ByteTrack实现癌细胞动态追踪与药物筛选
📅 2026/10/18 19:51:10
培养皿从培养箱拿出来的时候我的第一反应是完了这批细胞已经用药处理了24小时如果YOLOv26的追踪结果能帮我数清楚每一对子细胞是怎么来的药效结论就根本不靠肉眼猜。显微镜下的癌细胞密密麻麻亮场图像又没有荧光标记细胞边缘经常糊成一团用传统图像分割去数个数都会翻车更别提实时追踪癌细胞分裂过程。后来我把整个流程拆成检测、跟踪、事件识别三段才真正把实时追踪癌细胞分裂过程辅助药物筛选这件事跑通。这篇文章不是科普是我自己从标注、训练、推理到药物筛选统计的一整套实践记录。如果你也在做类似的高通量显微成像分析或者正准备用目标检测模型解决细胞动态追踪问题可以直接参考里面的管线设计和踩坑清单。我尽量把关键的参数、逻辑和失败经验都写出来。1. 为什么说细胞动态追踪是个目标检测关联的双重难题1.1 显微镜画面里最难受的不是细胞少而是细胞多我第一次做的时候犯过一个很幼稚的错误以为细胞检测跟通用目标检测差不多检测出框框就行。实际上培养皿里长满的癌细胞在 2048x2048 的图像上动辄几千个而且细胞之间经常贴着亮场下的灰度又只在很小的范围内波动。YOLO 这类单阶段检测器在一般场景下已经很强但在这种拥挤、低对比、重叠多的图像上第一步检测能不能把每个细胞稳稳框住就已经很考验模型。更重要的是我们要的不是某一帧里有没有细胞而是这一堆细胞里哪一个从单个变成了两个。这个信息必须靠时间维度上的关联才能得到。单帧检测得再准没有稳定的轨迹你依然不知道第 37 个细胞是不是刚才第 12 个细胞分裂出来的。1.2 癌细胞分裂在图像上到底长什么样癌细胞从间期进入有丝分裂形态变化其实很戏剧。我整理了一下我标注时常用的几个阶段方便你理解事件检测的难度阶段图像表现检测难度间期细胞呈不规则椭球轮廓相对稳定内部有可见核区中等数量多且粘连有丝分裂早期细胞变圆、变亮体积略缩边缘更清晰低形态很独特分裂中后期细胞被拉长中间出现缢痕呈哑铃形中高容易看成两个靠在一起的细胞分裂完成两个子细胞分开面积明显比母细胞小高两个子细胞可能紧贴在一起你可以看到分裂完成这个时刻在图像上并不好定义。两个子细胞刚分开时它们之间的距离可能比细胞直径还小检测器很容易把一对子细胞框成一个大框或者把其中一个漏掉。所以我的做法不是让模型直接输出这是一个正在分裂的细胞而是让模型输出更基础的细胞框再由追踪轨迹去判断分裂事件。这个思路后面展开。1.3 我为什么最后选了 YOLOv26 这一代的思路YOLOv26 作为 YOLO 系列的新一版我最看重它的几个点其实不是单纯的精度数字而是它对小目标和密集场景的召回有明显改善。细胞直径在 2048x2048 原图上大概只有 30 到 60 像素属于典型的小目标。它默认的检测头对这类目标更友好同时后处理里的动态 NMS 逻辑能减少相邻细胞框互相压掉的情况。另一个原因是它的训练吞吐高我手头只有一块 GPU要反复调实验训练速度快意味着我可以多试几版标注策略和超参。当然我不太建议为了追新版本而换模型。真正让效果变化的往往不是你选了哪个 YOLO而是你有没有把检测结果正确地串成轨迹、把轨迹正确地解释成分裂事件。YOLOv26 只是让第一层检测更稳了后面两层还需要自己去搭。2. 我的技术选型与整体管线检测、跟踪、事件识别三层解耦2.1 为什么没有直接做端到端的分裂检测做过视频理解的人可能第一反应是能不能直接用视频模型判断每一帧里哪个细胞在分裂我也尝试过这个方向但被数据量劝退了。要训练一个能自动捕捉时间上下文的分裂检测模型需要大量带精确时间点标注的视频片段而且模型解释性差很难说清楚它到底是因为形态变了还是因为位置变了才给出阳性结果。对于药物筛选这种场景结果必须能回溯、能解释、能人工复核所以我最后还是选择了更容易控制的三层结构。这个结构很简单明确第一层YOLOv26 逐帧检测细胞位置输出边界框和置信度。第二层跟踪器把不同帧的检测结果关联成轨迹给每个细胞一个稳定 ID。第三层基于轨迹的单细胞 - 双细胞变化判断是否发生了一次分裂事件。2.2 每层职责和关键参数检测层我用的目标是两类普通细胞和分裂态细胞。你可能会好奇为什么前面说不要模型直接判断分裂这里又加了分裂态类。因为这个分裂态对应的不是分裂完成瞬间而是细胞形态上已经开始变圆、变亮的早期有丝分裂状态。它只是一个视觉特征类别不代表事件发生。真正的事件确认还是要看跟踪结果里是否出现了分裂态细胞消失 - 两个普通细胞从原来位置出现的变化。跟踪层我选了 ByteTrack 作为基线。原因很简单它有低分框二次关联机制对细胞这种遮挡严重、检测器偶尔漏检的目标很友好。后来我也试过 Bot-SORT但细胞运动是典型的慢速随机游走不需要特别强的运动模型ByteTrack 在速度和稳定性上更平衡。具体的跟踪代码我会在第四章里给出来。事件识别层我写了一个轻量级的规则判断模块维护两条轨迹的关系一条母轨迹结束同时产生两条子轨迹。如果两个子轨迹的位置与母轨迹最后位置的重叠度高且出现时间在合理窗口内就记一次分裂事件。这个模块本身不跑深度学习却很关键。2.3 硬件与运行环境参考我自己的运行环境比较朴素一张 24GB 显存显卡Windows 和 Linux 双系统都跑通过。推理阶段其实不需要那么大的显存因为我是离线处理录好的视频再把结果汇总但训练阶段显存大一点会省心很多。如果你只是做小规模验证YOLOv26 的 n / s 级权重在 8GB 显存上也能跑起来。整体管线我用 Python 编写核心依赖是 PyTorch、OpenCV、NumPy 和加速库 boxmot。数据组织上一张 2048x2048 的图我会切成四块 1024x1024 的重叠瓦片送去检测这样小目标不会被缩放丢失同时显存压力也小。3. 数据准备与标注把分裂变成模型能学的标签3.1 视频怎么切帧、怎么筛选有效视野我的原始数据是培养箱内的显微镜每 5 分钟拍一张的序列图一个孔位往往有上千帧。直接全量标注不现实所以我先做了两轮筛选去掉失焦严重的帧。显微镜自动聚焦偶尔会抽风画面模糊的帧检测出来全是低置信度框没有训练价值。每隔一段时间抽帧。5 分钟一帧其实已经够密训练时我按 3 到 5 帧间隔抽样本避免相邻帧太像导致过拟合。抽帧之后我用一个初版模型先做自动预标注再人工修正。这样看起来多了一步实际比完全从头标注快很多。预标注虽然会漏检但能把我从画框的重复劳动里解放出来。3.2 标注类别和分裂的时间维度定义上面提到我标两类普通细胞normal和分裂态细胞mitotic。普通细胞的框是单细胞紧密包围框分裂态细胞的框是正在变圆或已经拉长的单个细胞紧密包围框。需要特别强调的是两个子细胞刚分开时我并不会把这对细胞标成一个分裂态而是标成两个普通细胞。这个选择让我在后处理时可以用一个母轨迹分裂成两个子轨迹来定义事件逻辑非常清晰。画框时还有一个反直觉的经验不要为了把细胞完整包进去而把框拉得太大。背景框太多会让检测器把相邻细胞或培养基里的杂质也被框进来。标注工具里我会刻意把框的四边贴着细胞边缘宁可比手抖稍微小一点也不要包进太多背景。3.3 数据增强策略和样本平衡问题癌细胞的图像亮度、对比度会随着培养液变化所以我对原始图做了不少光度增强随机亮度扰动、对比度扰动、高斯噪声、局部伽马校正。几何增强方面我用的主要是随机翻转和小角度旋转不太敢用太大的随机缩放因为细胞尺寸本身是判断分裂的重要线索缩太狠会让模型分不清间期和分裂期。分裂态细胞在所有细胞里占比很低大概只有 1% 到 3%。类别不平衡会导致模型倾向把所有细胞都预测成普通细胞分裂态召回率极差。我的处理方式是给两个类别设置不同的损失权重同时在采样时对包含分裂态细胞的样本做重复采样。效果最明显的是一组对比实验不处理类别不平衡时分裂态 F1 只有 0.41处理好之后能到 0.76。这个差距直接决定了最终追踪的稳定性。4. 训练与推理让 YOLOv26 在低对比度显微图像上稳定工作4.1 输入尺寸、预训练和训练参数的选择原图是 2048x2048直接整图缩放成训练尺寸会造成严重的信息丢失。我最终采用瓦片方式训练从原图上切 1024x1024 的块步长 512重叠区域保留。这样单个细胞在输入图像中的尺寸和实际显微镜里的比例接近模型学到的形态特征更真实。训练尺寸我设为 1024x1024用 YOLOv26 的默认预训练权重做微调初始学习率 1e-3warmup 后衰减总共训练大概 150 轮左右。如果从零训练小目标的收敛速度会慢很多我不推荐。4.2 跟踪器到底怎么接入检测结果无论你用的是 ByteTrack、DeepSORT 还是 Bot-SORT核心输入都包括检测框、置信度和类别。细胞运动缓慢IOU 关联基本够用因此我让 ByteTrack 的匹配阈值稍微宽松一些避免漏检导致的轨迹中断。下面这段是我推理时的核心代码骨架import cv2 import numpy as np from ultralytics import YOLO from boxmot import ByteTrack model YOLO(yolov26m_cell.pt) tracker ByteTrack( track_thresh0.35, match_thresh0.8, frame_rate12, ) cap cv2.VideoCapture(well_03.avi) fps cap.get(cv2.CAP_PROP_FPS) while True: ok, frame cap.read() if not ok: break frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results model.predict( frame_rgb, imgsz1024, conf0.25, iou0.45, verboseFalse, ) # 将 YOLO 的检测结果整理成 [x1,y1,x2,y2,score,class] dets [] if results and len(results[0].boxes) 0: boxes results[0].boxes.xyxy.cpu().numpy() scores results[0].boxes.conf.cpu().numpy() cls_ids results[0].boxes.cls.cpu().numpy().astype(int) for b, s, c in zip(boxes, scores, cls_ids): dets.append([b[0], b[1], b[2], b[3], s, c]) dets np.array(dets) tracks tracker.update(dets, frame) # tracks 每行是 [x1, y1, x2, y2, track_id, score, class] for t in tracks: x1, y1, x2, y2, track_id, score, cls map(float, t[:7]) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, str(int(track_id)), (int(x1), int(y1)-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) cv2.imshow(cell_tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里有个容易被忽略的细节frame_rate 参数必须和你输入视频的真实帧率一致。显微镜视频每隔 5 分钟一帧时我把它统一转成每秒 12 帧的序列图再传给跟踪器。如果你用原始时间间隔去传跟踪器内部的时间预测单元会失衡轨迹 ID 反复横跳。4.3 为什么要把推理结果落盘而不是实时可视化很多人一听到实时追踪就先想可视化跑起来多酷但实际做药物筛选时绝大多数时间花在离线批量处理上。我的做法是把每一帧的轨迹结果保存成 JSON 或 CSV包含帧号、track_id、框坐标、类别、置信度这些字段。等全部处理完之后再重新读轨迹做事件判断。这样既方便调阈值重新计算也可以随时抽帧检查某一条轨迹对不对。针对几十个孔位的批次处理用一个简单的并行池把不同视频分配到多进程里速度基本够用。5. 从轨迹到分裂事件如何判定一次癌细胞分裂发生了5.1 事件判定规则拿到轨迹序列后真正决定药效结论的是分裂事件这件事。我的判定规则如下一条轨迹 T_a 结束同时出现两条新轨迹 T_b、T_c。T_b、T_c 的起始位置与 T_a 最后位置的重叠区域大于 50%。T_b、T_c 的起始帧与 T_a 结束帧相差不超过 3 帧。T_b、T_c 在后续至少 5 帧内持续存在排除误检造成的假轨迹。这些规则看起来简单但每个阈值都要根据实际数据调。重叠度阈值太严会把慢速分裂过程中母细胞位置轻微漂移的样本漏掉阈值太松又容易把两个相邻细胞一进一出当成分裂事件。我最后是把阈值做成配置项再对人工标注的一小批事件做回验选到召回和精确平衡的那组。5.2 药物筛选场景里到底看什么指标药物筛选不能只数分裂了几次就下结论还需要把分裂事件密度归一化。我常用的指标有三个分裂事件率单位时间内每千个追踪细胞中检测到的分裂事件数。母代倍增时间从单细胞轨迹开始到分裂事件发生的中位时间。子代存活追踪时长子轨迹出现后能持续追踪多久间接反映细胞是否受损或死亡。这三个指标组合起来基本能看出药物是抑制了分裂启动还是让细胞在分裂后很快解体。只看第一个指标会漏掉很多毒性效应。5.3 药物筛选数据看板我自己的实验设置是四组空白对照、低浓度给药、高浓度给药、阳性对照组每组保留三个重复孔。处理完所有视频后统计表格大概长这样分组追踪细胞总数分裂事件率/千细胞/小时中位分裂时间小时子代消失比例空白对照31288.612.44.2%低浓度组29856.115.87.5%高浓度组27432.3未达到中位23.8%阳性对照20101.1未达到中位19.1%从这个结果能明显看到高浓度组的分裂事件率断崖下降同时子代消失比例升高。这说明药物不仅仅是抑制分裂还可能在细胞分裂完成后造成了损伤。这种信息是传统的终点法读取细胞活力很难给出的也是我做这套追踪系统最大的意义。5.4 事件结果的复核机制自动判定一定会有错所以我给每次批处理都加了一个待复核事件列表。只有满足事件判定条件、但其中一个子轨迹起始置信度低于 0.5或者轨迹生命周期特别短的才会进入这个列表。复核时我会把母轨迹最后三帧和子轨迹前五帧的图像拼成小图一张一张看。几千个事件看下来虽然费眼睛但比直接相信模型输出要安心得多。6. 踩过的坑和解决方式6.1 粘连细胞导致 ID Switch这是我把追踪结果交给同事看之后被问得最多的问题为什么同一个细胞 ID 会突然从左边跳到右边原因通常是两个细胞紧贴着前行检测框互相覆盖跟踪器在 IOU 匹配时把轨迹错误转移到另一个细胞上。解决措施有几个层次检测阶段用更高分辨率的瓦片减少粘连细胞被压成一个框。若条件允许使用 YOLOv26 的实例分割权重输出细胞 mask用 mask 的质心和轮廓去匹配比纯框的鲁棒性好不少。跟踪匹配时把类别信息也加入代价矩阵让分裂态不能轻易匹配到普通态。我最后用的是第二个方案牺牲了一点推理速度但 ID Switch 掉了一半以上。6.2 失焦和闪烁让轨迹断裂培养箱里的液面波动、自动对焦滞后都会让画面出现轻微模糊。这时候检测器置信度会整体下降ByteTrack 的低分框二次关联能救回来一部分但连续十几帧失焦时轨迹还是会断。我的解决办法是在事件判断阶段只允许轨迹在缺失不超过 5 帧的情况下进行线性插值补全。同时我会统计每帧的平均检测置信度如果一段窗口内低于设定阈值就把这段时间标为低质量区间药效统计时单独看待。6.3 阳性板里的死细胞碎片被当成细胞用药之后会出现大量细胞碎片和调亡小体它们也会被检测成普通细胞进而产生大量假轨迹。这个问题我一开始完全没料到因为训练数据里几乎没有这种样本。后来我从推理结果里抽出所有低置信度框发现其中一大半都是碎片于是我专门从给药后的视频里标了一批debris类重新训练模型。虽然增加了标注成本但这才是让高浓度组结果可信的关键一步。如果你要处理药物处理过的样本这一步基本绕不开。6.4 阈值统一不是万能解我最初想用同一套 conf 和 NMS 阈值处理所有孔位结果不同光照条件下检测结果差异很大。后来我把单帧检测和轨迹生成分开调单帧检测可以稍微过召回保留更多候选最终轨迹是否进入统计用另一个轨迹置信度阈值来控制比如要求轨迹至少连续 10 帧且平均置信度超过 0.4。这样比直接把单帧阈值调高更不容易丢掉弱信号。7. 最后想说的体会把 YOLOv26 用在显微镜细胞追踪这件事真正难的不是跑通一个 demo而是让你的结果能经得起生物背景的追问。我中间一度陷在调检测精度里出不来后来才意识到检测只是地基轨迹的稳定性和分裂事件的判定才是这套系统有没有用的关键。每次批处理跑完我都会随机抽 50 条轨迹人工看一眼再决定要不要调整事件规则里的时间窗和重叠度。这一份笨功夫比换任何模型结构都更能提升结论的可靠性。如果你现在正准备做类似的系统我建议你从一开始就把数据管理做成结构化的每孔、每帧、每个 track_id 都能对应到原始图像。这样后面不管换模型还是重新调参数都不至于推倒重来。显微镜下的细胞很微妙但一套清晰、可复核的管线能让你在密密麻麻的细胞和漫长的视频序列里真正看清药物起了什么作用。