☰
工业零件裂纹检测实战:YOLO实时检测与部署全流程
2026/10/10 16:59:08 网站建设 项目流程

简介:面向计算机视觉与工业质检场景的YOLO实时物体检测资源包,涵盖齿条、螺栓、螺母及裂缝检测等典型工业任务。包内共2000个文件,其中1903个txt标注或配置文件,46个c源文件、49个h头文件,另含1个cpp与1个md说明文档,压缩包整体约199.17MB。txt数量庞大,多用于存放类别标签、边界框坐标或训练超参数,配合完整C源码可复现检测流程。源码涵盖数据读取、网络构建、层实现与推理逻辑等核心模块,有助于深入理解YOLOv1至YOLOv4的演进细节,尤其适合对小尺寸工业零件与缺陷进行检测训练和验证。资源结构规整,既适合计算机视觉初学者对照源码理解算法原理,也便于工程师在此基础上进行模型选型与二次开发。目前已有67人学习,是一份从理论到实践都颇具参考价值的实时检测资料。

1. 齿条、螺栓、螺母加裂纹检测:YOLO实时物体检测为什么是正确起点

一条产线上同时要盯齿条齿面、螺栓杆部、螺母端面,还得在几十毫秒内把裂纹这种缺陷框出来——这就是标题里那串英文的实际场景:yolo real-time object detection 负责零件定位,crack detection 负责表面缺陷。齿条、螺栓、螺母跟常见的行人、车辆、猫狗不一样,它们是小目标、金属反光背景、缺陷样本稀缺,直接把 COCO 80 类的权重拿过来跑肯定是废的,得从数据到训练到部署重走一遍。这篇笔记就是按这个路径来拆:怎么标裂纹、怎么调训练参数、怎么导出 ONNX 上边缘设备,以及我踩过的几个真坑。

2. 工业零件数据集:齿条、螺栓、螺母与裂纹样本的采集和标注

2.1 光照和相机选型决定数据集质量,不要先急着标

很多人拿到齿条和螺栓的第一反应是打开标注工具开干,我建议先花两天把采集环境定下来。YOLO 对光照的敏感度比你想象的高,金属零件的反光会让同一批零件在不同角度下看起来像两个类别。常见的做法是环形光源加同轴光,齿面用低角度环形光把齿形轮廓打出来,螺栓螺母用漫射光避免镜面反射。相机方面,不要一上来就上彩色工业相机,黑白相机配高分辨率更合适——裂纹在灰度图上对比度往往比彩色图更清晰,而且图像体积小一半,推理也快。

采集时还有一个容易被忽略的点:分辨率要留足余量。齿条单齿在画面里可能只有 20×30 像素,螺栓头部直径 15 像素,这个尺寸在 640×640 的输入下几乎看不见。我一般会把相机架到让最小的目标至少占 32×32 像素,实在达不到就靠后面的切片推理补救。另外,裂纹样本别只拍水平方向的,垂直裂纹、斜向裂纹、贯穿齿根的裂纹都要有,不然模型只认一个角度。

2.2 用 YOLO 标注格式存裂纹:细长目标的三条标注规则

YOLO 的标注是 txt 文件,每行格式是class_id cx cy w h,坐标全部归一化到 0~1。普通零件按外接矩形框标没问题,但裂纹是细长线状目标,一条裂纹长 200 像素、宽只有 3 像素,直接框的话矩形面积 99% 都是背景,训练时损失会被背景主导。我总结的三条规则:

第一条,裂纹必须分段标。把一条长裂纹切成 3~5 段,每段单独一个框,段与段之间留 2~3 像素重叠,防止训练时框的抖动把裂纹边缘甩掉。第二条,框的方向跟裂纹主轴对齐,不要为省事标成水平框,长宽比超过 1:10 的框会让模型学到“框里面大部分是背景”,推理时置信度普遍偏低。第三条,类别命名别用纯数字,用rack_good、bolt_good、nut_good、crack这种可读名称,后续出报表和排查误检时能少花一半时间。

2.3 让几十张裂纹样本撑起一个类:过采样和图像增强代码

工业场景的常态是有 5000 张合格零件,裂纹样本只有 60 张。用原始比例训练,模型会直接忽略裂纹类,因为负样本占比太高。我的做法是先过采样再增强,把裂纹样本复制到与合格品接近的比例(比如 1:3),然后做数据增强。

下面这段脚本是我处理金属零件增强的常用模板:

import cv2 import numpy as np from pathlib import Path src_dir = Path("raw_crack") # 原始裂纹样本目录 out_dir = Path("aug_crack") # 增强输出目录 out_dir.mkdir(exist_ok=True) TARGET_COUNT = 400 # 增强后裂纹样本目标数量 CLAHE_CLIP = 2.0 # CLAHE 对比度限制,金属件建议 1.5~3.0 CLAHE_TILE = (8, 8) # 分块大小,块越小局部对比度越强 def augment_crack(image, bboxes): """对裂纹图像做增强,同时按相同变换更新标注框。""" h, w = image.shape[:2] augs = [] # 1. CLAHE 局部对比度增强:把裂纹从金属纹理里拉出来 lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) l_chan = lab[:, :, 0] clahe = cv2.createCLAHE(CLAHE_CLIP, CLAHE_TILE) l_enh = clahe.apply(l_chan) lab[:, :, 0] = l_enh image_clahe = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) augs.append((image_clahe, bboxes)) # 2. 高斯模糊模拟低景深,迫使模型学形状而不是纹理 image_blur = cv2.GaussianBlur(image, (3, 3), 0) augs.append((image_blur, bboxes)) # 3. 旋转 ±15°,注意旋转后框的角点要重新算 for angle in (-15, 15): M = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) img_rot = cv2.warpAffine(image, M, (w, h), borderMode=cv2.BORDER_REFLECT) bboxes_rot = [] for x, y, bw, bh in bboxes: # 把中心点坐标旋转后还原为 xywh cx, cy = x + bw / 2, y + bh / 2 cx2 = m00 * cx + m01 * cy + M[0, 2] cy2 = m10 * cx + m11 * cy + M[1, 2] bboxes_rot.append((cx2 - bw / 2, cy2 - bh / 2, bw, bh)) augs.append((img_rot, bboxes_rot)) # 4. 加椒盐噪声模拟灰尘颗粒 noise = np.random.randint(0, 255, image.shape[:2]) salt = noise < 20 pepper = noise > 235 image_noise = image.copy() image_noise[salt] = (255, 255, 255) image_noise[pepper] = (0, 0, 0) augs.append((image_noise, bboxes)) return augs # 循环处理所有原始裂纹图,输出增强后的图和 YOLO 标注 crack_id = 3 # class_id,假设 0=rack 1=bolt 2=nut 3=crack for img_path in src_dir.glob("*.jpg"): image = cv2.imread(str(img_path)) label_path = src_dir / (img_path.stem + ".txt") bboxes = [] for line in label_path.read_text().strip().splitlines(): parts = line.split() cls_id = int(parts[0]) if cls_id == crack_id: # 归一化坐标转像素坐标 _, cx, cy, bw, bh = map(float, parts) w, h = image.shape[1], image.shape[0] bboxes.append((int((cx - bw/2) * w), int((cy - bh/2) * h), int(bw * w), int(bh * h))) for idx, (img_aug, bb_aug) in enumerate(augment_crack(image, bboxes)): out_name = f"{img_path.stem}_aug{idx}.jpg" cv2.imwrite(str(out_dir / out_name), img_aug) with open(out_dir / (out_name.replace(".jpg", ".txt")), "w") as f: for x, y, bw, bh in bb_aug: cx = (x + bw / 2) / img_aug.shape[1] cy = (y + bh / 2) / img_aug.shape[0] f.write(f"3 {cx:.6f} {cy:.6f} {bw / img_aug.shape[1]:.6f} {bh / img_aug.shape[0]:.6f}\n")

这段脚本的关键在于增强和标注框同步变换。我见过不少翻车案例是图像转了 15°,标注框还停留在原位置,模型训练时损失直接爆炸。旋转部分的坐标变换最容易写错,建议导出一张图人工检查框的位置再批量跑。CLAHE 的参数对金属件影响很大,clip 值超过 3.0 会把划痕也增强成裂纹,不可逆。

3. 训练参数与损失函数:YOLOv8 在零件检测上的调参路径

3.1 按推理设备选模型:参数量不是越大越好

很多新人一上来就选 YOLOv8x,认为精度最高。但你要先想清楚推理设备在哪:产线工控机上跑,还是 AGX Orin 这种边缘盒子?YOLOv8n 参数量 3.2M,yolov8s 是 11.2M,v8m 是 25.9M,参数量的差异直接反映到延迟上。我对齿条检测的建议是:如果目标在图中超过 32 像素,用 v8s 起步;如果裂纹和小螺栓占比高,用 v8m 配 1024 输入,精度收益远大于换成 v8x。

选型时还要看输出通道数。YOLOv8 的检测头输出格式是[batch, 4 + num_classes, grid_h, grid_w],4 对应 box 的 xywh,类数越多通道越宽。你的场景只有 4 个类(rack、bolt、nut、crack),通道数不算压力,但如果你后续加划痕、凹坑、锈蚀等缺陷类别,通道会线性增长,推理延迟也会跟着涨。

另外一个容易忽略的点:YOLO 的 Anchor-Free 设计意味着不需要手动设置锚框尺寸。老版本的 YOLOv5 要跑 k-means 聚类出 anchors,v8 改成解耦头后,这个步骤省了,训练配置里不用再折腾 anchor 相关参数。

3.2 训练配方的四个必调参数:imgsz、batch、epochs 和增强

训练命令看似简单,真正影响结果的是四个参数的组合:

# crack_train.yaml path: ./industrial_parts train: images/train val: images/val nc: 4 names: ["rack", "bolt", "nut", "crack"]
yolo train \ model=yolov8s.pt \ data=crack_train.yaml \ imgsz=1024 \ batch=16 \ epochs=300 \ lr0=0.01 \ mosaic=1.0 \ mixup=0.2 \ close_mosaic=10

imgsz是最关键的一个。齿条齿面这种小目标,640 和 1024 的 mAP 差距能有 8 个点以上,但显存占用涨了一倍。我一般先看显卡:单张 24G 显存用 batch 16 + imgsz 1024;显存不够就 640 加切片推理兜底。batch别太大也别太小,16 到 32 之间,太小的话 BN 层统计不稳定,裂纹类本来就样本少,batch 再小就完全失去了统计意义。epochs至少 300,工业数据集中后几百轮才开始稳定,不要看 train loss 降了就提前停。

增强参数里mosaic=1.0和mixup=0.2是我调过相对稳的组合。纯金属零件表面纹理相似,mosaic 拼图能让模型减少对背景的过拟合。但最后 10 轮一定要close_mosaic,否则验证时因为训练和验证分布不一致,mAP 会虚高,部署到产线就露馅。

3.3 从损失曲线判断“裂纹类”是否在收敛

YOLOv8 的损失由三部分组成:分类损失 BCE、回归损失 DFL 和 CIoU。训练日志里的cls_loss和dfl_loss要分开看,不要只盯着box_loss。

裂纹这类样本少的类别,最典型的信号是cls_loss下降缓慢但dfl_loss在正常下降——这说明模型已经学会框住裂纹的大概位置,但分类置信度上不去。遇到这种情况,先检查是不是类别权重失衡:裂纹样本在 batch 里平均占比不到 5%,BCE 损失被其他类主导。解决办法是在数据加载器里对裂纹样本做加权采样,或者在损失函数里给裂纹类加权重,不用改源码的话,最简单的是把裂纹样本在数据目录里多复制几份,让采样概率自然提升。

# 用这段代码统计训练集各类别占比,低于 5% 就要干预 from collections import Counter label_dir = Path("industrial_parts/labels/train") counts = Counter() for txt in label_dir.glob("*.txt"): for line in txt.read_text().strip().splitlines(): counts[int(line.split()[0])] += 1 total = sum(counts.values()) print({k: v / total for k, v in counts.items()})

如果裂纹类占比确实在 5% 以下,建议回到第 2 章做过采样,而不是在训练代码里塞复杂的损失加权。损失权重调起来很玄学,一组参数在一个数据集上有效,换到另一种光照下可能完全失效。

4. 实时推理与边缘部署:ONNX 导出、TensorRT 加速和产线接入

4.1 导出 ONNX 并确认输入输出尺寸

训练完成后第一步是导出 ONNX,目的是脱离 PyTorch 环境、接入生产推理框架。

yolo export model=best.pt format=onnx imgsz=1024 opset=17

导出参数opset=17是为了兼容比较新的 TensorRT 版本;imgsz必须和训练时一致,不要导出时图省事用 640,部署时推理 1024 会收到尺寸不匹配的报错。

导出后最好用onnxruntime快速验证一下输出。YOLOv8 的 ONNX 输出形状是[1, 84, 1024, 1024],84 = 4 + 4 类,也就是每个格子输出 4 个框坐标和 4 个类别置信度。很多人栽在预处理上:模型训练时用的归一化是[0, 1]还是[0, 255],取决于 ultralytics 版本,导出后用一张裂纹图对比 PyTorch 和 ONNX 的输出,偏差超过 0.5% 就优先检查输入归一化和通道顺序。

4.2 TensorRT 在 AGX Orin 上的精度和延迟平衡

ONNX 在 CPU 上跑 1024×1024 大概要 80 到 120 毫秒,产线节拍要求通常是 30 到 50 毫秒,所以必须上 TensorRT。AGX Orin 的典型转换命令:

trtexec \ --onnx=best.onnx \ --saveEngine=best_fp16.engine \ --fp16 \ --minShapes=input:1x3x1024x1024 \ --optShapes=input:1x3x1024x1024 \ --maxShapes=input:1x3x1024x1024 \ --workspace=4096

--fp16这块要注意:裂纹是低对比度细线目标,FP16 的精度损失有时候会让小裂纹直接消失。我遇到过的案例是 FP16 引擎的 mAP 比 FP32 掉了 3 个点,肉眼看不出来,但产线漏检率翻倍。做法是先跑 FP16 测召回,不合格就退回 FP32,或者做 INT8 量化加校准集——INT8 在工业检测上收益大但坑也深,建议第二版再碰。

TensorRT 推理的 Python 接口不复杂,直接读 engine 文件执行:

import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np logger = trt.Logger(trt.Logger.ERROR) with open("best_fp16.engine", "rb") as f: runtime = trt.Runtime(logger) engine = runtime.deserialize_cuda_engine(f.read()) ctx = engine.create_execution_context() # 分配输入输出显存 h_input = np.zeros((1, 3, 1024, 1024), dtype=np.float32) h_output = np.zeros((1, 84, 4096, 1024), dtype=np.float32) # 1024/32 的网格是 32x32 d_input = cuda.mem_alloc(h_input.nbytes) d_output = cuda.mem_alloc(h_output.nbytes) cuda.memcpy_htod(d_input, h_input) ctx.execute_v2(bindings=[int(d_input), int(d_output)]) cuda.memcpy_dtoh(h_output, d_output)

TensorRT 的 binding 顺序和 ONNX 输入输出顺序一致,不确定时可打印engine.binding_names确认。另外,ctx.execute_v2在同一 context 内重复调用是线程不安全的,产线如果有多个相机并行推理,每个摄像头单独一个 context 实例。

4.3 与 D435i 深度相机配合:2D 检测框对齐深度

齿条、螺栓检测之外的常见需求是测距定位——机械臂抓取前想知道零件在三维空间的位置。D435i 输出 RGB 流和深度流,两路画面的像素不是天然对齐的,要用相机内参做对齐。

import pyrealsense2 as rs import numpy as np pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.depth, 1280, 720, rs.format.z16, 30) config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30) profile = pipeline.start(config) depth_sensor = profile.get_device().first_depth_sensor() depth_scale = depth_sensor.get_depth_scale() # 默认 0.001 米 # 关键:把深度图像对齐到彩色图的坐标空间 align = rs.align(rs.stream.color) frames = pipeline.wait_for_frames() aligned_frames = align.process(frames) depth_frame = aligned_frames.get_depth_frame() color_frame = aligned_frames.get_color_frame() depth_image = np.asanyarray(depth_frame.get_data()) color_image = np.asanyarray(color_frame.get_data())

对齐后,把 YOLO 检测框的中心点坐标映射到深度图上,取该点邻域 5×5 像素的中值作为距离。不要只取单点,金属表面在深度图上有噪点,单点距离经常跳变。距离值的单位记得乘depth_scale,D435i 默认是毫米。

5. 避坑排查:裂纹漏检、螺栓误检和实时性不达标的五个真实记录

5.1 裂纹漏检率居高不下,问题出在标注框

现象:训练损失正常下降,验证 mAP 也有 0.85,但产线上 30% 的裂纹完全没有框出来。 原因:标注裂纹时框太随意,一条裂纹一个水平框,框内 95% 是背景,模型学到的是“背景模式”而不是裂纹纹理。置信度普遍在 0.2 以下,测试阈值一调高就全漏了。 解决:回到第 2.2 节的分段标注规则,把长裂纹切成至少 3 段,每段框长宽比控制在 1:5 以内,重新训练后召回率直接拉回 90% 以上。这个教训我吃了两次,第一次以为是模型问题,换了 v8m、v8x 都没用,后来才发现是标注质量拖累。

5.2 螺栓螺母全丢,因为 imgsz=640 对小目标太苛刻

现象:齿条检测还行,螺栓螺母几乎一个都框不出。 原因:螺栓头部在 640×640 输入下只有 10×10 像素,YOLO 的下采样比是 32,这个尺寸落在特征图上还不到一个格子,检测头根本看不到。 解决:把imgsz提高到 1024,同时开启多尺度训练。如果显存紧,另一个路子是切分推理——把原图按 512 重叠切片分别推理再合并结果,效果等同放大输入,但推理时间翻倍。我建议先上 1024 输入,还不行再用切片。

5.3 金属高光被识别成裂纹,误检频发

现象:无缺陷的螺栓杆部被框成裂纹,误检率 15%。 原因:高光区域的灰度值梯度大,形态上跟裂纹相似,尤其是在同轴光照射下抛光面会产生镜面反射,形成亮白色条带。 解决:物理手段优先,换偏振光源或调整光源角度消除反射;数据层面给训练集增加高光样本,把无缺陷但有反光的零件也拍进来标为背景。误检没法完全清零,但把阈值从 0.25 调到 0.4 能显著拉低,前提是裂纹的真阳性分数在 0.6 以上。

5.4 缺陷类样本太少,训练曲线震荡不收敛

现象:loss 在训练中期反复横跳,验证集 mAP 上下波动超过 3 个点。 原因:裂纹样本只有几十张,一个 epoch 里模型见到的裂纹次数太少,梯度不稳定。 解决:过采样加增强把裂纹样本提到 400 张以上,并降低mixup强度。特别提醒:mixup=1.0时裂纹样本会被大量混合进背景,模型更学不到缺陷特征,缺陷类场景控制在 0.1~0.2 之间。

5.5 导出 TensorRT 后掉点,FP16 和校准要分开看

现象:FP16 引擎在验证集上 mAP 下降 5 个点,齿条边缘的细小裂纹全部丢失。 原因:FP16 的动态范围对低对比度梯度不友好,细线目标的前景像素在 FP16 下容易被舍入成背景。 解决:对裂纹类单独评估召回率,而不是只看整体 mAP。如果在 AGX Orin 上必须用 FP16 才够快,就退一步用 FP32 引擎跑 1024 输入,延迟可能从 25ms 涨到 60ms,但漏检比延迟更要命。产线节拍允许的情况下,我宁愿保精度。

6. 进阶:从检测框到裂纹度量,用分割和骨架化算出缺陷长度

检测框能告诉你裂纹在哪,但产线质检往往需要的是“这条裂纹多长、面积多大、是否超标”。YOLOv8 / YOLOv11 自带实例分割版,模型名类似yolov8n-seg.pt,输出除了检测框还有每个目标的掩膜。分割版对细长裂纹的效果比检测框好很多,因为掩膜可以呈现曲线形态。我现在的做法是两阶段:先用检测版快速定位,再用分割版对裂纹候选区域做精细掩膜,然后把掩膜骨架化计算长度。

import cv2 import numpy as np from skimage.morphology import skeletonize def crack_length(mask, pixel_per_mm): """从裂纹掩膜计算真实长度(毫米)。""" mask_bin = (mask > 0.5).astype(np.uint8) * 255 # 形态学闭运算,把断裂的裂纹段连起来 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask_closed = cv2.morphologyEx(mask_bin, cv2.MORPH_CLOSE, kernel) # 骨架化:提取单像素曲线 skel = skeletonize(mask_closed > 0) # 统计骨架像素数量,乘以像素间隔即长度 pixels = np.sum(skel) length_px = pixels * 1.0 length_mm = length_px / pixel_per_mm return length_mm

骨架化前做闭运算是必要的。真实产线拍到的裂纹常被灰尘或噪点打断,直接骨架化会得到十几段碎片,长度严重低估。pixel_per_mm从标定板获得,D435i 配合特定工作距离下可以固定为一个常数,但在产线上如果用变焦镜头,每次切换焦距都要重新标定。

长度算出来后,按标准判定缺陷等级比直接输出“有裂纹/无裂纹”更有价值。把长度和宽度联合做阈值判断:长度小于 5mm 且宽度小于 0.5mm 的标记为观察,超过就判不合格。

最后一件事是验证闭环。不要只信训练集的 mAP,要专门留出一条产线连续运行 8 小时,统计漏检数和误检数。漏检率高于 0.5% 就回去看标注质量,误检率高于 5% 就调阈值或加后处理。这套路子走下来,YOLO 在齿条、螺栓、螺母加裂纹检测这个任务上是可以真正顶住产线压力的。我早期吃过标注随意的亏,后来所有项目都强制走“标注检查 → 训练 → 产线试跑”三步闭环,再没出现过批量漏检。希望帮到你。

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

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

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

立即咨询