news 2026/9/28 17:25:33

基于CNN的工件毛刺分类:从数据准备到ONNX部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于CNN的工件毛刺分类:从数据准备到ONNX部署全流程

简介:这份资源是面向计算机、人工智能、自动化等专业学生与教师的深度学习实战项目包,以CNN卷积神经网络为核心,解决工业场景下工件毛刺的自动分类识别问题,可作为毕业设计、课程设计或大作业的完整参考方案。压缩包共1294个文件,约152.71MB,其中1210个png与40个jpg构成图像数据集,24个py脚本负责模型搭建与训练,另有pth权重文件、csv标签文件、json配置及md说明文档,覆盖从数据到推理的全流程。目前已有117人学习下载。项目代码均经过测试运行,读者可拿到完整的网络结构定义、训练与预测脚本、已训练模型权重以及带标注的图像数据,便于快速复现分类效果,并在此基础上修改网络层数、更换数据集或迁移到其他缺陷检测任务,适合入门进阶与项目立项演示。

1. 工件毛刺分类为什么值得用 CNN 做一遍

车间里最常见的质检场景之一,就是金属件铣削、车削、冲压之后边缘残留的毛刺。毛刺大小不一、形状随机,有的像细丝,有的像鱼鳞,有的干脆是一小块翻边。传统做法是老师傅拿放大镜看,或者用接触式探针抽检,效率低不说,判定标准还随人波动。把「Python + 深度学习 + CNN」这套组合拳打到工件毛刺分类上,本质是用卷积神经网络替代人眼做二分类或多分类:有毛刺 / 无毛刺,或者按毛刺严重程度分成轻微、中等、严重。它适合三类人:机械设计制造及其自动化专业做毕业设计的学生、想入门深度学习但缺真实工业数据集的工程师、以及产线上想做视觉质检预研的技术员。整套方案的门槛并不高,一台带独显的笔记本、一份标注好的工件端面图像、一份能跑通的 CNN 训练脚本,就能把准确率做到 90% 以上。真正难的不是网络结构,而是数据怎么来、怎么标、怎么让模型在换批次工件时不翻车。

2. 从工件图像到 CNN 输入:数据准备与增强的完整链路

2.1 工件毛刺图像的数据集该怎么攒

工业图像和 ImageNet 那种自然图像完全不是一回事。工件毛刺图像的特点是:背景高度一致(大多是金属灰、机床台面)、目标区域小(毛刺往往只占图像边缘几个像素宽)、类间差异细微(轻微毛刺和加工纹理容易混)。我一般建议按下面的规格来攒数据:

项目建议值说明
单类样本数不少于 300 张低于这个数 CNN 很容易过拟合
图像分辨率原图 1280×960 以上后续统一缩放到 224×224
拍摄距离固定工位、固定焦距避免尺度变化引入噪声
光照环形 LED 白光,避免反光金属表面反光是最大干扰源
标注方式按图像打类别标签毛刺分类是图像级分类,不需要框
类别划分无毛刺 / 轻微 / 严重三分类比二分类更贴近产线需求

如果手头没有真实产线图像,可以用公开的机械加工表面缺陷数据集做迁移,或者自己拿手机在台钳上拍。注意一点:同一批工件拍出来的图不能全放进训练集,否则验证集准确率虚高,这是毕业设计答辩时最容易被老师问倒的地方。

2.2 用 Python 做数据清洗与增强

拿到原始图像后第一步不是直接喂网络,而是清洗。下面这段脚本做三件事:统一尺寸、剔除模糊图、按 7:2:1 划分数据集。

import os import cv2 import numpy as np import shutil from sklearn.model_selection import train_test_split RAW_DIR = "raw_images" # 原始图像,按类别放在子文件夹 OUT_DIR = "dataset" # 输出目录 IMG_SIZE = 224 # CNN 标准输入尺寸 BLUR_THRESHOLD = 100.0 # 拉普拉斯方差阈值,低于此值判为模糊 def is_blurry(img_path): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: return True return cv2.Laplacian(img, cv2.CV_64F).var() < BLUR_THRESHOLD def prepare(): classes = os.listdir(RAW_DIR) for split in ["train", "val", "test"]: for c in classes: os.makedirs(os.path.join(OUT_DIR, split, c), exist_ok=True) for c in classes: c_dir = os.path.join(RAW_DIR, c) files = [f for f in os.listdir(c_dir) if f.lower().endswith((".jpg", ".png", ".bmp"))] valid = [] for f in files: p = os.path.join(c_dir, f) if is_blurry(p): continue img = cv2.imread(p) img = cv2.resize(img, (IMG_SIZE, IMG_SIZE)) valid.append((f, img)) names = [v[0] for v in valid] imgs = [v[1] for v in valid] # 先 7:3 切训练和临时集,再从临时集 2:1 切验证和测试 X_train, X_tmp, n_train, n_tmp = train_test_split( imgs, names, test_size=0.3, random_state=42, stratify=None) X_val, X_test, n_val, n_test = train_test_split( X_tmp, n_tmp, test_size=0.33, random_state=42) for split, data, names_ in [("train", X_train, n_train), ("val", X_val, n_val), ("test", X_test, n_test)]: for img, name in zip(data, names_): cv2.imwrite(os.path.join(OUT_DIR, split, c, name), img) if __name__ == "__main__": prepare()

逻辑说明:is_blurry用拉普拉斯算子方差判断图像清晰度,金属件拍摄时手抖或对焦不准的图必须剔除,否则模型会学到「模糊=有毛刺」这种伪特征。train_test_split做了两次切分,最终比例约 7:2:1。参数BLUR_THRESHOLD=100是经验值,如果你的图像整体偏暗,可以降到 60 左右;IMG_SIZE=224是为了后续直接套用预训练模型,如果自己从零搭网络,128 也够用。

2.3 在线增强:让 300 张图发挥 3000 张的效果

工件图像不能随便做水平翻转,因为毛刺方向有工艺含义。我一般只用以下几种增强:随机旋转 ±15 度、亮度抖动 ±20%、高斯噪声、随机裁剪 90% 区域再缩放回去。用torchvision.transforms写出来是这样:

from torchvision import transforms train_tf = transforms.Compose([ transforms.RandomRotation(15), # 小幅旋转,模拟工件摆放偏差 transforms.ColorJitter(brightness=0.2, contrast=0.2), # 光照波动 transforms.RandomResizedCrop(224, scale=(0.85, 1.0)), # 随机裁剪 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], # ImageNet 统计值 std=[0.229, 0.224, 0.225]), transforms.RandomErasing(p=0.1) # 随机遮挡,提升鲁棒性 ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

注意RandomErasing的概率不要超过 0.2,否则毛刺区域被遮掉,模型学不到关键特征。Normalize用的均值和方差是 ImageNet 的统计值,如果你完全从零训练、不用预训练权重,可以改成自己数据集的均值和方差,但差别通常不大。

3. CNN 模型搭建:从零写还是拿预训练权重改

3.1 两种路线的选型对比

毕业设计里最常见的纠结是:自己搭一个 CNN,还是用 ResNet18 这类预训练模型微调?我的建议很直接——数据少于 1000 张,一律用预训练模型微调;数据超过 5000 张且和 ImageNet 差异极大,才考虑从零训练。工件毛刺图像通常只有几百到一两千张,所以走迁移学习路线。

路线适用数据量训练时间预期准确率代码复杂度
从零搭 4 层 CNN< 500短75%~85%低
ResNet18 微调500~5000中90%~96%中
EfficientNet-B0 微调1000~10000中长92%~97%中
从零搭 + 强增强> 5000长88%~94%高

3.2 用 PyTorch 搭一个可复现的基线模型

下面这个模型结构是我在多个工件分类任务里反复用过的基线:主干用 ResNet18 预训练权重,把最后的全连接层换成三分类输出,前面几层冻结,只训练后两层和分类头。

import torch import torch.nn as nn from torchvision import models def build_model(num_classes=3, freeze_backbone=True): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 冻结前面所有层,只训练 layer4 和 fc if freeze_backbone: for name, param in model.named_parameters(): if not name.startswith("layer4") and not name.startswith("fc"): param.requires_grad = False # 替换分类头 in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 128), nn.ReLU(inplace=True), nn.Linear(128, num_classes) ) return model if __name__ == "__main__": net = build_model(num_classes=3) total = sum(p.numel() for p in net.parameters()) trainable = sum(p.numel() for p in net.parameters() if p.requires_grad) print(f"总参数: {total}, 可训练参数: {trainable}")

逻辑说明:freeze_backbone=True时只训练layer4和fc,可训练参数大约占总参数的 30% 左右,这样在小数据集上不容易过拟合。Dropout(0.3)放在全连接前,是防止分类头记住训练样本的简单手段。如果你的数据量超过 2000 张,可以把freeze_backbone设为False,全网络微调,但学习率要降到 1e-4 以下。

3.3 训练循环与关键超参数

训练脚本里最容易被忽视的是类别不平衡处理。工件毛刺数据里「无毛刺」样本往往远多于「严重毛刺」,直接训练会让模型偏向多数类。解决办法是在损失函数里加权重。

import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR def train(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") train_ds = ImageFolder("dataset/train", transform=train_tf) val_ds = ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) # 按类别频率计算权重,缓解不平衡 counts = [0] * len(train_ds.classes) for _, label in train_ds.samples: counts[label] += 1 weights = torch.tensor([1.0 / c for c in counts], dtype=torch.float32).to(device) weights = weights / weights.sum() * len(counts) model = build_model(num_classes=len(train_ds.classes)).to(device) criterion = torch.nn.CrossEntropyLoss(weight=weights) optimizer = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=3e-4, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30) best_acc = 0.0 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) pred = model(imgs).argmax(dim=1) correct += (pred == labels).sum().item() total += labels.size(0) acc = correct / total print(f"Epoch {epoch+1}, Val Acc: {acc:.4f}") if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "best_model.pth") if __name__ == "__main__": train()

参数说明:lr=3e-4是微调预训练模型的常用起点,如果 loss 震荡明显就降到 1e-4;weight_decay=1e-4是 AdamW 的默认正则强度;T_max=30要和总 epoch 数一致,余弦退火才能完整走完一个周期;batch_size=32在 6GB 显存上跑 224×224 的 ResNet18 刚好够用,显存不够就降到 16。类别权重那段代码是很多毕业设计忽略的,但它在三分类不平衡数据上通常能带来 3~5 个百分点的提升。

4. 训练完不算完:模型评估、导出与推理部署

4.1 别只看准确率:混淆矩阵和每类召回率

工件毛刺分类里,把「严重毛刺」误判成「无毛刺」的代价远大于反过来。所以评估时必须看每类召回率,而不是总体准确率。

import torch import numpy as np from sklearn.metrics import classification_report, confusion_matrix from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def evaluate(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") test_ds = ImageFolder("dataset/test", transform=val_tf) test_loader = DataLoader(test_ds, batch_size=32, shuffle=False) model = build_model(num_classes=len(test_ds.classes)).to(device) model.load_state_dict(torch.load("best_model.pth", map_location=device)) model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs = imgs.to(device) preds = model(imgs).argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names=test_ds.classes, digits=4)) if __name__ == "__main__": evaluate()

classification_report会输出每类的 precision、recall、f1-score。重点看「严重毛刺」那一行的 recall,如果低于 0.85,说明模型对这类样本学得不够,需要回头补数据或者调类别权重。混淆矩阵能直观看出哪两类最容易混,通常是「轻微毛刺」和「无毛刺」之间的边界模糊。

4.2 导出 ONNX 并在无 Python 环境推理

毕业设计答辩时经常需要现场演示,而答辩教室的电脑不一定装了 PyTorch。把模型导出成 ONNX 格式,用onnxruntime推理,依赖只有几十 MB。

import torch def export_onnx(): model = build_model(num_classes=3) model.load_state_dict(torch.load("best_model.pth", map_location="cpu")) model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, "burr_classifier.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=11 ) print("导出完成") if __name__ == "__main__": export_onnx()

导出后用下面这段代码做单张图推理,不依赖 PyTorch:

import onnxruntime as ort import cv2 import numpy as np def predict(img_path): sess = ort.InferenceSession("burr_classifier.onnx") img = cv2.imread(img_path) img = cv2.resize(img, (224, 224)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) std = np.array([0.229, 0.224, 0.225], dtype=np.float32) img = (img - mean) / std img = np.transpose(img, (2, 0, 1))[None, ...] out = sess.run(None, {"input": img})[0] cls = int(np.argmax(out)) return ["无毛刺", "轻微毛刺", "严重毛刺"][cls], out if __name__ == "__main__": label, logits = predict("test_sample.jpg") print(label, logits)

注意预处理必须和训练时完全一致,包括 BGR 转 RGB、归一化参数、通道顺序。ONNX 推理最常见的翻车就是预处理对不上,导致线上结果和验证结果差十几个百分点。

5. 避坑与排查:工件毛刺分类里最容易翻车的 5 个地方

5.1 验证集准确率 98%,换一批工件掉到 60%

现象:训练日志里 val acc 一路涨到 0.98,但拿新拍的工件图测试,模型几乎全判成同一类。原因:训练集和验证集来自同一批拍摄,背景、光照、工件型号完全一致,模型学到了「背景特征」而不是「毛刺特征」。解决:按工件批次划分数据集,而不是随机划分。具体做法是给每张图打上批次号,同一批次的图只出现在 train 或 val 其中一个里。如果数据量不够,至少保证验证集里有不同光照条件下拍的图。

5.2 Loss 不下降,准确率卡在 33%

现象:三分类任务,训练 loss 从 1.1 降到 1.09 就不动了,准确率始终在 0.33 附近。原因:学习率太大,预训练权重被破坏;或者数据标签有问题,比如所有图都被标成了同一类。解决:先把学习率降到 1e-5 试一个 epoch,如果 loss 开始降,说明是学习率问题。如果还是不动,用ImageFolder打印几个 batch 的标签分布,确认标签没有全错。我遇到过最离谱的一次是文件夹名字写错,三个类别的图全在一个文件夹里。

5.3 显存溢出(CUDA out of memory)

现象:训练到第几个 batch 突然报RuntimeError: CUDA out of memory。原因:batch_size太大,或者num_workers太多导致内存泄漏,或者验证时没有加torch.no_grad()。解决:先把batch_size减半,再把num_workers设为 2 或 0 试试。验证循环一定要包在with torch.no_grad():里,否则计算图会一直累积。如果还不行,用torch.cuda.empty_cache()在每个 epoch 结束后清一次缓存。

5.4 模型对光照变化极度敏感

现象:同一工件,换个角度打光,预测结果就变了。原因:训练数据光照太单一,模型把亮度当成了判别特征。解决:在增强里加大亮度抖动范围,ColorJitter(brightness=0.4, contrast=0.3),同时补拍不同光照条件下的样本。如果条件允许,在预处理里加一步直方图均衡化,把光照影响压下去。

5.5 导出 ONNX 后推理结果和 PyTorch 不一致

现象:PyTorch 里预测是「严重毛刺」,ONNX 推理出来是「无毛刺」。原因:预处理不一致,最常见的是 PyTorch 用 RGB、OpenCV 读进来是 BGR,忘了转换;或者归一化参数写错。解决:写一个对比脚本,同一张图分别走 PyTorch 和 ONNX,打印预处理后的张量,逐像素对比。只要预处理对齐,ONNX 和 PyTorch 的输出差异通常在 1e-5 以内。

6. 把模型推到产线边缘:一个可落地的推理加速技巧

前面训练和导出都跑通之后,最后一个现实问题是速度。产线节拍通常要求单张图推理在 100ms 以内,而 ResNet18 在 CPU 上跑 224×224 大约要 200~300ms。我一般用两个手段压下来:一是把输入分辨率从 224 降到 160,工件毛刺这种边缘特征在 160 下仍然保留得不错;二是用 ONNX Runtime 的量化功能做 INT8 推理。

from onnxruntime.quantization import quantize_dynamic, QuantType def quantize(): quantize_dynamic( model_input="burr_classifier.onnx", model_output="burr_classifier_int8.onnx", weight_type=QuantType.QUInt8 ) print("量化完成") if __name__ == "__main__": quantize()

量化后模型体积大约缩小到原来的四分之一,CPU 推理速度提升 2~3 倍。代价是准确率可能掉 1~2 个百分点,需要重新在测试集上验证一遍。如果掉得太多,可以只量化卷积层,保留全连接层为 FP32,ONNX Runtime 支持按节点指定量化类型。

另一个容易被忽视的点是输入尺寸和模型结构的匹配。如果你把输入改成 160×160,而模型里还有自适应池化层,通常不用改代码就能跑;但如果模型里有硬编码的全连接输入维度,就需要重新导出。我自己的习惯是:训练时就用最终部署的分辨率,避免训练和推理两套预处理。

最后说一个验证技巧:拿 20 张产线实拍图,人工标好标签,分别用 PyTorch 原模型、ONNX FP32、ONNX INT8 跑一遍,记录三者的预测一致率和单张耗时。如果 INT8 和 FP32 的一致率低于 95%,就放弃量化,改用降分辨率方案。这套对比流程我每次上线前都会走一遍,比拍脑袋调参靠谱得多。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 17:23:19

DeepSeekV4.1-Flash推理提速实战:MoE显存优化与KV Cache管理

1. 从"Flash"这个后缀说起&#xff1a;DeepSeekV4.1-Flash到底在解决什么问题第一次看到"DeepSeekV4.1-Flash"这个命名&#xff0c;我下意识地把它和之前那些"Turbo""Lite""Mini"之类的后缀放在一起比较。但仔细琢磨"F…

作者头像 李华
网站建设 2026/9/28 17:22:52

802.11ax调度技术全解:从OFDMA到BSS Coloring的配置与排障

1. 项目概述——从“ax”热词说起最近“ax调度”这几个字在无线网络圈子里热度极高&#xff0c;无论是厂商发布会还是技术论坛&#xff0c;都在反复强调这个词。说到底&#xff0c;“ax”就是 Wi-Fi 6 的正式标准代号802.11ax&#xff0c;而“调度”则是这一代协议里最核心、最…

作者头像 李华
网站建设 2026/9/28 17:22:24

SM2246EN SSD修复实战:ROM短接与量产开卡全指南

1. 项目概述&#xff1a;为什么SM2246EN主控的SSD值得花时间亲手修复&#xff1f;手把手教你用SM2246EN主控工具修复固态硬盘&#xff08;附ROM短接实操指南&#xff09;——这句话不是营销话术&#xff0c;而是我过去三年在二手SSD回收站、维修小店和DIY玩家群中反复验证过的硬…

作者头像 李华
网站建设 2026/9/28 17:22:12

treg CLI Agent 入门:OpenRouter 密钥管理与多模型路由实战

1. 从“treg”这个标题说起&#xff1a;一个被低估的CLI Agent入口第一次看到“treg”这四个字母&#xff0c;大多数人会一头雾水。它不像“codex cli”那样直白&#xff0c;也不像“claude cli”那样自带品牌辨识度。但如果你最近在折腾 agent 开发、OpenRouter 密钥管理、或者…

作者头像 李华
网站建设 2026/9/28 17:21:54

ESP32-C3智能电池盒:ADC采样、分压电阻计算与BLE电量显示

1. 项目缘起与整体设计思路1.1 为什么要做智能电池盒手里攒了一堆18650和21700锂电池&#xff0c;充电器是那种几十块钱的傻充&#xff0c;插上去就一个红灯&#xff0c;充满了也不告诉你&#xff0c;全靠估摸着时间拔。更麻烦的是&#xff0c;我经常把两节电池串起来给一些小设…

作者头像 李华
网站建设 2026/9/28 17:21:42

CLI-Anything:将命令行工具封装为AI Agent可调用能力

1. 从"CLI-Anything"说起&#xff1a;命令行工具正在被重新定义第一次看到"CLI-Anything"这个说法&#xff0c;我脑子里蹦出来的不是某个具体工具&#xff0c;而是一种趋势判断&#xff1a;命令行界面正在从"人敲命令"变成"人描述意图&…

作者头像 李华