两个月前,一家做汽车零部件的供应商朋友找到我,说他们产线上的齿轮出厂质检还在靠老师傅肉眼一个个翻,强度大、效率低,偶尔还有漏检被客户投诉。问我能不能用机器视觉做一套自动化检测,先不说替代人,至少能把明显缺陷拦下来。我结合现场的工况和预算,最终用YOLOv8在PyTorch框架下搭了一套齿轮缺陷检测系统,图像、视频、摄像头实时检测三条链路全部跑通。这篇文章就是把整个项目从数据准备到模型训练再到检测落地的过程完整捋一遍,给同样在搞工业视觉、或者拿YOLOv8做毕设项目的朋友一个可复现的参考。
这套系统本质上是典型的目标检测任务:给定一张齿轮图像,算法要回答"有没有缺陷、缺陷在哪、属于哪一类"。缺陷定位靠检测框,缺陷分类靠网络输出的类别概率。支撑它的是PyTorch深度学习框架和YOLOv8模型,同时用OpenCV解决视频流读取和画面显示。如果你之前只跑过YOLOv8的官方demo,或者被网上各种"一键训练"教程搞得云里雾里,这篇能帮你把整个项目的关键环节串起来。
1. 方案选型:为什么是YOLOv8,而不是传统视觉和别的检测模型
1.1 传统机器视觉在齿轮检测上的软肋
很多刚接触这个场景的人会问:齿轮缺陷不是能用OpenCV做边缘检测、模板匹配吗?答案是能,但只能解决非常有限的问题。传统方案的思路是写规则,比如检测齿廓边缘的连续性、对比模板图的灰度差异。实际现场跑起来你会发现三个问题:
第一,齿轮是金属件,表面反光严重,同一颗齿轮在不同打光角度下拍出来,灰度分布差异很大。边缘检测对光照极其敏感,稍微有点油污或者氧化色,就会把正常齿面误判成缺陷。
第二,缺陷种类不是一种。缺齿、裂纹、划伤、点蚀、毛刺,形态差异极大。划伤是细长线状,点蚀是一堆小麻点,裂纹可能只有几个像素宽,用固定形态学的算子很难同时覆盖这么多情况。你要写规则,等于每种缺陷都要单独设计一套滤波和判断逻辑,周期长不说,换一种齿轮型号可能全部推倒重来。
第三,产线对误检率容忍度很低。传统算法最容易犯的毛病是"宁杀错不放过",把大量正常工件判成异常,质检员反而要花更多时间复核,实际价值大打折扣。
1.2 YOLOv8在工业检测场景里的优势
YOLOv8是Ultralytics在2023年初推出的目标检测框架,相比传统方法,它是端到端的学习方式:输入图像,网络自己提取特征、回归边界框、输出类别概率。人工不需要设计任何特征算子,只需要提供标注好的数据集。这对缺陷检测来说是革命性的,因为缺陷的视觉模式千变万化,与其手工设计特征,不如让模型从标注中直接学。
具体到YOLOv8这个版本,它的网络结构做了几处明显改进:C2f模块替换了之前的C3模块,梯度能更丰富地流动,对小目标特征提取更友好;检测头改为anchor-free设计,省去了大量anchor超参调节,对不同尺寸物体的适应性更强;配合PAN-FPN结构做多尺度特征融合,让模型能同时兼顾大尺度缺齿和小尺度裂纹这种混合场景。
我实际对比过YOLOv5和YOLOv8在同一批齿轮数据上的表现,结论是:在相同训练条件下,v8的mAP50普遍高出2到4个百分点,尤其是裂纹和点蚀这两个小目标类别,召回率提升比较明显。至于Faster R-CNN这类两阶段检测器,精度确实高,但推理速度直接劝退,一张图几十毫秒起步,根本撑不起摄像头实时检测的需求。
1.3 系统整体架构和技术栈
系统整体分三层。第一层是输入层,支持三种来源:单张图片、视频文件、摄像头实时画面。第二层是推理层,加载训练好的YOLOv8权重,对每一帧画面做预处理、前向推理、后处理(置信度过滤加NMS去重),输出检测框和类别。第三层是输出层,把检测结果绘制在原图上,标注缺陷类别和置信度,同时支持图片保存、视频导出和实时画面显示。
技术栈选型很直接:Python作为主语言,模型和推理全部基于PyTorch,检测框架用Ultralytics提供的YOLOv8实现,视频处理用OpenCV。选这套组合的原因很简单:生态成熟、资料多、调试方便。Ultralytics的接口对新手友好,一个YOLO类就能串联训练和推理,但项目真正上线时还是需要自己写封装逻辑,不能只停留在跑通demo的阶段。
注意:如果你只是调用Ultralytics官方推理脚本,那叫体验API,不叫搭建系统。真正做项目,模型加载、结果解析、业务逻辑、异常处理这几块都要自己动手,这也是本文重点展开的内容。
2. 齿轮缺陷分类与数据构建:决定模型效果的下限
2.1 先梳理清楚要检测哪些缺陷类型
动手之前,我专门去产线蹲了半天,和质检老师傅聊了聊,把常见的缺陷类型归成六类。这个步骤千万别省,因为缺陷类型定义直接决定标注工作量、类别平衡策略和最终模型结构。
| 缺陷类型 | 形态特征 | 检测难点 |
|---|---|---|
| 缺齿 | 齿根断裂或整齿缺失 | 大目标,但样本少,容易漏检 |
| 裂纹 | 细线状,可能极短 | 像素占比小,典型小目标 |
| 划伤 | 长条状,方向随机 | 与正常加工纹路易混淆 |
| 齿面磨损 | 大面积灰度变化 | 边界模糊,框标注不确定 |
| 点蚀 | 密集小麻点 | 多个小目标聚集,NMS容易误删 |
| 毛刺 | 齿缘突起,金属反光 | 光线变化下特征不稳定 |
这六类里面,缺齿和毛刺相对好检,前者目标大、特征显著,后者位置固定、对比度高。真正让人头疼的是裂纹和点蚀:裂纹细到只有几个像素宽,点蚀则是一群人小目标挤在一起。这两类缺陷对标注精度要求极高,稍微框偏一点模型就学偏。
2.2 数据采集和打光方案
数据质量是深度学习项目的生命线。我见过太多人一上来就训练,结果数据全是网上找的示意图,和现场实拍差异巨大,模型在测试集上跑得挺好,一到现场就崩。这项目我们用的是现场采集为主、公开数据和合成数据为辅的策略。
现场采集最关键的是打光。齿轮是金属旋转体,表面反光强,用普通环形光会出现高光区域把缺陷盖住的情况。我们试过几种方案,最后效果比较稳定的是低角度条形光加同轴光组合:低角度光让表面微小的凹凸产生阴影,突出划伤和点蚀;同轴光负责把整体亮度拉均匀,减少镜面反射干扰。相机的选择上,工业相机带硬触发配合传送带拍照当然是正解,但前期验证阶段用普通USB相机也能跑通算法,不用一上来就上万元级别的设备。
数据多样性也要控制好。不同型号的齿轮齿数、模数、表面处理都不同,训练集里必须包含多种型号,否则模型会产生严重过拟合,换个型号就失效。我们采集时特意覆盖了大齿轮、小齿轮、淬火件、发黑件等不同工艺状态,每个类别保证几百张真实缺陷图。
没有现场数据怎么办?我当时还做了一批合成数据:在正常齿轮图上,用图像处理方式人工叠加裂纹、划伤、点蚀,再贴回不同背景下。合成数据能有效补充底层纹理特征,但只能锦上添花,不能替代真实缺陷样本,因为真实缺陷形态要复杂得多。
2.3 数据标注:格式、工具和避坑要点
YOLOv8需要的是YOLO格式的txt标注文件,每一行对应一个目标:类别ID 中心点X 中心点Y 宽度 高度。中心的坐标都是相对于图片宽度和高度的归一化值,取值范围0到1。
0 0.5234 0.6125 0.0842 0.1153 2 0.3718 0.4289 0.0417 0.0234这行数据的意思是:类别0的目标,中心点在图片的52.34%宽度、61.25%高度位置,目标宽度为图片宽度的8.42%,高度为图片高度的11.53%。
标注工具我用过几个:LabelImg是老牌开源工具,支持PascalVOC和YOLO格式,轻量够用;Label Studio功能强,支持多人协作,适合样本量大到一个人标不完的情况。不管用哪个,标注前的规范必须定死:裂纹这种细长目标,框要紧贴缺陷外接矩形;点蚀密集区域,规则是单个点独立标注,聚成一团无法分割时用一个大框包住整团;边缘被遮挡的缺陷,宁可标得保守也不要框进大面积正常区域。
标注完还有一个隐蔽的坑:类别不平衡。我统计过第一版数据集,正常图片两千多张,但裂纹只有不到两百个实例,缺齿更少,一百个都不到。这种数据集直接训练,模型会严重偏向样本多的类别。后面对策分两步:一是过采样,把缺齿和裂纹图片在训练集中复制加入;二是增广,对这两类图片做更强的增强。同时调整损失函数里的类别权重,让模型在训练时对小样本类别更加敏感。
2.4 数据增广:YOLOv8内置的增强够不够
YOLOv8训练时默认开了一整套数据增强,包括Mosaic、MixUp、HSV色彩扰动、随机翻转、缩放裁剪等。Mosaic会把四张图拼成一张,强制模型学习更复杂的背景和尺度变化,对工业场景帮助很大。
但默认增强有两个地方需要改:第一,随机翻转。齿轮的齿形通常是轴对称的,水平翻转看似无害,但标注框会跟着镜像,如果某些缺陷形态在镜像后物理上不成立(比如毛刺总是在齿轮同一侧),就会给模型灌输错误信息。我们直接把fliplr和flipud关掉了。第二,HSV扰动参数。工业图的光照相对稳定,不需要像自然图像那样大幅调整色彩,反而把饱和度扰动拉太大容易让模型对颜色变化过于敏感,所以在超参里把hsv_s从默认的0.7调低到0.3。
3. 模型训练全流程:参数选择、过程监控与评估
3.1 环境搭建和数据集目录结构
训练环境我用的是Python 3.10、PyTorch 2.1、CUDA 11.8,显卡用单张消费级GPU。Ultralytics的安装非常省事:
pip install ultralytics数据集目录结构必须严格按照YOLOv8的约定:
gear_dataset/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/data.yaml里面写明路径和类别名称:
path: /home/user/gear_dataset train: images/train val: images/val names: 0: chipped_teeth 1: crack 2: scratch 3: wear 4: pitting 5: burr训练集和验证集的划分要按现场实际情况来:同一个齿轮的多次拍摄帧必须放到同一侧,绝不能出现训练集和验证集来自同一颗齿轮的相似角度画面,否则验证指标会虚高。用随机划分最容易踩这个坑,我是按工件ID做分组的。
3.2 模型规格和训练参数怎么定
YOLOv8一共有n、s、m、l、x五个规格,参数量和推理速度依次递增。我的经验是先从中等规格起步,不要一上来就贪大。这个项目最终选择了yolov8s作为主力模型,原因是产线对实时性要求比较高,s规格在精度和速度之间正好平衡。如果后续还想提精度,再往m上迁移也很简单,权重只差一个前缀。
| 模型规格 | 参数量 | 推理速度(相对) | 适用场景 |
|---|---|---|---|
| yolov8n | 最小 | 最快 | 边缘设备、实时性极致要求 |
| yolov8s | 小 | 快 | 本项目选择,生产环境主流 |
| yolov8m | 中 | 中等 | 精度优先,算力充裕 |
| yolov8l/x | 大 | 慢 | 离线批量检测、研究实验 |
训练命令看起来不长,但每个参数都值得说清楚:
yolo detect train data=gear_data.yaml model=yolov8s.pt epochs=200 imgsz=640 batch=16 patience=30 project=runs name=gear_yolov8simgsz=640是通用的输入分辨率。但对于裂纹这种小目标,640可能不够,需要试1280。把分辨率拉到1280后,训练时间和显存占用会明显上涨,而且不一定带来收益,必须亲测对比。我当时两种都跑了,1280在裂纹类别上确实能提高约3个点的召回率,所以最终训练采用1280,部署时再把输入尺寸对齐,不能训练和推理差太多。
batch=16是经验值,受显存限制。显存不足时报CUDA out of memory,就把batch调小,同时按比例降低学习率,否则模型容易震荡。patience=30是早停参数,连续30个epoch验证集指标没有提升就自动停止,省得空跑。
类别不平衡的处理可以在训练时通过class_weights参数指定。YOLOv8的loss里每个类别可以单独加权,权重可以用样本数的倒数归一化。比如裂纹样本只有200个,毛刺有1500个,那裂纹的权重就远大于毛刺。
3.3 训练过程怎么监控和调优
训练开始后,我会盯两个东西:loss曲线和验证集mAP曲线。TensorBoard和Ultralytics自带的results.csv都能看。正常的loss曲线是平滑下降后趋于平缓,如果训练集loss持续下降但验证集loss掉头向上,说明过拟合了,early stopping会兜底,但最好提前干预,把patience调短或者用更强的增广。
另一个容易忽略的点是验证集的单类别指标。总体mAP好不代表每个缺陷都能检出来。我训练到100个epoch时看了每个类别的AP,划伤和毛刺都在0.9以上,但裂纹AP只有0.6左右,点蚀是0.7。这说明模型对这两个小目标类别的学习还不充分。针对这个问题我在数据增广上又加了小目标随机裁剪放大,模拟图片中的裂纹区域被放大后的特征,下一轮训练裂纹AP涨到了0.78。
3.4 评估指标和模型选型结论
检测模型最常用的指标是mAP50和mAP50-95。mAP50看的是预测框和真实框IoU大于0.5时算不算检中,工业场景里用得最多。mAP50-95更严格,把IoU从0.5一路提到0.95取平均,对框的定位精度非常敏感。
我最后定版模型的mAP50在0.93左右,mAP50-95在0.72左右。单看这个数字挺好看,但落地时我更关心每个类的召回率,特别是裂纹和点蚀,因为它们直接对应客户投诉的高危缺陷。权衡之后决定:如果现场出现比较多的小目标漏检场景,就切到yolov8m或者进一步提升分辨率,而不是硬守实时性。
4. 检测模块实现:图像、视频、摄像头三条链路
4.1 单张图像检测:最简洁的调用单元
训练完成后,模型权重文件best.pt就是整个系统的核心资产。图像检测代码是整个系统的基础,因为视频和摄像头检测本质上都是对每一帧图像调用同一个推理接口。先看最基础的用法:
from ultralytics import YOLO model = YOLO("runs/gear_yolov8s/weights/best.pt") results = model("gear_img.jpg", conf=0.25) for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = box.conf[0].item() cls_id = int(box.cls[0].item()) print(f"类别{cls_id},置信度{conf:.2f},坐标({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f})")置信度阈值conf是第一批要调的超参。定得太低,模型会把很多正常齿面的纹理误判成裂纹;定得太高,又容易放过真正的缺陷。我现场调试时先用0.25跑一批图,统计误检情况后逐步上调到0.4左右,找到了当前场景下的稳定点。不同批次的齿轮表面状态不完全一样,所以阈值别写死,留成配置文件或启动参数。
生产环境里,曝光不足、分辨率不够的图,模型再强也没用。图像检测接入业务系统时,代码里要加图片尺寸、清晰度的前置校验,这些判断用OpenCV很容易实现,别把脏图直接送进模型,白费算力。
4.2 视频检测:逐帧推理与结果输出
视频检测和图像检测的区别在于:视频是一个连续帧序列,你要处理帧读取、推理、结果绘制、视频写出,同时还要控制单帧处理速度,防止最终输出视频流畅度太差。
import cv2 from ultralytics import YOLO model = YOLO("best.pt") cap = cv2.VideoCapture("gear_video.mp4") fps = cap.get(cv2.CAP_PROP_FPS) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) out = cv2.VideoWriter("result.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)) while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame, conf=0.4, iou=0.5) annotated = results[0].plot() out.write(annotated) cap.release() out.release()视频检测的优化思路是跳帧和抽帧。如果产线上齿轮运行速度不快,画面每5帧检测一次就够了,中间帧直接复用最近一次的检测结果,能省下可观的推理时间。另一个坑是VideoWriter的视频编码选择。要保证兼容性,就用mp4v;要压缩体积,可以用avc1,但不是所有环境都支持,最好先测试后输出。
4.3 摄像头实时检测:性能是核心问题
摄像头实时检测是视频检测的在线版本,难点从"检测准不准"变成了"延迟能不能接受"。如果单帧推理要100毫秒,画面就会像幻灯片一样卡顿,现场根本没法用。
单个线程串行处理视频流和推理,是新手最容易犯的问题。因为cv2.VideoCapture读取摄像头画面是阻塞式操作,一旦帧读取变慢,推理线程就得等。正确做法是拆成两个线程:采集线程只管把帧写进队列,推理线程从队列取帧做检测。
import cv2 import threading import queue from ultralytics import YOLO model = YOLO("best.pt") frame_queue = queue.Queue(maxsize=2) result_queue = queue.Queue(maxsize=2) def camera_loop(cap): while True: ret, frame = cap.read() if not ret: break if frame_queue.full(): frame_queue.get() frame_queue.put(frame) def inference_loop(): while True: if frame_queue.empty(): continue frame = frame_queue.get() results = model(frame, conf=0.4, iou=0.5) result_queue.put(results[0].plot()) # 主线程从 result_queue 获取绘制结果并显示队列长度设成2就够了,因为实时画面本来就是瞬时的,队列太长反而会给延迟添砖加瓦。推理时可以顺手把fps算出来显示在画面上,便于评估性能是否达标。
如果摄像头不是USB摄像头而是工业相机,比如千兆网口相机或Camera Link相机,OpenCV的VideoCapture会失效。这时候要用厂商SDK或第三方通用库。工业相机SDK通常自带采集回调函数,把回调里的帧送进同一个队列就行,后面的推理逻辑完全不用改。
4.4 结果可视化与统一封装
检测结果的可视化直接影响现场工人的使用体验。我用不同的框颜色区分缺陷类型:红色框是缺齿和裂纹,黄色框是划伤和磨损,蓝色框是点蚀和毛刺。同时在框上方显示类别名和置信度数值。Ultralytics的plot()方法默认会做这些,但如果你想在生产项目中控制表现细节,还是要自己用OpenCV的rectangle和putText绘制。
真正面向业务系统时,所有检测逻辑应该封装成一个类或一个服务,对外暴露detect_image、detect_video、detect_webcam三个接口。接口内部处理模型加载、输入校验、推理、格式转换,调用方不需要关心模型细节。传参也只传文件路径或帧数组,返回值统一成结构化数据,比如JSON,方便后续对接MES系统或者生成检测报表。
5. 踩坑实录与性能优化清单
5.1 常见问题速查表
项目从开发到现场调试,踩的坑不少,整理成一张表方便查阅。
| 现象 | 根因分析 | 解决方案 |
|---|---|---|
| 训练时CUDA out of memory | batch太大或分辨率太高 | 调小batch,同步降低学习率 |
| 模型在验证集mAP高,现场漏检 | 训练数据和现场数据分布不一致 | 现场采图补充训练集,做多批次混合 |
| 裂纹和点蚀小目标漏检多 | 输入分辨率不够或模型太小 | 提高imgsz到1280,换大模型,切分大图 |
| 正常齿面被误判为划伤 | 置信度阈值偏低且类间特征重叠 | 调高conf阈值,增加正常负样本训练 |
| 摄像头画面卡顿 | 帧读取和推理串行 | 改多线程 + 队列解耦 |
| 检测框抖动厉害 | NMS后处理不稳定 | 对置信度做EMA平滑,加帧间位置滤波 |
| 换一种齿轮型号完全失效 | 模型过拟合于训练型号 | 数据集覆盖多型号,增加增广随机性 |
5.2 实时性优化的三板斧
如果摄像头实时检测的帧率达不到25FPS以上,从这三个方向排查和优化。第一,模型轻量化。yolov8n的推理速度比yolov8s快约一倍,精度损失通常在3到5个点以内。用分布式量化和剪枝也能压缩模型体积。第二,推理引擎加速。把PyTorch模型导出成ONNX格式,再用ONNX Runtime或者TensorRT跑推理,比原生PyTorch快不少。TensorRT在NVIDIA显卡上能把延迟压到几毫秒级别,但导出时踩的坑比较多,算子兼容性要自己验证。
第三,输入尺寸合理控制。不是所有场景都需要1280分辨率,如果检测目标没有小到看不见,把输入分辨率从1280降到640,推理时间能降到原来的四分之一。批处理在视频场景中用不上,实时画面是一帧一帧来的,不要做多余的batch组装。
5.3 从源码到交付物的工程细节
项目交付时,除了代码还要准备这些东西:requirements.txt锁定所有依赖版本,Ultralytics版本不同,API接口和行为都会有差异,版本必须固定;训练好的best.pt权重文件要和源码分离,方便替换升级;配置文件里存放类别列表、置信度阈值、摄像设备编号等参数,不要让用户到源码里翻常量。
日志记录要加上。每次检测完,把检测结果、处理耗时写到本地日志,甚至落库。没有日志,现场出了问题只能靠拍脑袋排查。设备编号也是个问题,一台电脑上插多个摄像头时cv2.VideoCapture的索引会变,需要提供一个参数让使用者指定用哪个设备。
模型更新机制值得提前预留。产线工况变了,缺陷样本积累了,模型肯定要迭代。给模型文件加版本号,检测服务加载时读取版本信息并输出,这样排查问题时能知道现场到底跑的是哪版模型。这个小习惯能帮你省去很多不必要的扯皮。
这套系统落地之后,实际效果比预期要好。原本质检线上人均要看几百个工件,漏检偶有发生,现在程序只把置信度不够、需要人工复核的图弹出来给质检员确认,一个班次处理量翻了几倍,人力疲劳带来的漏检也基本消失了。
我自己最大的体会是:这种工业视觉项目,数据工程花的精力远超模型调参。模型结构大家都在用,差距不是靠换新模型拉开的,而是谁的数据更贴近现场、更干净、标注更准确。后续想在这个方向继续深入的话,我会考虑两条线:一是用YOLOv8的分割模型统计缺陷的具体面积,给质量部门提供量化指标;二是引入无监督异常检测做未知缺陷的兜底,毕竟工厂里的新问题永远比标注过的类型多。先把这条检测主线跑稳,再谈扩展,这比一开始就上复杂方案要实际得多。