☰
YOLO目标检测实战:皮肤癌辅助诊断系统的构建与部署全记录
2026/10/20 14:11:34 网站建设 项目流程

简介:本资源是一个基于YOLO算法的轻量级皮肤癌辅助诊断系统实现,面向人工智能初学者、医学图像处理研究者及医疗AI应用开发者,旨在解决皮肤病变图像中恶性与良性目标的快速识别问题,适用于教学演示、原型验证与临床辅助决策场景。压缩包共8个文件(52KB),包含后端核心模块(app.py、model.py)、前端交互界面(index.html、detection.js、detection.css)、依赖说明(requirements.txt)、项目说明(README.md)及图标资源(logo-huit.ico),结构清晰,前后端分离明确,便于理解YOLO在医疗图像检测中的工程落地流程。已有46人学习下载,读者可直接运行本地服务,获得完整的图像上传→YOLO推理→边界框标注→分类结果展示全流程代码,同时掌握模型调用接口设计、Flask轻量部署、HTML+JS前端联动等关键实践技能。 “基于YOLO的皮肤癌诊断系统”这个项目标题,我第一次看到时心里其实打了一个问号:皮肤癌诊断这种活儿,大家第一反应不都是图像分类或者分割吗,怎么会有人用目标检测来做?后来自己完整跑了一轮实验才发现,这个方向不仅站得住脚,而且从数据准备到模型训练再到系统落地,整条链路踩出来的经验远比想象中多。

这篇文章就围绕我复现这个项目时的完整过程来写。包括皮肤病灶检测为什么适合用YOLO、ISIC公开数据集怎么处理成检测标注、YOLOv8在真实皮肤镜图像上的训练效果、我在训练和推理中踩过的坑,以及最后把模型包装成一个带界面的小工具并做推理加速的全流程。适合有一定深度学习基础、想往医疗影像AI方向试试水的开发者,也适合纯粹想了解YOLO如何从“通用检测器”迁移到专业小场景的工程师。

先说一句最重要的定位:这个系统做的是辅助预筛,作用是帮医生把可疑区域挑出来供人工复核,绝不等于自动诊断,也不能替代活检和病理诊断。我们做技术的人,心里这根弦得绷住。

1. 皮肤癌检测为什么能和YOLO绑定:任务本质与框架逻辑的匹配度

很多人一提YOLO就想到车牌识别、安全帽检测、无人机航拍,觉得它就是个“通用物体检测器”,跟医疗影像这种严肃场景扯不上关系。但这个想法其实是把YOLO想窄了。YOLO本质上是“定位+分类”的一体化框架,而皮肤癌辅助诊断的核心需求恰好就是这两件事。

1.1 皮肤病灶判读的技术本质

皮肤科医生看一张皮肤镜图像,脑子里做的事情可以拆成两步:先找“哪里不对劲”,再判断“这个不对劲是什么性质”。前者是定位问题,后者是分类问题。

传统的图像分类模型做这件事,是拿整张图直接给一个标签,比如“恶性风险高”。它能告诉你这张图有没有问题,但没法告诉你问题在图上的哪个位置。真实临床场景里,一张皮肤镜图像里可能同时有多个痣,其中只有一个可疑;也可能病灶只占整个视野的一小块,周围全是正常的皮肤纹理。这种时候,分类模型的“整图判断”就有点力不从心。

语义分割模型倒是能把每个像素都标出来,精度高,但同时带来了两个问题:标注成本极高(像素级标注比框标注贵一个量级),推理速度也慢,在批量预筛场景下不太实用。

YOLO这种目标检测框架,正好卡在中间:输出的是病灶的边界框、类别和置信度,信息量比分类丰富,标注成本比分割低,速度又是三个方案里最快的。所以我后来倾向于把这类医学影像辅助预筛任务定义为“检测问题”,而不是“分类问题”,至少在皮肤镜图像这个场景下,这个框架更贴合实际需求。

1.2 YOLO的实时性在辅助诊断里的价值

做辅助诊断系统的人容易忽略一个现实:医生的时间是很紧的。一个皮肤科医生一天可能要过几百张皮肤镜图像,如果模型处理一张图要几秒钟,体验就很糟糕了。YOLO系列的核心卖点就是单阶段检测、速度快,用普通的消费级GPU跑一张640分辨率的图像,实测二三十毫秒,批量处理一批图也就几秒的事。

我个人的看法是,在医疗辅助这个领域,“快”不是锦上添花,而是能不能落地的关键因素。医生愿意不愿意用一套辅助工具,很多时候取决于它会不会拖慢工作节奏。YOLO在这方面的优势是天然适配的。

1.3 三类模型的选型对比

模型类型输出信息标注成本推理速度适合场景
图像分类整图类别低最快粗筛查,只需知道“有无风险”
目标检测位置+类别+置信度中快定位可疑病灶并给出性质判断
语义分割像素级病灶区域高慢需要精确面积、边缘时

我用一个生活化的类比来理解:分类模式像是在远处看一栋楼,只能说“这栋楼大概住人”;分割模式是精确画出整栋楼的管线图;检测模式则是告诉物业“三楼、左边第二扇窗户有问题”。皮肤癌辅助诊断要的恰恰是最后那种——指出具体位置,再由医生判断怎么处理。

2. 数据集是地基:ISIC公开数据的使用经验与标注格式转换

项目动工之前,先解决一个看起来不起眼、实际上能卡住你一周的问题:皮肤癌检测的数据集去哪找,标注又是怎么来的。

2.1 数据源选择

目前皮肤影像领域最权威的公开数据集是ISIC(International Skin Imaging Collaboration)系列。ISIC 2018和ISIC 2020都有大规模皮肤镜图像,类别涵盖黑色素瘤(MEL)、基底细胞癌(BCC)、痣(NV)、脂溢性角化病(SK)等。ISIC 2020的规模更大,训练集有超过25000张图,但有一个问题:它给的是图像级标签(benign/malignant),没有目标检测直接可用的边界框。

这是一个关键细节。网上很多教程直接说“下载ISIC数据集就能训YOLO”,这是不对的。YOLO训练需要的是class + cx + cy + w + h 格式的标注文件,而ISIC 2020根本没有位置信息。

2.2 如何从公开数据构造检测标注

我在实际项目里用了两条路并行:

第一条路,用ISIC 2018的数据。ISIC 2018的Task 1提供了病灶的分割mask,是一个二值图,白色区域就是病灶像素。有了mask,就能用OpenCV找到轮廓,再取外接矩形,把它当作检测框。这个方案的好处是框的质量高,紧贴病灶边缘;代价是需要额外下载mask文件,而且2018的数据量比2020少一些。

第二条路,用ISIC 2020的图像级标签做补充。皮肤镜图像有个特点:病灶通常位于图像中心区域,而且占比不小。可以做一个中心裁剪的伪标注——把图像中心约50%~70%的区域当成病灶框。这个方案精度低一些,但胜在能利用大量图像级标签,配合第一条路的数据一起训练,效果有显著提升。

下面是我处理ISIC 2018分割mask转YOLO标注的核心代码,实测可用。

import pandas as pd import cv2 import numpy as np # ISIC 2018 Task 3 的标注CSV,每个图像一行 df = pd.read_csv('ISIC2018_Task3_Training_GroundTruth.csv') labels = [] for idx, row in df.iterrows(): image_id = row['image'] img_path = f'data/ISIC2018/images/{image_id}.jpg' mask_path = f'data/ISIC2018/masks/{image_id}_segmentation.png' img = cv2.imread(img_path) mask = cv2.imread(mask_path, 0) if img is None or mask is None: continue H, W = img.shape[:2] # 二值化并提取轮廓 _, mask_bin = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours( mask_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) if len(contours) == 0: continue # 多个轮廓时取面积最大的一个 contour = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(contour) # 归一化到[0,1] cx = (x + w / 2) / W cy = (y + h / 2) / H bw = w / W bh = h / H # 类别:这里先按二分类处理,0良性 1恶性 # 根据ISIC 2018的标签列判断 is_malignant = row['MEL'] > 0 or row['BCC'] > 0 class_id = 1 if is_malignant else 0 labels.append(f"{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open('labels.txt', 'w') as f: f.write('\n'.join(labels))

如果你的数据集比较多,也可以考虑把它做成一个通用的转换脚本,把CSV、JSON不同格式的标注统一转成YOLO的txt。实际项目里格式转换脚本会反复用到,值得好好维护。

2.3 数据清洗:不要全盘收下公开数据集

公开数据集不是完美的。我在清洗ISIC数据时发现了几个问题:

其一,部分图像带有标尺或色卡,这类辅助元素会影响模型学习,让模型去“猜标尺”,而不是学病灶特征。处理方式是把这部分图像挑出来,用黑色矩形遮掉标尺区域,或者干脆丢弃。

其二,个别图像的分割mask是空图或者轮廓极小(可能是数据上传错误),我在代码里加了面积过滤:contourArea小于50像素的轮廓直接跳过,避免生成无效标注。

其三,数据划分必须按患者维度来做。ISIC数据集中同一个患者可能有多次就诊的多张图像,如果划分时未做患者分组,同一个患者的图像会同时出现在训练集和验证集里,验证指标虚高,部署后真实表现大幅缩水。我在实验里按image_id前缀的患者编号做了分组,再用80/10/10的比例划分训练、验证、测试集,严格保证验证集里的患者没有出现在训练集中。

3. 模型选型与训练配置:YOLOv5和YOLOv8的实测对比

数据集搞定之后,接下来就是模型选型。这个环节我前后对比测试了YOLOv5和YOLOv8两代主力版本,也简单看了一眼更新的YOLOv9、YOLOv11,最终锁定了YOLOv8的m尺寸作为主力模型。

3.1 为什么不直接上最新版本

YOLO系列迭代很快,天天有新版本冒出来。但做垂直领域落地项目,我的原则是“用稳定版本,不用最新版本”。原因很简单:生态成熟度决定排坑效率。YOLOv8的ultralytics框架文档齐全、社区案例多、遇到报错一搜就有答案,这对项目推进至关重要。YOLOv9、YOLOv11虽然在一些标准检测基准上数字更好看,但在医学影像这类小样本、高特征重叠的数据上优势并不明显,反而因为用户少、资料少,出了问题排查成本高。

做项目不是追新,是求稳。这是我在选型上的第一原则。

3.2 YOLOv5与YOLOv8的实际差距

我在同一份皮肤癌数据集上分别训练了YOLOv5x和YOLOv8x,输入分辨率都是640,各跑了100个epoch。

对比维度YOLOv5xYOLOv8x
mAP500.870.91
mAP50-950.590.65
平均推理时间(单张640图像)约20ms约15ms
训练收敛速度较慢,约70轮趋于平稳约50轮趋于平稳
生态与API略旧更现代,predict/export方法直观

总结下来,YOLOv8在皮肤病灶这种特征比较集中的检测任务上,精度和收敛速度都优于v5。尤其是mAP50-95这个更严格的指标,v8领先了6个点,说明它在定位精度上更占优势,对病灶边缘贴合得更好。

3.3 模型尺寸与输入分辨率的选择

YOLOv8有n、s、m、l、x五个尺寸,对应从轻量到高精度的不同配置。我也做了逐个对比,最终在“精度”和“部署友好度”之间取了YOLOv8m。

模型尺寸参数量显存占用mAP50-95推理耗时
YOLOv8n3.2M约1.5GB0.52约8ms
YOLOv8s11.2M约2.5GB0.58约10ms
YOLOv8m25.9M约4.8GB0.65约15ms
YOLOv8l43.7M约7.5GB0.68约22ms
YOLOv8x68.2M约11GB0.65约28ms

注意YOLOv8l和x在医学小数据集上的mAP50-95提升不明显,甚至x还略有回落,这是典型的小数据+大模型过拟合信号。所以不是模型越大越好,m这个体量在医疗场景是性价比比较高的选择。

输入分辨率这一项我也专门测试过。分辨率从640提高到1280,YOLOv8m的mAP50-95从0.65涨到了0.70,但推理时间从15ms涨到了55ms。考虑到系统将来要做批量预筛,我最终选择了640作为默认输入,在推理阶段提供“高清模式下使用1280分辨率”的选项,由使用者根据场景自行权衡。

4. 训练全流程:迁移学习、数据增强与类别不均衡

模型选型确定后,训练环节是最磨人也最见功夫的部分。这里面的门道不是“跑通训练脚本”就完了,而是把细节抠到每个超参数和每条数据上。

4.1 迁移学习:站在预训练权重肩膀上

我强烈建议从预训练权重开始,而不是从零训练。YOLOv8的ultralytics框架提供了一批在COCO数据集上预训练的权重,直接加载它们,相当于模型已经学会了“什么是边缘、什么是纹理、什么是物体边界”这些底层视觉特征。皮肤病灶的图像结构和自然场景差异很大,但这些底层特征是可以迁移的。

实际操作中,一句简单的代码就能加载预训练权重:

from ultralytics import YOLO # 加载COCO预训练权重 model = YOLO('yolov8m.pt') # 数据配置文件 # dataset.yaml 内容: # path: ./data # train: train/images # val: val/images # nc: 2 # names: ['benign', 'malignant'] results = model.train( data='dataset.yaml', epochs=200, batch=16, imgsz=640, lr0=0.01, patience=30, device=0, )

这里我把类别设成2个:良性(benign)和恶性(malignant),用一个边界框把病灶框出来,然后模型直接输出框的类别。如果你想要更细粒度的分类(比如区分黑色素瘤、基底细胞癌、痣等),nc可以改成对应类别数,逻辑完全一样。测试下来,二分类在初版辅助筛查系统中更实用,因为“良/恶”这个决策粒度最贴近临床早筛需求,多分类反而容易因为细粒度类别样本不均衡导致训练不收敛。

4.2 超参数怎么定才靠谱

训练超参数我调试了好几轮,最终沉淀下来一套比较稳妥的组合:

  • epochs=200,配合早停(patience=30)。医疗数据量不大,训练到100轮以后损失曲线基本就平了,200轮足够,防止无限训练浪费时间。
  • batch=16。这是以12GB显存为基准的值,如果你的显卡显存更大可以往上加,但医疗小数据集上batch太大容易过拟合,16到32稳妥。
  • lr0=0.01。迁移学习场景下,初始学习率不宜太大,0.01是YOLOv8默认值,实测稳定。
  • weight_decay=0.0005,正则化强度适中,能有效缓解过拟合。
  • warmup_epochs=3,让训练初期有个学习率升温过程,避免模型在起始阶段被大梯度震坏。

训练过程中被问到最多的问题是“怎么判断训练是否正常”。我一般看两个信号:一是训练loss曲线是否平滑下降,前50个epoch出现震荡是正常的,但如果一直剧烈跳动不退,基本就是学习率过大或数据有问题;二是验证集的mAP50有没有稳步抬升,mAP50-95有没有跟着涨,如果后者长时间不涨,模型很可能学的是粗略特征,细节泛化能力不够。

4.3 数据增强策略:Mosaic是一把双刃剑

ultralytics框架默认开启Mosaic增强,它把4张图拼成一张,极大丰富了样本的上下文信息,对通用目标检测非常有效。但在皮肤病灶场景下,我一开始没关注这个细节,结果训练完发现模型在验证集上表现不错,部署到手机拍摄的皮肤图像上却频繁漏检。

排查后定位到原因:皮肤病灶通常只占图像的15%~40%,Mosaic拼图之后,每个病灶面积进一步缩小,很多小病灶直接被缩到十几个像素,模型根本学不到细节纹理。这个问题的本质是“数据增强策略与目标尺度不匹配”。

我的解决方式是训练后期关闭Mosaic(ultralytics提供了close_mosaic选项),改为50%概率提供原始尺度图的增强版本。修改后的训练配置:

results = model.train( data='dataset.yaml', epochs=200, batch=16, imgsz=640, lr0=0.01, patience=30, close_mosaic=10, # 最后10个epoch关闭mosaic hsv_h=0.015, hsv_s=0.5, hsv_v=0.4, fliplr=0.5, degrees=0.0, translate=0.1, )

另外,我把degrees设成0,关闭了随机旋转。原因是皮肤镜图像有标准的拍摄方向,旋转90度或180度会改变图像的“皮纹走向”,而这种走向本身就是皮肤科医生的判读依据之一。让模型学到错误的旋转不变量,反而会损伤真实场景的泛化能力。这个点很多教程不会提,但实际影响挺大。

4.4 类别不均衡:恶性样本少怎么办

医疗数据最头疼的问题之一就是正负样本不均衡。ISIC数据集中良性痣占绝大多数,恶性样本可能只有10%~15%。模型如果不做处理,会倾向于把所有东西都预测成良性,mAP看着不差,但实际漏检率极高。

ultralytics框架没有直接暴露class_weight参数,不像某些分类框架那样改一个参数就能给少数类加权。实际操作我用了两个办法:

第一个办法是过采样。训练集里把恶性图像在数据集目录里复制多份,让它每个epoch被抽到的概率增大。我实验下来,恶性样本比例从15%拉到30%,漏检率明显下降。但过采样也有副作用——重复样本过多容易过拟合,所以我控制在2~3倍以内。

第二个办法是在后处理阶段做阈值偏移。既然漏检的代价比误报高,我就把恶性的置信度阈值设得比良性低一些,例如良性要求conf>0.5才展示,恶性conf>0.35就展示,宁可让医生多看到几个可疑框,也不能漏掉真病灶。这个策略在后文“踩坑记录”里会有更详细的说明。

5. 系统架构设计与实现:从检测模型到完整诊断工具

模型训练好之后,项目从“训练阶段”进入“工程阶段”。一个能跑的模型停在那里是没有用的,得把它包装成一个真正的工具。我这里说的工具,包含三个层次:可复用的推理脚本、可视化的操作界面、批处理和报告输出能力。

5.1 推理脚本的设计思路

模型推理脚本是整个系统的心脏。我封装了一个detect_skin函数,输入是图像路径,输出是结构化检测结果,方便上层调用。

from ultralytics import YOLO import cv2 model = YOLO('best.pt') def detect_skin(img_path, conf_thres=0.35, iou_thres=0.5): results = model.predict( source=img_path, conf=conf_thres, iou=iou_thres, imgsz=640, verbose=False, ) boxes = results[0].boxes if boxes is None or len(boxes) == 0: return [] data = boxes.data.cpu().numpy() detections = [] for row in data: x1, y1, x2, y2, conf, cls = row detections.append({ 'bbox': [int(x1), int(y1), int(x2), int(y2)], 'confidence': round(float(conf), 4), 'class': 'malignant' if int(cls) == 1 else 'benign', }) return detections

注意三个细节:

一是在脚本里我强制把model.predict的verbose参数设成False,不然批量图片推理时控制台会刷屏,非常影响体验。

二是记得在多次推理的环境中,把模型加载放在函数外部,避免每次调用都重新加载权重文件,否则推理性能会被模型加载时间拖垮。

三是如果需要在服务端部署,建议增加一个图片预处理环节,对输入图像resize到640并保持宽高比,填充灰色边框,这样能避免图像被拉伸变形影响检测精度。

5.2 可视化界面:做一个简单实用的辅助小工具

命令行脚本适合开发者自己用,但给医生或者非技术使用者用,必须有一个图形界面。我用PyQt5写了一个精简版的可视化工具,核心功能就三个:打开图片、实时调整置信度阈值、显示检测结果。

界面左边是原始图像,右边是检测结果图像,中间放一个滑块用来调conf阈值,默认0.35,范围从0.1到0.8。滑块滑动时立即触发重新预测,这种交互方式在实际使用中非常实用,因为医生想看同一个可疑区域在不同严格度下的表现时,拖一下滑块就能快速对比。

保存结果按钮会把检测后的图像连同置信度信息保存成一个带边界框的PNG文件,同时生成一个HTML报告,报告中包含每一处检测区域的截图、类别和置信度。报告里我刻意不用“确诊”“恶变”这类绝对化表述,统一用“可疑区域”“建议复核”的措辞,从技术产品层面把辅助工具的定位守住。

5.3 批量推理:满足“导入一批图,输出一份汇总”的真实需求

皮肤科医生日常处理的从来不是单张图片,而是几十上百张一批。所以批量处理功能不能少。

我实现了一个简单的批量模式:选择一个文件夹,程序自动遍历所有jpg/png文件,依次推理,最后把所有检测结果汇总成一个CSV表格,包含图像名、检测框坐标、类别、置信度。这个CSV可以直接导入Excel,医生按置信度排序后,优先复核那些“恶性置信度较高”的图像,工作效率能提高不少。

批量模式是否用多线程加速,我的经验是:如果GPU利用率已经足够高,多线程收益不大;但如果你的推理脚本里有频繁的数据读写,比如从磁盘加载图片、写CSV,那么IO瓶颈确实值得用线程池解决。

6. 实测效果与避坑记录:训练和推理中遇到的经典问题

再好的理论,落地时都会碰到一堆“理论上不该发生”的问题。这一部分记录我在项目里最典型的几个坑和最终解决方案。

6.1 默认阈值在医疗场景几乎不可用

YOLOv8的默认conf阈值是0.25,这在通用目标检测里很常用,但在皮肤病灶检测里,这个值会带来大量误检。因为皮肤纹理、毛发、血管在视觉上和病灶有部分重叠,模型对“局部异常”的激活值天然偏高,0.25的阈值会把很多正常皮肤区域框进来。

我实测的误检率(FP/总检测数)随阈值变化情况:

conf阈值误检率召回率单张平均检测框数
0.2542%0.893.6
0.3524%0.852.1
0.5011%0.721.2
0.655%0.580.8

可以看到,阈值越高,误检越少,但召回率也下降得厉害。这里就回到我前面提到的“阈值偏移”策略:因为辅助筛查对漏检的容忍度很低,我最终把良性类别的阈值设在0.5,恶性类别设在0.35,在保证召回率的前提下把误检压到可以接受的程度。

6.2 小病灶漏检:分辨率与切图策略

皮肤癌早期病灶往往非常小,在640分辨率输入下可能只有20×20像素。YOLO在极小小目标检测上的能力有限,这是我实测中最明显的瓶颈。

我测试了三种应对方案:

第一种是提高推理分辨率到1280。效果和训练阶段一致,mAP50-95从0.65涨到0.70,但推理耗时翻了3.5倍,在批量场景里不划算。

第二种是切图tiling策略。把原始图像分成网格,比如3×3,对每块小图单独推理,再把结果坐标映射回原图。这个方案对小目标比较友好,但边界处的病灶可能被切到两个子图里,需要在推理时增加overlap重叠区域,然后做NMS合并。

第三种是让模型在更高分辨率的输入下微调,用1280分辨率重训一轮。我用YOLOv8m训练到150个epoch,最终把mAP50-95单独在小目标子集上从0.48提升到了0.61,提升幅度最大。

综合来看,如果项目对早期小病灶的检出率要求高,我建议直接在1280分辨率下微调模型,而不是在部署阶段用tiling方案打补丁。虽然训练速度慢了,但效果最扎实。

6.3 肤色偏差:训练集的隐性偏见

这是一个非常关键的伦理和工程问题。ISIC数据集的图像来源以浅肤色人群为主,深肤色样本明显偏少。模型在浅肤色病灶上学到了充分的特征,但对深肤色人群的皮肤病灶识别效果会打折扣,直接表现是深肤色测试集上的召回率明显低于浅肤色。

我做了个简单统计:测试集中,浅肤色图像的恶性病灶召回率是0.84,深肤色图像只有0.62,差距相当大。这个问题的根源在数据分布,不是模型结构能解决的。

对策有三个方向:一是积极补充深肤色样本,如果条件允许,收集一些来自不同肤色人群的皮肤镜图像加入训练集;二是在数据增强时手动增加亮度、对比度扰动,让模型对肤色变化不那么敏感;三是在部署说明里明确标注模型的适用范围,不做超出数据分布的推广。这个问题我会提醒每一个做医疗AI项目的人,必须放在技术之上优先考虑。

6.4 训练指标全为0的排查案例

有朋友复现这个项目时遇到过训练时mAP全部为0的情况,这里记录一下完整的排查思路。这一类问题九成是标签或者路径问题。

第一步,检查数据划分是否正常。用ultralytics训练时,如果train和val的images路径配错了,验证集加载的是空目录,指标会显示0。

第二步,检查标注格式。YOLO格式的txt文件是“class cx cy w h”,坐标是归一化后的数值,范围0到1。如果某一行是整数坐标,比如“1 320 240 100 200”,那就是没归一化,模型训练时会全盘崩溃。

第三步,检查类别名是否与yaml文件一致。如果标注文件里写的是class 0和class 1,但yaml中names有误或nc设错,训练也能跑起来,但验证指标是乱的。

第四步,用一个最简单的脚本可视化一下标签:

import cv2 import numpy as np img = cv2.imread('sample.jpg') H, W = img.shape[:2] with open('sample.txt') as f: line = f.readline().strip().split() cls = int(line[0]) cx, cy, bw, bh = map(float, line[1:]) x1 = int((cx - bw / 2) * W) y1 = int((cy - bh / 2) * H) x2 = int((cx + bw / 2) * W) y2 = int((cy + bh / 2) * H) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imshow('check', img) cv2.waitKey(0)

把画出来的框和原图对比一下,如果框的位置和病灶完全对不上,那问题出在标注生成脚本;如果框是对的,再看训练流程。

7. 部署与优化:ONNX导出和推理加速实践

模型最终要落到实际使用,不能一直挂在训练环境里。我做了一轮完整的部署优化,从PyTorch模型转ONNX,再转TensorRT进行FP16推理加速。

7.1 ONNX导出

ultralytics框架内置了导出接口,一步就能把训练好的模型导出为ONNX:

yolo export model=best.pt format=onnx imgsz=640

导出时如果不指定opset,默认值是12。如果后续要对接TensorRT,我建议把opset提到17,因为新版本TensorRT对高版本opset的支持和优化更好。

导出的ONNX文件可以用onnxruntime直接跑:

import onnxruntime as ort import numpy as np import cv2 session = ort.InferenceSession('best.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) # 预处理 img = cv2.imread('test.jpg') img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img, dtype=np.float32) img /= 255.0 input_tensor = img[None, ...] # 推理 outputs = session.run(None, {'images': input_tensor}) # outputs[0] 形状是 [1, 84, 8400] 或 [1, 6, 8400] 取决于模型输出头

如果onnxruntime测试出来的输出形状和预期不一致,记得检查一下导出时是不是带了NMS后处理。ultralytics的export默认不带NMS,输出是raw的预测结果,需要自己在后处理里解码。如果你希望导出端到端的带NMS模型,需要在export时设置nms=True,这样部署时的集成压力会小很多,但灵活度会降低。我的方案是导出不带NMS的版本,把后处理留在业务代码里,方便动态调阈值。

7.2 TensorRT加速实测

TensorRT是NVIDIA的推理优化引擎,能把模型做层融合、精度校准、内核自动调优。我导出的TensorRT FP16模型在部署机上的实测数据如下:

推理后端平均耗时(单张640图像)模型体积是否需要GPU
PyTorch28ms83.7MB是
onnxruntime CPU45ms84.1MB否
onnxruntime GPU16ms84.1MB是
TensorRT FP169ms47.0MB是

TensorRT FP16在相同精度下,耗时从PyTorch的28ms降到了9ms,提速超过3倍,模型体积也从84MB压缩到47MB。这个性能在批量预筛场景下非常舒服,处理100张图大概不到1秒推理时间。

7.3 关于INT8量化,我的建议是谨慎

TensorRT还支持INT8量化,理论上能让模型再缩小一半、速度更快。但在医疗影像场景,我明确不建议用INT8。

原因有两个:一是INT8量化会把激活值的动态范围压缩到256个级别,病灶纹理这种精细信息很容易丢失,实测INT8模型在测试集上的mAP50-95比FP16版本掉了6个点;二是医疗场景对精度变化的容忍度极低,为了速度牺牲精度在临床上是不负责任的。我最终选择了FP16作为部署精度,在性能和精度之间取了平衡。

7.4 部署形态总结

我最终交付的系统包含三套可运行形态:

  • 开发形态:Python脚本+PyQt界面,适合在有GPU的本地工作站上运行;
  • 服务形态:导出ONNX模型,用FastAPI封装一个HTTP接口,接收图片返回JSON检测结果,方便接入其他系统;
  • 轻量形态:导出TensorRT FP16引擎,集成到C++或Python推理服务里,跑在边缘GPU设备上,用于门诊实时预筛。

这个分层设计的好处是:同一个训练好的模型,可以根据使用环境和硬件条件灵活切换部署方式,而不需要重新训练。

最后再说一点个人体会。这个项目做下来,我最深的感受是:在医疗AI这个领域,数据质量、样本分布和任务定义的重要性,远高于模型结构本身。YOLO只是一个顺手好用的工具,真正决定系统价值的,是数据怎么来、标注怎么挖、阈值怎么调、边界怎么守住。用这套系统批量过图能帮医生省下大量重复阅片的时间,但每一次自动检测的结果,都必须保留人工复核的环节。技术能做的是辅助,不能替代专业判断。如果后续继续扩展,我打算在检测框基础上加一个细粒度分类头,或者引入分割模型做病灶边缘分析,这些方向都能让这个系统的价值再上一个台阶。

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

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

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

立即咨询