ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于labelme的公路隧道漏水识别分割:27张小样本数据集的训练与优化实战

基于labelme的公路隧道漏水识别分割:27张小样本数据集的训练与优化实战 1. 27张图也能做分割先看清这个数据集的真实定位拿到公路隧道漏水识别分割数据集labelme格式27张1类别这个描述时我第一反应是这数据量也太小了。但仔细想想这恰恰是很多工程落地场景的真实起点——不是每个项目都有几万张标注图更多时候你手上只有几十张现场采集的样本还得先把流程跑通。这个数据集的核心价值不在于训练一个通用模型而在于验证技术路线是否可行。公路隧道漏水检测属于典型的工业缺陷检测场景它的难点和普通语义分割任务完全不同漏水区域边界模糊、形态不规则、和隧道壁面的对比度低而且实际采集时受光照、湿度、拍摄角度影响极大。27张图、1个类别漏水区域意味着这是一个二分类语义分割任务——背景和漏水区域。适合谁参考如果你是做基础设施巡检、隧道健康监测、或者工业缺陷检测方向的同学这个数据集可以帮你快速搭建一套从标注到训练的完整pipeline。如果你只是想学习labelme标注流程和分割数据集制作它也够用。但如果你指望用它训练出能直接上生产线的模型那得先解决数据量的问题——后面我会讲怎么用这27张图做数据增强和迁移学习。关键词里出现的labelme、分割数据集、漏水识别基本框定了这篇文章的范围标注工具的使用、分割数据集的制作规范、以及小样本场景下的训练策略。我下面会从数据集本身的结构讲起一路讲到怎么把这27张图用出最大价值。2. labelme标注文件的结构拆解与格式转换2.1 一个labelme JSON文件里到底存了什么很多人用labelme标完图就直接扔给训练脚本结果报错一堆。问题出在没搞清楚labelme的JSON结构。我用这个数据集里的一个典型文件给你拆开看{ version: 5.8.3, flags: {}, shapes: [ { label: leakage, points: [[120.5, 340.2], [125.8, 345.6], ...], group_id: null, shape_type: polygon, flags: {} } ], imagePath: tunnel_001.jpg, imageData: null, imageHeight: 1080, imageWidth: 1920 }关键字段就几个shapes里存的是每个漏水区域的多边形顶点坐标label是类别名这里只有leakage一个类别imagePath是原图文件名。注意imageData通常是null因为labelme默认不把图片base64编码进去——这是个好设计不然JSON文件会大到没法看。提示如果你拿到的数据集里imageData不是null说明标注时勾选了保存图像数据这会导致JSON体积暴增建议在转换脚本里忽略这个字段。2.2 从JSON到训练可用的掩码图labelme标出来的是多边形顶点但语义分割模型需要的是像素级掩码图mask。转换的核心逻辑是用多边形顶点在空白画布上填充出二值区域。我常用的转换脚本是这样的import json import numpy as np from PIL import Image, ImageDraw def json_to_mask(json_path, output_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) h, w data[imageHeight], data[imageWidth] mask Image.new(L, (w, h), 0) draw ImageDraw.Draw(mask) for shape in data[shapes]: if shape[shape_type] polygon: points [tuple(p) for p in shape[points]] draw.polygon(points, fill1) mask.save(output_path) # 批量处理 import os json_dir annotations/ mask_dir masks/ os.makedirs(mask_dir, exist_okTrue) for fname in os.listdir(json_dir): if fname.endswith(.json): json_to_mask( os.path.join(json_dir, fname), os.path.join(mask_dir, fname.replace(.json, .png)) )这段代码跑完你会得到和原图同尺寸的PNG掩码图漏水区域像素值为1背景为0。注意fill1而不是255因为后续训练时通常用0/1二值标签省得再归一化。2.3 27张图的标注质量检查清单小数据集最怕标注错误因为每一张的权重都很高。我建议在转换前先做一轮人工检查重点看这几个地方多边形是否闭合labelme里如果顶点没连上转换出来的mask会有缺口。检查方法很简单看JSON里points的首尾坐标是否接近。是否有重叠标注同一区域被标了两次转换后mask值会叠加虽然二值化后看不出来但说明标注时手抖了。边界是否贴合漏水区域边缘模糊标注时容易标大或标小。我的经验是宁大勿小因为后续可以用形态学操作收缩但标小了就丢信息了。图像尺寸是否一致如果27张图分辨率不统一训练前必须resize否则dataloader会报错。3. 小样本分割的训练策略27张图怎么用出270张的效果3.1 数据增强不是随便转个角度就行27张图直接训练模型必然过拟合。数据增强是必须的但分割任务的数据增强和分类任务完全不同——你不仅要变换图像还要同步变换mask而且几何变换必须严格对齐。我常用的增强组合是这样的基于albumentations库import albumentations as A transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.3), A.RandomRotate90(p0.5), A.ShiftScaleRotate(shift_limit0.1, scale_limit0.2, rotate_limit30, p0.7), A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.6), A.GaussNoise(var_limit(10.0, 50.0), p0.3), A.ElasticTransform(alpha1, sigma50, p0.2), ])重点说几个参数的选择理由ShiftScaleRotate的scale_limit0.2隧道漏水区域在画面中的占比变化不会太大缩放20%足够覆盖拍摄距离的波动。ElasticTransform这个对漏水检测特别有用因为漏水痕迹本身就是不规则的流状形态弹性变形能模拟不同渗流路径。RandomBrightnessContrast隧道内光照条件差异大这个增强能提升模型对曝光的鲁棒性。注意不要用Cutout或GridMask这类遮挡增强因为漏水区域本身就可能被管道、线缆遮挡再用遮挡增强会让模型学到错误的特征。3.2 迁移学习从通用分割模型借力27张图从零训练基本没戏必须用预训练权重。我的建议是用Cityscapes或COCO上预训练的DeepLabV3或U-Net然后只微调最后几层。具体操作上我通常冻结backbone的前几个stage只训练解码器和最后两个stage。学习率设小一点比如1e-4因为预训练权重已经学到了通用的边缘、纹理特征微调时不需要大改。如果你用的是MMSegmentation框架配置文件里这样改model dict( pretrainedopen-mmlab://resnet50_v1c, backbonedict( typeResNetV1c, depth50, frozen_stages2, # 冻结前两个stage ), decode_headdict( typeFCNHead, in_channels2048, channels256, num_classes2, # 背景漏水 ), ) optimizer dict(typeSGD, lr1e-4, momentum0.9, weight_decay0.0005)frozen_stages2的意思是前两个stage的权重不更新这样可训练参数少了很多27张图也能跑得动。3.3 损失函数的选择Dice Loss为什么比CrossEntropy更适合漏水区域在整张图里占比很小可能只有5%到10%的像素。这种情况下用CrossEntropy Loss模型会倾向于全部预测为背景因为这样loss也能降到很低。Dice Loss直接优化预测区域和真实区域的重叠度对类别不平衡不敏感。我通常用Dice Loss CrossEntropy的组合前者负责拉高重叠度后者负责稳定训练class CombinedLoss(nn.Module): def __init__(self, weight_ce0.5, weight_dice0.5): super().__init__() self.ce nn.CrossEntropyLoss() self.weight_ce weight_ce self.weight_dice weight_dice def forward(self, pred, target): ce_loss self.ce(pred, target) pred_soft torch.softmax(pred, dim1)[:, 1] target_float target.float() intersection (pred_soft * target_float).sum() dice_loss 1 - (2 * intersection 1e-6) / (pred_soft.sum() target_float.sum() 1e-6) return self.weight_ce * ce_loss self.weight_dice * dice_loss实测下来这种组合在27张图的训练集上能把IoU从0.3左右拉到0.6以上效果比单用CrossEntropy好很多。4. 从标注到部署labelme使用中的那些坑4.1 labelme安装报错pyqt5-sip的根因关键词里出现了labelme 无法安装 pyqt5和labelme error pyqt5-sip这几乎是每个新手都会遇到的问题。根本原因是PyQt5和sip的版本不兼容尤其是在Python 3.9以上的环境里。我的解决方案是用conda建独立环境不要混用pip和condaconda create -n labelme python3.8 conda activate labelme pip install labelme5.8.3为什么指定Python 3.8因为labelme 5.8.3对Python 3.9的支持不太稳定3.8是经过大量验证的版本。如果还是报sip相关的错手动装一下pip install pyqt55.15.4 pip install pyqt5-sip12.9.0 pip install labelme5.8.3提示如果你在国内pip安装慢的话可以用清华镜像但注意镜像同步有延迟有时候最新版labelme还没同步过去这时候指定版本号反而更稳。4.2 标注效率提升快捷键和自动保存27张图听起来不多但如果你一张一张手动点多边形顶点标完也得大半天。labelme有几个快捷键必须掌握CtrlN下一张图CtrlS保存当前标注CtrlZ撤销上一个点Ctrl鼠标滚轮缩放图像空格键临时切换到手型工具方便拖动画布另外labelme默认不会自动保存标完一张一定要按CtrlS。我吃过亏标了20多个点没保存切到下一张图全没了。还有一个技巧先用大致的多边形框出漏水区域再逐个调整顶点。不要一开始就追求精确先把区域圈出来再放大微调边界。这样比直接精确标注快至少一倍。4.3 中文路径和中文标签的坑labelme对中文路径的支持一直有问题如果图片放在中文目录下可能会报编码错误。我的建议是全程用英文路径包括图片目录、JSON输出目录、以及标签名。标签名也尽量用英文比如leakage而不是漏水。虽然labelme本身支持中文标签但后续转换脚本和训练框架对中文的处理不一定一致容易出乱码。如果你拿到的数据集里标签已经是中文了转换时做个映射label_map {漏水: 1, 背景: 0}5. 漏水区域的分割后处理让结果更可用5.1 形态学操作去除小噪点模型输出的mask往往有一些零散的小区域这些不是真正的漏水而是误检。用形态学开运算先腐蚀后膨胀可以去掉这些小噪点import cv2 import numpy as np mask cv2.imread(pred_mask.png, 0) kernel np.ones((5, 5), np.uint8) cleaned cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)kernel大小根据图像分辨率调整1080p的图用5x5比较合适4K图可以用7x7或9x9。5.2 连通域分析过滤误检开运算之后可能还有大块的误检区域这时候用连通域分析只保留面积超过阈值的区域num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(cleaned, connectivity8) min_area 500 # 最小面积阈值根据实际场景调整 final_mask np.zeros_like(cleaned) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: final_mask[labels i] 1min_area怎么定我的经验是取训练集中最小漏水区域面积的1/2。比如你标注的漏水区域最小是1000像素那min_area设500比较合理既能过滤噪点又不会漏掉真实的小漏水。5.3 边缘平滑让分割结果更贴合实际漏水区域的边缘通常是渐变的模型输出的mask边缘会有锯齿。用高斯模糊阈值化的方式可以让边缘更自然blurred cv2.GaussianBlur(final_mask.astype(np.float32), (7, 7), 0) smoothed (blurred 0.5).astype(np.uint8)这一步在可视化展示时特别有用锯齿状的边缘看起来很不专业平滑之后视觉效果提升明显。6. 27张图之后数据集扩展的可行路径6.1 半自动标注用训练好的模型预标注当你用27张图训练出一个初步模型后可以用它来预标注新的图片然后人工修正。这样标注效率能提升3到5倍。具体流程是模型预测mask - 把mask转成labelme的JSON格式 - 人工在labelme里微调 - 保存为新标注。转换脚本的核心是把二值mask的轮廓提取出来转成多边形顶点import cv2 import json def mask_to_labelme_json(mask_path, image_path, output_path, labelleakage): mask cv2.imread(mask_path, 0) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) shapes [] for contour in contours: if cv2.contourArea(contour) 100: continue epsilon 0.002 * cv2.arcLength(contour, True) approx cv2.approxPolyDP(contour, epsilon, True) points approx.reshape(-1, 2).tolist() shapes.append({ label: label, points: points, shape_type: polygon, flags: {} }) img cv2.imread(image_path) h, w img.shape[:2] data { version: 5.8.3, flags: {}, shapes: shapes, imagePath: image_path.split(/)[-1], imageData: None, imageHeight: h, imageWidth: w } with open(output_path, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)approxPolyDP的epsilon参数控制多边形简化程度0.002是个比较平衡的值既能减少顶点数量又不会丢失太多细节。6.2 从视频中抽帧扩充数据隧道巡检通常有视频记录从视频里抽帧是扩充数据集的低成本方式。但要注意不要抽连续帧因为连续帧之间差异太小对训练帮助不大。我的做法是每隔30帧抽一帧这样既能保证多样性又不会引入太多冗余。抽帧之后还要做去重用感知哈希pHash过滤掉相似度太高的帧import imagehash from PIL import Image def is_duplicate(img_path1, img_path2, threshold5): hash1 imagehash.phash(Image.open(img_path1)) hash2 imagehash.phash(Image.open(img_path2)) return abs(hash1 - hash2) threshold6.3 合成数据用纹理贴图生成漏水样本如果实在收集不到足够的真实数据可以考虑合成。思路是拍一张干净的隧道壁面作为背景然后把漏水纹理贴上去同时生成对应的mask。漏水纹理可以从现有的27张图里裁剪出来用泊松融合Poisson Blending贴到新背景上这样边缘过渡会比较自然。OpenCV的seamlessClone就能做import cv2 import numpy as np background cv2.imread(clean_wall.jpg) leak_texture cv2.imread(leak_patch.png) mask cv2.imread(leak_mask.png, 0) center (background.shape[1]//2, background.shape[0]//2) result cv2.seamlessClone(leak_texture, background, mask, center, cv2.NORMAL_CLONE)合成数据的质量取决于纹理的多样性和融合的自然度建议合成后人工筛选一遍去掉明显不真实的样本。7. 评估指标与模型选择的实际考量7.1 IoU和Dice哪个更能反映漏水检测的效果IoU交并比和Dice系数是分割任务最常用的两个指标它们高度相关但侧重点不同。IoU对预测区域和真实区域的并集更敏感Dice对重叠部分更敏感。在漏水检测场景下我更关注Dice系数因为漏水区域的边界本身就有模糊性IoU对边界偏差的惩罚过重。实际项目中Dice达到0.7以上就可以考虑部署了0.8以上算优秀。但要注意27张图的验证集可能只有5到6张指标波动会很大。我的建议是用交叉验证把27张图分成5折每折轮流做验证取平均指标。这样虽然训练次数多了但评估结果更可靠。7.2 推理速度隧道巡检场景的硬约束隧道巡检通常是车载或手持设备推理速度直接影响作业效率。DeepLabV3的推理速度在1080p图像上大概200ms左右GPU如果换成MobileNetV3作为backbone能降到50ms以内但精度会下降3到5个点。我的建议是先用大模型跑通流程再根据实际部署环境做轻量化。如果最终要部署到边缘设备可以考虑知识蒸馏用大模型的输出作为软标签训练一个小模型。7.3 可视化让非技术人员也能看懂结果分割结果最终要给巡检人员看所以可视化很重要。我通常会把原图、真实mask、预测mask叠在一起展示用不同颜色区分绿色预测正确TP红色误检FP蓝色漏检FN这样一眼就能看出模型的问题在哪里。如果红色多说明模型太激进如果蓝色多说明模型太保守。def visualize_result(image, gt_mask, pred_mask): overlay image.copy() overlay[gt_mask 1] [0, 255, 0] # 真实区域绿色 overlay[pred_mask 1] [0, 0, 255] # 预测区域红色 overlay[(gt_mask 1) (pred_mask 1)] [255, 255, 0] # 重叠区域黄色 return overlay这种可视化方式在项目汇报时特别有用非技术人员也能快速理解模型的表现。8. 我在小样本分割项目中的几条实战心得第一不要迷信数据量。27张图确实少但如果标注质量高、增强策略合理、迁移学习到位跑出一个能用的baseline完全没问题。我见过太多人卡在数据不够的心理障碍上迟迟不动手。第二标注质量比数量重要十倍。27张图里如果有3张标错了模型学到的就是错误特征。我建议在标注完成后至少花半天时间做一轮交叉检查让另一个人重新标一遍对比差异。第三先跑通再优化。不要一上来就调参、换模型、改损失函数。先用最简单的U-Net加CrossEntropy跑一遍看看baseline在哪里然后再有针对性地改进。我见过有人花了两周调参结果发现是数据加载的归一化写错了。第四保存每一次实验的配置和结果。小样本训练的不确定性很大同样的代码跑两次结果可能差很多。用MLflow或TensorBoard记录每次实验的超参数和指标方便回溯。第五部署时留足后处理空间。模型输出的mask不可能完美形态学操作、连通域过滤、边缘平滑这些后处理步骤往往能把可用性提升一个档次。不要指望模型端到端输出完美结果。最后说一个容易被忽略的点隧道漏水检测的最终目的是指导维修所以分割结果不仅要准还要能计算出漏水面积、位置、严重程度这些量化指标。我在项目里通常会加一个后处理模块把mask转换成结构化的检测报告包括漏水区域的数量、总面积、最大连通区域面积等。这些信息对维修决策的价值远大于一张分割mask图。
返回列表