ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SVM的焊接缺陷图像识别分类:Python源码与调参实战

基于SVM的焊接缺陷图像识别分类:Python源码与调参实战 简介基于SVM的焊接缺陷图片识别分类项目面向计算机相关专业在校学生、老师及企业员工可作为课程设计、毕业设计或机器学习入门练手素材也适合对工业视觉质检感兴趣的读者。压缩包共144个文件主要由140张PNG焊接缺陷样本图片、3个Python脚本及1个zip补充包组成整体体积仅1.7MBPNG样本覆盖未焊透、未熔合等典型缺陷类别Python脚本包含特征提取、SVM建模、训练与分类评估环节模型需自行训练后使用。代码经过测试可运行结构清晰、注释完整按流程即可复现实验并核对分类效果目前已有518人浏览学习。项目未附带预训练权重使用者可从数据加载开始完整经历一次SVM图像分类实验也可在现有特征或核函数参数上调整扩展至其他缺陷检测与图像识别任务具有较强的学习借鉴与二次开发价值。1. 为什么焊接缺陷识别还值得用SVM小样本、低算力与可解释性流水线质检的痛点从来不是算法不够聪明而是漏检和误检的代价不对称。漏掉一条裂纹可能把整批结构件送进返工甚至报废流程误检一次则打断产线节拍、让老师傅对着合格焊缝反复打磨。你拿到的这份「基于SVM实现焊接缺陷图片识别分类Python源码(含数据集需自己训练模型).zip」走的正是小样本分类的老派但可靠路线不用显卡、不用预训练大模型把几百张缺陷图喂给支持向量机照样能产出可用的分类结果。它的价值在于数据集、特征提取和训练脚本已经替你铺好了路你需要做的是理解参数、跑通流程、再把模型调到能上线的程度。这篇笔记就把这条路的原理、参数、坑和落地流程完整拆开讲。2. 焊接缺陷长什么样先把图像变成SVM能读的数字SVM不会直接看图像它只认识数值向量。这一章的目标是把一张缺陷图片变成一组有区分度的特征同时说清楚为什么在焊接缺陷这种小数据集场景里人工特征比端到端卷积更稳。2.1 常见缺陷类型与成像特征焊缝合不合格影像上大致分五类气孔、夹渣、未熔合、咬边和裂纹。气孔在灰度图上表现为近圆形暗斑边缘有一定过渡夹渣呈不规则块状有时比背景亮、有时偏灰裂纹是细长条方向不定局部灰度跳变剧烈未熔合和咬边则表现为沿焊缝走向的线状沟槽。这些差异本质上体现在三个方面局部灰度值、梯度方向和纹理分布。这意味着特征工程的重心应该放在描述局部灰度变化上。HOG描述子擅长刻画梯度方向分布对气孔、裂纹这种边缘明显的目标很有效GLCM统计灰度共生关系能捕捉夹渣和未熔合这类块状区域的纹理差异。两者拼接后图像就变成了几百维的特征向量足够让SVM去画分类超平面。2.2 HOG特征用局部梯度描述缺陷轮廓HOG的全称是Histogram of Oriented Gradients核心思路是先把图像分成小格子cell在每个格子内统计梯度方向的直方图再把相邻格子合成块block做归一化。焊接缺陷的轮廓无论旋转还是平移梯度方向的分布基本不变这正是HOG对轻微光照变化不敏感的原因。import numpy as np from skimage.feature import hog from skimage.io import imread from skimage.color import rgb2gray def extract_hog(image_path, target_size(128, 128)): img imread(image_path) if img.ndim 3: img rgb2gray(img) from skimage.transform import resize img resize(img, target_size, anti_aliasingTrue) fd hog( img, orientations9, pixels_per_cell(16, 16), cells_per_block(2, 2), block_normL2-Hys, visualizeFalse, feature_vectorTrue, ) return fdorientations9表示把360度梯度方向分到9个桶这是OpenCV和skimage里的默认值对大多数纹理分类问题已经够用。pixels_per_cell(16, 16)决定cell大小值越小特征越细、维度越高但128×128的图像下如果改成(8, 8)特征维度会翻四倍容易过拟合。cells_per_block(2, 2)用于局部对比度归一化块过大会抹掉细节块过小则归一化失去意义。这段代码的输出是一个一维数组维数由图像尺寸和参数共同决定。128×128图像下cell数是8×8block数是7×7每个block包含4个cell总维度是7×7×9×41764。如果你觉得这个维度对SVM来说还是太高就把图像压到96×96维度会降到6×6×9×4≈1296。2.3 GLCM纹理特征补充灰度分布统计HOG只描述梯度对灰度分布本身不敏感。焊接夹渣往往灰度值整体偏高或偏低仅仅靠梯度方向无法区分。灰度共生矩阵GLCM统计的是灰度为i的像素与灰度为j的像素在给定距离和角度上共现的频率从这个矩阵里再提取对比度、相关性、能量、熵等统计量就能补上HOG缺失的纹理通道。from skimage.feature import graycomatrix, graycoprops def extract_glcm(image_path, target_size(128, 128)): img imread(image_path) if img.ndim 3: img rgb2gray(img) from skimage.transform import resize img resize(img, target_size, anti_aliasingTrue) # 灰度量化到16级避免256级矩阵过于稀疏 img_quant (img * 15).astype(np.uint8) glcm graycomatrix( img_quant, distances[1, 3], angles[0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], levels16, symmetricTrue, normedTrue, ) props [contrast, dissimilarity, homogeneity, energy, correlation, ASM] features [] for prop in props: feat graycoprops(glcm, prop) # shape: (len(distances), len(angles)) features.extend(feat.flatten()) return np.array(features)这里有个关键但容易被忽略的参数levels16。如果直接对0255灰度范围建共生矩阵矩阵是16×16还是256×256我用256级试过矩阵极度稀疏很多格子是0纹理统计结果几乎全是噪声。量化到16级后每个像素对能落在矩阵里的概率显著提高提取出的对比度、能量才有实际意义。distances[1, 3]分别捕捉精细纹理和稍粗纹理四个角度覆盖水平和垂直方向保证对裂纹这类方向性缺陷不偏科。输出的特征数是2×4×648维虽然不多但和HOG拼接后能有效提升混淆类别的区分度。2.4 特征拼接与标准化把HOG和GLCM两个向量拼起来之前必须先意识到它们不在同一个量纲上。HOG特征的取值范围基本在01之间GLCM的对比度可能到几十、能量可能只有0.1。SVM的间隔计算对特征尺度极其敏感量级大的维度会主导决策边界。所以特征拼接后必须做标准化。常见做法是实例化一个StandardScaler在训练集上fit得到均值和方差然后用同一套参数变换训练集和测试集。这里有个容易踩的坑如果你先拼全量特征再统一标准化等于把测试集的信息泄露给了训练过程。正确顺序是先切分训练集和测试集再对训练特征fit_transform对测试特征只做transform。后续模型的泛化评估才可信。3. SVM分类器选型核函数、多分类策略与不平衡处理特征工程决定特征的上限分类器决定你离上限有多近。SVM家族在sklearn里有好几个入口选错了会走不少弯路。3.1 SVC、NuSVC和LinearSVC三个类该怎么选很多人第一次写SVM都从SVC开始这没错但了解另外两个入口的定位能帮你省事。类名核函数支持参数含义适用场景SVClinear / poly / rbf / sigmoidC控制误分类惩罚gamma控制RBF核半径绝大多数中小样本分类焊接缺陷识别的主力NuSVC与SVC相同nu近似替代C表示支持向量比例的上界/误差比例的下界你明确知道样本噪声比例时调节更直观LinearSVC仅linear底层是LibLinear能高效处理大规模稀疏特征特征维度很高、样本很大且确信线性可分时焊接缺陷图片数据集通常只有几百到几千张图片特征维度一般几千维SVC配RBF核是稳妥的选择。LinearSVC虽然训练更快但它的多分类策略是OvR一对一 vs 一对其余在类别重叠严重的缺陷分类上不如OvO稳定而且LinearSVC没有predict_proba后续想做置信度阈值就麻烦了。3.2 RBF核为什么是默认首选焊接缺陷特征在原始空间里几乎不可能是线性可分的气孔的暗斑和裂纹的细线在梯度直方图上距离很近直接画一条线分不开。RBF核函数的本质是计算两个样本在高维空间的相似度公式是$K(x, x) \exp(-\gamma ||x - x||^2)$它能用超平面在升维后的空间里切分复杂的边界又不至于像多项式核那样高阶数时数值不稳定。RBF核有两个必须调的参数。C是误分类惩罚系数C大了模型倾向把所有训练样本都分对容易过拟合C小了容忍更多误分换决策边界的光滑。gamma控制单个训练样本的影响半径gamma越大决策边界越复杂容易圈住噪声gamma越小边界越平滑但可能欠拟合。这两者不是独立调出来的需要配合网格搜索一起看。3.3 多分类策略与类别不平衡sklearn里的SVC虽然你只写了一个SVC(kernelrbf, decision_function_shapeovr)但底层默认是OvO也就是在每两个类别之间都训练一个二分类器最后投票。decision_function_shape只会影响decision_function输出的形状不改变底层训练方式。多分类任务里OvO比OvR稳定因为每次只面对两个类边界学习更充分代价是需要训练的模型数量多但焊接缺陷类别通常就四五类完全扛得住。焊接缺陷数据集的另一个真实问题是类别不平衡。气孔在焊接现场最常见良品样本更是容易攒到几百张而裂纹、未熔合这类关键缺陷往往只能收集到十几张。这种情况下SVM会被多数类带走。处理思路有两个一是class_weightbalanced让sklearn根据类别频率自动缩放惩罚权重这是成本最低的干预二是配合数据增强把少数类样本量提上来旋转、水平翻转、加高斯噪声这些传统增强手段在缺陷图上同样有效。from sklearn.svm import SVC model SVC( kernelrbf, C100.0, gammascale, class_weightbalanced, decision_function_shapeovo, probabilityTrue, random_state42, )这段配置是一个不错的起点。gammascale让sklearn根据特征方差自动估算gamma初始值再结合网格搜索去精调probabilityTrue会额外训练一次Platt缩放代价是训练时间变长、且predict_proba的绝对值不够可靠但后续可以用它做阈值控制。等网格搜索跑完再把C和gamma替换成最优值。4. 从zip到模型完整训练流程与代码拆解拿到这个zip之后先别急着跑脚本。第一步是整理数据目录、确认图片格式和类别分布再逐步执行特征提取、训练、调参、保存。整个过程用两份脚本分开管理训练脚本负责产出模型推理脚本负责加载模型并预测新图。4.1 数据集目录组织与加载zip里的数据集解压后常见组织方式是按类别分文件夹。保持这个结构训练代码直接用pathlib遍历即可不要手动去改文件名。如果发现某些类别的图片尺寸相差很大建议在特征提取前统一resize到同一尺寸否则特征向量长度不一致SVM根本没法训练。import pathlib import numpy as np train_dir pathlib.Path(dataset/train) test_dir pathlib.Path(dataset/test) def load_image_paths(data_dir): X_paths, y [], [] for label_idx, class_dir in enumerate(sorted(data_dir.iterdir())): if not class_dir.is_dir(): continue for img_path in class_dir.glob(*.jpg): X_paths.append(str(img_path)) y.append(label_idx) return X_paths, np.array(y) X_train_paths, y_train load_image_paths(train_dir) X_test_paths, y_test load_image_paths(test_dir) print(类别分布训练集:, np.bincount(y_train))这段代码有一个细节是刻意处理的sorted(data_dir.iterdir())按类名排序保证类别和数字编号的对应关系稳定。如果你自己往数据集里新增类别这行排序能避免标签错位。用glob(*.jpg)只抓jpg文件如果zip里的图是png或bmp把扩展名一起加上。4.2 特征提取与多线程加速把前文的HOG和GLCM函数合并成一个总入口逐张图片提取特征。几百张图串行跑也许只要几分钟但特征提取本身是重复性劳动多线程可以明显提速。from concurrent.futures import ThreadPoolExecutor def extract_feature_vector(image_path): hog_feat extract_hog(image_path) glcm_feat extract_glcm(image_path) return np.hstack([hog_feat, glcm_feat]) def build_feature_matrix(path_list, batch_size64): features [] with ThreadPoolExecutor(max_workers4) as executor: for i in range(0, len(path_list), batch_size): batch_paths path_list[i:i batch_size] batch_feats list(executor.map(extract_feature_vector, batch_paths)) features.extend(batch_feats) return np.array(features)ThreadPoolExecutor在这里比ProcessPoolExecutor更适合因为skimage的HOG计算会释放GIL多线程能吃到并行红利还不用处理子进程的序列化开销。batch_size64控制内存占用避免一次性把所有特征全部载入。如果你的图片非常大可以考虑在extract_hog里先缩小到96×96特征维度降下来后训练速度会快很多分类精度损失通常在两个点以内。4.3 分离特征标准化避免数据泄露这是整个流程里最容易被忽略的环节。特征矩阵构建完成后先切分训练集和测试集再对训练集做标准化fit_transform测试集只做transform。from sklearn.preprocessing import StandardScaler X_train build_feature_matrix(X_train_paths) X_test build_feature_matrix(X_test_paths) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) np.save(X_train_scaled.npy, X_train_scaled) np.save(X_test_scaled.npy, X_test_scaled) np.save(y_train.npy, y_train) np.save(y_test.npy, y_test)把标准化后的特征矩阵存成npy文件是个好习惯网格搜索要对同一份特征反复训练很多次每次都重新提取特征纯属浪费时间。之后调参时直接np.load这四个文件省时省内存。如果你在调参过程中发现某些类别一直分错回头检查的也是这份特征文件而不是重新跑一遍提取流水线。4.4 网格搜索确定C和gamma的最优组合网格搜索的核心是穷举参数组合并用交叉验证评估。焊接缺陷场景下C的建议范围是10的等比数列gamma用一个合理范围包住scale的估算值。scoring建议用f1_macro而不是accuracy因为类别不平衡时准确率是虚高的。from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC from sklearn.metrics import classification_report param_grid { C: [0.1, 1.0, 10.0, 100.0, 1000.0], gamma: [0.0001, 0.001, 0.01, 0.1, 1.0], } base_svm SVC(kernelrbf, class_weightbalanced, random_state42) grid GridSearchCV( base_svm, param_grid, cv5, scoringf1_macro, n_jobs-1, verbose1, ) grid.fit(X_train_scaled, y_train) print(Best params:, grid.best_params_) print(Best CV score:, grid.best_score_) y_pred grid.predict(X_test_scaled) print(classification_report(y_test, y_pred, digits3))cv5表示五折交叉验证每折拿80%训练、20%验证五次结果取平均。五折对小数据集来说每个验证折包含足够的样本评估比单独留出验证集更稳。n_jobs-1会把所有CPU核心都投进去25个参数组合 × 5折 125次SVM训练普通笔记本大概跑几分钟不必焦虑。classification_report会逐步打印每个类别的精确率、召回率和F1值。不要只看总F1逐类看如果裂纹的召回率只有0.6说明有一半裂纹被漏掉了这在焊接场景里是不可接受的后面第5章会专门讲怎么处理这种不对称代价。4.5 保存模型与训练/推理分离网格搜索跑完后最佳模型在grid.best_estimator_里。要把它和标准化器一起保存因为新图片预测时必须用同一个scaler做变换不能重新fit。import joblib final_model grid.best_estimator_ joblib.dump(final_model, weld_defect_svm.joblib) joblib.dump(scaler, weld_defect_scaler.joblib) # 记录类别映射推理时反向还原 class_names sorted([p.name for p in train_dir.iterdir() if p.is_dir()]) with open(class_names.txt, w, encodingutf-8) as f: f.write(\n.join(class_names))joblib是sklearn官方推荐的模型持久化方式对大数组比pickle更高效。class_names.txt记录了类别名称和数字标签的对应关系这份文件在部署时必须和模型一起带上。如果你后续要调整模型只需修改训练脚本重跑一遍推理脚本不用动。5. SVM训练避坑五个翻车现场与排查方法SVM的翻车往往不是算法本身的问题而是数据处理和参数理解不到位。下面这五条是我在这个方向上反复踩过的坑写出来帮你提前绕开。5.1 特征不归一化导致模型完全失效现象训练集准确率95%测试集准确率只有50%或者GridSearchCV里最好的分数很高但交叉验证曲线非常不稳定。原因HOG特征基本在01区间GLCM的对比度可能到几十甚至上百。SVM的RBF核基于欧氏距离计算相似度量级大的维度会直接压倒其他维度导致分类边界由对比度这一个维度决定。更隐蔽的是不同缺陷的纹理差异恰好体现在量级小的维度上直接被淹没了。解决严格按照第4.3节的做法在训练集上fit_transform测试集只transform。同时注意gammascale会使用特征的方差做自动估算标准化后这个估算才稳定如果你在未经标准化的特征上设gamma0.01含义完全不同。5.2 样本太少而特征太多模型记住噪声现象训练集F1接近1.0测试集F1掉到0.7以下或者交叉验证的均值还行但每一折之间的分数差距超过10个点。原因1764维HOG特征 48维GLCM特征如果训练集只有200张图参数空间远大于样本量SVM很容易在训练集上过拟合。支持向量数量接近训练样本数量就是一个危险信号。解决先减少特征维度。把HOG的pixels_per_cell从(16, 16)改成(24, 24)或(32, 32)图像缩到96×96特征维度能压到几百维。再用GridSearchCV缩小C的搜索范围到[0.1, 1.0, 10.0]小数据集配大C几乎必然过拟合。最后看model.n_support_如果支持向量占比超过60%考虑加入更多训练样本或做数据增强。5.3 类别不平衡让模型把所有样本判成多数类现象分类报告里气孔的F1很高但裂纹、未熔合的召回率是0打印预测结果发现几乎全是某一个大类。原因SVM的优化目标是最小化总误差。当某一类样本量是其他类的10倍时把少数类全判错带来的总误差增量很小模型自然选择偏向多数类。解决第一步用class_weightbalanced让少数类的误分类代价按比例放大。第二步对少数类做传统数据增强旋转10度、水平翻转、随机裁剪、加高斯噪声注意增强要在特征提取之前做也就是对原始图片增强而不是对特征向量增强。第三步实在不行就人工核对一下少数类样本排除掉拍摄角度、亮度过暗、标注错误这类脏数据。5.4 网格搜索分数高但真实测试掉点现象GridSearchCV里best_score_0.95测试集上跑出来只有0.8而且这不是偶发每次重跑都这样。原因最常见的是数据泄露。如果你在切分训练集和测试集之前就做了标准化相当于测试集的均值和方差参与了训练过程测试分数自然虚高。另一个原因是网格搜索的交叉验证折数太少或数据划分不均匀cv3在类别分布偏斜时会有某折里只有一个类别的样本。解决把标准化严格限制在训练折内。严谨的做法是用Pipeline把StandardScaler和SVC包起来让交叉验证在每个折里独立做标准化。cv从5起步如果某个类别样本特别少改StratifiedKFold(n_splits5, shuffleTrue)保证每个折里各类别比例和全量一致。5.5 忽略预测概率误检代价没有控制现象模型在测试集上F1不错但上产线后误检率极高老师傅频繁被无效报警打断或者漏检了某一条关键裂纹导致批量返工。原因predict()只返回一个硬分类它把概率略超0.5的样本也判成正类。焊接缺陷场景里漏检和误检的代价完全不同漏检一条裂纹要返工整批焊缝误检一次只是多花几分钟人工复核。单靠默认阈值做不到这种不对称控制。解决用decision_function或CalibratedClassifierCV得到置信度再设定一个保守阈值。倾向漏检控制的场景把阈值压到0.3让更多可疑样本进入人工复核倾向减少误检的场景把阈值抬高到0.7。阈值不是一个全局值最好按类别分别设定气孔类误检可接受度其实比裂纹类高得多。6. 把模型用起来批量推理与一个可靠调参习惯训练脚本和推理脚本分开是工程上的好习惯。训练脚本只在模型更新时运行推理脚本则要稳定、简单、不依赖训练时留下的中间变量。joblib.load加载模型和scaler后单张图片的推理逻辑是固定的读取 → 灰度化 → resize → HOG GLCM → scale变换 →predict_proba。下面的函数可以直接放进生产脚本里import joblib import numpy as np model joblib.load(weld_defect_svm.joblib) scaler joblib.load(weld_defect_scaler.joblib) def predict_single(image_path, threshold0.5): feat extract_feature_vector(image_path).reshape(1, -1) feat_scaled scaler.transform(feat) proba model.predict_proba(feat_scaled)[0] cls model.classes_[np.argmax(proba)] confidence np.max(proba) if confidence threshold: return uncertain, confidence return cls, confidence返回uncertain而不是硬给一个类别是产线上很实用的习惯。把低置信度样本留给人工复核比强行走一个分类结果更安全。再分享一个调参习惯。我在网格搜索跑完之后不会直接用best_params_了事而是用best_params_重新训练一次完整模型然后打印出每一类的分类报告逐条检查哪一类、哪些样本被分错了。把分错样本的图片路径打印到日志里按图像名回看原始图片你很快会发现某一些气孔被拍成了细长形和裂纹的特征向量重叠了。这时候与其继续调SVM参数不如回去检查特征提取参数或增加裂纹类样本。模型调参到一定程度后瓶颈往往不在分类器而在特征表达和数据质量。这是我的经验之谈希望帮到你。本文还有配套的精品资源点击获取
返回列表