基于TensorFlow与OpenCV的焊缝识别与毫米级宽度测量方法

发布时间:2026/10/11 16:39:40
基于TensorFlow与OpenCV的焊缝识别与毫米级宽度测量方法
简介优秀毕设“基于TensorFlowOpenCV的焊缝识别”完整项目包面向机器学习与计算机视觉方向的学生和开发者定位为工业视觉检测入门及毕业设计参考。项目融合TensorFlow构建CNN模型与OpenCV图像预处理、边缘检测、数据增强等环节完整展示数据集构建、模型训练、评估与导出部署链路。压缩包共27个文件、约7.52MB其中主要包含PNG样本图像、XML标注文件、Python训练/测试脚本、TensorFlow模型权重data/index/pb及说明文档解压后可直接查看代码、训练样本与模型文件结构。已有219人浏览学习适合需要借鉴焊缝识别等工业目标检测方案或希望快速上手深度学习视觉实战的读者。通过这份材料可理解焊接图像的数据组织方式、模型调用与OpenCV配合的实际路径也能看清模型部署所需的文件格式对完成类似检测任务有直接参考价值。1. 焊缝识别真正要解决什么弧光、飞溅与毫米级宽度判定一条焊缝在工业相机里看起来是一条灰白色的亮带周围全是焊渣飞溅和弧光残留。用OpenCV边缘检测直接抓轮廓十张图里总有六七张把焊渣当成焊缝边缘因为飞溅在亮度分布上和焊缝本体太像了。这个TensorFlowOpenCV焊缝识别项目思路是把“识别”和“测量”分开用TensorFlow训练一个分割网络回答“哪些像素是焊缝”再用OpenCV把分割出来的掩码变成轮廓、中心线和毫米级宽度数据。适合正在做视觉类毕设的同学也适合刚进工业视觉检测方向、想快速搭出一套可演示流程的工程师。2. 方案选型与数据集构建先定识别粒度再准备训练资料2.1 为什么选“TensorFlow分割OpenCV修正”而不是纯目标检测焊缝识别的难点首先是目标形态特殊。一条焊缝可能长达几十厘米宽度却只有几毫米走向还带弯曲。目标检测算法输出的是矩形框框里除了焊缝还包着大面积反光、氧化色和飞溅根本没法从框里直接算宽度。毕设答辩时老师大概率会追问“为什么不用目标检测”你可以从输出信息粒度不够这个角度回答——质检需要的不是“这里有一条焊”而是“这条焊宽度是否达标、有没有断点”。分割模型能逐像素输出掩码从掩码可以算宽度、中心线、断点个数信息是完整的。TensorFlow承担语义识别OpenCV做几何后处理两者的分工很干净深度模型不碰几何测量传统视觉不碰语义判断这个组合在焊缝类项目里是常见做法。方案输出形态能否测宽抗飞溅鲁棒性工程复杂度目标检测矩形框否中低语义分割像素级掩码是高中纯边缘检测形态学边缘点集勉强差低从表格可以看出纯边缘检测实现成本最低但在飞溅多、光照乱的场景下误检率太高。目标检测能找到焊缝大致位置却满足不了测量需求。分割方案是最适合“识别量化”一体的路线。2.2 焊缝图像采集与像素级标注的三个原则先说采集。不要在一个角度拍几百张去凑数那样模型学到的是固定视角下的光照分布而不是焊缝本身。我一般会分三个批次采集正对焊缝拍一组侧视约30度拍一组再把工件旋转90度拍一组。光照上保留一部分正常车间光一部分强反光。过曝样本必须留因为真实施工环境里弧光干扰是躲不掉的。再说标注。用像素级标注工具画出焊缝区域背景不用标输出为PNG单通道掩码焊缝像素值设为255背景为0。三个原则焊渣不标、高光不标、边缘模糊区域只在能凭几何连续性判断为焊缝时才标。飞溅一旦标进掩码模型就会把亮斑当成焊缝来学这种噪声会在训练中被放大成固定的误检模式。2.3 构建TFRecord数据集把图像和掩码打包成模型能读的格式如果采集时已经把图片统一到了同一个尺寸构建数据集的脚本并不复杂。下面是把图像和掩码写入TFRecord的核心函数。import tensorflow as tf import numpy as np from PIL import Image def bytes_feature(value): return tf.train.Feature(bytes_listtf.train.BytesList(value[value])) def write_tfrecord(image_path, mask_path, writer): image Image.open(image_path).convert(RGB) mask Image.open(mask_path).convert(L) # 统一尺寸掩码用最近邻插值防止产生灰色过渡像素 image image.resize((512, 512), Image.BILINEAR) mask mask.resize((512, 512), Image.NEAREST) image_raw np.array(image).astype(np.uint8).tobytes() mask_raw (np.array(mask) 127).astype(np.uint8).tobytes() feature { image: bytes_feature(image_raw), mask: bytes_feature(mask_raw), } example tf.train.Example(featurestf.train.Features(featurefeature)) writer.write(example.SerializeToString())这个函数做了三件事把彩色图和掩码统一到512x512把掩码二值化成0/1再序列化成TFRecord。掩码用最近邻插值很关键如果用双线性插值焊缝边缘会出现一圈灰色过渡像素训练时模型会在边缘上犹犹豫豫。TFRecord把数据打包成二进制训练时IO更稳定还能直接配合tf.data的shuffle和prefetch。512x512是精度和显存的一个平衡点机器显存紧张可以降到384x384。TFRecord的shard数量建议按总样本数除以500来定太多会拖慢读取速度。注意如果只是快速验证不追求训练吞吐直接用tf.data.from_tensor_slices读文件路径也能跑TFRecord不是必选项。2.4 类别不平衡背景占90%以上时怎么让模型认真学焊缝焊缝掩码在整张512x512图里通常只占几个百分点训练很容易收敛成“全部输出0”。两个补救手段加权重统计训练集掩码里正负像素比例把loss里背景项的权重压到0.2到0.3焊缝项的权重提到1.0到1.5。采样策略用tf.data做随机裁剪patch裁剪时优先选择含有焊缝像素的区域让每张训练图里焊缝占比提高到25%以上。一个带亮度扰动和随机裁剪的增强函数可以这样写def augment(image, mask): # 模拟弧光过曝与光照波动 image tf.image.random_brightness(image, max_delta0.3) image tf.image.random_contrast(image, lower0.8, upper1.2) # 随机裁剪到256x256image和mask保持同样裁剪位置 concat tf.concat([image, mask], axis-1) concat tf.image.random_crop(concat, size[256, 256, 4]) image concat[..., :3] mask concat[..., 3] mask tf.cast(mask, tf.float32) return image, mask这里max_delta0.3意味着亮度在正负30%范围内波动能模拟弧光突然变强的场景。random_contrast的0.8到1.2是对比度区间目的是覆盖不同材质表面的反光差异。增强不是越多越好焊缝的边缘语义很明确不需要像自然图像分类那样做复杂的复制粘贴增强重点应该放在光照和多角度覆盖上。3. Unet训练与参数调优TensorFlow侧的关键设置3.1 用Keras搭建轻量Unet为焊缝分割调整的三个结构点Unet是这个场景下比较稳的选择标注成本低、对小目标友好TensorFlow的Keras API实现起来也快。下面是四层下采样的Unet结构。from tensorflow.keras import layers, Model def conv_block(x, filters): x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) return x def build_unet(input_shape(512, 512, 3), filters32): inputs layers.Input(input_shape) e1 conv_block(inputs, filters) # 512x512 p1 layers.MaxPool2D(2)(e1) # 256x256 e2 conv_block(p1, filters * 2) # 256x256 p2 layers.MaxPool2D(2)(e2) # 128x128 e3 conv_block(p2, filters * 4) # 128x128 p3 layers.MaxPool2D(2)(e3) # 64x64 e4 conv_block(p3, filters * 8) # 64x64 d layers.UpSampling2D(2)(e4) # 128x128 d layers.Concatenate()([d, e3]) d conv_block(d, filters * 4) d layers.UpSampling2D(2)(d) # 256x256 d layers.Concatenate()([d, e2]) d conv_block(d, filters * 2) d layers.UpSampling2D(2)(d) # 512x512 d layers.Concatenate()([d, e1]) d conv_block(d, filters) output layers.Conv2D(1, 1, activationsigmoid)(d) return Model(inputs, output)结构上有三个点需要照着调。第一下采样深度只到64x64不再往32x32以下探因为焊缝细长下采样过深会把只有几毫米宽的焊缝直接抹没。第二上采样用UpSampling2D加卷积而不是转置卷积能避免棋盘格伪影对小目标更友好。第三初始filters从32开始比16更稳比64更省显存。input_shape必须和数据预处理保持一致BatchNormalization放在卷积后激活前最后一个输出用sigmoid。因为只有焊缝和背景两类不需要softmax。3.2 损失函数与训练策略BCEDice混合损失为什么比单一损失稳纯BCE在背景占绝大比例时梯度会被背景主导模型会偷懒学着把一切都预测成背景。纯Dice对类别不平衡鲁棒但梯度整体偏平滑收敛偏慢。混合损失把两者结合起来代码很简单。import tensorflow as tf def dice_loss(y_true, y_pred, smooth1.0): y_true tf.cast(y_true, tf.float32) intersection tf.reduce_sum(y_true * y_pred) return 1 - (2 * intersection smooth) / ( tf.reduce_sum(y_true) tf.reduce_sum(y_pred) smooth) def combined_loss(y_true, y_pred): bce tf.keras.losses.binary_crossentropy(y_true, y_pred) return 0.7 * bce 0.3 * dice_loss(y_true, y_pred)dice_loss里的smooth参数是为了防止分母为0同时让训练更稳定。混合比例0.7和0.3不是固定的如果预测掩码断裂严重就把Dice权重提到0.4让模型更关注结构完整性。训练参数按下面的表格初始化基本不太会翻车参数值理由优化器Adam对分割任务收敛稳定学习率1e-3常用起点后续靠回调衰减batch_size8512x512输入下的显存友好值epochs100配EarlyStopping实际跑多少看回调回调ModelCheckpoint ReduceLROnPlateau EarlyStopping兼顾保存最优权重与自动调学习率训练主循环加上回调callbacks [ tf.keras.callbacks.ModelCheckpoint( best.h5, save_best_onlyTrue, monitorval_loss), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience8), tf.keras.callbacks.EarlyStopping( patience15, restore_best_weightsTrue), ] model.compile(optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscombined_loss, metrics[accuracy]) model.fit(train_ds, validation_dataval_ds, epochs100, callbackscallbacks)ReduceLROnPlateau每8轮验证损失不下降就把学习率减半EarlyStopping在15轮无改善后停止。restore_best_weightsTrue很关键否则训练结束后模型停在最后一个epoch而不是验证损失最低的那个权重。3.3 训练过程监控别只盯loss要看掩码输出与IoU训练时除了看loss曲线每个epoch结束最好保存一张验证集的预测掩码叠加图肉眼看模型到底把哪里切开了。IoU和Dice的计算代码可以单独封装。import numpy as np def compute_iou_dice(y_pred, y_true, threshold0.5): pred (y_pred threshold).astype(np.uint8) true (y_true 0.5).astype(np.uint8) intersection np.logical_and(pred, true).sum() union np.logical_or(pred, true).sum() iou intersection / max(union, 1) dice 2 * intersection / max(pred.sum() true.sum(), 1) return iou, dicemax(union, 1)和max(pred.sum() true.sum(), 1)是为了防止整张图都是背景时除零。对焊缝这种细长目标IoU能到0.9以上但掩码碎成几十个连通域是常有的事。模型评估不要只看IoU还要统计掩码的连通域数量。焊缝在语义上应该是一个整体如果预测掩码断成很多块后处理怎么修都补不回来这时候要回头调整损失函数里的Dice权重或者降低下采样深度。4. OpenCV后处理流程把分割结果变成可量化结果4.1 mask二值化与形态学修复闭运算的参数选择思路模型输出的是一张0到1的概率图不能直接拿去算宽度要先二值化再用形态学操作修复细小的断裂和孤立的噪点。import cv2 import numpy as np def postprocess_mask(mask_pred, thresh0.45): mask (mask_pred thresh).astype(np.uint8) # 闭运算先膨胀后腐蚀把细长断裂接上 kernel_close cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_close, iterations1) # 开运算先腐蚀后膨胀去掉孤立亮斑和细小飞溅 kernel_open cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_open, iterations1) return mask阈值取0.45而不是0.5是有讲究的。细长焊缝的边界在sigmoid输出上存在渐变区取0.5会让边缘像素丢得比较厉害0.45能多保留一圈边界再交给形态学修整。闭运算的kernel是5x5能把预测断裂处连接起来开运算的kernel是7x7孤立的小亮斑大部分会被移除。操作kernel大小迭代次数作用闭运算5x51连接细长断裂、填平小空洞开运算7x71移除飞溅产生的孤立噪声如果输入图片分辨率更高比如到1024kernel要按比例放大到7x7或9x9。不要用过大的kernel否则会把两条平行焊缝的掩码连到一起造成合并误检。4.2 轮廓提取与坐标映射从像素到工件坐标的四个步骤处理完形态学之后下一步是提取轮廓并把它映射到工件坐标系。轮廓提取在OpenCV 4.x下的写法是def get_contours(mask): mask_uint8 (mask * 255).astype(np.uint8) contours, _ cv2.findContours( mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) # 按面积降序排序取最大的轮廓作为主焊缝 contours sorted(contours, keycv2.contourArea, reverseTrue) return contours坐标映射的核心是一个透视变换矩阵。在标定图片上取4个已知点用cv2.getPerspectiveTransform求出矩阵M再对轮廓点做变换。def pixel_to_work_coord(contour, M): pts contour.reshape(-1, 2).astype(np.float32) pts cv2.perspectiveTransform(np.expand_dims(pts, 1), M) return pts.reshape(-1, 2)坐标映射的四个步骤是取外轮廓、按面积排序、多边形近似、透视变换。标定时把棋盘格或自定义标记放到工件同一平面标4个点求M相机固定以后M可以复用。如果工件在流水线上移动M必须随位置更新不能一个矩阵用到黑。4.3 焊接宽度连续测量中心线为什么比轮廓距离稳定宽度的连续测量是焊缝识别落到质检环节的关键一步。一个稳定做法是用距离变换求中心线再根据距离变换值近似宽度。def measure_width(mask_uint8): dist cv2.distanceTransform(mask_uint8, cv2.DIST_L2, 5) # 用局部极大值近似中心线 dilated cv2.dilate(dist, np.ones((5, 5), np.uint8)) center_mask (dist dilated) (dist 0) ys, xs np.where(center_mask) widths dist[ys, xs] * 2 return widths.mean(), widths.min(), widths.max(), len(xs)中心线上的每个像素到掩码边缘的最短距离乘以2就是这个位置的近似宽度。这个近似在毕设精度下完全够用。如果想做更严格的版本需要先平滑中心线再逐点取切线方向、计算法线与轮廓的交点距离但噪声会被放大处理起来麻烦不少。widths.min()一旦连续低于工艺阈值说明存在咬边或断弧风险widths.max()和均值之间的差大小则反映了宽窄波动。我会把widths序列输出成CSV再用matplotlib画出沿焊缝长度的宽度曲线答辩时这个图比IoU数字更有说服力。5. 焊缝识别避坑标注噪声、过拟合与误检的排雷记录5.1 现象弧光高光区域被持续误检为焊缝训练完跑第一轮验证发现弧光高光区域被成片识别成焊缝。去翻训练集标注果然有些掩码把高光背景也圈进去了。模型学到的不是“焊缝的几何特征”而是“亮区即焊缝”。解决分两步第一步把所有高光样本的掩码重新过一遍把飞溅和镜面反射区域从焊缝掩码中擦掉第二步在数据增强里把亮度扰动范围加大到max_delta0.3让模型见过更宽的亮度区间。如果工件表面反光太强我还会在OpenCV后处理里加一条规则灰度值高于240的像素从掩码中剔除。5.2 现象细长焊缝预测中间断裂掩码像虚线模型在验证集上IoU不低但把掩码画出来一看一条完整的焊缝被断成好几段。原因是下采样到16x16以下时焊缝宽度已经小于一个像素空间信息丢失严重。另一个原因是Dice损失对细长目标的断裂不够敏感因为断裂产生的Dice惩罚没有想象中那么大。解决方法是把下采样深度控制在64x64以上同时提高混合损失里BCE的权重让模型更关注边缘像素的逐点准确度。如果仍然断裂就用后处理的闭运算兜底。5.3 现象验证集IoU高换一台相机或换一个角度就翻车训练时IoU能到0.93换一台相机、换个拍摄角度效果立刻崩掉。这是因为训练集采集视角太单一模型记住了特定角度下的纹理分布而不是焊缝本身的语义。解决方法是按2.2节的思路采集多角度数据并且专门留出一部分不同角度拍摄的图像完全不参与训练作为独立测试集衡量泛化能力。部署前对新的相机做一次直方图归一化让亮度分布和训练集尽量对齐。如果角度差异实在太大最可靠的做法是重新采一小批数据微调模型。5.4 现象训练loss掉得很低但掩码输出全黑训练loss一路降到0.1以下验证集上预测掩码却全是黑的。这个是最典型的类别不平衡翻车现场模型直接输出常量背景Dice loss在全是背景的图上计算出来就是很低的loss看起来模型在收敛实际什么也没学到。解决方法是给损失函数加权重并且在训练时单独打印正样本的Dice不要只看总loss。如果某个epoch的Dice一直为0优先检查数据管线把训练数据里的掩码单独抽出来可视化确认掩码没有在增强过程中被丢掉。5.5 现象验证集结果忽好忽坏表现像“玄学”同一套训练代码换一次机器验证集IoU波动很大。这也算是分割训练里常见的“玄学”问题。多数情况是没固定随机种子加上数据增强强度在epoch之间变化太大。解决方法是固定NumPy和TensorFlow的随机种子在验证时关闭数据增强用一份纯净的验证集做评估和增强版的验证结果做对比。两者指标差距超过0.05说明增强过猛要把max_delta调小一些。6. 用毫米坐标验证分割精度一个值得写进毕设结论的量化方法6.1 棋盘格标定把像素误差换算成物理误差分割模型输出的精度到底是多少毫米这个问题迟早会被问到。用棋盘格做一次标定就能回答。import cv2 import numpy as np def calc_pixel_mm_scale(chessboard_img, square_size_mm10.0): gray cv2.cvtColor(chessboard_img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, (9, 6), None) if not ret: raise RuntimeError(未检测到棋盘格角点) # 计算相邻角点的平均像素距离 diff np.diff(corners[:, 0, :], axis0) pixel_dist np.mean(np.linalg.norm(diff, axis1)) return square_size_mm / pixel_dist标定时把棋盘格平放在被测工件表面同一高度让棋盘格占据和焊缝区域相近的深度位置。得到的scale单位是mm/像素把第四章算出的像素宽度乘上scale就是实际毫米宽度。然后用游标卡尺实测焊缝最窄和最宽处的值和模型输出对比误差控制在0.5mm以内就很有说服力。6.2 连续帧稳定性测试同一个焊缝多拍几帧相机固定、角度不变在同一个位置连续采集10帧分别推理计算两两之间的IoU或宽度极差。如果宽度极差超过0.3mm说明模型对光照微扰太敏感。这个测试成本极低但说服力很强答辩时老师几乎都会问“方案稳定性怎么保证”把连续帧数据拿出来就能回答。我第一次做这个项目时只盯着测试集IoU觉得0.95就大功告成结果连续帧测试里同一位置的宽度输出一跳一跳的才意识到照明才是最大的坑。现在每次做焊缝识别我都会先拍一组多角度和亮度变化的样本做完连续帧稳定性验证再谈精度。希望帮到你。本文还有配套的精品资源点击获取