计算机视觉:YOLO不够用了之后

先复现,再谈优化。

很多人一上来就讲计算机视觉的全景图;我更想先把这次卡住的点说清楚。

为什么要从YOLO开始

刚开始调研的时候,项目组里有人建议用传统图像处理,有人建议用 Faster R-CNN。传统方法的逻辑清晰,但对光照变化、纹理差异太敏感;Faster R-CNN 精度够,但推理速度在工控机上有点吃紧。

最后还是选了 YOLOv8。理由很现实:

  1. 社区活跃,遇到问题能搜到现成解决方案
  2. 工程化程度高,部署折腾少
  3. 推理速度够快,在一张 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 手工标注。这个过程真的很无聊,但也有意外收获:

  1. 以前没注意到的缺陷类型,标注过程中被发现了
  2. 光源波动、相机抖动、背景杂物,这些现实问题在标注时就意识到了
# 安装 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 内完成图像采集、推理、输出结果。

优化方案:

  1. 减小输入尺寸:从 640×640 降到 512×512,精度损失 1-2%,速度提升 40%
  2. 使用 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%。

问题三:边缘案例

系统运行一个月后,出现了一个意外情况:有些工件的表面有油污,模型把油污误判成缺陷。油污本身不是质量问题,但外观上和某种缺陷纹理很像。

解决这个问题,我们加了两个策略:

  1. 在数据集中加入油污样本,标记为"正常"
  2. 后处理规则:如果检测到"缺陷",但区域纹理的某些统计特征接近油污,则降低置信度
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)

通过日志分析,我们发现:

  1. 光照强度低于 400 lux 时,误报率明显上升
  2. 某些时间段(比如换班时)缺陷检出率下降,怀疑是操作员调整了相机位置
  3. 周末停机后第一天的检测速度比平时慢 10%,可能是系统预热问题

这些发现直接指导了后续的改进:加装光源稳定装置、定期校准相机位置、优化冷启动逻辑。

一点体会

做完这个项目,感受最深的是:现场环境理解比换模型重要。YOLOv8 代码和社区都省心,难的是把实验室 0.95 mAP 变成产线 0.92 检出率、0.05 误报率。

类别不平衡、增强参数、光照抖动、油污误判——都不是论文里的"前沿问题",但每一个都直接决定系统能不能用。下次类似项目,我会多留时间在产线看数据怎么来的,而不是第一周就纠结该用 YOLO 还是 DETR。

版权声明: 本文首发于 指尖魔法屋-计算机视觉:YOLO不够用了之后https://blog.thinkmoon.cn/post/155-computer-vision-yolo-industrial-detection-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!