一、概述

1.1 背景介绍:传统视觉的局限性

在上一篇文章中,我们成功地运用传统OpenCV算法实现了对螺丝尺寸的精确测量。这展示了传统视觉算法在处理几何特征明确(如长度、圆形度)问题上的强大能力。然而,当我们面对形态不规则、纹理多样的表面瑕疵(如划痕、油污、凹坑)时,会发现很难再写出固定的、普适的规则来描述它们。

强行用传统算法处理这类问题,往往会导致代码变得异常复杂,且对光照、角度等环境变化极为敏感,鲁棒性差。这正是传统视觉算法的“天花板”。要突破它,我们需要一种全新的方法。

1.2 AI的范式转移:让机器自主学习

【核心概念:从“编写规则”到“学习规律”】

传统算法的思路是人类工程师教计算机做事,我们把识别规则(如“颜色小于某个阈值且面积大于某个值的就是瑕疵”)一条条地写入代码。

而深度学习的思路则完全不同,它模仿人类大脑的学习方式,变成了计算机自己从数据中学习规律。我们不再编写具体的识别规则,而是:

  1. 准备大量带有“正确答案”的样本(例如,数千张已经圈出瑕疵的图片)。
  2. 构建一个“神经网络”模型(可以想象成一个结构复杂、有无数旋钮的“黑箱”)。
  3. 让模型一张张地“看”这些图片,并尝试自己去判断。
  4. 每判断一次,就将它的答案与“正确答案”对比,然后自动微调内部的无数个“旋钮”,让下一次的判断更准一些。
  5. 重复数万次后,这个“黑箱”内部的旋钮就自动调整到了一种能够准确识别瑕疵的状态。它自己总结出了规律,甚至是一些人类都无法用语言描述的规律。

【核心概念:目标检测 - “它是什么,它在哪”】

深度学习视觉任务中,与我们需求最匹配的就是目标检测(Object Detection)。它要解决两个核心问题:

  1. 分类(Classification): 图片里有什么?(例如,这是一处scratch_head瑕疵)。
  2. 定位(Localization): 它在图片的哪个位置?(用一个边界框把它圈出来)。

这完美地契合了工业瑕疵检测的需求。

【核心概念:YOLOv8 - 速度与精度的王者】

在目标检测领域,YOLO(You Only Look Once)系列算法因其卓越的速度和精度而闻名。与早期需要多个步骤才能完成检测的算法不同,YOLO创造性地将整个检测过程统一为一个步骤,实现了真正的端到端实时检测,使其在工业界得到了广泛应用。YOLOv8是该系列的高阶版本,由Ultralytics公司开发,它不仅性能顶尖,而且其配套的Python库极为易用,极大地降低了开发门槛。

1.3 为何本章切换至Python?

本系列教程以Qt/C++为主线,但本章的模型训练部分将完全使用Python完成。这是一个基于行业主流实践的慎重选择:

  1. AI生态系统: 当今几乎所有的主流深度学习框架(如PyTorch, TensorFlow)都以Python作为其首选和核心的开发语言。YOLOv8的官方库ultralytics也是纯Python的。Python拥有无与伦比的社区支持、丰富的第三方库和海量的教程资源,使得模型的研发、训练和调试过程变得极为高效和便捷。
  2. 职责分离: 在现代软件工程中,“算法研发”与“软件部署”通常是两个独立的阶段。使用Python进行快速的模型迭代和训练(研发阶段),然后将训练好的模型导出,用C++进行高性能的部署和集成(部署阶段),是目前业界最成熟、最高效的工作流。
  3. 学习效率: 直接在C++中搭建训练环境和编写训练代码,其复杂度和工作量远超Python。为了让读者能专注于理解和实践AI模型训练的核心流程,而不是陷入繁琐的环境配置中,使用Python是最佳选择。

因此,本章将引导读者搭建一个最小化的Python环境,完成模型训练这一特定任务。在下一章,我们将立刻回归C++的世界,学习如何将本章的“劳动成果”——训练好的模型,部署到我们的Qt应用程序中。

1.4 学习目标

通过本篇的学习,读者将能够:

  1. 搭建一个独立的Python环境,并安装必要的AI库。
  2. 掌握使用开源工具LabelImg对图像瑕疵进行标注的核心技能。
  3. 使用Ultralytics YOLOv8框架,在CPU上亲手训练一个能识别多种螺丝瑕疵的AI模型。
  4. 将训练好的PyTorch模型(.pt)导出为部署友好的ONNX格式,为下一章在C++中调用做好准备。

二、Python环境与数据准备

2.1 安装Python与配置虚拟环境

为了不干扰全局环境,推荐使用虚拟环境来管理项目的Python依赖。

1. 安装Python

  • 访问Python官网,下载适用于Windo
  • ws的最新稳定版Python安装包(例如 Python 3.10.0)。
  • 运行安装程序,**务必勾选“Add python.exe to PATH”**选项,然后选择“Install Now”即可。
    在这里插入图片描述

2. 创建虚拟环境

  • 在项目根目录(例如D:\code\ScrewDetector)下,打开终端(CMD或PowerShell)。
  • 运行以下命令创建虚拟环境:
    python -m venv .venv
    
    这会在当前目录下创建一个名为.venv的文件夹,其中包含了独立的Python解释器和库。
  • 激活虚拟环境:
    .\.venv\Scripts\activate
    
    激活成功后,会看到终端提示符前面出现了(.venv)的字样,如下图所示。之后所有的pip安装都将局限于这个环境中。
    在这里插入图片描述

2.2 核心概念:从“异常样本”到“训练数据”

YOLOv8这类监督学习算法,就像一个需要“教科书”来学习的学生。这本“教科书”就是我们的训练数据集。它必须包含两个部分:

  1. 图片(问题): 即带有各种瑕疵的螺丝图片。
  2. 标签(答案): 一个与之对应的文件,明确告诉模型图片中的瑕疵是什么(类别)以及在哪里(边界框坐标)

而我们使用的MVTec AD数据集,其test文件夹下虽然有大量瑕疵图片,但缺少对应的“答案”(标签文件)。good文件夹中的图片由于没有瑕疵,暂时不用于训练(它们将在模型评估时作为负样本使用)。因此,在训练之前,我们必须先完成数据建模数据标注这两个关键步骤。

2.3 数据建模:定义我们的瑕疵类别

【核心概念:归纳与合并】

我们的目标是设计一套清晰、无歧义、易于模型学习的瑕疵类别。分析MVTec screw数据集的test文件夹,它包含了manipulated_front, scratch_head, scratch_neck, thread_side, thread_top这5种已分类的瑕疵。

为了让模型更容易学习,也为了让我们的教学目标更聚焦,我们采用一种更务实的策略:将视觉上相似或物理位置上相近的瑕疵进行归纳合并

我们的合并逻辑如下:

  1. 所有发生在头部的表面划痕(scratch_head),都属于“头部瑕疵”。
  2. 所有发生在颈部的划痕(scratch_neck),都属于“颈部瑕疵”。
  3. 所有发生在螺纹区域的损伤,无论是侧面的(thread_side)、顶部的(thread_top)还是驱动槽的损伤(manipulated_front),都统一归为“螺纹瑕疵”。

由此,我们得到了一个更简洁、更鲁棒的分类方案:

类别ID 类别名称 (Class Name) 含义解释 包含的原始瑕疵
0 head_defect 头部区域的任何可见损伤。 scratch_head
1 neck_defect 颈部区域的任何可见损伤。 scratch_neck
2 thread_defect 螺纹区域的任何可见损伤。 thread_side, thread_top, manipulated_front

对应的dataset.yaml文件应如下:

# ...
names:
  0: head_defect
  1: neck_defect
  2: thread_defect

在接下来的标注和训练环节,我们将完全基于这个三分类方案进行。


2.4 实战:使用经典标注工具Labelme

为了确保标注过程的稳定、高效和离线可用,我们将使用一款在学术界和工业界都广受赞誉的经典开源工具——Labelme

【核心概念:稳定可靠的桌面端标注】

Labelme是一个用Python编写的、跨平台的图形化图像标注软件。

  • 为何选择 Labelme?
    1. 稳定与成熟: Labelme由麻省理工学院(MIT)的计算机科学与人工智能实验室(CSAIL)发起,经过了长时间的发展和迭代,功能非常稳定,是许多经典数据集的“御用”标注工具。
    2. 安装简单: 作为一个纯Python应用,它可以通过pip轻松安装,避免了复杂的环境依赖问题。
    3. 多功能: 虽然我们主要用它画矩形框,但它本身支持多边形、圆形、线条等多种标注形式,为未来可能的复杂任务(如语义分割)提供了扩展性。
    4. 格式转换方便: Labelme默认保存为JSON格式,但有大量现成的脚本可以轻松将其转换为YOLO格式。

1. 安装与启动

  • 确保虚拟环境已激活,在终端中运行:
    pip install "numpy<2.0" onnxruntime==1.18.0 labelme -i https://pypi.tuna.tsinghua.edu.cn/simple
    
  • 安装完成后,运行以下命令启动程序:
    labelme
    
  • 一个图形化的应用程序窗口将会启动,如下图所示:

在这里插入图片描述

2. 标注流程演示

  • (1) 标注设置:单击顶部菜单栏文件将其展开,将 “同时保存图像数据”复选框的勾去掉,然后单击“自动保存”按钮。此操作在每次打开Labelme软件时均需操作。
  • (2) 打开目录: 在Labelme菜单栏中,选择文件 -> 打开目录,然后选择我们数据集中的一个瑕疵图片文件夹,例如 .../dataset/screw/test/scratch_head
    在这里插入图片描述
  • (3) 创建矩形框: 在顶部菜单栏中,单击编辑 -> 创建矩形按钮。
  • (4) 拖动画框并输入标签: 在图片中的瑕疵区域拖动鼠标,画出一个矩形框。松开鼠标后,会弹出一个对话框,让你输入该标注的标签(类别名称)。输入我们定义好的类别,如head_defect,然后点击OK。
    在这里插入图片描述
  • (d) 保存: 点击文件 -> 保存(或按快捷键Ctrl+S)。Labelme会在图片同目录下创建一个同名的.json文件,里面详细记录了标注信息。
  • (e) 切换与重复: 使用上一幅下一幅按钮切换图片,重复(b)至(d)的步骤,直到所有图片标注完成。

2.5 数据整理与格式转换

经过上一步,我们已经在各个瑕疵子文件夹(如scratch_head, thread_side等)中,为每张图片都生成了一个对应的.json标注文件。然而,这些数据还很分散,并且存在文件名冲突(每个文件夹下都有000.png)。此外,Labelme的JSON格式也不是YOLOv8能直接使用的格式。

本节将通过一个自动化脚本,一步到位地解决所有问题。

【核心概念:YOLO标注格式】

在进行转换前,有必要了解YOLO的.txt标注格式。对于每个图像文件(如image.png),都有一个对应的文本文件(image.txt),其内容格式如下:

<class_id> <x_center> <y_center> <width> <height>
  • <class_id>: 类别的索引号(从0开始),对应于labels.txt文件中的行号。
  • <x_center> <y_center>: 边界框中心的归一化坐标(值在0到1之间)。
  • <width> <height>: 边界框的归一化宽度和高度。

归一化意味着所有坐标值都除以了图像的原始宽度或高度。我们的自动化脚本将处理所有这些复杂的计算。

【核心概念:两步走策略】

我们的自动化流程将分为清晰的两步:

  1. 数据整理 (Python): 使用Python脚本,将所有分散的瑕疵图片和.json文件合并到一个统一的临时文件夹中,同时完成重命名和JSON内部路径的修正。
  2. 格式转换与划分 (labelme2yolo): 调用高效的labelme2yolo命令行工具,让它自动处理上一步生成的临时文件夹,完成从JSON到YOLO .txt的格式转换,并自动按比例划分好训练集和验证集。

【例7-1】 自动化数据整理与转换脚本

1. 安装依赖库

  • 确保虚拟环境已激活,安装labelme2yolo库。它会自动处理所有依赖。
    pip install labelme2yolo -i https://pypi.tuna.tsinghua.edu.cn/simple
    

2. 编写代码 (prepare_dataset.py)

  • 在项目根目录下的dataset文件夹中,创建一个名为prepare_dataset.py的Python脚本文件。
import os
import shutil
import json
import subprocess

def prepare_dataset(base_dir='.'):
    """
    自动化处理MVTec Screw数据集:
    1. 使用Python合并、重命名、修正JSON,创建一个统一的数据池。
    2. 调用labelme2yolo命令行工具,完成格式转换和数据集划分。
    """
    screw_dir = os.path.join(base_dir, 'screw', 'test')
    # 临时目录,用于存放所有整理好的图片和JSON文件
    temp_json_dir = os.path.join(base_dir, 'temp_labelme_dataset')
    
    # 清理并创建临时目录
    if os.path.exists(temp_json_dir):
        shutil.rmtree(temp_json_dir)
    os.makedirs(temp_json_dir, exist_ok=True)

    print("--- 步骤1: 开始整理原始数据 ---")

    # --- 合并、重命名与修正JSON ---
    file_count = 0
    for defect_type in os.listdir(screw_dir):
        defect_folder = os.path.join(screw_dir, defect_type)
        if os.path.isdir(defect_folder) and defect_type != 'good':
            print(f"  处理文件夹: {defect_type}")
            for filename in os.listdir(defect_folder):
                if filename.endswith('.json'):
                    base_name = os.path.splitext(filename)[0]
                    img_filename = base_name + '.png'
                    
                    # 创建唯一的新文件名
                    new_filename_base = f"{defect_type}_{base_name}"
                    
                    # 复制并重命名图片
                    shutil.copy2(os.path.join(defect_folder, img_filename), 
                                os.path.join(temp_json_dir, new_filename_base + '.png'))
                    
                    # 读取、修改并保存JSON
                    json_path = os.path.join(defect_folder, filename)
                    with open(json_path, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                    
                    data['imagePath'] = new_filename_base + '.png' # 修正imagePath
                    
                    new_json_path = os.path.join(temp_json_dir, new_filename_base + '.json')
                    with open(new_json_path, 'w', encoding='utf-8') as f:
                        json.dump(data, f, indent=4)
                    
                    file_count += 1

    print(f"数据整理完成,共处理 {file_count} 个文件,已全部存放在 '{temp_json_dir}'")
    
    print("\n--- 步骤2: 调用labelme2yolo进行转换和划分 ---")
    
    # --- 调用labelme2yolo命令行工具 ---
    # 构建命令行指令
    command = [
        "labelme2yolo",
        "--json_dir", temp_json_dir,
        "--val_size", "0.1" # 指定10%的数据作为验证集
    ]
    
    # 执行命令
    try:
        print(f"  执行命令: {' '.join(command)}")
        subprocess.run(command, check=True)
        print("\nlabelme2yolo成功执行!")
        # labelme2yolo会自动在temp_json_dir下创建YOLODataset文件夹
        final_dataset_path = os.path.join(temp_json_dir, "YOLODataset")
        print(f"最终的YOLO数据集已生成在: {final_dataset_path}")

        # (可选) 清理临时文件夹
        # shutil.rmtree(temp_json_dir)
        # print("已清理临时文件夹。")

    except subprocess.CalledProcessError as e:
        print(f"\n错误:labelme2yolo执行失败!请检查是否已正确安装。错误信息: {e}")
    except FileNotFoundError:
        print("\n错误:找不到'labelme2yolo'命令。请确保已激活虚拟环境,并且labelme2yolo已安装。")


if __name__ == '__main__':
    # 确保在dataset目录下运行此脚本
    prepare_dataset()

3. 运行数据整理与转换脚本

  • 运行: 在dataset目录下打开终端(确保虚拟环境已激活),并运行脚本:
    python prepare_dataset.py
    
  • 脚本会依次执行两个步骤,并打印出详细的日志。完成后,在dataset/temp_labelme_dataset文件夹下,会生成一个名为YOLODataset的文件夹。这,就是我们最终用于训练的标准YOLO数据集! 其内部结构如下:
    temp_labelme_dataset/
    └── YOLODataset/
        ├── images/
        │   ├── train/ (*.png)
        │   └── val/   (*.png)
        ├── labels/
        │   ├── train/ (*.txt)
        │   └── val/   (*.txt)
        └── dataset.yaml
    
    dataset.yaml文件也会被labelme2yolo自动创建好,内容类似如下:
path: \\?\D:\code\ScrewDetector\dataset\temp_labelme_dataset\YOLODataset  # 数据集根目录的绝对路径
train: images/train   # 训练集图片路径 (相对于path)
val: images/val       # 验证集图片路径 (相对于path)
test:
# 类别名称
names:
    0: neck_defect
    1: thread_defect
    2: head_defect

至此,数据准备工作才算真正大功告告成!通过一个自动化脚本,我们将一个复杂的数据工程问题变得简单、清晰而可复现。

2.6 提供预处理数据集

手动标注和处理所有图片非常耗时。为了让读者能专注于模型训练本身,这里提供一个已经从MVTec数据集中挑选、标注并划分好的、可直接用于YOLOv8训练的数据集包。

  • 下载链接: https://pan.baidu.com/s/1aJSD9eFiCTXPctk0AulT8w?pwd=bkst 提取码: bkst

  • 目录结构: 解压后,其内部结构如下:

    YOLODataset/
    ├── train/
    │   ├── images/ (*.png)
    │   └── labels/ (*.txt)
    ├── val/
    │   ├── images/ (*.png)
    │   └── labels/ (*.txt)
    └── dataset.yaml
    

三、模型训练 (CPU版)

现在,万事俱备,可以开始训练我们的AI模型了。为了确保所有读者都能成功复现,本教程将全程使用CPU进行训练。

3.1 安装YOLOv8库

确保虚拟环境已激活,然后在终端中运行:

pip install ultralytics onnx -i https://pypi.tuna.tsinghua.edu.cn/simple

3.2 编写并运行训练脚本

【例7-1】 YOLOv8模型训练。

在项目根目录创建一个train.py文件。

from ultralytics import YOLO

if __name__ == '__main__':
    # 1. 加载一个预训练模型
    # yolov8n.pt 是最小的模型,适合快速教学和CPU训练
    model = YOLO('yolov8n.pt')

    # 2. 开始训练
    # data: 指向我们的数据集配置文件
    # epochs: 训练轮次,表示要将整个数据集看多少遍。对于教学,设置一个较小的值。
    # imgsz: 训练时图像的尺寸
    # device: 明确指定使用CPU进行训练
    results = model.train(data='./dataset/yolo_screw_dataset/dataset.yaml',
                          epochs=50,
                          imgsz=640,
                          device='cpu')

    print("训练完成!模型保存在:", results.save_dir)

在终端中运行此脚本:

python train.py

程序会自动下载预训练模型,然后开始在CPU上进行训练。相比GPU,CPU训练会慢很多,请耐心等待。终端中会打印出每一轮的训练进度和性能指标。

最终输出如下:

50 epochs completed in 0.356 hours.
Optimizer stripped from runs\detect\train\weights\last.pt, 6.2MB
Optimizer stripped from runs\detect\train\weights\best.pt, 6.2MB

Validating runs\detect\train\weights\best.pt...
Ultralytics 8.3.170  Python-3.10.0 torch-2.7.1+cpu CPU (12th Gen Intel Core(TM) i9-12900K)
Model summary (fused): 72 layers, 3,006,233 parameters, 0 gradients, 8.1 GFLOPs
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 1/1 [00:01<00:00,  1.04s/it]
                   all         12         12      0.829       0.67      0.789      0.538
           neck_defect          1          1      0.916          1      0.995      0.895
         thread_defect          9          9      0.572      0.333      0.377      0.147
           head_defect          2          2          1      0.677      0.995      0.572
Speed: 1.3ms preprocess, 61.4ms inference, 0.0ms loss, 4.6ms postprocess per image
Results saved to runs\detect\train
训练完成!模型保存在: runs\detect\train

在验证集图像上的部分结果如下:
在这里插入图片描述
可以看到,模型的准确率还是比较高的。

【关于GPU训练的说明】
虽然本教程为了通用性采用了CPU训练,但在实际的工程项目中,深度学习模型的训练几乎全部在GPU设备上完成。GPU拥有数千个并行处理核心,其训练速度相比CPU可以有数十倍甚至上百倍的提升。

如果读者的电脑配备了NVIDIA显卡并正确安装了CUDA环境,只需将device='cpu'改为device=0(0代表第一块GPU),即可享受高速训练的体验。对于没有本地GPU设备的开发者,也可以考虑租用云服务商提供的线上GPU服务器来完成训练,这已成为业界非常普遍的做法。

3.3 模型导出为ONNX

训练完成后,最好的模型(通常是best.pt)会保存在runs/detect/train/weights目录下。.pt是PyTorch格式,为了能在C++的OpenCV DNN模块中使用,我们需要将其导出为更通用的**ONNX (Open Neural Network Exchange)**格式。

【例7-2】 模型导出。

创建一个export.py文件。

from ultralytics import YOLO

if __name__ == '__main__':
    # 1. 加载我们自己训练好的模型
    # 路径需要根据实际训练结果的输出目录进行修改
    model = YOLO('./runs/detect/train/weights/best.pt')

    # 2. 导出为ONNX格式
    # opset是ONNX的版本,12或更高通常有较好的兼容性
    success_path = model.export(format='onnx', opset=12)

    if success_path:
        print("模型成功导出为ONNX格式:", success_path)

运行python export.py后,会在相同的权重目录下生成一个best.onnx文件。这个文件,就是我们下一篇文章中C++后端需要加载的AI模型!

四、进阶:模型迭代与增量训练

在实际的工业现场,模型开发绝不是“一锤子买卖”。随着产线的运行,会收集到新的瑕疵样本,或者发现当前模型对某些特定角度的瑕疵识别不准。这时,需要把新数据加入进来,对模型进行微调(Fine-tuning)

但在进行这一步时,有一个极易被忽视但致命的坑类别ID漂移

4.1 核心痛点:类别ID一致性问题

在YOLO的训练数据中,类别是用数字(0, 1, 2…)来表示的。

  • 旧模型的认知:0=neck_defect, 1=thread_defect, 2=head_defect
  • C++端的逻辑:也是写死的,收到ID=0就报“颈部瑕疵”。

当引入新数据并重新运行数据转换工具(如labelme2yolo)时,默认行为通常是根据扫描到标签的字母顺序或出现顺序来生成ID。如果新加入的数据让排序发生了变化,可能会生成:

  • 新数据集的定义:0=head_defect, 1=neck_defect

如果用这就错位的ID去训练,模型就会彻底“精神分裂”,C++端的判断也会全部出错。因此,在数据转换阶段强制锁定类别顺序是增量训练的第一要务。

4.2 解决方案:强制锁定Label Map

需要修改之前编写的prepare_dataset.py脚本。在调用labelme2yolo时,显式地传入一个固定的类别列表,确保无论数据如何变化,neck_defect永远是0。

【例7-3】 包含ID锁定的数据准备脚本 (prepare_dataset_v2.py)

请在dataset目录下新建或修改脚本,核心变化在于labelme2yolo命令中增加了--label_list参数。

import os
import shutil
import json
import subprocess

# --- 核心配置:在此处定义固定的类别顺序 ---
# 警告:这个顺序必须与第一次训练时的 dataset.yaml 保持完全一致!
# 0: neck_defect, 1: thread_defect, 2: head_defect
FIXED_LABEL_LIST = ["neck_defect", "thread_defect", "head_defect"]

def prepare_dataset_v2(base_dir='.'):
    """
    支持增量更新的数据准备脚本:
    强制使用固定的类别ID映射,防止训练迭代时ID错乱。
    """
    # 假设你把新采集并标注好的图片也放到了 screw/test 下的对应文件夹中
    # 或者你可以指定一个新的源目录
    screw_dir = os.path.join(base_dir, 'screw', 'test') 
    temp_json_dir = os.path.join(base_dir, 'temp_labelme_dataset_v2')
    
    if os.path.exists(temp_json_dir):
        shutil.rmtree(temp_json_dir)
    os.makedirs(temp_json_dir, exist_ok=True)

    print("--- 步骤1: 整理全量数据 (旧数据 + 新增数据) ---")
    
    # ... (此处省略与前文相同的 合并/重命名/JSON修正 代码,逻辑不变) ...
    # 为了节省篇幅,请保留例7-1中的文件遍历和复制逻辑
    # 实际操作时,建议把新数据也按类别放入 screw/test 目录,重新跑一遍全量整理
    
    # 这里简单模拟复制过程,实际使用请补全例7-1的循环代码
    # ... [插入例7-1中的 for 循环代码块] ...
    # ------------------------------------------------------
    
    # 假设此时 temp_json_dir 里已经有了所有的图片和json
    print(f"数据池准备完毕: {temp_json_dir}")
    
    print("\n--- 步骤2: 锁定类别ID进行转换 ---")
    
    # 将列表转换为逗号分隔的字符串,例如 "neck_defect,thread_defect,head_defect"
    label_list_str = ",".join(FIXED_LABEL_LIST)
    
    # 构建命令行指令
    command = [
        "labelme2yolo",
        "--json_dir", temp_json_dir,
        "--val_size", "0.1",
        # 【关键修改】: 显式传入 label_list,强制指定ID映射关系
        "--label_list", label_list_str 
    ]
    
    try:
        print(f"  执行命令 (锁定类别): {' '.join(command)}")
        subprocess.run(command, check=True)
        print("\n成功!生成的 dataset.yaml 类别顺序已锁定。")
        
        # 验证生成的 yaml 文件
        yaml_path = os.path.join(temp_json_dir, "YOLODataset", "dataset.yaml")
        print(f"请务必检查: {yaml_path} 中的 names 顺序是否符合预期。")

    except subprocess.CalledProcessError as e:
        print(f"转换失败: {e}")

if __name__ == '__main__':
    prepare_dataset_v2()

操作步骤:

  1. 将新采集并用Labelme标注好的数据,按类别放入dataset/screw/test对应的子文件夹中(或者直接混合进原始数据文件夹)。
  2. 运行python prepare_dataset_v2.py
  3. 脚本生成的dataset.yaml中,类别顺序将被强制锁定为:0: neck_defect, 1: thread_defect, 2: head_defect

4.3 微调(Fine-tuning)训练

数据准备好后,我们不需要从头训练(即不从yolov8n.pt开始),而是基于我们上一次训练好的“聪明”模型继续学习。这样既能保留已学到的特征,又能快速适应新数据。

【例7-4】 加载旧权重进行微调 (finetune.py)

from ultralytics import YOLO

if __name__ == '__main__':
    # 1. 加载之前的最佳模型
    # 注意:这里不再是 'yolov8n.pt',而是指向你上次训练出的 best.pt
    # 路径根据你实际的保存位置修改
    previous_best_model = './runs/detect/train/weights/best.pt'
    
    print(f"正在加载预训练模型: {previous_best_model} ...")
    model = YOLO(previous_best_model)

    # 2. 基于新生成的数据集进行微调
    # 注意:epochs 可以设置得少一点,比如 20-30 轮,因为模型已经有底子了
    # lr0 (初始学习率): 通常微调时可以将学习率调低一点,例如 0.005,防止破坏原有知识
    results = model.train(data='./dataset/temp_labelme_dataset_v2/YOLODataset/dataset.yaml',
                          epochs=30,
                          imgsz=640,
                          device='cpu', 
                          lr0=0.005) # 可选:降低学习率

    print("微调完成!新模型保存在:", results.save_dir)

微调的关键点:

  1. 加载权重: 使用YOLO('path/to/best.pt')
  2. 全量数据: 虽然叫“微调”,但在工业质检中,通常建议将新旧数据合并后一起训练(如4.2节的做法)。如果只用新数据训练,模型很容易出现灾难性遗忘(Catastrophic Forgetting),即学会了新瑕疵,却把旧瑕疵忘光了。
  3. 验证: 训练完成后,务必查看验证集结果,确保新旧瑕疵的检出率都达标。

五、总结与下章预告

在本章中,我们不仅完成了从0到1的模型训练,还掌握了应对工业现场实际需求的“增量迭代”技能。我们解决了两个关键工程问题:

  1. 数据闭环:利用Python脚本实现了从Labelme标注数据到YOLO训练数据的自动化转换。
  2. 一致性保证:通过锁定label_list,确保了在数据不断增加的过程中,模型与C++后端程序的接口协议(类别ID)始终保持稳定。

现在的你,手里拿的不再是一个静态的Demo模型,而是一个具备持续进化能力的AI核心。

下一步:跨越语言的部署
现在模型已经准备就绪(记得将微调后的模型再次导出为ONNX!)。在下一篇文章【《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——8. AI赋能(下):在Qt中部署YOLOv8模型】中,我们将正式回归C++的世界。我们将解析ONNX模型的内部结构,并编写高效的C++代码,将这个“AI大脑”植入我们的Qt检测软件中,实现真正的实时瑕疵检测。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐