
血常规检验是医院检验科每天的必修课红细胞、白细胞、血小板的计数直接关系着感染、贫血、凝血功能的判断。过去靠人工瞪显微镜数细胞费眼费神不说结果还容易受疲劳影响。这几年基于深度学习的目标检测算法逐渐普及我最近就用YOLOv12做了一套完整的血细胞检测系统模型端基于YOLOv12和公开血细胞数据集完成训练界面端实现了登录注册UI和检测主界面整套代码用Python写成可以打包成桌面应用直接跑起来。这篇文章就当一次完整的技术复盘把思路、踩坑、实操参数都摊开讲讲。1. 项目整体设计与技术选型1.1 血细胞检测到底难在哪很多人一听说检测红细胞、白细胞、血小板第一反应是“数数而已图像分割或者阈值不就搞定了”。真上手就知道不是这么回事。血液涂片经过染色之后细胞在显微镜下呈现的特征确实有规律但真实样本里挑战点特别多目标尺寸差异大。红细胞直径大约6到8微米白细胞10到20微米血小板只有2到4微米。在一张640乘640的图像里血小板可能只占十几个像素属于典型的小目标检测问题。密度极高且互相粘连。红细胞在视野里常常一个贴着一个边界模糊人眼都要仔细看。传统分割算法在这种场景下很难把细胞一个个分出来。类别严重不均衡。一张正常血涂片里红细胞数量是白细胞的几百倍甚至上千倍血小板数量居中。模型很容易被红细胞带偏导致稀有类别检出率低。这些特点决定了用传统图像处理只能做做简单场景想要在真实血涂片图像里稳定数出三类细胞还是得走目标检测的路子。1.2 为什么选了YOLOv12而不是其他模型我最早用的其实是YOLOv5和Faster R-CNN后来也试过YOLOv8和YOLOv10。简单说下我的感受模型优势在这个场景里的问题Faster R-CNN精度确实高两阶段检测对小目标友好推理速度慢帧率上不去很难做成实时辅助工具YOLOv5生态成熟资料多对于密集小目标的召回率不太够YOLOv10无NMS设计推理快在密集重叠场景下容易出现重复框或漏检YOLOv12引入注意力机制精度提升速度保持刚出不久参考资料相对少需要自己多试YOLOv12是今年初发布的模型架构和之前版本最大的区别是把注意力机制和YOLO的CSP结构做了有效融合用的是一种叫区域注意力Area Attention的机制。简单理解传统Transformer里的注意力要对整张图两两计算相似度计算量大YOLOv12的区域注意力把计算范围限制在局部区域显著降低计算量和内存占用同时保留了对关键特征的建模能力。放在血细胞检测这个场景里这种“精度和速度兼顾”的路线正好契合需求一方面细胞尺寸小、密度大需要有足够强的特征表达能力另一方面临床辅助工具讲究响应够快最好能做到实时或接近实时。我实测下来同样的硬件条件下YOLOv12的精度比YOLOv10有明显提升推理速度没有明显退步所以最终选了它。1.3 系统架构与模块划分整个系统我按三层来拆数据层负责数据集的整理、标注格式转换、数据增强。这一层决定了模型能学到什么比重最大。模型层YOLOv12模型的训练、验证、导出。训练阶段跑出最优权重推理阶段提供检测和统计结果。界面层Python实现的桌面应用包含登录注册窗口和检测主窗口。用户在主窗口上传图片、在线推理、查看结果和统计数量。之所以把界面单独做成桌面应用而不是用Web原因是我希望这套系统能在不联网的环境下用比如检验科的内网电脑。Python的PyQt5打包成exe之后没有网络也能跑部署成本最低。这套架构对于想快速用起来的场景很实用后续要加摄像头实时检测、批量处理之类也都在解耦后的模块上做扩展。2. 数据集构建与预处理2.1 数据来源与清洗思路我做这个项目用的血细胞数据集选取了常用的公开血液涂片数据集包含了红细胞、白细胞、血小板三类目标的标注框图像是显微镜下的血液涂片样本。公开数据集的好处是标注相对规范拿来就能用适合做模型验证。但有一说一公开数据集和真实医院检验科拍出来的图还是有差异的主要体现在染色条件不同、视野区域不同、放大倍数不同。所以在拿到数据之后我做了两步过滤剔除模糊和染色异常的图。有些图明显对焦不准标注再准也没有训练价值反而会把模型带偏。检查标注框是否贴合目标边界。偶尔会有标注框把背景大片框进去的情况这种样本在训练时会引入噪声。工业落地的时候建议再补充自己实验室的涂片数据。每个人实验室的染色方案、显微镜型号不同图像分布会偏移用公开数据集做预验证可行但要放到真实环境得靠自己的数据做微调。2.2 标注格式转换与数据集划分YOLO系列训练需要的是txt格式的标注文件每行代表一个目标框class_id x_center y_center width height注意这里的坐标值是归一化到0到1的浮点数不是像素坐标。而很多公开数据集的标注是PASCAL VOC格式的XML文件里面直接给的是bbox的像素坐标。所以第一步要做的就是把XML转成YOLO格式的txt。转换的核心逻辑是读取XML里的bndbox信息做归一化import xml.etree.ElementTree as ET def xml_to_yolo(xml_path, out_txt_path, class_map): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): class_name obj.find(name).text if class_name not in class_map: continue class_id class_map[class_name] box obj.find(bndbox) x_min float(box.find(xmin).text) y_min float(box.find(ymin).text) x_max float(box.find(xmax).text) y_max float(box.find(ymax).text) x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines))转换完成之后建立标准的目录结构dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/数据集划分比例我用的是8:1:1即80%训练集、10%验证集、10%测试集。划分的时候要注意先按“图像”为单位随机打乱千万不要直接按文件名顺序切不然后面的样本全是同一批染色条件验证集就失去了意义。2.3 数据增强与类别不平衡血细胞数据集天然存在类别不平衡红细胞数量太多白细胞太少。如果直接灌进去训练模型会倾向于把一切像细胞的东西都判定为红细胞白细胞的召回率会很难看。处理方式我做了三件事统计类别分布。先跑一遍脚本把三个类别的标注数量统计出来。拿到数字之后对数量偏少的类别给更高的样本权重。针对少数类做过采样。训练时对包含白细胞或血小板较多的图像提高采样概率相当于让模型在每一轮训练里多看几遍这些稀有样本。数据增强。YOLOv12自带的数据增强策略要充分利用起来我在配置里主要开了这么几项增强方式参数设置作用HSV颜色抖动hsv_h0.015, hsv_s0.4, hsv_v0.4模拟不同染色条件下的颜色差异随机水平翻转fliplr0.5增加样本多样性Mosaicmosaic1.0把四张图拼在一起提升模型对密集场景的适应能力MixUpmixup0.1图像融合增强缓解过拟合有一点需要注意血细胞涂片不适合做大幅旋转增强。细胞本身有形态学特征比如红细胞是中间凹陷的盘状旋转90度甚至180度之后形态特征会产生歧义模型容易被带偏。所以我刻意没有开rotate增强保留flip和颜色抖动就够用了。3. YOLOv12核心机制与训练实录3.1 YOLOv12的关键改进YOLOv12对应的论文标题是“Attention Is All You Need”思路在YOLO架构里的实践但它没有生搬硬套Transformer而是做了两个非常关键的取舍。首先是区域注意力机制。传统注意力理论上是全局的即每个像素都要和所有其他像素计算相关性这在图片尺寸稍大一点的时候内存就扛不住了。YOLOv12将注意力计算限制在目标中心的局部邻域内每片区域独立计算大幅降低计算量。从效果看这个设计对中小尺寸目标特别奏效因为小目标本身需要的就是局部上下文不属于“全局信息依赖型”的目标。其次是注意力与跨阶段特征的融合方式。YOLO系列一直用CSP结构减少计算冗余YOLOv12把注意力模块嵌入到这个结构里不是简单地在输出端挂一个注意力层而是让注意力参与不同层级特征的融合保持实时性的同时提升表征能力。对我这种做实际项目的人来说我关心的不是理论多漂亮而是这个模型在不上大显卡的前提下能不能用。我在一块消费级显卡上跑YOLOv12n批大小为16640输入分辨率显存占用在可控范围内这比想象中友好。3.2 训练配置与超参数选择训练配置这里我把能直接用的YAML和命令贴出来# blood_cell.yaml path: ./dataset train: images/train val: images/val test: images/test names: 0: RBC 1: WBC 2: Platelets训练命令yolo train modelyolov12n.pt datablood_cell.yaml epochs150 imgsz640 batch16 device0 optimizerAdamW lr00.001 patience20几个参数的选择逻辑imgsz640是默认值但如果目标是血小板这种小目标我建议提高到768甚至960。分辨率越高小目标的像素占比就越大检测效果通常显著变好代价是训练推理速度下降。我做了一组对比640输入下血小板AP50接近80%960输入下能到86%左右。optimizerAdamW。YOLO系列默认优化器是SGD但我用AdamW在微调阶段收敛更平稳。如果是从零开始训练SGD也完全可以。patience20是早停参数。验证集mAP连续20轮没有上升就停掉避免无效的训练时间。3.3 训练过程与技术指标训练过程大概分了三个阶段前30轮loss下降非常快从十几降到五左右这是模型在快速学习主导类别红细胞的特征30到80轮loss下降变缓主要是在提升白细胞和血小板的识别能力80轮之后进入平台期早停在120轮左右触发。最终在测试集上的指标参考类别PrecisionRecallmAP50mAP50-95红细胞0.950.970.990.79白细胞0.910.860.930.72血小板0.900.830.880.58整体0.920.890.930.70这个结果基本可用。血小板mAP50-95偏低是正常的小目标的边界框预测容错空间本来就小IOU稍高一点就判为错不影响实际计数使用。如果非要追求更高的mAP50-95可以试试SAHI切片推理把大图切块放大再检测只是速度会慢很多。4. UI界面、登录注册与系统集成4.1 界面框架选型项目里需要的界面有两块登录注册窗口和检测主窗口。我选了PyQt5来做理由是控件生态完善文件选择、图片展示、表格展示都有现成组件。布局用Qt Designer或者直接代码写都比较顺手适合快速开发。打包成exe的成熟度高配合PyInstaller踩坑少。Tkinter也可以但做这种带图片显示、实时状态更新的界面PyQt5的体验好不少。代价是打包体积会大一些但几十MB的安装包在本地部署完全可以接受。4.2 用户系统的代码实现先看数据库设计。用户信息我用SQLite存建表语句CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, salt TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );密码不能明文存储这是底线。我用了加盐哈希方案注册时生成一个随机盐把盐和密码拼接之后做SHA256迭代哈希把盐和哈希结果都存到数据库里。登录验证时用同样的盐把输入的密码哈希一遍再和数据库存储值比对。下面的代码是核心部分import hashlib import os import sqlite3 def hash_password(password, saltNone): if salt is None: salt os.urandom(16).hex() digest hashlib.pbkdf2_hmac(sha256, password.encode(), salt.encode(), 100000) return salt, digest.hex() def register_user(username, password): conn sqlite3.connect(system.db) cursor conn.cursor() if cursor.execute(SELECT id FROM users WHERE username?, (username,)).fetchone(): return False, 用户名已存在 salt, digest hash_password(password) cursor.execute(INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?), (username, digest, salt)) conn.commit() conn.close() return True, 注册成功这里的核心是pbkdf2_hmac迭代10万次避免彩虹表和暴力破解。实际做的时候我还加了登录失败次数限制连续失败五次锁定五分钟防止有人写脚本暴力爆破。4.3 检测主界面与整体交互检测主界面的逻辑不复杂但有一个关键点模型推理必须放到子线程不能放在UI主线程里。否则检测一张大图需要一两秒界面会直接卡成未响应。我用了QThread来做异步推理class DetectorThread(QThread): result_ready pyqtSignal(dict) def __init__(self, model_path, image_path, conf_thres0.25, iou_thres0.45): super().__init__() self.model YOLO(model_path) self.image_path image_path self.conf_thres conf_thres self.iou_thres iou_thres def run(self): results self.model.predict( sourceself.image_path, confself.conf_thres, iouself.iou_thres )[0] boxes results.boxes counts {RBC: 0, WBC: 0, Platelets: 0} for cls_id, conf in zip(boxes.cls.tolist(), boxes.conf.tolist()): class_name results.names[int(cls_id)] counts[class_name] 1 self.result_ready.emit({ counts: counts, img: results.plot(), conf: conf, total: int(len(boxes)) })主界面布局我分了三个区域左上图像展示区。用QLabel承载QPixmap显示原始图和检测结果图。右上操作区。上传图片、打开摄像头、开始检测、参数设置。底部统计栏。显示三类细胞的计数、置信度平均值、总耗时。对于细胞计数场景我还在统计栏里额外加了“细胞数量占比”的显示因为检验医生不仅看绝对值也会关注白细胞比例是否异常这个比例在感染诊断里很有参考价值。实测下来这个小功能比想象中受欢迎。5. 常见问题排查与性能优化5.1 模型层面小目标漏检、误检这个项目里90%的调优时间都花在小目标检测上。我踩过的坑和解决路径如下漏检大量血小板。起初我把置信度阈值设到了0.5血小板这种小目标置信度天然偏低大量被过滤。解决方式是阈值降到0.25同时对血小板类别在测试集上单独做置信度校准。把染色杂质误检为细胞。血液涂片里常有染色沉淀物大小和血小板接近模型早期容易误报。加了HSV颜色抖动增强后模型学会了分辨细胞和杂质误检明显减少。白细胞和血小板混淆。个别淋巴细胞体积偏小边界框和大型血小板很接近。这种场景纯视觉目标检测的区分其实有限我最后的处理是保持边界框原样但在统计栏对白细胞做特别提示让医生肉眼复核。5.2 系统层面卡顿、打包、模型文件路径界面卡顿八成是前面说的线程问题。如果你打开界面上传图片之后就点不了按钮十有八九是推理代码放在了UI线程。把推理挪到QThread里界面立刻就顺畅了。打包成exe之后还有一个非常经典的坑模型文件路径找不到。开发环境里直接写weights/best.pt没问题但PyInstaller打包后程序的当前工作目录可能会变导致模型文件加载失败。解决办法是用绝对路径定位程序所在目录import os import sys def resource_path(relative_path): base_path getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base_path, relative_path) model YOLO(resource_path(weights/best.pt))另外exe启动后如果杀毒软件拦截多半是PyQt5的动态库行为被误报添加信任目录就好不是代码问题。5.3 几条实战调试建议训练之前先只跑十几个step看loss能不能降下来能降再全量训练省时间。训练过程中的可视化看板建议开起来loss和mAP曲线一目了然遇到异常能尽早发现。导出模型推理时如果追求速度可以导出成ONNX格式用ONNXRuntime跑推理实测比PyTorch原生快一倍左右。摄像头实时检测时不要每帧都推理做跳帧处理比如每三帧推理一次中间两帧沿用上一次结果体验提升很大。最后再分享一点实际操作中的体会做这类辅助诊断系统最忌讳的就是把模型指标当成一切。YOLOv12的mAP做到93%听起来不错但真正能放进检验工作流里还要考虑数据偏移、边界情况、界面交互的顺畅度。我在这套系统的开发过程中花了不少时间做数据清洗和界面细节微调模型结构反而是最省心的部分。这个项目后续还有很多可以扩展的方向比如增加白细胞五分类检测、接入摄像头实时多视野扫描、把计数结果自动生成检验报告等。如果你也在做类似的血细胞检测系统我建议先把单张图像的检测效果打磨扎实再一步步往工程化方向走少走很多弯路。