☰
基于labelme的公路隧道漏水识别分割:27张小样本数据集的训练与优化实战
2026/9/29 7:52:26 网站建设 项目流程

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', encoding='utf-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, fill=1) mask.save(output_path) # 批量处理 import os json_dir = 'annotations/' mask_dir = 'masks/' os.makedirs(mask_dir, exist_ok=True) 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。注意fill=1而不是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(p=0.5), A.VerticalFlip(p=0.3), A.RandomRotate90(p=0.5), A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2, rotate_limit=30, p=0.7), A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.6), A.GaussNoise(var_limit=(10.0, 50.0), p=0.3), A.ElasticTransform(alpha=1, sigma=50, p=0.2), ])

重点说几个参数的选择理由:

  • ShiftScaleRotate的scale_limit=0.2:隧道漏水区域在画面中的占比变化不会太大,缩放20%足够覆盖拍摄距离的波动。
  • ElasticTransform:这个对漏水检测特别有用,因为漏水痕迹本身就是不规则的流状形态,弹性变形能模拟不同渗流路径。
  • RandomBrightnessContrast:隧道内光照条件差异大,这个增强能提升模型对曝光的鲁棒性。

注意:不要用Cutout或GridMask这类遮挡增强,因为漏水区域本身就可能被管道、线缆遮挡,再用遮挡增强会让模型学到错误的特征。

3.2 迁移学习:从通用分割模型借力

27张图从零训练基本没戏,必须用预训练权重。我的建议是用Cityscapes或COCO上预训练的DeepLabV3+或U-Net,然后只微调最后几层。

具体操作上,我通常冻结backbone的前几个stage,只训练解码器和最后两个stage。学习率设小一点,比如1e-4,因为预训练权重已经学到了通用的边缘、纹理特征,微调时不需要大改。

如果你用的是MMSegmentation框架,配置文件里这样改:

model = dict( pretrained='open-mmlab://resnet50_v1c', backbone=dict( type='ResNetV1c', depth=50, frozen_stages=2, # 冻结前两个stage ), decode_head=dict( type='FCNHead', in_channels=2048, channels=256, num_classes=2, # 背景+漏水 ), ) optimizer = dict(type='SGD', lr=1e-4, momentum=0.9, weight_decay=0.0005)

frozen_stages=2的意思是前两个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_ce=0.5, weight_dice=0.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, dim=1)[:, 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和conda:

conda create -n labelme python=3.8 conda activate labelme pip install labelme==5.8.3

为什么指定Python 3.8?因为labelme 5.8.3对Python 3.9+的支持不太稳定,3.8是经过大量验证的版本。如果还是报sip相关的错,手动装一下:

pip install pyqt5==5.15.4 pip install pyqt5-sip==12.9.0 pip install labelme==5.8.3

提示:如果你在国内,pip安装慢的话可以用清华镜像,但注意镜像同步有延迟,有时候最新版labelme还没同步过去,这时候指定版本号反而更稳。

4.2 标注效率提升:快捷键和自动保存

27张图听起来不多,但如果你一张一张手动点多边形顶点,标完也得大半天。labelme有几个快捷键必须掌握:

  • Ctrl+N:下一张图
  • Ctrl+S:保存当前标注
  • Ctrl+Z:撤销上一个点
  • Ctrl+鼠标滚轮:缩放图像
  • 空格键:临时切换到手型工具,方便拖动画布

另外,labelme默认不会自动保存,标完一张一定要按Ctrl+S。我吃过亏,标了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, connectivity=8) 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] = 1

min_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, label='leakage'): 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', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)

approxPolyDP的epsilon参数控制多边形简化程度,0.002是个比较平衡的值,既能减少顶点数量,又不会丢失太多细节。

6.2 从视频中抽帧扩充数据

隧道巡检通常有视频记录,从视频里抽帧是扩充数据集的低成本方式。但要注意不要抽连续帧,因为连续帧之间差异太小,对训练帮助不大。我的做法是每隔30帧抽一帧,这样既能保证多样性,又不会引入太多冗余。

抽帧之后还要做去重,用感知哈希(pHash)过滤掉相似度太高的帧:

import imagehash from PIL import Image def is_duplicate(img_path1, img_path2, threshold=5): hash1 = imagehash.phash(Image.open(img_path1)) hash2 = imagehash.phash(Image.open(img_path2)) return abs(hash1 - hash2) < threshold

6.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图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询