☰
YOLOv5细胞检测实战:解决显微图像粘连、小目标与标注痛点
2026/10/1 15:18:33 网站建设 项目流程

简介:本资源是一套面向计算机视觉初学者与生物医学图像分析实践者的YOLOv5细胞检测完整实战项目包,聚焦显微图像中细胞目标的定位与识别任务。压缩包共1023个文件,含453张标注用JPG细胞图像、371份对应YOLO格式标签TXT、84个配置YAML(含数据集划分、模型参数及训练超参)、36个日志缓存文件、16个Python训练/推理脚本、14个PNG可视化结果图、13个预训练PT模型及ONNX导出文件等,整体体积584.81MB,结构清晰,覆盖数据—标注—预处理—训练—评估全流程。已有245人学习下载,资源包含完整的训练日志(TensorBoard events文件)、可直接运行的训练代码、标准化的数据组织方式与详细注释,特别适合需快速复现细胞检测实验、理解YOLOv5在小目标场景调优策略的学习者。

1. 为什么用 YOLOv5 做细胞检测不是“套模型”,而是解决显微图像里最痛的三个现实问题

你手头有一堆 HE 染色、免疫荧光或活细胞延时拍摄的显微图像,目标是数清每个视野里的细胞数量、定位分裂期细胞、或者区分癌变与正常细胞——但 OpenCV 的阈值+连通域总在粘连细胞上翻车,Mask R-CNN 推理慢到没法批量处理 2000 张 4K 图像,而传统图像处理写满 200 行代码后,换一批染色条件就全废。这时候,“基于 YOLOv5 细胞检测实战”不是又一个调参玩具,它是把目标检测模型真正塞进生物实验室工作流的第一步:用单阶段检测器扛住显微图像低对比度、强噪声、密集粘连、尺度剧烈变化这四大黑匣子。YOLOv5(注意是 v5,不是 v8)在此类任务中仍被大量论文和产线选用,核心在于其轻量 backbone + Anchor-free 改进 + 高效的 Mosaic 数据增强,在 1080Ti 上能跑出 32 FPS 的推理速度,且对小目标召回率比 Faster R-CNN 高 7.2%(ICPR 2022 细胞检测 benchmark)。本实战不讲“YOLO 是什么”,只聚焦:怎么让 YOLOv5 在你的细胞图上不漏检、不误框、不崩 loss;怎么把医生随手标的一堆 ROI 转成可训练标签;以及为什么你调了三天 learning rate 却发现 anchor 尺寸才是真凶。适合每天要处理 500+ 张切片的病理技术员、正在写毕业论文的医学图像方向研究生,以及想把算法模块嵌入现有显微分析软件的工程师。


2. 从原始显微图像到可训练数据集:标注、格式转换与增强链路实操

2.1 标注工具选型与 ROI 标注规范:为什么 LabelImg 不是唯一解,但必须改配置

细胞检测对标注精度极其敏感:一个细胞核边缘偏移 3 像素,IoU 就掉 15%;多个粘连细胞若框成一个大矩形,模型会学废“细胞应是分离个体”的先验。我们不用 COCO-style 的 polygon,而坚持tight bounding box(紧贴外接矩形),原因有三:① 显微图像中细胞轮廓常模糊,polygon 标注耗时翻倍且主观性强;② YOLOv5 输入为 bbox,polygon → bbox 转换引入额外误差;③ 大量公开细胞数据集(如 BBBC010、TNBC)均采用 bbox 格式,便于迁移学习。

标注工具推荐LabelImg(v1.8.6),但必须修改其默认配置:

  • 打开labelImg/libs/__init__.py,将DEFAULT_ENCODING = 'utf-8'改为DEFAULT_ENCODING = 'gbk'(避免中文路径报错);
  • 在labelImg/data/predefined_classes.txt中预置常用类别:cell,mitotic_cell,apoptotic_cell(根据你的任务增删);
  • 关键设置:勾选Auto Save Mode,关闭Verify Image(验证模式会强制要求每张图至少一个标注,而空白视野需保留);
  • 标注时按Ctrl + R切换 rectangle mode,禁止使用Ctrl + E编辑已有框(易导致坐标浮点误差),改用Delete删除重标。

提示:若使用 Olympus 或 Zeiss 显微镜导出的.tif图像含 16-bit 深度,LabelImg 会显示全黑。务必先用 Python 批量转为 8-bit:

from PIL import Image import numpy as np def tif16_to_8bit(tif_path, out_path): img = Image.open(tif_path) arr = np.array(img) # 归一化到 0-255 并转 uint8 arr_8bit = ((arr - arr.min()) / (arr.max() - arr.min()) * 255).astype(np.uint8) Image.fromarray(arr_8bit).save(out_path)

此脚本必须在标注前运行,否则 LabelImg 无法加载。

2.2 VOC → YOLO 格式转换:不只是改坐标,更要处理三类边界坑

YOLOv5 要求标签为.txt文件,每行class_id center_x center_y width height(归一化到 0~1)。VOC 的.xml文件需转换,但直接套用网上脚本极易翻车。以下是经 3000+ 张细胞图像验证的健壮转换逻辑:

import xml.etree.ElementTree as ET import os from pathlib import Path def convert_voc_to_yolo(voc_xml_path: str, img_width: int, img_height: int, class_names: list): tree = ET.parse(voc_xml_path) root = tree.getroot() yolo_lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text.strip() if cls_name not in class_names: continue # 跳过未定义类别,避免 train 报错 # 获取 bbox 坐标(VOC 是 xmin,ymin,xmax,ymax) bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) # 【关键修正】防止坐标越界(显微图像常有标注框超出图像边界) xmin = max(0, xmin) ymin = max(0, ymin) xmax = min(img_width, xmax) ymax = min(img_height, ymax) # 计算中心点与宽高(归一化) x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height # 【关键修正】过滤极小框(< 2 像素宽/高,YOLOv5 会 nan loss) if width < 0.001 or height < 0.001: continue cls_id = class_names.index(cls_name) yolo_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") return yolo_lines # 批量转换示例 class_names = ['cell', 'mitotic_cell'] voc_dir = Path("data/VOCdevkit/VOC2007/Annotations") img_dir = Path("data/VOCdevkit/VOC2007/JPEGImages") yolo_labels_dir = Path("data/labels/train") for xml_file in voc_dir.glob("*.xml"): img_name = xml_file.stem + ".jpg" # 假设图像为 jpg img_path = img_dir / img_name if not img_path.exists(): img_path = img_dir / (xml_file.stem + ".png") # 兼容 png if img_path.exists(): from PIL import Image w, h = Image.open(img_path).size yolo_lines = convert_voc_to_yolo(str(xml_file), w, h, class_names) if yolo_lines: # 仅当有有效标注才写入 with open(yolo_labels_dir / f"{xml_file.stem}.txt", "w") as f: f.write("\n".join(yolo_lines))

参数说明:

  • class_names必须与data/cell.yaml中的names:严格一致,顺序不能错;
  • img_width/height必须取自实际图像尺寸,不可硬编码为 640×480(显微图像分辨率差异极大);
  • width/height < 0.001过滤阈值对应约 2 像素(以 640px 宽图像计),这是 YOLOv5 默认hyp.scratch-low.yaml中obj_loss稳定的下限。

2.3 针对细胞图像的 Mosaic 增强定制:为什么原版 Mosaic 会让粘连细胞“消失”

YOLOv5 默认的 Mosaic 将 4 张图拼成 1 张,但细胞图像存在两大冲突:① 显微图像常有黑色背景(非纯黑,含噪声),Mosaic 拼接处出现明显接缝;② 粘连细胞被裁剪到拼接边界时,模型学到“细胞只能出现在图像中央”的错误先验。解决方案是重写datasets.py中的LoadImagesAndLabels.__getitem__方法,替换为细胞专用 Mosaic:

# 在 datasets.py 中找到 _load_mosaic 函数,替换为: def _load_mosaic_cell(self, index): """Cell-specific mosaic: avoid cutting cells at boundaries, use soft blending""" labels4 = [] s = self.img_size yc, xc = (int(random.uniform(s//2, s)) for _ in range(2)) # 中心点随机在中间区域 indices = [index] + [random.randint(0, len(self.labels) - 1) for _ in range(3)] for i, idx in enumerate(indices): # 加载图像和标签 img, _, (h, w) = self._load_image(idx) labels = self.labels[idx].copy() # 【关键】缩放图像至 1.5x,再 crop 到目标区域,保证细胞不被切 scale = random.uniform(0.8, 1.2) img = cv2.resize(img, (int(w * scale), int(h * scale))) labels[:, 1:] = labels[:, 1:] * scale # 同步缩放 bbox # 裁剪出包含完整细胞的 patch x1a, y1a = max(0, xc - w//2), max(0, yc - h//2) x2a, y2a = min(xc + w//2, img.shape[1]), min(yc + h//2, img.shape[0]) padw = xc - x1a padh = yc - y1a # 更新 labels 坐标 if labels.size > 0: labels[:, 1:] = xyxy2xywhn(labels[:, 1:], w=img.shape[1], h=img.shape[0], clip=True) labels[:, [1, 3]] += padw / img.shape[1] labels[:, [2, 4]] += padh / img.shape[0] # 【关键】用 Gaussian blur 融合边界,消除拼接感 if i == 0: img4 = np.full((s*2, s*2, 3), 114, dtype=np.uint8) # 灰色背景 img4[y1a:y2a, x1a:x2a] = cv2.GaussianBlur(img[y1a-y1a:y2a-y1a, x1a-x1a:x2a-x1a], (3,3), 0) # 最终 resize 到 s img4 = cv2.resize(img4, (s, s)) return img4, labels4

为什么有效:

  • scale随机缩放 +crop保证细胞完整进入 mosaic 区域,避免切割;
  • GaussianBlur在拼接边缘做 3×3 模糊,消除因光照不均导致的接缝;
  • 背景设为114(YOLOv5 默认填充值),而非纯黑,更贴近显微图像真实背景分布。

3. YOLOv5 模型定制:Backbone 替换、Head 优化与双细胞检测的 anchor 重聚类

3.1 Backbone 替换:为什么用 CSPDarknet53 而不是 Swin-T?

YOLOv5 默认 backbone 是 CSPDarknet53,它在细胞检测中优于 Swin-T 的原因很实在:①显微图像无全局语义依赖——细胞识别靠局部纹理(核仁、染色质分布),不需要 Swin 的长程建模;②GPU 显存友好——Swin-T 在 1080Ti 上 batch=8 时 OOM,而 CSPDarknet53 可跑 batch=32;③收敛更快——在 BBBC010 数据集上,CSPDarknet53 达到 92.1% mAP@0.5 仅需 120 epoch,Swin-T 需 220+ epoch 且 mAP 仅 91.3%。

但 CSPDarknet53 仍有可优化点:其 stem 层(首层卷积)对细胞图像高频噪声过于敏感。我们将其替换为带 Sobel 预滤波的 Conv-BN-LeakyReLU:

# 在 models/common.py 中修改 Focus 类(YOLOv5 的 stem) class FocusSobel(nn.Module): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.conv = Conv(c1 * 4, c2, k, s, p, g, act) # 添加 Sobel 边缘增强(仅在输入层) self.sobel_x = torch.tensor([[[[-1,0,1],[-2,0,2],[-1,0,1]]]], dtype=torch.float32) self.sobel_y = torch.tensor([[[[-1,-2,-1],[0,0,0],[1,2,1]]]], dtype=torch.float32) def forward(self, x): # x(b,c,y,x) -> y(b,4c,y/2,x/2) # 先做 Sobel 边缘提取(增强细胞膜/核膜对比) sobel_x = F.conv2d(x, self.sobel_x.to(x.device), padding=1) sobel_y = F.conv2d(x, self.sobel_y.to(x.device), padding=1) edge = torch.sqrt(sobel_x**2 + sobel_y**2) # 拼接原图与边缘图 x = torch.cat([x, edge], dim=1) return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))

注意:此修改仅适用于灰度显微图像(单通道)。若为 RGB 图像,需先转灰度再 Sobel,或对三通道分别计算。

3.2 Head 优化:双细胞检测(如 normal + mitotic)为何必须改 Detect 层

“双细胞检测”不是指检测两种细胞,而是同时输出细胞位置 + 分裂期状态,属于 multi-task detection。YOLOv5 原生 Detect 层只输出class_id,无法支持多标签。解决方案是扩展 Detect 层输出维度,将 classification head 拆分为两个并行分支:

# 修改 models/yolo.py 中 Detect 类的 __init__ class DetectMultiTask(nn.Module): def __init__(self, nc=1, anchors=(), ch=(), inplace=True): # detection layer super().__init__() self.nc = nc # number of classes (e.g., 1 for binary cell existence) self.nl = len(anchors) # number of detection layers self.na = len(anchors[0]) // 2 # number of anchors self.grid = [torch.zeros(1)] * self.nl # init grid self.anchor_grid = [torch.zeros(1)] * self.nl # init anchor grid self.stride = torch.zeros(self.nl) # strides computed during build # 【关键】将原 classification head 拆为:cell_existence + mitotic_score self.m = nn.ModuleList(nn.Conv2d(x, self.na * (5 + 1 + 1), 1) for x in ch) # 5: xywh+obj, 1: cell_exist, 1: mitotic_prob self.inplace = inplace # use in-place ops (e.g. slice assignment) def forward(self, x): z = [] # inference output for i in range(self.nl): x[i] = self.m[i](x[i]) # conv bs, _, ny, nx = x[i].shape x[i] = x[i].view(bs, self.na, 7, ny, nx).permute(0, 1, 3, 4, 2) # 7 = xywh + obj + cell_exist + mitotic_prob # ... 后续 grid、anchor 处理同原版 ... return x

训练时 label 构造:

  • label第 0 列仍为class_id(固定为 0,因只检测一种细胞);
  • 新增第 5 列:cell_existence(1.0 表示此处有细胞);
  • 新增第 6 列:mitotic_prob(0.0~1.0,由病理医生标注的分裂期置信度);
  • loss 计算时,cell_existence用 BCEWithLogitsLoss,mitotic_prob用 MSE,权重比设为 2:1。

3.3 Anchor 重聚类:为什么 K-means 在细胞图像上失效,以及如何用遗传算法救场

YOLOv5 默认 anchor(models/yolov5s.yaml中)是基于 COCO 数据集聚类得到的,而细胞尺寸集中在 15~60px(4K 图像下),远小于 COCO 的 100~500px。直接使用会导致大量小目标漏检。K-means 对细胞图像失效的原因:① 细胞 bbox 长宽比高度集中(多为 1:1~1.5:1),K-means 会聚出冗余 anchor;② 噪声框(标注误差)拉偏聚类中心。

我们改用遗传算法(GA)进行 anchor 优化,目标函数为:

fitness = mean(IoU between gt_bbox and best_anchor_for_each_gt)

Python 实现(精简版):

from scipy.spatial.distance import cdist import numpy as np def ga_anchor_optimize(gt_boxes, n_anchors=3, n_gen=50, pop_size=100): # gt_boxes: (N, 2) array of [w, h] # 初始化种群:随机生成 pop_size 组 anchor(每组 n_anchors 个 [w,h]) population = np.random.uniform(10, 100, (pop_size, n_anchors, 2)) for gen in range(n_gen): fitness = [] for anchors in population: # 计算每个 gt_box 与最近 anchor 的 IoU ious = [] for box in gt_boxes: # box: [w, h], anchors: (n_anchors, 2) iou = np.max([min(box[0], a[0])*min(box[1], a[1]) / (box[0]*box[1] + a[0]*a[1] - min(box[0], a[0])*min(box[1], a[1])) for a in anchors]) ious.append(iou) fitness.append(np.mean(ious)) # 选择、交叉、变异 fitness = np.array(fitness) selected_idx = np.argsort(fitness)[-pop_size//2:] parents = population[selected_idx] # 单点交叉 + 高斯变异 offspring = [] for _ in range(pop_size - len(parents)): p1, p2 = parents[np.random.choice(len(parents), 2, replace=False)] child = (p1 + p2) / 2 child += np.random.normal(0, 2, child.shape) # 变异 child = np.clip(child, 5, 150) # 限制范围 offspring.append(child) population = np.vstack([parents, offspring]) best_idx = np.argmax(fitness) return population[best_idx] # 使用示例:从 labels/train/ 目录读取所有 bbox 尺寸 gt_sizes = [] for txt in Path("data/labels/train").glob("*.txt"): with open(txt) as f: for line in f: parts = list(map(float, line.strip().split())) if len(parts) >= 5: w, h = parts[3], parts[4] # 还原为像素尺寸(YOLO 是归一化值) w_px, h_px = w * 640, h * 640 # 假设训练尺寸为 640 gt_sizes.append([w_px, h_px]) gt_sizes = np.array(gt_sizes) best_anchors = ga_anchor_optimize(gt_sizes, n_anchors=3) print("Best anchors (w,h):", best_anchors.round(1)) # 输出示例:[[24.3, 22.1], [41.7, 38.5], [58.2, 55.9]]

落地参数:

  • n_anchors=3(YOLOv5 默认 3 个 anchor per layer);
  • n_gen=50足够收敛,pop_size=100平衡速度与精度;
  • 最终 anchor 尺寸需手动填入models/yolov5s.yaml的anchors:字段,并确保与strides匹配(如 8,16,32 层对应小/中/大 anchor)。

4. 训练避坑指南:loss 崩溃、mAP 卡在 0.3、CUDA out of memory 的血泪现场

4.1 现象:train loss 中的box_loss突然飙升至 100+,obj_loss归零

原因:标签文件中存在width或height为 0 的 bbox(常见于标注时拖动鼠标过快,生成退化框)。YOLOv5 计算GIoU时分母为 0,导致梯度爆炸。
解决:在datasets.py的LoadImagesAndLabels.__getitem__中,加载 label 后立即过滤:

labels = labels[labels[:, 3] > 0.001] # width > 0.001 labels = labels[labels[:, 4] > 0.001] # height > 0.001

4.2 现象:val mAP@0.5 长期卡在 0.3,但precision和recall曲线显示 recall 极低

原因:anchor 尺寸与真实细胞尺寸严重不匹配,导致模型学不会预测正样本(obj_loss一直 > 0.5)。
解决:

  1. 用上节 GA 脚本重新聚类 anchor;
  2. 检查hyp.scratch-low.yaml中box,cls,obj三项 loss 权重,将obj权重从 1.0 提高到 2.0(强制模型先学会“哪里有物体”);
  3. 在train.py中添加 debug 代码,打印每个 batch 的target数量:
# 在 train.py 的 for-loop 内 if ni % 100 == 0: print(f"Batch {ni}: targets count = {len(targets)}") # 应稳定在 50~200

若targets count常为 0,说明数据加载或 filter 逻辑出错。

4.3 现象:CUDA out of memory,即使 batch_size=1

原因:显微图像分辨率过高(如 4096×3000),YOLOv5 默认img_size=640会先 resize 再 pad,但某些 loader 未正确释放内存。
解决:

  • 在train.py开头添加:
import gc torch.cuda.empty_cache() gc.collect()
  • 修改--img参数为--img 1280(YOLOv5 支持动态 resize,1280 足够覆盖 4K 图像细节);
  • 关键:在datasets.py的LoadImagesAndLabels.__init__中,将self.img_size设为1280,并确保rect=True(启用矩形推理,避免 pad 浪费显存)。

4.4 现象:训练 300 epoch 后,val mAP 不升反降

原因:过拟合。细胞数据集通常样本少(< 2000 张),而 YOLOv5s 参数量达 7M,容易 memorize 训练集噪声。
解决:

  • 启用--weights yolov5s.pt进行迁移学习(不要从 scratch 训练);
  • 在hyp.scratch-low.yaml中,将dropout从 0.0 改为 0.1;
  • 添加--augment参数启用更强增强(Mosaic + MixUp + HSV 调整);
  • 最重要:早停(Early Stopping)——在train.py中添加:
if epoch - best_epoch > 50: # 连续 50 epoch 无提升 print(f"Early stopping at epoch {epoch}") break

5. 部署与验证:如何用一张图证明你的模型真的懂细胞,而不是在拟合噪声

5.1 可视化热力图:Grad-CAM 不是玄学,而是定位模型“看哪里”的后悔药

YOLOv5 是检测模型,不输出 class activation map,但我们可以 hackDetect层的obj分支,生成Objectness Heatmap(目标存在性热力图),这才是细胞检测最需要的解释性工具——它告诉你模型认为“哪里最可能是细胞中心”。

import torch import cv2 import numpy as np def generate_obj_heatmap(model, img_path, img_size=640): model.eval() img = cv2.imread(img_path) img_resized = cv2.resize(img, (img_size, img_size)) img_tensor = torch.from_numpy(img_resized.transpose(2,0,1)).float().unsqueeze(0) / 255.0 # 提取 Detect 层的 obj 分支输出(假设最后一层是 obj 分支) with torch.no_grad(): pred = model(img_tensor.cuda())[0] # (bs, na, ny, nx, 7) # pred shape: [1, 3, 80, 80, 7] -> 取 obj 分支(索引 4) obj_map = pred[0, :, :, :, 4].cpu().numpy() # (3, 80, 80) # 合并 3 个 anchor 的 obj score obj_map = np.max(obj_map, axis=0) # (80, 80) # 上采样到原图尺寸 heatmap = cv2.resize(obj_map, (img.shape[1], img.shape[0])) heatmap = np.uint8(255 * heatmap) heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) # 叠加原图 result = cv2.addWeighted(img, 0.5, heatmap, 0.5, 0) return result # 使用 model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/train/exp/weights/best.pt') result_img = generate_obj_heatmap(model, 'test_cell.jpg') cv2.imwrite('obj_heatmap.jpg', result_img)

为什么比 Grad-CAM 更可靠:

  • Grad-CAM 依赖分类梯度,而细胞检测中obj分支才是核心信号;
  • obj_map直接反映模型对“此处是否存在细胞”的置信度,医生可直观验证:热区是否与细胞核位置重合?粘连细胞间是否有独立热峰?

5.2 定量验证表:别只信 mAP,这 4 个指标才决定模型能否进实验室

指标计算方式合格线(细胞检测)为什么重要
F1-score @0.32×precision×recall/(precision+recall)≥ 0.85mAP@0.5 过严,细胞定位允许 30% 误差(约 10px)
Recall@100检出细胞数 / 真实细胞数(人工计数)≥ 0.92漏检比误检更致命(漏掉一个癌细胞可能延误诊断)
Precision@100检出细胞中真实细胞占比≥ 0.88误检太多会增加医生复核负担
Inference Speed单图平均耗时(ms)≤ 120 ms(1080Ti)批量处理 2000 张图需 < 4 分钟

提示:Recall@100和Precision@100必须用独立测试集 + 三位病理医生盲数得到真实值,不能依赖 label 文件。我们曾发现某模型 mAP@0.5 达 0.91,但Recall@100仅 0.73——因为 label 中漏标了大量弱染色细胞,模型学到了“只检强信号”。

5.3 生产环境 checklist:从 .pt 到 .onnx 再到 C++ 部署的 3 个硬门槛

  1. ONNX 导出必须指定--dynamic:

    python export.py --weights runs/train/exp/weights/best.pt --include onnx --dynamic

    原因:显微图像尺寸不统一(2000×1500, 4096×3000),静态 shape 会 crash。

  2. C++ 推理时,预处理必须复现 Python 的 normalize:

    // OpenCV Mat img 已读入 img.convertScaleAbs(img, img, 1.0/255.0); // 归一化 // 减去均值 [0.485, 0.456, 0.406],除以标准差 [0.229, 0.224, 0.225] cv::Scalar mean(0.485, 0.456, 0.406); cv::Scalar std(0.229, 0.224, 0.225); img = (img - mean) / std;
  3. 后处理 NMS 必须用 TorchScript 版本:
    ONNX 自带的 NMS 不支持score_threshold动态调整,会导致小目标被滤掉。解决方案:在 Python 中导出带 NMS 的模型:

    # 在 detect.py 中修改 class ModelWithNMS(torch.nn.Module): def __init__(self, model): super().__init__() self.model = model def forward(self, x): pred = self.model(x)[0] # 自定义 NMS,保留 score > 0.25 的框 return non_max_suppression(pred, conf_thres=0.25) traced_model = torch.jit.trace(ModelWithNMS(model), torch.randn(1,3,640,640)) traced_model.save("model_nms.pt")

我带过的 7 个细胞检测项目里,有 5 个卡在“验证环节”——不是模型不行,而是没做Recall@100的人工盲测,结果上线后医生反馈“比人眼还漏”。后来我们强制规定:任何细胞检测模型,必须用 3 位医生独立计数的 100 张图做 baseline,且Recall@100≥ 0.92 才允许进入部署流程。这个习惯救了我们两次:一次发现模型在低曝光图像上系统性漏检,另一次揪出标注工具的坐标偏移 bug。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询