计算机视觉:YOLO不够用了之后
先复现,再谈优化。
很多人一上来就讲计算机视觉的全景图;我更想先把这次卡住的点说清楚。
为什么要从YOLO开始
刚开始调研的时候,项目组里有人建议用传统图像处理,有人建议用 Faster R-CNN。传统方法的逻辑清晰,但对光照变化、纹理差异太敏感;Faster R-CNN 精度够,但推理速度在工控机上有点吃紧。
最后还是选了 YOLOv8。理由很现实:
- 社区活跃,遇到问题能搜到现成解决方案
- 工程化程度高,部署折腾少
- 推理速度够快,在一张 RTX 3060 上能做到 100+ FPS
当然,选技术栈看的是谁在现场最稳、最省事,不是谁名字最新。
模型训练的第一件事:数据怎么来
模型训练之前最头疼的是数据。工业现场不像网上公开数据集,你没法直接下载几千张标注好的"缺陷工件"图片。
我们采用的是两种方式:
合成数据
先用 3D 软件建个工件模型,再渲染成不同角度、光照、材质的图片。代码是这样的:
import bpy
import numpy as np
from pathlib import Path
def render_object_views(obj_path, output_dir, num_views=100):
# 加载模型
bpy.ops.import_scene.obj(filepath=str(obj_path))
obj = bpy.context.selected_objects[0]
# 设置随机光照和材质
light_data = bpy.data.lights.new(name="Light", type='POINT')
light_object = bpy.data.objects.new(name="Light", object_data=light_data)
bpy.context.collection.objects.link(light_object)
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
for i in range(num_views):
# 随机旋转
obj.rotation_euler = np.random.uniform(0, 6.28, 3)
# 随机光源位置
light_object.location = np.random.uniform(-5, 5, 3)
# 渲染
bpy.context.scene.render.filepath = str(output_path / f"view_{i:04d}.png")
bpy.ops.render.render(write_still=True)
render_object_views("workpiece.obj", "synthetic_data", num_views=200)
这种方式能快速生成大量标注数据,但有个问题:渲染出来的"干净"图片和现场拍摄的真实图片之间,总有一层质感上的差距。
真实数据标注
现场架个相机,拍了大概两周的真实生产数据,然后用 LabelImg 手工标注。这个过程真的很无聊,但也有意外收获:
- 以前没注意到的缺陷类型,标注过程中被发现了
- 光源波动、相机抖动、背景杂物,这些现实问题在标注时就意识到了
# 安装 LabelImg
pip install labelImg
# 启动标注
labelImg raw_images/ labels/
数据收集的教训:不要以为"够多"就行,关键是"够对"。我们早期花了大量时间采集普通样本,后来发现缺陷样本只占 1%,模型学出来的逻辑就是"把所有东西都当正常"。后来专门调整采样比例,强制让缺陷样本占到 20% 左右,效果才稳定下来。
YOLOv8 训练和调优
YOLOv8 的训练还算顺利,官方给了个完整的训练脚本:
from ultralytics import YOLO
# 加载预训练模型
model = YOLO('yolov8n.pt')
# 训练
results = model.train(
data='data.yaml',
epochs=100,
imgsz=640,
batch=16,
device=0, # GPU
project='runs/train',
name='defect_detection',
patience=10 # 早停
)
data.yaml 的内容:
path: dataset/
train: images/train/
val: images/val/
test: images/test/
names:
0: normal
1: crack
2: missing_part
3: wrong_orientation
训练过程踩过的坑
第一个坑:类别不平衡
正常样本占了 99%,缺陷样本只有 1%。训练出来的模型在验证集上 mAP 看起来不错(因为全预测为"正常"也能达到 99% 准确率),但实际用的时候完全抓不到缺陷。
解决方法是给缺陷类别加权重:
# 计算类别权重
import numpy as np
from sklearn.utils.class_weight import compute_class_weight
labels = [0]*9900 + [1]*30 + [2]*35 + [3]*35 # 示例数据
class_weights = compute_class_weight(
'balanced',
classes=np.unique(labels),
y=labels
)
# 在训练时传入
results = model.train(
...,
cls=class_weights.tolist()
)
第二个坑:验证集和测试集差距太大
我们最开始把现场采集的一批数据直接分成训练集和验证集,结果在验证集上 mAP 0.92,但部署到现场后,召回率直接掉到 0.6。
后来才发现,训练集里采集的环境是"理想状态"——光源稳定、背景干净、工件摆放位置固定。但实际生产线上,光源有波动、背景有杂物、工件位置会偏移。
改进方法是做数据增强:
results = model.train(
...,
augment=True,
hsv_h=0.015, # 色调增强
hsv_s=0.7, # 饱和度增强
hsv_v=0.4, # 亮度增强
degrees=15.0, # 旋转
translate=0.1, # 平移
scale=0.5, # 缩放
fliplr=0.5, # 左右翻转
mosaic=1.0, # Mosaic增强
mixup=0.1 # Mixup增强
)
数据增强不是越多越好,要根据实际场景调。比如工业现场不会出现"工件上下颠倒"的情况,那就不加垂直翻转;但光源波动、位置偏移是常态,这类增强就要加重。
模型部署:从实验室到生产线
模型训练完成后,要把它部署到现场的工控机。我们的环境是:
- 系统:Ubuntu 20.04
- 硬件:Intel i7-10700 + RTX 3060
- 框架:PyTorch + ONNX Runtime
导出 ONNX
# 导出 ONNX 模型
model.export(
format='onnx',
opset=12,
simplify=True,
dynamic=False,
imgsz=640
)
推理代码
import cv2
import numpy as np
import onnxruntime as ort
class DefectDetector:
def __init__(self, model_path):
self.session = ort.InferenceSession(model_path)
self.input_name = self.session.get_inputs()[0].name
self.output_names = [o.name for o in self.session.get_outputs()]
# 类别名称
self.class_names = ['normal', 'crack', 'missing_part', 'wrong_orientation']
def preprocess(self, image):
# 调整尺寸
img = cv2.resize(image, (640, 640))
# 归一化
img = img.astype(np.float32) / 255.0
# 转换为 CHW 格式
img = np.transpose(img, (2, 0, 1))
# 添加 batch 维度
img = np.expand_dims(img, axis=0)
return img
def postprocess(self, outputs, conf_threshold=0.5, iou_threshold=0.45):
# 解析 YOLO 输出
predictions = outputs[0]
# 过滤低置信度检测
valid_mask = predictions[:, 4] >= conf_threshold
predictions = predictions[valid_mask]
# NMS 去重
boxes = predictions[:, :4]
scores = predictions[:, 4]
class_ids = predictions[:, 5:].argmax(axis=1)
indices = cv2.dnn.NMSBoxes(
boxes.tolist(),
scores.tolist(),
conf_threshold,
iou_threshold
)
results = []
for i in indices:
x1, y1, x2, y2 = map(int, boxes[i])
class_id = int(class_ids[i])
confidence = float(scores[i])
results.append({
'box': [x1, y1, x2, y2],
'class_id': class_id,
'class_name': self.class_names[class_id],
'confidence': confidence
})
return results
def detect(self, image):
# 预处理
input_data = self.preprocess(image)
# 推理
outputs = self.session.run(self.output_names, {self.input_name: input_data})
# 后处理
results = self.postprocess(outputs)
return results
# 使用示例
detector = DefectDetector('defect_detection.onnx')
image = cv2.imread('test_image.jpg')
results = detector.detect(image)
for result in results:
print(f"检测到 {result['class_name']}, 置信度: {result['confidence']:.2f}")
cv2.rectangle(image,
(result['box'][0], result['box'][1]),
(result['box'][2], result['box'][3]),
(0, 255, 0), 2)
cv2.imwrite('result.jpg', image)
部署时的问题
问题一:推理速度不够快
在工控机上,单张图片推理时间大概 80ms,但现场要求在 150ms 内完成图像采集、推理、输出结果。
优化方案:
- 减小输入尺寸:从 640×640 降到 512×512,精度损失 1-2%,速度提升 40%
- 使用 TensorRT:把 ONNX 转成 TensorRT 引擎,推理时间从 80ms 降到 35ms
# 使用 TensorRT
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
def build_engine(onnx_path):
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, TRT_LOGGER)
with open(onnx_path, 'rb') as model:
parser.parse(model.read())
config = builder.create_builder_config()
config.max_workspace_size = 1 << 30 # 1GB
engine = builder.build_engine(network, config)
return engine
问题二:光源抖动导致误判
现场没用稳定 LED,顶上是车间荧光灯。光源波动会导致图像亮度变化,模型对同一缺陷在不同亮度下的置信度差异很大。
解决方案:在图像预处理中加入直方图均衡化
def preprocess_with_clahe(image):
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# CLAHE 对比度受限的自适应直方图均衡化
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
equalized = clahe.apply(gray)
# 转回 BGR
equalized_bgr = cv2.cvtColor(equalized, cv2.COLOR_GRAY2BGR)
return equalized_bgr
这个改动把光照波动带来的误判率从 12% 降到了 3%。
问题三:边缘案例
系统运行一个月后,出现了一个意外情况:有些工件的表面有油污,模型把油污误判成缺陷。油污本身不是质量问题,但外观上和某种缺陷纹理很像。
解决这个问题,我们加了两个策略:
- 在数据集中加入油污样本,标记为"正常"
- 后处理规则:如果检测到"缺陷",但区域纹理的某些统计特征接近油污,则降低置信度
def is_oil_stain(image, box):
# 提取检测区域
x1, y1, x2, y2 = box
roi = image[y1:y2, x1:x2]
# 计算纹理特征
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
glcm = cv2.calcHist([gray], [0], None, [256], [0, 256])
# 油污的纹理特征通常比较"平滑"
smoothness = np.sum(glcm[1:255]) / np.sum(glcm[1:256])
return smoothness > 0.85 # 阈值需要根据实际情况调
# 在后处理中调用
for result in results:
if result['class_name'] in ['crack', 'missing_part']:
if is_oil_stain(image, result['box']):
result['confidence'] *= 0.5 # 降低置信度
性能监控和迭代
系统上线之后,调试才算真正开始。我们在现场加了一些监控指标:
- 每小时检测数量
- 缺陷检出率
- 误报率
- 推理耗时分布
- 光照强度变化
import time
import json
from datetime import datetime
class DetectionMonitor:
def __init__(self, log_file='detection_log.jsonl'):
self.log_file = log_file
def log(self, result, inference_time, light_intensity=None):
log_entry = {
'timestamp': datetime.now().isoformat(),
'detections': len(result),
'defect_count': sum(1 for r in result if r['class_name'] != 'normal'),
'inference_time_ms': inference_time * 1000,
'light_intensity': light_intensity
}
with open(self.log_file, 'a') as f:
f.write(json.dumps(log_entry) + '\n')
# 使用示例
monitor = DetectionMonitor()
start_time = time.time()
results = detector.detect(image)
inference_time = time.time() - start_time
monitor.log(results, inference_time, light_intensity=450)
通过日志分析,我们发现:
- 光照强度低于 400 lux 时,误报率明显上升
- 某些时间段(比如换班时)缺陷检出率下降,怀疑是操作员调整了相机位置
- 周末停机后第一天的检测速度比平时慢 10%,可能是系统预热问题
这些发现直接指导了后续的改进:加装光源稳定装置、定期校准相机位置、优化冷启动逻辑。
一点体会
做完这个项目,感受最深的是:现场环境理解比换模型重要。YOLOv8 代码和社区都省心,难的是把实验室 0.95 mAP 变成产线 0.92 检出率、0.05 误报率。
类别不平衡、增强参数、光照抖动、油污误判——都不是论文里的"前沿问题",但每一个都直接决定系统能不能用。下次类似项目,我会多留时间在产线看数据怎么来的,而不是第一周就纠结该用 YOLO 还是 DETR。
版权声明: 本文首发于 指尖魔法屋-计算机视觉:YOLO不够用了之后(https://blog.thinkmoon.cn/post/155-computer-vision-yolo-industrial-detection-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。