1. 为什么你需要关注SimpleNet?
如果你正在工业质检的岗位上,每天面对成千上万的产品图片,用肉眼去挑刺,那你肯定懂我的痛苦。效率低、容易疲劳、标准不一,而且招个熟练的质检员成本越来越高。这几年,基于深度学习的视觉异常检测火了起来,但很多方案要么是“屠龙之技”,理论复杂得让人头大;要么就是“盆景项目”,看着漂亮,一拿到产线上就各种水土不服,对硬件要求高,训练和部署都麻烦。
2023年出现的SimpleNet,就像它的名字一样,给我带来了惊喜。它没有去搞那些花里胡哨的复杂结构,而是用了一种非常“聪明”且直接的方法来解决异常检测和定位问题。简单来说,它的核心思想是:我只用正常样本学习,然后想办法“造”出一些异常样本,最后训练一个简单的分类器去区分正常和“人造异常”。这个方法听起来有点“反直觉”,但实测下来效果非常能打,尤其是在工业场景常见的纹理、结构缺陷检测上。
我花了大概一周时间,从零开始复现并部署了这套系统。整个过程比我想象的顺利,没有遇到太多玄学调参的坑。这篇文章,我就把自己从环境搭建、数据准备、模型训练到调优测试的全过程,像朋友聊天一样分享给你。无论你是算法工程师想快速验证一个idea,还是产线工程师需要一套可落地的解决方案,跟着我的步骤走,你都能在半天内跑通一个基础版本,看到实际效果。
2. 动手之前:彻底搞懂SimpleNet的“简单”哲学
在撸起袖子敲代码之前,我建议你先花十分钟理解一下SimpleNet到底“简单”在哪里。这能帮你避开很多后续的疑惑,比如“为什么这里要这么设计”。我刚开始看论文时也觉得,这方法是不是太“取巧”了?但深入实践后,才发现它的巧妙之处。
核心流程三步走:
- 特征提取:用一个在ImageNet上预训练好的模型(比如WideResNet50)作为“主干网络”。这一步我们不训练,直接“白嫖”它强大的特征提取能力。你可以把它想象成一个经验丰富的老师傅,看一眼产品就能抓住关键特征。
- 特征适配与“造假”:这是SimpleNet的灵魂。首先,用一个叫“特征适配器”的小网络,把老师傅提取的通用特征,微调到我们当前具体的产品上(比如从猫狗图片特征适配到螺丝钉特征)。然后,关键操作来了:我们在这些正常的特征上,直接加一点高斯噪声。这个加了噪声的特征,就被当作是“异常特征”。这个思路很妙,因为它绕开了收集真实异常样本的难题——在工业上,缺陷样本本来就少,而且千奇百怪。
- 训练判别器:我们准备了一个非常简单的多层感知机(MLP),通常就两三层。它的任务就是学会区分“正常的特征”和“加了噪声的异常特征”。训练完成后,这个判别器就变成了一个敏感的“缺陷探测器”。
为什么说它适合工业落地?
- 数据要求低:只需要正常样本,完美契合工业场景“正常品多、缺陷品少”的数据现状。
- 训练速度快:主干网络不动,只需要训练很小的适配器和判别器。在我用的单卡3090上,训练MVTec AD里的一个类别(比如“瓶子”),大概20分钟就能达到很好的效果。
- 定位能力强:它是在图像“块”级别进行操作和预测的,最后能把异常区域热力图画出来,告诉你具体哪里出了问题,而不仅仅是“这张图有问题”。
- 容易调优:需要关注的超参数不多,主要就是噪声大小、判别器结构等,调整起来方向明确。
理解了这套逻辑,后面的代码操作就不再是黑盒了。你会知道每一行代码大概在干什么,出了问题也知道该从哪个环节去排查。
3. 从零开始:搭建你的开发环境
好了,理论部分点到为止,我们直接进入实战。首先是把“战场”准备好。我个人的习惯是使用Anaconda来管理Python环境,这样能避免各种包版本冲突的破事。下面是我的环境配置清单,你跟着做就行。
3.1 创建并激活虚拟环境
打开你的终端(Linux/Mac)或Anaconda Prompt(Windows),依次输入以下命令:
# 创建一个名为simplenet的新环境,指定Python版本为3.8 conda create -n simplenet python=3.8 -y # 激活这个环境 conda activate simplenet3.2 安装PyTorch及其依赖
这是最核心的一步。SimpleNet原作者使用的是PyTorch 2.0.0和CUDA 11.8。你需要根据自己电脑的CUDA版本(用nvidia-smi命令查看)去PyTorch官网找对应的安装命令。我这里以CUDA 11.8为例:
# 安装PyTorch、Torchvision和CUDA工具包 conda install pytorch==2.0.0 torchvision==0.15.0 torchaudio==2.0.0 pytorch-cuda=11.8 -c pytorch -c nvidia # 安装其他必要的科学计算和图像处理库 pip install numpy==1.22.4 opencv-python==4.5.1 scikit-learn pandas matplotlib tqdm注意:如果你没有NVIDIA显卡,或者只想用CPU跑(速度会慢很多),可以安装CPU版本的PyTorch:conda install pytorch==2.0.0 torchvision==0.15.0 torchaudio==2.0.0 cpuonly -c pytorch。
3.3 获取SimpleNet源码并安装项目依赖
直接从GitHub上把代码克隆下来是最稳妥的:
# 克隆项目仓库到本地 git clone https://github.com/donaldrr/simplenet.git cd simplenet项目里通常会有一个requirements.txt文件,但根据我的经验,直接pip install有时候会漏掉一些隐式依赖。更保险的做法是,结合官方列出的和我们在运行中遇到的报错来安装。你可以先尝试安装项目要求的包:
pip install -r requirements.txt如果运行时报错,提示缺少某个模块,再用pip install单独安装即可。比如我运行时就遇到了scipy和kornia的缺失,补上就行:
pip install scipy kornia环境到这里就基本配好了。你可以运行python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"来验证PyTorch是否安装成功以及GPU是否可用。看到返回True,就可以开心地进入下一步了。
4. 准备数据:让模型认识你的产品
模型再好,没有数据也是巧妇难为无米之炊。SimpleNet论文和代码默认使用MVTec AD数据集进行基准测试。这个数据集是工业异常检测领域的“标准试卷”,包含了15类常见的工业产品图像,每类都有正常的训练集和包含多种缺陷的测试集。
4.1 下载与解压MVTec AD数据集
你可以从官方网站(https://www.mvtec.com/company/research/datasets/mvtec-ad)申请下载。下载完成后,得到一个名为mvtec_anomaly_detection.tar.xz的压缩包。我建议把它放到一个空间充足的目录,比如/home/yourname/datasets/。
# 假设你下载的压缩包在Downloads文件夹 cd ~/Downloads # 解压到目标目录 tar -xf mvtec_anomaly_detection.tar.xz -C /home/yourname/datasets/解压后的目录结构应该是这样的:
mvtec_anomaly_detection/ ├── bottle/ │ ├── train/ │ │ └── good/ # 大量正常瓶子的图片 │ ├── test/ │ │ ├── good/ # 测试用的正常瓶子 │ │ ├── broken_large/ # 有“大破损”缺陷的瓶子 │ │ ├── broken_small/ # 有“小破损”缺陷的瓶子 │ │ └── contamination/ # 被污染的瓶子 │ └── ground_truth/ # 像素级的缺陷标注掩膜(用于评估定位) ├── cable/ ├── capsule/ └── ... (其他13个类别)这个结构非常清晰,train/good里全是正常样本,用于训练;test/下的各个子文件夹对应不同类型的缺陷,用于测试模型能否发现并定位它们。
4.2 理解数据并做初步检查
在开始训练前,我强烈建议你用几行代码快速浏览一下数据。这能帮你建立直观感受,也能提前发现数据损坏等问题。
import cv2 import os import matplotlib.pyplot as plt # 以bottle类别为例 data_base = '/home/yourname/datasets/mvtec_anomaly_detection' category = 'bottle' # 看一张训练集正常图片 train_good_path = os.path.join(data_base, category, 'train', 'good', '000.png') img_train = cv2.imread(train_good_path) img_train_rgb = cv2.cvtColor(img_train, cv2.COLOR_BGR2RGB) print(f"训练图片尺寸: {img_train.shape}") # 看一张测试集缺陷图片及其标注 test_defect_path = os.path.join(data_base, category, 'test', 'broken_large', '000.png') gt_path = os.path.join(data_base, category, 'ground_truth', 'broken_large', '000_mask.png') img_test = cv2.imread(test_defect_path) img_test_rgb = cv2.cvtColor(img_test, cv2.COLOR_BGR2RGB) gt = cv2.imread(gt_path, cv2.IMREAD_GRAYSCALE) # 标注是黑白的 print(f"测试图片尺寸: {img_test.shape}, 标注尺寸: {gt.shape}") # 画出来看看 fig, axes = plt.subplots(1, 3, figsize=(12,4)) axes[0].imshow(img_train_rgb) axes[0].set_title('Train - Good') axes[0].axis('off') axes[1].imshow(img_test_rgb) axes[1].set_title('Test - Broken Large') axes[1].axis('off') axes[2].imshow(gt, cmap='gray') axes[2].set_title('Ground Truth Mask') axes[2].axis('off') plt.show()运行这段代码,你能看到正常的瓶子、有裂纹的瓶子以及标注出裂纹位置的掩膜图。这能让你对模型要完成的任务有更具体的认识:不仅要分类“有无异常”,还要在图片上框出“异常在哪”。
5. 配置与运行:启动你的第一次训练
环境有了,数据齐了,现在可以点燃引擎了。SimpleNet项目提供了一个非常方便的Shell脚本run.sh来启动训练。我们不需要一开始就理解所有参数,但要知道关键的几个怎么改。
5.1 解读与修改运行脚本
用文本编辑器打开项目根目录下的run.sh文件,你会看到类似下面的内容。我逐段加上自己的注释:
#!/bin/bash # 数据集路径,修改成你实际存放MVTec AD的路径 datapath=/root/autodl-tmp/mvtec_anomaly_detection # 要训练的数据集类别,默认是全部15类。你可以先只跑一个类别,比如‘bottle’,加快验证速度。 datasets=('bottle') # 我先只测试瓶子类 # 下面这行不用动,它会将上面的列表转换成命令参数 dataset_flags=($(for dataset in "${datasets[@]}"; do echo '-d '"${dataset}"; done)) # 主训练命令 python3 main.py \ # GPU和设备相关 --gpu 0 \ # 使用第0号GPU,如果你只有一块卡,就是0 --seed 0 \ # 随机种子,固定它以保证实验结果可复现 # 日志和结果保存 --log_group simplenet_mvtec \ # 实验分组名,方便在TensorBoard等工具里查看 --log_project MVTecAD_Results \ --results_path ./results \ # 所有输出结果(模型、热力图、指标)会存在这里 --run_name run_bottle_test \ # 本次实验的名字,取个有意义的,如‘run_bottle_lr1e-4’ # 网络结构配置(这是核心参数区) net \ -b wideresnet50 \ # 骨干网络,用WideResNet50,效果和速度平衡得好 -le layer2 -le layer3 \ # 使用网络第2和第3层的特征,浅层细节+深层语义都要 --pretrain_embed_dimension 1536 \ # 预训练特征的维度,配合WideResNet50 --target_embed_dimension 1536 \ --patchsize 3 \ # 图像块大小,3x3是个常用起点 --meta_epochs 40 \ # 训练的总轮数,40对于大多数类别够了 --embedding_size 256 \ # 适配器输出的特征维度 --gan_epochs 4 \ # 判别器训练的轮数 --noise_std 0.015 \ # **关键参数**:加噪声的标准差,控制“造假”的幅度 --dsc_hidden 1024 \ # 判别器隐藏层大小 --dsc_layers 2 \ # 判别器层数,简单点好 --dsc_margin .5 \ # 判别器的边界损失参数 --pre_proj 1 \ # 使用预投影 # 数据加载配置 dataset \ --batch_size 8 \ # 批次大小,根据你的GPU内存调整。8对于24G显存很安全。 --resize 329 \ # 先将图像短边缩放到329 --imagesize 288 \ # 再中心裁剪到288x288作为网络输入 "${dataset_flags[@]}" \ # 这里会自动插入 -d bottle mvtec \ # 数据集类型 $datapath # 数据集路径第一次运行前,你需要修改的地方:
datapath:改成你的MVTec AD数据集绝对路径。datasets:建议先改成('bottle'),只训练一个类别,快速验证流程。--gpu:如果你的GPU不是0号,或者想用CPU,需要修改。--results_path:可以改成你喜欢的路径。
5.2 解决可能遇到的“拦路虎”
直接运行bash run.sh,你大概率会遇到一两个报错。别慌,这都是小问题。我把我踩过的坑告诉你。
报错1:np.bool属性错误这是代码依赖的NumPy版本更新导致的。打开metrics.py文件,找到报错行(大概在50行左右),进行修改:
# 将原来的: binary_amaps = np.zeros_like(amaps, dtype=np.bool) # 修改为: binary_amaps = np.zeros_like(amaps, dtype=bool) # 或者 np.bool_报错2:DataFrame的append方法错误Pandas新版本废弃了df.append。在同一个文件里,找到类似df = df.append(...)的语句(可能在计算ROC曲线附近):
# 将原来的: df = df.append({"pro": np.mean(pros), "fpr": fpr, "threshold": th}, ignore_index=True) # 修改为: new_row = pd.DataFrame([{"pro": np.mean(pros), "fpr": fpr, "threshold": th}]) df = pd.concat([df, new_row], ignore_index=True)修改保存后,再次运行bash run.sh。这时,你应该能看到训练日志开始刷屏了,包括当前的epoch、损失值等。恭喜你,模型已经开始学习了!
6. 训练过程监控与初步评估
训练启动后,别干等着。我们可以通过一些方法来监控训练状态,判断它是否在朝着好的方向发展。
6.1 观察日志与损失曲线
在终端输出的日志中,关注meta epoch和Discriminator loss。正常情况下,判别器的损失会逐渐下降并趋于稳定。如果损失值剧烈震荡或者变成NaN,可能是学习率太高或噪声设置不合理。
项目代码默认可能没有集成可视化的训练曲线。我通常会在main.py或训练循环里添加几行代码,把损失值记录下来并画图。一个更简单的方法是使用TensorBoard。如果你安装了tensorboard,并且代码中有保存日志(--results_path指定的目录下会有子文件夹),可以启动TensorBoard来查看:
# 在项目根目录下打开另一个终端 tensorboard --logdir ./results然后在浏览器打开http://localhost:6006,就能看到损失曲线等图表了。
6.2 理解输出的结果文件
训练完成后,./results目录下(或你在run.sh里指定的路径)会生成一系列文件和文件夹:
模型文件:通常是*.pth或*.pt文件,保存了训练好的特征适配器和判别器的权重。热力图(Heatmaps):在测试阶段,模型会对测试集的每一张图片生成一张异常热力图。越亮(越红)的区域代表模型认为异常的可能性越高。这些图片通常保存在以类别命名的文件夹里。评估指标文件:一个包含各类评估指标的文本文件(如metrics.txt)或CSV文件。最重要的指标是图像级的AUROC(检测异常图片的能力)和像素级的AUROC(定位异常区域的能力)。
6.3 进行第一次推理测试
训练脚本通常会自动在测试集上评估并保存结果。但我们也可以写一个简单的脚本,用训练好的模型对单张图片进行推理,直观感受效果。
import torch import cv2 import numpy as np from simplenet import SimpleNet # 假设你能导入模型类 from torchvision import transforms # 1. 加载训练好的模型 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SimpleNet(backbone_name='wideresnet50').to(device) checkpoint = torch.load('./results/your_model_path.pth', map_location=device) model.load_state_dict(checkpoint['model_state_dict']) model.eval() # 切换到评估模式 # 2. 准备单张测试图片 def preprocess_image(image_path, resize=329, imagesize=288): img = cv2.imread(image_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 简单的预处理:缩放、裁剪、归一化 transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize(resize), transforms.CenterCrop(imagesize), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) return transform(img).unsqueeze(0) # 增加batch维度 test_img_path = 'path_to_your_test_image.jpg' input_tensor = preprocess_image(test_img_path).to(device) # 3. 推理 with torch.no_grad(): # 模型会返回异常分数和热力图 anomaly_score, anomaly_map = model(input_tensor) anomaly_map = anomaly_map.squeeze().cpu().numpy() # 得到HxW的热力图 print(f"异常分数: {anomaly_score.item():.4f}") # 4. 可视化热力图 import matplotlib.pyplot as plt original_img = cv2.cvtColor(cv2.imread(test_img_path), cv2.COLOR_BGR2RGB) original_img = cv2.resize(original_img, (anomaly_map.shape[1], anomaly_map.shape[0])) fig, axes = plt.subplots(1, 2, figsize=(10,5)) axes[0].imshow(original_img) axes[0].set_title('Original Image') axes[0].axis('off') # 将热力图叠加到原图上 heatmap = cv2.applyColorMap((anomaly_map * 255).astype(np.uint8), cv2.COLORMAP_JET) superimposed = cv2.addWeighted(original_img, 0.6, heatmap, 0.4, 0) axes[1].imshow(superimposed) axes[1].set_title('Anomaly Heatmap') axes[1].axis('off') plt.show()运行这个脚本,你就能看到模型对你提供的图片的“诊断结果”:一个异常分数和一张高亮显示可疑区域的热力图。分数越高,代表异常可能性越大。
7. 调优实战:让模型在你的数据上表现更好
第一次跑通并看到结果,成就感是有的。但如果效果不理想,或者你想在自家产品数据上获得最佳性能,调优是必不可少的。SimpleNet需要调的超参数不多,但每个都很关键。
7.1 关键超参数调优指南
根据我的经验,按以下顺序和范围调整,效率最高:
噪声标准差
--noise_std(默认 0.015):- 这是什么:控制添加到正常特征上的噪声大小。噪声太小,“造假”的异常特征和正常特征太像,判别器学不到东西;噪声太大,造出来的异常太假,和真实缺陷不匹配。
- 怎么调:这是首要调整对象。如果模型对明显缺陷都不敏感(分数低),尝试增大到0.02或0.025。如果模型把很多正常区域也标红了(过敏感),尝试减小到0.01或0.005。建议以0.005为步长进行网格搜索。
特征层
-le(默认 layer2, layer3):- 这是什么:决定从骨干网络的哪几层提取特征。浅层(如layer1)包含更多边缘、纹理细节,利于检测微小纹理缺陷;深层(如layer3)包含更多语义信息,利于检测结构性缺陷或大块缺失。
- 怎么调:对于纹理类产品(地毯、皮革、木材),可以尝试加入
-le layer1。对于物体类产品(瓶子、胶囊),默认的layer2和layer3通常就够了。你可以对比-le layer2 -le layer3和-le layer1 -le layer2 -le layer3的效果。
输入图像大小
--imagesize(默认 288):- 这是什么:网络输入图像的大小。更大的尺寸能保留更多细节,提升定位精度,但会显著增加内存消耗和训练时间。
- 怎么调:如果你的缺陷非常细小(如微裂纹),并且GPU内存充足,可以尝试增大到320或352。如果只是为了快速验证或数据本身分辨率不高,保持默认即可。
批次大小
--batch_size(默认 8)和元训练轮数--meta_epochs(默认 40):- 批次大小:在GPU内存允许的前提下,适当增大批次大小(如16)可能使训练更稳定。如果内存不够,减小批次大小可能需要同时减小学习率(但原代码学习率是固定的)。
- 训练轮数:观察训练损失曲线。如果损失在30个epoch后就基本不变了,可以尝试减少到30以节省时间。如果40个epoch后损失还在稳步下降,可以增加到50或60。
7.2 针对特定场景的调优策略
场景一:缺陷对比度低,不明显
- 现象:产品表面划痕很浅,或者污渍颜色和背景接近。
- 策略:优先尝试减小
--noise_std(如0.01),让模型对更微小的特征变化敏感。同时,确保使用包含更多细节的浅层特征(加入layer1)。
场景二:缺陷种类多,形状大小不一
- 现象:同一类产品可能有划痕、凹坑、脏污等多种缺陷。
- 策略:尝试增大
--noise_std(如0.02),并考虑使用多尺度特征(同时使用layer1,layer2,layer3)。这能让模型同时捕捉细节和全局异常。
场景三:正常样本本身也有一定波动
- 现象:即使是合格品,其纹理、颜色也可能有正常范围内的变化(比如天然木材纹理)。
- 策略:这是最棘手的情况。首先,确保你的训练集尽可能覆盖这些正常波动。然后,可以尝试稍微增大
--dsc_margin(如从0.5到0.8),让判别器的决策边界更宽松一些,避免将正常波动判为异常。
调优是一个循环过程:修改参数 -> 重新训练 -> 在验证集上评估 -> 分析结果 -> 再修改。建议你准备一个小的验证集(从测试集中划出一部分,或者自己标注一些),用验证集上的像素级AUROC作为调优的指导指标,避免在测试集上过拟合。
8. 迈向实际应用:部署与集成考量
模型在MVTec AD上表现不错,不代表就能直接扔到产线上用。从实验代码到稳定可用的系统,还有几步路要走。
8.1 模型固化与加速
训练完成后,我们得到的是PyTorch的模型状态字典(.pth文件)。为了部署,我们通常需要将其转换为更高效的格式。
TorchScript:PyTorch自带的序列化工具,可以脱离Python环境运行模型。
# 示例:将模型转换为TorchScript model.eval() example_input = torch.rand(1, 3, 288, 288).to(device) traced_script_module = torch.jit.trace(model, example_input) traced_script_module.save("simplenet_deploy.pt")部署时,可以用C++的LibTorch库加载这个
.pt文件进行推理,速度更快。ONNX:一种开放的模型格式,可以被多种推理引擎支持(如TensorRT, OpenVINO)。
torch.onnx.export(model, example_input, "simplenet.onnx", input_names=['input'], output_names=['score', 'heatmap'], dynamic_axes={'input': {0: 'batch_size'}})转换为ONNX后,你可以利用TensorRT在NVIDIA GPU上进行图优化和量化,进一步提升推理速度。
8.2 设计实时推理流水线
产线上的推理不是跑一张图那么简单,需要考虑整个流水线:
- 图像采集与预处理:从工业相机抓取图片,进行尺寸缩放、归一化,格式转换(BGR转RGB)等。这部分可以用OpenCV在CPU上高效完成。
- 模型推理:将预处理后的张量送入模型。为了低延迟,可以使用批处理(batch inference)。即使产线是一个个产品过来,也可以积攒几帧(如4帧)后一起推理,能更好地利用GPU算力。
- 后处理与决策:
- 异常分数阈值化:设定一个阈值,高于阈值则报警。这个阈值需要你在验证集上根据误报率和漏报率的平衡来确定。
- 热力图后处理:模型输出的热力图可能比较粗糙。可以对其进行高斯模糊、阈值化、连通域分析,得到精确的缺陷边界框(Bounding Box)或区域。
- 结果反馈与日志:将报警信息、缺陷位置、原始图片、热力图等保存下来,用于后续追溯和模型迭代。
8.3 处理你自己的数据:数据准备是关键
如果你想用SimpleNet检测自家的产品,数据准备是最花时间但也最重要的一环。
- 收集正常样本:尽可能多角度、多光照、多批次地收集正常品图片,数量建议在500张以上,多样性越丰富,模型越鲁棒。
- 整理数据格式:按照MVTec AD的目录结构来组织你的数据。这是最省事的方法。
my_product/ ├── train/ │ └── good/ │ ├── 001.jpg │ ├── 002.jpg │ └── ... └── test/ ├── good/ │ └── ... (用于测试的正常样本) ├── scratch/ # 划痕缺陷 │ └── ... ├── dent/ # 凹坑缺陷 │ └── ... └── ground_truth/ # 如果有像素级标注最好 ├── scratch/ └── dent/
* **从零开始训练**:修改 `run.sh` 中的 `datapath` 和 `datasets` 参数,指向你的数据目录和类别名,然后像训练MVTec AD一样启动训练。第一次训练时,可以适当增加 `--meta_epochs`(比如60),因为你的数据分布和ImageNet预训练域差异可能更大。 我在一个金属件表面划痕检测的项目中应用了SimpleNet。最初直接用MVTec预训练的模型,效果一般。后来收集了约800张正常金属件图片,重新训练了特征适配器和判别器,像素级AUROC从0.85提升到了0.93。整个部署过程,从数据准备到最终集成到产线PLC系统,花了大概三周时间。现在这套系统已经稳定运行了半年多,替代了两个人工质检工位。