简介:基于SVM实现焊接缺陷图片识别分类的Python源码包,内置焊接缺陷图片数据,需自行训练模型;定位为机器学习/计算机视觉方向的教学与实战资源,面向计算机相关专业在校学生、指导老师及企业算法入门者,适用于课程作业、毕业设计或项目立项演示。压缩包内共144个文件,以140张焊接缺陷PNG图片为主,涵盖未焊透、未熔合等典型缺陷类别,这些图片均来自焊接工艺场景、类别划分明确,可用于有监督学习;另有3个Python脚本分别实现数据读取、特征提取、SVM模型训练与分类评估,并附1个zip原始数据集压缩包;整体仅1.7MB,体积轻量、目录结构清晰,便于快速下载和本地部署。代码已经过测试运行成功,可直接运行,能帮助读者走通图像预处理、特征提取、模型训练与预测的完整pipeline,同时可根据自身数据替换图片、调整核函数或超参数,灵活迁移到其他纹理/缺陷识别场景,也适合在此基础上修改模块、增加自定义功能。目前已有518人学习下载,对于希望入门传统机器学习图像分类、理解SVM原理与工程实现的学习者具有直接参考价值。
1. 焊接缺陷图片识别,SVM这套老方案为什么在小数据集上依然能打
工业焊接质检里,X光底片上有没有气孔、夹渣、未熔合,现在很多人第一反应就是用深度学习。真上一线才知道,深度学习吃标注数据,常见缺陷类别每类几百张起步,产线攒半年都未必够,而且训练和推理都要显卡,换产线还得重新调。反而是SVM加手工特征,在几百张图的小样本上能跑出90%以上的准确率,训练只要几秒钟到几分钟,CPU也能跑。这个Python源码包给的是完整的训练框架和焊接缺陷数据集,模型需要你自己跑训练脚本生成,正好避开了“拿别人训练好的黑匣子过来直接预测”的坑,也能让你把数据、特征、核函数、惩罚系数这条链路彻底摸清。适合手里有标注缺陷图、想快速落地一个可解释模型的质检工程师和算法学习者。
2. 从底片到特征向量:焊接缺陷数据集梳理与预处理
SVM不认图片,只认向量。这一步的核心是把焊接X光底片变成一行行数值特征,特征有没有区分度,直接决定后面分类能走多远,也比调参更值得花时间。
2.1 焊接缺陷数据集的目录结构、类别定义与读图陷阱
拿到压缩包后先解压,常见做法是数据集按类别分文件夹,每张缺陷图是一个独立样本。典型结构是下面这样:
dataset/ ├── good/ # 无缺陷焊缝 │ ├── weld_001.jpg │ ├── weld_002.jpg ├── porosity/ # 气孔 ├── slag/ # 夹渣 ├── incomplete/ # 未焊透 └── crack/ # 裂纹如果你自己采集数据,建议也保持这个格式,后边的特征提取脚本可以直接扫描文件夹。要注意一个非常常见的坑:一张X光片可能同时有气孔和夹渣两种缺陷,这种多标签样本在单标签分类任务里会让模型学糊涂。我一般会把“同时含多种缺陷”的样本单独归为“复合缺陷”一类,或者干脆删掉,避免同一个特征向量对应两个标签把SVM的决策边界搞乱。
读取图片时建议用OpenCV,并且强制以灰度模式读入。焊接缺陷X光底片本质是灰度图,彩色通道没有额外信息,读成灰度还能减少三分之一的计算量。代码这么写:
import cv2 import os import numpy as np def load_gray_images(base_dir, target_size=(64, 64)): """扫描dataset目录,读取所有图片并转为统一尺寸的灰度数组""" images, labels = [], [] for class_name in os.listdir(base_dir): class_path = os.path.join(base_dir, class_name) if not os.path.isdir(class_path): continue for file_name in os.listdir(class_path): if not file_name.endswith(('.jpg', '.jpeg', '.png')): continue img_path = os.path.join(class_path, file_name) # IMREAD_GRAYSCALE 直接丢弃彩色通道 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: print(f"图片读取失败: {img_path}") continue img = cv2.resize(img, target_size, interpolation=cv2.INTER_AREA) images.append(img) labels.append(class_name) return np.array(images), np.array(labels)这里的关键参数是target_size=(64, 64)。焊接X光原图分辨率通常在一千乘一千以上,直接拿来算特征矩阵会爆炸,64×64是一个速度和精度的平衡点。缩小图片用cv2.INTER_AREA而不是INTER_LINEAR,它对高频噪声有抑制作用,缺陷边缘不会因为缩放而出现摩尔纹。另外每张图在缺陷区域之外通常还有大片背景,如果标注文件里给了缺陷位置,优先裁剪出ROI区域再缩放,SVM学到的是缺陷本身的纹理,而不是焊件背景的灰度渐变。
2.2 灰度归一化与对比度增强:稳定特征取值区间
原始X光底片灰度值受曝光强度影响很大,同一个气孔在不同焊件上可能差几十个灰度级。如果不做归一化,SVM会把曝光差异当成分类依据,换一张底片就认不出来。我一般把像素值统一缩放到0到1之间,并做一次直方图均衡化增强局部对比度。
def preprocess_for_features(img): """单张灰度图的标准化预处理:归一化 + CLAHE对比度增强""" img_norm = img.astype(np.float32) / 255.0 # CLAHE限制对比度,比全局直方图均衡更温和,避免噪声被放大 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) img_clahe = clahe.apply((img_norm * 255).astype(np.uint8)) return img_clahe.astype(np.float32) / 255.0clipLimit=2.0表示对比度限制强度,太小起不到增强效果,太大反而让气孔和噪声同时变明显。tileGridSize=(8, 8)把图像切成8×8的小块做局部直方图均衡,适合焊接底片这种光照不均的场景。注意这里要先转回0到255的uint8才能传给CLAHE,再归一化回0到1,顺序反了会直接报错。
如果图像带有明显的规律性背景纹理,比如焊缝本身的金相纹路,可以再加一个cv2.GaussianBlur(img, (3, 3), 0)做去噪,核大小不要超过5,否则缺陷边缘也跟着糊掉。
2.3 特征提取:HOG、LBP、GLCM三组特征拼接,SVM的分类依据
焊接缺陷的本质区别在纹理和灰度共生规律:气孔是圆滑的暗斑点,夹渣是不规则的高亮块,裂纹是细长的暗线。没有哪个单一特征能把它们全部分开,所以常见做法是拼特征向量。
习惯上我固定用三组特征:
from skimage.feature import hog, local_binary_pattern from skimage.filters import sobel from skimage import color import numpy as np def extract_features(img, hog_n_orient=9, lbp_radius=1, lbp_n_points=8): """ img: 灰度预处理后的图像,范围[0,1],形状(64,64) 返回组合特征向量,维度 = HOG + LBP + GLCM """ # 1. HOG 方向梯度直方图 hog_feat = hog(img, orientations=hog_n_orient, pixels_per_cell=(8, 8), cells_per_block=(2, 2), block_norm='L2-Hys') # 2. LBP 局部二值模式直方图 lbp = local_binary_pattern(img, P=lbp_n_points, R=lbp_radius, method='uniform') # 统一LBP模式数是P*(P-1)+3,这里P=8时是59个bin n_bins = lbp_n_points * (lbp_n_points - 1) + 3 lbp_hist, _ = np.histogram(lbp.ravel(), bins=n_bins, range=(0, n_bins)) lbp_hist = lbp_hist / (np.sum(lbp_hist) + 1e-6) # 归一化成概率分布 # 3. GLCM 灰度共生矩阵统计量 gray_u8 = (img * 255).astype(np.uint8) glcm_feat = [] for angle in [0, 45, 90, 135]: glcm = graycomatrix(gray_u8, distances=[1], angles=[angle], levels=256, symmetric=True, normed=True) contrast = graycoprops(glcm, 'contrast')[0, 0] dissim = graycoprops(glcm, 'dissimilarity')[0, 0] homogeneity = graycoprops(glcm, 'homogeneity')[0, 0] energy = graycoprops(glcm, 'energy')[0, 0] correlation = graycoprops(glcm, 'correlation')[0, 0] glcm_feat.extend([contrast, dissim, homogeneity, energy, correlation]) return np.hstack([hog_feat, lbp_hist, np.array(glcm_feat)])这段代码是整套方案的承重墙。HOG的orientations=9表示梯度方向分成9个桶,pixels_per_cell=(8, 8)让每个小格对局部边缘敏感,block_norm='L2-Hys'做归一化让特征对光照变化鲁棒。LBP的method='uniform'把高频噪声模式合并,防止维度爆炸,归一化后的直方图能反映局部纹理出现频率。GLCM我改用了level=256,配合四个方向取平均,让特征具备旋转不变性——焊接时工件摆放角度不可能每次都一样。
特征维度加起来是:(9×9×4×4) + 59 + 20 ≈ 209维,这个体量喂给SVM非常舒服,既不会维度灾难,也保留了纹理判别信息。
特征提取循环一般写作:
X = np.array([extract_features(preprocess_for_features(img)) for img in images]) y = np.array(labels)这一步会花一点时间,几百张图大概十几秒。跑完后建议把X和y存成npz或npy文件,下次训练直接加载,不用每次重新提取。
3. 训练SVM分类器:GridSearchCV找参数,类别不平衡用class_weight解决
特征已经变成矩阵,接下来就是训练。SVM在这里有两个关键选择:核函数和正则化参数。焊接缺陷数据量小、特征维度中等,径向基核(RBF)通常是最优解,但C和gamma这两个参数必须用网格搜索来定,否则就是瞎猜。
3.1 数据划分:stratify保持各类比例,避免训练集里缺缺陷样本
焊接缺陷数据最头疼的是类别不均衡,正常焊件数量往往是缺陷样本的几倍。直接随机划分的话,训练集里可能完全没有裂纹样本,测试集里裂纹全错。train_test_split的stratify参数就是为了解决这个问题:
from sklearn.model_selection import train_test_split # stratify=y 让切分后的训练集和测试集中,每个类别的占比与原始数据一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 )test_size=0.2是比较稳的比例,数据总量只有几百张时,测试占比太高会让训练集不够,太低会让评估结果不可信。random_state=42固定随机种子,保证每次跑出来的划分一致,排查问题时不会因为随机性而前后对不上。
切分之后必须对特征做标准化处理。这一步很多人漏掉,SVM的RBF核依赖样本间的欧氏距离,如果某个特征维度(比如GLCM的contrast)数值范围是0到几百,而LBP直方图是0到1,距离计算会被大数值维度主导。用StandardScaler让每个特征维度都具有零均值单位方差:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)注意fit_transform和transform不能混:只能用训练集的均值和标准差去标准化测试集,不能拿测试集单独fit,否则会造成数据泄漏,评估结果虚高。
3.2 网格搜索C和gamma:RBF核的两个关键参数
RBF核的表达式是K(x, x') = exp(-gamma * ||x - x'||^2)。gamma控制单个样本的影响半径,gamma太大每个样本只影响自己周围一小块,容易过拟合;gamma太小所有样本都糊成一片,欠拟合。C是误分类惩罚系数,C越大,训练集上越苛刻,也越容易过拟合。焊接缺陷类别有些在特征空间里本身重叠,C设得过高会让模型强行为噪声样本划边界,换一批图就翻车。
from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.metrics import f1_score, make_scorer param_grid = { 'C': [0.1, 1, 10, 100], 'gamma': [0.01, 0.1, 1, 10], 'class_weight': [None, 'balanced'] } svm = SVC(kernel='rbf', probability=True, random_state=42) # 用加权F1作为评分依据,兼顾少数类 grid = GridSearchCV(svm, param_grid, cv=3, scoring='f1_weighted', n_jobs=-1, verbose=1) grid.fit(X_train_scaled, y_train) print(f"最优参数: {grid.best_params_}") print(f"最优得分: {grid.best_score_:.4f}")网格搜索的候选值按经验选。C的候选范围是0.1到100,按10倍步长。gamma的候选范围和特征标准化程度有直接关系,我的特征做过标准化,所以从0.01起步;如果没标准化,gamma小数值基本没意义。cv=3表示3折交叉验证,数据量小用5折可能会让每一折训练集太少,3折更稳。scoring='f1_weighted'很关键:类别不平衡时accuracy没有参考意义,一个全是正常样本的模型也能拿85%以上的accuracy,但F1会把少数类的查准查全都压进来。
参数选完之后,一般会用最优参数在整个训练集上再训练一遍,因为交叉验证时每折只用了三分之二的数据训练,最终部署模型需要吃满全部训练数据:
best_svm = SVC(kernel='rbf', probability=True, C=grid.best_params_['C'], gamma=grid.best_params_['gamma'], class_weight=grid.best_params_['class_weight'], random_state=42) best_svm.fit(X_train_scaled, y_train)3.3 类别不平衡:class_weight='balanced'与阈值调整
如果分布是正常焊件40%,气孔25%,夹渣20%,未熔合10%,裂纹5%,SVC默认把所有样本同等对待,模型把所有样本都判成最高频类别就能获得90%准确率。这是一个典型的假象模型。class_weight='balanced'会让SVM在损失函数里给少数类样本更高的惩罚权重,权重反比于类别频率:
# 手动计算各类权重 from sklearn.utils.class_weight import compute_class_weight classes = np.unique(y_train) weights = compute_class_weight('balanced', classes=classes, y=y_train) print(dict(zip(classes, weights)))输出示例:
{'crack': 3.2, 'good': 0.6, 'porosity': 1.8, 'slag': 2.1, 'incomplete': 1.4}裂纹样本每错判一个的代价是正常样本的5倍多,这样模型才会真正去学裂纹的特征。但class_weight='balanced'并不是万能药,如果某个类别只有十几张图,加权重后模型大概率把多数类的一个小邻域硬划给少数类,这在交叉验证里会表现为F1波动极大。遇到这种情况我通常先把这类样本复制增强,或者降低网格里的C上限,让决策边界更平滑。
调完class_weight之后重新跑一遍网格搜索,因为class_weight会改变最优C值,分开调更稳。
4. 模型评估与落地:混淆矩阵看分错方向,joblib持久化供生产调用
训练阶段的输出不只是那行accuracy,还要知道模型到底把哪两类缺陷互相认错,以及能不能直接把这套流程封装成一个推理脚本,让现场质检员传一张图就能出结果。
4.1 别只盯accuracy:用classification_report和混淆矩阵定位混淆对
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt y_pred = best_svm.predict(X_test_scaled) print(classification_report(y_test, y_pred, digits=3)) cm = confusion_matrix(y_test, y_pred, labels=grid.classes_) disp = ConfusionMatrixDisplay(cm, display_labels=grid.classes_) disp.plot(cmap='Blues') plt.savefig('confusion_matrix.png', dpi=150)看classification_report时重点看每个类别的recall,它表示的是“这个类别的真实样本有多少被我认出来了”。焊接场景里,裂纹的查全率(recall)比查准率(precision)更重要——漏掉一条裂纹在压力容器上是安全事件,错报一个还能人工复核。如果发现裂纹的recall不到70%,出路不是继续调C和gamma,而是回上一步检查特征提取,或者给裂纹样本增加数据增强。
混淆矩阵能直接展示互相认错的类别。焊接底片里最常见的是气孔和夹渣互相混淆,因为两者在局部都可能呈现为亮斑/暗斑;如果未熔合被大量错判成正常焊道,说明模型在“直线形长条暗区”和“正常焊缝纹理”之间没有学到区分信息,这时候要回头增加LBP的radius,让纹理统计范围更大。
4.2 保存模型和scaler:joblib比pickle更省心
训练完不能把模型留在内存里。用joblib.dump把SVM模型和标准化器一起持久化:
import joblib joblib.dump(best_svm, 'weld_svm_model.joblib') joblib.dump(scaler, 'weld_svm_scaler.joblib') joblib.dump(grid.classes_, 'weld_svm_classes.npy')同时保存三个文件是血泪经验:光存模型不存scaler的话,预测阶段输入特征没有标准化,模型算出来的距离全是错的;不存classes的话,predict输出的是0, 1, 2这种数字索引,还得自己映射回类别名。
joblib.dump在sklearn模型上比pickle.dump更安全,它内部对numpy数组做了优化,保存的模型文件体积更小,加载速度更快。加载时注意要放到与训练时完全一致的环境,sklearn小版本升级可能导致加载失败,建议在部署机上固定requirements.txt里的sklearn版本。
4.3 单张图片推理流水线:把预处理到结果串成一个函数
生产环境里不可能每次预测都写一遍20行代码。封装成单一predict_weld_defect是标准做法:
def predict_defect(img_path, svm_model, scaler, class_list): """输入一张焊接X光图片路径,返回缺陷类别和概率""" img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f"图片无法读取: {img_path}") img = cv2.resize(img, (64, 64), interpolation=cv2.INTER_AREA) feat = extract_features(preprocess_for_features(img)) feat_scaled = scaler.transform(feat.reshape(1, -1)) class_idx = svm_model.predict(feat_scaled)[0] class_name = class_list[class_idx] # probability=True 时才能输出概率 proba = svm_model.predict_proba(feat_scaled)[0] proba_dict = {cls: round(float(p), 4) for cls, p in zip(class_list, proba)} return class_name, proba_dict # 使用示例 model = joblib.load('weld_svm_model.joblib') scaler = joblib.load('weld_svm_scaler.joblib') classes = np.load('weld_svm_classes.npy', allow_pickle=True) cls, prob = predict_defect('sample_weld.jpg', model, scaler, classes) print(f"缺陷类型: {cls}, 概率: {prob}")注意predict_proba并不输出真正的贝叶斯概率,它是通过Platt缩放从决策函数距离折算出来的,在RBF核下比较平滑,但在类别不平衡时会出现偏差。不过用来给现场做排序参考已经够了,真正做风险决策还要结合产线工艺信息。allow_pickle=True在numpy 1.16.3以上版本必须显式开启,否则加载classes时报错。
5. 焊接缺陷SVM分类避坑指南:反复出现的高频翻车现场
这个方向看着简单,实际上细节非常多。以下几条是我在跑焊接缺陷分类时踩过或见过同行踩过的真实坑,每条都按“现象-原因-解决”列清楚。
5.1 现象:模型在测试集上准确率98%,但特征可视化后发现所有点挤在一起
原因:没有做StandardScaler。HOG特征的数值范围是0到几十,GLCM的contrast可以到几百,SVM的RBF核距离计算被contrast完全支配,其他特征等于白提。解决方法是回到第3.1节,先标准化再训练。判断是否中招有个quick check:打印X_train_scaled.mean(axis=0),如果每个维度的均值都接近0、标准差接近1,说明标准化生效了。
5.2 现象:网格搜索结果F1是0.93,拿到现场连续错判了三条焊缝
原因:训练集和测试集切分时没有按焊件ID切,而是按图片切。同一根焊件的不同帧可能同时落在训练集和测试集,构成数据泄漏,测试成绩虚高。解决方法是按焊件编号分组划分,例如GroupShuffleSplit或直接按文件名前缀划分,确保同一焊件的数据不会跨训练集和测试集出现。
5.3 现象:裂纹样本在训练集里一个都没被分对,全是预测成正常焊道
原因:裂纹样本量可能只有几十张,class_weight='balanced'给了很大权重,但SVM的RBF核在噪声特征上被带偏。解决方法是先可视化裂纹样本的特征,看是不是和“正常焊道”的特征分布重叠太高。更进一步的做法是裁出裂纹ROI区域再提取特征,而不是对整张图提取——裂纹是长条细线,在全图特征里面积占比太小,特征被背景淹没。
5.4 现象:脚本在Windows上能跑,部署到Linux服务器后读图片就报错
原因:焊接图片路径里可能包含中文、空格或特殊字符,Windows的OpenCV能解析,Linux下容易出现编码问题。解决方法是统一用pathlib.Path处理路径,并在读图前做编码检查,图片文件的命名规则强制改成拼音或英文加数字,这是一开始就应该定的规矩。另外一个隐蔽点是cv2.imread在路径含中文时静默返回None,不会抛异常,所以模拟上线前必须逐个图片校验加载成功。
5.5 现象:GridSearchCV在8核机器上跑了一晚上没出结果
原因:参数网格设太大,C有5个值,gamma有5个值,class_weight有2个值,再乘上cv=5,总共要训练250次SVM,特征维度还高,时间当然爆炸。解决方法是先用粗网格定位最优区间:C = [1, 10, 100]、gamma = [0.01, 0.1, 1]、cv=3,找到最优值范围后在附近做第二次细网格。特征维度超过1000的情况下先降维,比如PCA压缩到50维再跑网格。
6. 进一步提精度:特征融合简化、概率校准和往CNN过渡的路径
如果你发现SVM的表现离产线要求还差两三个百分点,有一个很划算的进阶操作:把HOG特征分块统计后,和LBP直方图按权重拼接,再用PCA降维去冗余。我习惯用PCA(n_components=0.95)保留95%方差,把209维特征压缩到60到80维,网格搜索的gamma候选值可以调小一个量级,因为降维后的特征间距更紧凑。这一步通常能省30%训练时间,精度还可能微涨一到两个点,因为我之前试过把GLCM里相关性和energy两个高相关的统计量重复计算,PCA直接把这些冗余信息抹掉了。
另一个提高现场可用性的操作是CalibratedClassifierCV。在类别不平衡场景下,SVM的predict_proba输出会偏向多数类,校准后的概率更接近真实分布,给质检人员使用的阈值判断也更有解释力。用法是把训练好的SVM再包一层:
from sklearn.calibration import CalibratedClassifierCV calibrated_svm = CalibratedClassifierCV(best_svm, cv=3, method='isotonic') calibrated_svm.fit(X_train_scaled, y_train)method='isotonic'在样本量较小时比sigmoid拟合更好,但需要测试集独立验证,防止参数泄漏。
至于要不要上CNN,我的建议很直接:如果缺陷类别扩充到10类以上,手工特征会越加越长,SVM调参时间成本成倍上涨,数据量积累到每类500张以上时直接迁移ResNet或MobileNet的特征提取层,把最后一层换成SVM或者逻辑回归,你会看到精度上一个台阶。但在此之前,SVM加手工特征仍是性价比最高的一条路:可解释、不依赖GPU、小样本也能交代清楚每条特征的含义。
做焊接缺陷识别这一年多,我最深的一个教训是别急着在参数上折腾,先把特征和数据处理链路的每个中间量都可视化一遍,很多所谓玄学问题当场就解决了。希望帮到你。
本文还有配套的精品资源,点击获取