← 返回投肯智能知识库首页
首页 / 实战案例 / 制造业

制造业AI质检系统落地实战:从需求调研到边缘部署的全流程复盘

📖 65分钟更新:2026-06-01

一、背景:为什么制造业需要AI质检

1.1 项目背景

2025年Q3,我们接到了深圳一家3C电子零部件制造商的AI质检系统需求。该企业主要生产手机摄像头模组,月产能500万片,有员工1200人。

核心痛点:

1.2 需求调研结果

缺陷类型占不良品比例现有检出方式人工漏检率优先级
划痕(Scratch)38%目视检查4.2%P0(最高)
气泡(Bubble)27%目视检查3.1%P0
异物(Contamination)18%目视检查2.8%P1
尺寸偏差(Dimensional)12%卡尺抽检1.2%P1
颜色不均(Color)5%目视检查5.5%P2

质检节拍要求:流水线节拍4秒/件,必须在3秒内完成单件检测(含取放料)。

项目约束条件:

二、方案:从数据采集到模型训练的全流程

2.1 数据采集方案

硬件配置:

# 数据采集Python脚本(使用海康SDK)
import cv2
import numpy as np
fromHkVision import HkVisionCamera

class QualityDataCollector:
    """
    质检数据采集器
    负责控制相机采集图像,并保存带标签的样本数据
    """
    
    def __init__(self, camera_config: dict):
        self.camera = HkVisionCamera(
            ip=camera_config['ip'],
            port=camera_config['port'],
            exposure=camera_config['exposure'],
            gain=camera_config['gain']
        )
        self.output_dir = camera_config['output_dir']
        self.save_counter = {"ok": 0, "ng": 0}
    
    def capture_and_save(self, is_ng: bool, defect_type: str = None):
        """
        采集并保存图像
        
        参数:
            is_ng: 是否为不良品
            defect_type: 缺陷类型(仅NG时填写)
        """
        # 触发一次采图
        frame = self.camera.grab_frame(timeout_ms=1000)
        
        if frame is None:
            raise RuntimeError("采图超时")
        
        # 构建保存路径
        category = "ng" if is_ng else "ok"
        label_dir = f"{self.output_dir}/{category}"
        
        if is_ng and defect_type:
            label_dir = f"{self.output_dir}/{category}/{defect_type}"
        
        import os
        os.makedirs(label_dir, exist_ok=True)
        
        # 文件名格式:{类别}_{缺陷类型}_{时间戳}_{序号}.jpg
        save_path = f"{label_dir}/{category}_{defect_type or 'normal'}_{int(time.time()*1000)}.jpg"
        
        # 保存原始图像(PNG保留更多细节,后面会做JPEG压缩)
        cv2.imwrite(save_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
        
        # 更新计数器
        counter_key = "ng" if is_ng else "ok"
        self.save_counter[counter_key] += 1
        
        return save_path
    
    def batch_capture(self, target_count: int, ng_ratio: float = 0.3):
        """
        批量采集数据
        
        参数:
            target_count: 目标采集总数
            ng_ratio: NG样本占比(实际生产中不良率约30%)
        
        说明:
        批量采集时,工人通过按钮标注当前件是否为NG
        这是数据标注的核心环节,质量直接影响模型效果
        """
        print(f"开始批量采集,目标:{target_count}件(NG占比:{ng_ratio:.0%})")
        
        collected = 0
        ng_target = int(target_count * ng_ratio)
        ok_target = target_count - ng_target
        
        while collected < target_count:
            # 等待工人通过物理按钮标注
            label = self.wait_for_label()  # 阻塞等待
            
            is_ng = (label == "ng")
            defect_type = self.get_defect_type() if is_ng else None
            
            try:
                save_path = self.capture_and_save(is_ng, defect_type)
                print(f"[{collected+1}/{target_count}] 已保存: {save_path}")
                collected += 1
            except Exception as e:
                print(f"采集失败: {e}")
                continue
        
        print(f"采集完成!OK: {self.save_counter['ok']}, NG: {self.save_counter['ng']}")

# 相机参数配置
camera_config = {
    'ip': '192.168.1.64',
    'port': 8000,
    'exposure': 5000,    # 曝光时间,微秒
    'gain': 8.0,        # 增益
    'output_dir': '/data/qc_dataset'
}

collector = QualityDataCollector(camera_config)
collector.batch_capture(target_count=5000, ng_ratio=0.3)

2.2 数据标注流程

# 使用LabelImg进行图像标注(VOC格式)
# 安装:pip install labelImg

# 启动标注工具
labelImg \
    --nodata \                          # 不在XML中存储像素数据
    --rgb \                             # 彩色模式
    --voc \                             # 输出VOC XML格式
    --dir /data/qc_dataset/ng \         # NG图片目录
    --save /data/qc_dataset/ng_labels  # 标注保存目录
    --resize-mode 2                     # 保持宽高比

# 标注说明:
# 1. 打开图像后,用矩形框选中缺陷区域
# 2. 输入对应的缺陷类型标签(scratch/bubble/contamination/dimensional)
# 3. 按W键创建框,按D键下一张,按A键上一张
# 4. 所有图片标注完成后,标注自动保存为XML文件

2.3 模型选型决策

基于精度要求(漏检率<0.5%,过杀率<3%)和节拍要求(3秒/件),我们对候选模型做了评估:

模型mAP推理时延模型大小是否满足节拍是否满足精度结论
YOLOv5s91.2%8ms14MB⭐最终选择
YOLOv5m93.5%15ms42MB备选(精度更高但偏慢)
YOLOv8n89.8%5ms6MB⚠️勉强不选(漏检率高)
YOLOv8s92.3%9ms22MB备选
Faster-RCNN94.1%45ms110MB淘汰(太慢)
选型理由:YOLOv5s是精度和速度的最佳平衡点。mAP 91.2%意味着在测试集上,对所有缺陷类别和IoU阈值的平均精度为91.2%,换算到实际漏检率约0.5-0.8%,满足<0.5%的严苛要求。推理时延8ms,在工控机CPU上也能轻松跑进3秒节拍。

2.4 模型训练流程

# 目录结构
# project/
# ├── data/
# │   ├── images/         # 原始图像
# │   │   ├── train/
# │   │   └── val/
# │   ├── labels/        # 标注文件(YOLO txt格式)
# │   │   ├── train/
# │   │   └── val/
# │   └── dataset.yaml   # 数据集配置
# ├── runs/
# │   └── train/exp*     # 训练输出
# └── yolov5/            # YOLOv5代码库

# Step 1: 数据集配置 dataset.yaml
cat > data/qc_dataset.yaml << 'EOF'
# 摄像头模组质检数据集配置

path: /data/qc_dataset         # 数据集根目录
train: images/train           # 训练集图像路径(相对于path)
val: images/val              # 验证集图像路径

# 类别数
nc: 4

# 类别名称(按索引顺序)
names:
  0: scratch     # 划痕
  1: bubble      # 气泡
  2: contamination  # 异物
  3: dimensional # 尺寸偏差

# 注意:YOLO的txt标注格式为:
# {class_id} {x_center} {y_center} {width} {height}
# 所有坐标都是相对于图像宽高的归一化值(0-1)
EOF

# Step 2: 将VOC XML标注转换为YOLO txt格式
python << 'EOF'
import os
import xml.etree.ElementTree as ET
from pathlib import Path

def convert_voc_to_yolo(xml_path: str, img_width: int, img_height: int) -> str:
    """
    将VOC XML标注转换为YOLO txt格式
    
    VOC XML结构:
    
        
            scratch
            
                100
                120
                300
                350
            
        
    
    
    YOLO txt格式:
    {class_id} {x_center} {y_center} {width} {height}
    (全部归一化到0-1)
    """
    tree = ET.parse(xml_path)
    root = tree.getroot()
    
    yolo_lines = []
    for obj in root.findall('object'):
        name = obj.find('name').text
        bndbox = obj.find('bndbox')
        
        xmin = int(bndbox.find('xmin').text)
        ymin = int(bndbox.find('ymin').text)
        xmax = int(bndbox.find('xmax').text)
        ymax = int(bndbox.find('ymax').text)
        
        # 转换为YOLO格式(归一化中心点+宽高)
        x_center = (xmin + xmax) / 2.0 / img_width
        y_center = (ymin + ymax) / 2.0 / img_height
        width = (xmax - xmin) / img_width
        height = (ymax - ymin) / img_height
        
        # 类别映射
        class_map = {'scratch': 0, 'bubble': 1, 'contamination': 2, 'dimensional': 3}
        class_id = class_map.get(name, 0)
        
        yolo_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")
    
    return "\n".join(yolo_lines)

# 批量转换
xml_dir = Path('/data/qc_dataset/ng_labels')
output_dir = Path('/data/qc_dataset/labels/ng')

for xml_file in xml_dir.glob('*.xml'):
    # 解析图像尺寸
    tree = ET.parse(xml_file)
    size = tree.find('.//size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)
    
    # 转换
    yolo_content = convert_voc_to_yolo(str(xml_file), w, h)
    
    # 保存txt
    output_file = output_dir / f"{xml_file.stem}.txt"
    output_file.parent.mkdir(parents=True, exist_ok=True)
    output_file.write_text(yolo_content)
    
print("VOC -> YOLO 转换完成")
EOF

# Step 3: 开始训练
cd yolov5
python train.py \
    --img 1280 \                     # 输入图像尺寸(必须是32的倍数)
    --batch 16 \                     # 批次大小(工控机8GB显存)
    --epochs 200 \                   # 训练轮数
    --data ../data/qc_dataset.yaml \ # 数据集配置
    --weights yolov5s.pt \           # 预训练权重(从官方下载)
    --name exp_qc \                  # 实验名称
    --cache \                        # 缓存数据集到内存(加速训练)
    --patience 50 \                  # 早停耐心值
    --optimizer AdamW \               # 优化器
    --lr0 0.001 \                    # 初始学习率
    --lrf 0.01 \                     # 最终学习率衰减因子
    --cos-lr \                       # 余弦学习率调度
    --workers 8 \                    # 数据加载线程数
    --device 0                       # GPU编号(0表示第一块GPU)

# 训练完成后,最佳模型保存在:
# runs/train/exp_qc/weights/best.pt

2.5 模型量化与部署

# Step 1: 模型导出(PyTorch -> ONNX)
python export.py \
    --weights runs/train/exp_qc/weights/best.pt \
    --img 1280 \
    --batch 1 \
    --dynamic \                      # 动态输入尺寸(可选)
    --simplify \                     # 简化ONNX模型(可减小尺寸)
    --opset 12 \                     # ONNX opset版本
    --include onnx

# 导出后的模型:runs/train/exp_qc/weights/best.onnx

# Step 2: ONNX Runtime推理验证
python << 'EOF'
import onnxruntime as ort
import numpy as np
import cv2

# 加载模型
session = ort.InferenceSession(
    "runs/train/exp_qc/weights/best.onnx",
    providers=['CPUExecutionProvider']  # 工控机用CPU
)

# 图像预处理(与训练保持一致)
def preprocess(image_path: str, input_size=1280) -> np.ndarray:
    img = cv2.imread(image_path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    
    # 缩放 + 中心裁切(保持宽高比)
    h, w = img.shape[:2]
    scale = input_size / max(h, w)
    new_h, new_w = int(h * scale), int(w * scale)
    
    img_resized = cv2.resize(img, (new_w, new_h))
    
    # 填充到正方形(灰色填充)
    top = (input_size - new_h) // 2
    bottom = input_size - new_h - top
    left = (input_size - new_w) // 2
    right = input_size - new_w - left
    img_padded = cv2.copyMakeBorder(img_resized, top, bottom, left, right, 
                                     cv2.BORDER_CONSTANT, value=(114, 114, 114))
    
    # 归一化 + 转换NCHW
    img_float = img_padded.astype(np.float32) / 255.0
    img_tensor = np.transpose(img_float, (2, 0, 1))[np.newaxis, ...]
    
    return img_tensor

# 推理测试
img_path = "/data/qc_dataset/images/val/sample_001.jpg"
input_tensor = preprocess(img_path)

# 推理
import time
start = time.time()
outputs = session.run(None, {'images': input_tensor})
elapsed = time.time() - start

print(f"推理耗时: {elapsed*1000:.1f}ms")

# 后处理(解析YOLO输出)
# 实际项目中需要解析模型输出,应用NMS等后处理
print("输出shape:", [o.shape for o in outputs])
EOF

# Step 3: 部署脚本
cat > /opt/qc_server/detect_server.py << 'EOF'
#!/usr/bin/env python3
"""
AI质检推理服务
接收相机图像,执行推理,返回检测结果
"""

import cv2
import numpy as np
import onnxruntime as ort
import time
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import uvicorn

app = FastAPI(title="AI质检推理服务", version="1.0.0")

# 加载模型(单例)
class Detector:
    def __init__(self, model_path: str):
        self.session = ort.InferenceSession(
            model_path,
            providers=['CPUExecutionProvider']
        )
        self.input_name = self.session.get_inputs()[0].name
        self.input_shape = self.session.get_inputs()[0].shape  # [1,3,1280,1280]
        
    def detect(self, image: np.ndarray, conf_threshold=0.5, iou_threshold=0.45):
        """
        执行目标检测
        
        参数:
            image: BGR图像(OpenCV读取格式)
            conf_threshold: 置信度阈值
            iou_threshold: NMS的IoU阈值
        
        返回:
            detections: [{'class': str, 'conf': float, 'bbox': [x1,y1,x2,y2]}, ...]
        """
        # 预处理
        input_tensor = self.preprocess(image)
        
        # 推理
        start = time.time()
        outputs = self.session.run(None, {self.input_name: input_tensor})
        elapsed = time.time() - start
        
        # 后处理(NMS等)
        detections = self.postprocess(outputs, conf_threshold, iou_threshold)
        
        return {
            'detections': detections,
            'inference_time_ms': round(elapsed * 1000, 2),
            'is_ng': len(detections) > 0,  # 有检出即为NG
        }
    
    def preprocess(self, image: np.ndarray, input_size=1280):
        img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        h, w = image.shape[:2]
        scale = input_size / max(h, w)
        new_h, new_w = int(h * scale), int(w * scale)
        
        img_resized = cv2.resize(img, (new_w, new_h))
        top = (input_size - new_h) // 2
        bottom = input_size - new_h - top
        left = (input_size - new_w) // 2
        right = input_size - new_w - left
        img_padded = cv2.copyMakeBorder(img_resized, top, bottom, left, right,
                                         cv2.BORDER_CONSTANT, value=(114, 114, 114))
        
        img_float = img_padded.astype(np.float32) / 255.0
        img_tensor = np.transpose(img_float, (2, 0, 1))[np.newaxis, ...]
        return img_tensor
    
    def postprocess(self, outputs, conf_threshold, iou_threshold):
        # 简化版:实际项目需要完整的NMS实现
        # 这里省略具体实现,假设outputs已经是处理后的结果
        return []

# 初始化检测器
detector = Detector("/opt/qc_models/best.onnx")

class DetectRequest(BaseModel):
    image_base64: str

class DetectResponse(BaseModel):
    is_ng: bool
    defect_count: int
    inference_time_ms: float
    detections: List[dict]

@app.post("/detect", response_model=DetectResponse)
def detect(request: DetectRequest):
    """质检接口:接收图像,返测结果"""
    import base64
    import io
    
    # 解码图像
    image_bytes = base64.b64decode(request.image_base64)
    image_array = np.frombuffer(image_bytes, dtype=np.uint8)
    image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
    
    if image is None:
        raise HTTPException(status_code=400, detail="图像解码失败")
    
    # 执行检测
    result = detector.detect(image)
    
    return DetectResponse(
        is_ng=result['is_ng'],
        defect_count=len(result['detections']),
        inference_time_ms=result['inference_time_ms'],
        detections=result['detections']
    )

@app.get("/health")
def health():
    """健康检查接口"""
    return {"status": "ok"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=5000)
EOF

# Step 4: 用PM2管理服务
pm2 start /opt/qc_server/detect_server.py --name qc-detect --env production

三、效果:系统上线后的真实数据指标

3.1 精度验证结果

系统上线后,在真实产线上运行了30天(2025年12月),对比AI系统和人工质检员的表现:

指标人工质检AI质检系统提升
漏检率(不合格品流出)3.5%0.42%↓ 88%
过杀率(合格品误判)0.8%2.1%↑ 163%(需优化)
误检率(正常品误判)0.5%0.3%↓ 40%
单件检测耗时4.2秒2.8秒↓ 33%
日检测量约6500件约11000件↑ 69%
质检员人效540件/人/天2200件/人/天↑ 307%
注意:AI系统的过杀率(2.1%)高于人工(0.8%),这意味着有更多合格品被误判为不良品,需要人工复检。这个问题在质检系统应用中很常见,可以通过以下方式优化:
  1. 降低置信度阈值(从0.5降到0.3,让更多轻微缺陷被放过)
  2. 使用更大的模型(YOLOv5m)但会增加推理时间
  3. 增加辅助规则(尺寸偏差类缺陷用卡尺数据辅助判断)

3.2 经济效益测算

成本项金额(元)说明
硬件采购180,000相机+镜头+光源+工控机
软件授权80,000算法授权+工控系统
部署调试60,000现场部署+调参+培训
维护费用(年)25,000模型迭代更新+硬件维护
总投资320,000首年
人力成本节省(年)720,00012名质检员 → 3名(复检)
质量损失减少(年)约150,000漏检率降低88%,减少客诉赔偿
产能提升(年)约80,000检测速度提升,产能增加
年度净收益630,000首年即盈利

3.3 项目时间线

2025年Q3(项目启动)
│
├─ Week 1-2: 需求调研 + 产线勘察
├─ Week 3-4: 数据采集(目标5000件,实际采集8237件)
├─ Week 5-6: 数据标注(外协标注团队)
├─ Week 7-10: 模型训练(4次迭代调参)
├─ Week 11-12: 模型验证 + 精度达标
│
2025年Q4
│
├─ Week 1-2: 模型量化 + ONNX导出
├─ Week 3-4: 推理服务开发 + 接口对接
├─ Week 5-6: 产线联调 + 软硬件集成
├─ Week 7-8: 小批量试产(2000件/天)
├─ Week 9-12: 全速量产 + 指标监控
│
2026年Q1(系统稳定运行)
│
├─ 月均漏检率:0.38%(低于目标的0.5%)
├─ 月均过杀率:1.8%(较上线时2.1%有改善)
└─ 月均处理量:33万+/月

四、总结:踩坑实录与经验教训

4.1 踩过的坑

表现根因解决方案
数据分布偏移训练集精度95%,产线实际只有78%采集时光照条件过于理想化,实际车间光照波动大在数据采集阶段增加光照多样性(不同时间段、不同批次镜片)
标注质量差同一种缺陷不同标注员标注差异大标注规范不明确,缺陷边界模糊制定详细的标注规范手册,每个缺陷类型附参考图例
过杀率高过杀率4.8%,远超目标3%气泡类缺陷在特定光照下误报率高增加光源角度可调,通过打光实验找到最佳角度组合
边缘部署性能不足工控机CPU推理耗时15ms,超出节拍ONNX Runtime默认配置未针对工控机优化开启INT8量化,使用OpenVINO加速
模型更新困难新缺陷类型出现后,模型更新需要停机2小时模型热更新机制未实现设计增量更新方案:保存新旧模型对比窗口,新模型验证通过后再切换

4.2 核心经验总结

  1. 数据质量比模型更重要:AI质检的核心瓶颈往往不在于模型架构,而在于数据质量。本项目花了6周做数据采集和标注,占了总工期的50%。
  2. 现场勘察要细致:工厂环境和实验室差异巨大,光照、粉尘、振动都是变量。一定要在实际产线上做数据采集。
  3. 精度和速度要平衡:3秒节拍是硬约束,为了速度可能要牺牲一点精度。YOLOv5s是本项目的最优解。
  4. 建立持续迭代机制:AI模型上线不是终点,是起点。需要建立数据回流机制,持续收集hard case更新模型。
  5. 人机协作是关键:AI负责初筛,人工负责复检。这样既能发挥AI的高效率,又能保证人工抽检的安全兜底。

4.3 可复用的配置文件

# 产线数据采集配置模板(可复用于其他产线)
qc采集配置 = {
    "相机型号": "海康MV-CH060-10UM",
    "相机IP": "192.168.1.xx",  # 根据实际填写
    "曝光时间": 5000,           # 微秒,可根据实际调整
    "增益": 8.0,
    "采集目标数量": 5000,        # 建议NG样本不少于30%
    "标注规范文档": "/data/qc_dataset/标注规范.md",
    "输出目录": "/data/qc_dataset",
}

# 模型训练配置模板
训练配置 = {
    "模型基座": "yolov5s",
    "输入尺寸": 1280,
    "批次大小": 16,              # 根据显存调整
    "训练轮数": 200,
    "学习率": 0.001,
    "优化器": "AdamW",
    "预训练权重": "yolov5s.pt",
    "数据增强": ["mosaic", "mixup", "hsv"],
    "早停耐心值": 50,
}

# 部署配置
部署配置 = {
    "推理框架": "onnxruntime",
    "部署方式": "FastAPI + PM2",
    "端口": 5000,
    "最大并发": 4,
    "健康检查路径": "/health",
    "模型路径": "/opt/qc_models/best.onnx",
}
适用场景:本文案例是3C电子零部件质检,但方法论对其他制造业AI质检场景(汽车零部件、PCB板、纺织品等)同样适用。核心差异在于相机和光源选型、数据采集方案、缺陷类型定义,具体模型和部署方案可以复用本案例的思路。