简介:这份资源是面向计算机视觉学习者、铁路智能监控方向研究者与AI应用开发者的无人机铁路障碍物图像分割系统完整项目包,融合图像识别、人工智能与机器学习技术,用于从复杂铁路背景中分离石块、树木、动物等潜在障碍物,为自动化铁路安全监控提供数据支撑。压缩包共25个文件,约4.19MB,包含19张png图像样本、4个py脚本(涵盖训练、验证、预测与界面交互)、1份md说明及1份docx文档,结构清晰,便于直接运行与二次开发。已有38人学习关注。读者可从中获取基于卷积神经网络的图像分割实现思路、模型训练与推理脚本、可视化界面代码以及配套数据集样例,适合作为课程设计、毕业设计或铁路巡检项目的参考方案,帮助快速理解从数据采集、模型训练到障碍物定位分类的完整流程,并在此基础上调整网络结构与参数以提升分割精度。
1. 无人机铁路障碍物图像分割系统:从航拍到掩膜的落地拆解
铁路巡检场景里,无人机航拍回来的图像往往堆成几百上千张,靠人眼一张张找轨道上的落石、异物、行人、施工机具,效率低还容易漏。无人机铁路障碍物图像分割系统要解决的就是这件事:把航拍图喂进去,自动输出每个障碍物的像素级掩膜,告诉你「哪里有什么、占多大面积」。它和普通目标检测的区别在于,检测只给框,分割给的是精确轮廓,这对判断障碍物是否侵入限界、面积是否达到报警阈值很关键。适合两类人:一类是做铁路智能巡检的算法工程师,想快速搭一套能跑的分割流水线;另一类是有无人机航拍数据、想验证图像分割算法在自己场景下效果的从业者。下面按「数据怎么准备 → 模型怎么选和训 → 推理怎么落地 → 坑在哪」这条线讲透。
2. 数据准备:铁路障碍物数据集怎么攒、怎么标、怎么转格式
2.1 铁路场景的数据从哪来,为什么不能直接拿公开数据集凑
铁路障碍物分割最卡脖子的不是模型,是数据。公开的无人机识别数据集、无人机航拍工地数据公开资源里,铁路场景占比很低,直接拿来训会出现「模型在公路和工地上表现不错,一到铁轨就翻车」的情况。常见做法是三条腿走路:一是自己飞,沿线路按固定高度和重叠率采集;二是找铁路扣件缺陷数据集这类同域数据做预训练或扩充;三是用少量真实铁路图做微调。
自己采集时几个参数要定死:飞行高度建议 30~60 米,太低单张覆盖范围小、效率低,太高障碍物像素太少、分割边缘糊;重叠率航向 70%、旁向 60% 以上,方便后续拼接和样本增广;相机尽量用带机械快门的,避免卷帘快门在高速飞行时把轨道拉成斜线。光照上要覆盖顺光、逆光、阴天三种,否则模型一到逆光就把轨道阴影当成障碍物。
标注环节,分割要求的是多边形掩膜,不是框。工具用 LabelMe 或 CVAT 都行,类别建议先收敛到 5~8 类:落石、异物(塑料布、树枝)、行人、施工机具、动物,其余归为背景。类别太多而每类样本又少,是新手最容易踩的坑,后面训练时类别不平衡会直接把小类吃掉。
2.2 从 LabelMe 到训练格式:转换脚本与四个边界坑
LabelMe 存的是 JSON,训练要的是掩膜 PNG 或 COCO 格式。下面这个脚本把 LabelMe 的 JSON 批量转成二值掩膜,每个类别一张图,方便后续按类别组装。
import json import os import numpy as np from PIL import Image, ImageDraw # 类别到索引的映射,背景固定为 0 CLASS_MAP = {"rock": 1, "debris": 2, "person": 3, "machine": 4, "animal": 5} def json_to_mask(json_path, out_dir, img_shape): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) # 初始化单通道掩膜,0 表示背景 mask = Image.new("L", (img_shape[1], img_shape[0]), 0) draw = ImageDraw.Draw(mask) for shape in data["shapes"]: label = shape["label"] if label not in CLASS_MAP: continue # 未定义类别直接跳过,避免污染标签 pts = [tuple(p) for p in shape["points"]] # 多边形填充,注意 points 是 [x, y] 顺序 draw.polygon(pts, fill=CLASS_MAP[label]) name = os.path.splitext(os.path.basename(json_path))[0] mask.save(os.path.join(out_dir, name + ".png")) if __name__ == "__main__": src = "./labelme_json" dst = "./masks" os.makedirs(dst, exist_ok=True) # 假设图像统一为 1920x1080,实际按你的原图尺寸改 for fn in os.listdir(src): if fn.endswith(".json"): json_to_mask(os.path.join(src, fn), dst, (1080, 1920))逻辑说明:脚本遍历 JSON,按shapes里的多边形逐类填充到单通道掩膜,像素值就是类别索引。参数上CLASS_MAP必须和训练配置里的类别顺序完全一致,差一位标签就全错。img_shape传的是 (高, 宽),和 PIL 的 (宽, 高) 顺序相反,这是第一个坑。
四个边界坑:一是多边形自相交,LabelMe 允许画出自交多边形,draw.polygon填充结果会出空洞,标注时要提醒标注员别画交叉;二是点坐标越界,飞手裁图后坐标可能超出图像范围,转换前要 clamp;三是类别名大小写不一致,Rock和rock会被当成两类;四是空 JSON,有些图没标任何目标,转换出来是全 0 掩膜,训练时如果全 0 样本太多,模型会倾向预测背景,建议按比例下采样。
2.3 数据增广:铁路场景该加什么、不该加什么
增广不是越多越好。铁路场景里,水平翻转可以用,因为轨道左右对称;垂直翻转慎用,天空和地面翻转后不符合物理;随机旋转小角度(±10°)可以模拟无人机姿态抖动;色彩抖动要克制,过度调色会让轨道锈色和落石灰混淆。真正有用的是运动模糊和亮度渐变,模拟飞行抖动和隧道口进出光。别加马赛克、Cutout 这类遮挡增广,障碍物本身就可能被部分遮挡,再加遮挡会让小目标彻底消失。
3. 模型选型与训练:分割网络怎么挑、参数怎么设
3.1 语义分割还是实例分割,铁路障碍物该选哪个
这是选型第一问。语义分割只告诉你「这个像素是落石」,不区分是哪一块落石;实例分割会给出「落石 1、落石 2」。铁路巡检里,如果只是判断轨道上有没有异物、面积多大,语义分割够用,模型轻、推理快;如果要统计「有几块落石、每块多大」,就得上实例分割。
从业常见做法:先上语义分割做基线,用 DeepLabV3+ 或 SegFormer,backbone 选轻量的 MobileNetV3 或 MiT-B0,能在机载或边缘盒子上跑到实时。等基线跑通、数据积累够了,再换 Mask2Former 或 YOLOv8-seg 做实例分割。别一上来就怼大模型,铁路数据量通常撑不起,过拟合是必然。
3.2 用 SegFormer 训一版基线:配置与关键参数
下面给一个基于 HuggingFace 的 SegFormer 训练骨架,重点是几个必须调的参数。
import torch from transformers import SegformerForSemanticSegmentation, SegformerImageProcessor from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np class RailSegDataset(Dataset): def __init__(self, img_dir, mask_dir, processor, size=(512, 512)): self.img_dir = img_dir self.mask_dir = mask_dir self.processor = processor self.size = size self.names = [n for n in os.listdir(img_dir) if n.endswith(".jpg")] def __len__(self): return len(self.names) def __getitem__(self, idx): name = self.names[idx] img = Image.open(os.path.join(self.img_dir, name)).convert("RGB") mask = Image.open(os.path.join(self.mask_dir, name.replace(".jpg", ".png"))) img = img.resize(self.size) mask = mask.resize(self.size, Image.NEAREST) # 掩膜必须最近邻,否则类别值被插值污染 encoding = self.processor(images=img, return_tensors="pt") pixel_values = encoding["pixel_values"].squeeze(0) return pixel_values, torch.tensor(np.array(mask), dtype=torch.long) # 6 类:背景 + 5 个障碍物类别 model = SegformerForSemanticSegmentation.from_pretrained( "nvidia/segformer-b0-finetuned-ade-512-512", num_labels=6, ignore_mismatched_sizes=True, ) processor = SegformerImageProcessor(do_resize=False, do_normalize=True) loader = DataLoader(RailSegDataset("./images", "./masks", processor), batch_size=4, shuffle=True) optimizer = torch.optim.AdamW(model.parameters(), lr=6e-5, weight_decay=0.01)逻辑说明:num_labels=6对应背景加 5 类,必须和掩膜里的像素值范围一致。do_resize=False是因为我们在 Dataset 里已经 resize 了,避免二次缩放。掩膜 resize 用NEAREST,用双线性会把类别 1 和 2 插值成 1.5,标签直接废掉,这是血泪经验。
关键参数:学习率 6e-5 是 SegFormer 微调的常用起点,太大前期 loss 震荡,太小收敛慢;batch size 受显存限制,4 或 8 都行,小了就开梯度累积;ignore_mismatched_sizes=True是因为换了分类头,不加会报错。训练轮数先跑 40~60 epoch,看验证集 mIoU 曲线,涨不动就停。
3.3 损失函数与类别不平衡:小目标被吃掉怎么救
铁路障碍物里,落石、异物这类小目标像素占比可能不到 1%,交叉熵会被背景主导。常见做法是交叉熵加 Dice 的混合损失,Dice 对小目标更敏感。
def dice_loss(pred, target, num_classes=6, eps=1e-6): # pred: [B, C, H, W] logits, target: [B, H, W] pred = torch.softmax(pred, dim=1) target_onehot = torch.nn.functional.one_hot(target, num_classes).permute(0, 3, 1, 2).float() intersection = (pred * target_onehot).sum(dim=(2, 3)) union = pred.sum(dim=(2, 3)) + target_onehot.sum(dim=(2, 3)) dice = (2 * intersection + eps) / (union + eps) return 1 - dice.mean() # 总损失 = 0.5 * CE + 0.5 * Dice逻辑说明:Dice 按类别算交并比再平均,小类即使像素少,只要预测对了,Dice 贡献也明显。权重 0.5/0.5 是起点,如果小类还是差,把 Dice 权重提到 0.7。另一个手段是给交叉熵加类别权重,按像素频率的倒数设,但别设太极端,否则背景预测崩了,整图全是障碍物。
4. 推理与部署:从单张图到整条线路的批量处理
4.1 单张推理与掩膜后处理
训练完要落地,第一步是单张推理跑通。SegFormer 输出的是低分辨率 logits,要上采样回原图尺寸。
import torch.nn.functional as F def infer(model, processor, img_path, device="cuda"): model.eval().to(device) img = Image.open(img_path).convert("RGB") inputs = processor(images=img, return_tensors="pt").to(device) with torch.no_grad(): logits = model(**inputs).logits # [1, C, h, w] # 上采样回原图大小 up = F.interpolate(logits, size=img.size[::-1], mode="bilinear", align_corners=False) pred = up.argmax(dim=1).squeeze(0).cpu().numpy() return pred # 每个像素是类别索引逻辑说明:F.interpolate的size要传 (高, 宽),img.size是 (宽, 高),所以用[::-1]反转。mode="bilinear"对 logits 插值没问题,因为最后是 argmax,插值的是概率不是标签。后处理阶段可以做连通域分析,把小于 50 像素的预测块滤掉,减少噪点误报。
4.2 批量处理整条线路:分块推理与拼接
航拍图动辄 5000×4000,直接塞进网络显存扛不住,而且小目标缩得太小。常见做法是滑窗分块推理再拼回去。
def sliding_infer(model, processor, img, patch=512, stride=384, device="cuda"): W, H = img.size full = np.zeros((H, W), dtype=np.uint8) for y in range(0, H, stride): for x in range(0, W, stride): box = (x, y, min(x + patch, W), min(y + patch, H)) crop = img.crop(box) pred = infer(model, processor, crop, device) if crop.size == (patch, patch) else None if pred is None: continue full[y:y+pred.shape[0], x:x+pred.shape[1]] = pred return full逻辑说明:stride小于patch是为了重叠,避免边界处目标被切断。重叠区域后写的会覆盖先写的,简单但够用;要更精细可以做投票。参数上patch=512匹配训练尺寸,stride=384是 75% 重叠,兼顾速度和边界完整性。边缘不足一块的 crop 直接跳过,实际工程里会做 padding 补齐。
4.3 报警阈值怎么定:面积、位置、置信度三要素
分割出掩膜只是第一步,铁路真正要的是报警。三个维度:面积占比,障碍物像素占轨道区域的比例,超过阈值报警;位置,是否落在轨道中心线一定范围内,侵入限界才报;置信度,模型输出的类别概率均值,太低的不报。阈值没有万能值,要在验证集上按误报率和漏报率权衡,通常先保证漏报率低于 1%,再压误报。
5. 避坑与排查:铁路分割落地最常见的五个翻车点
5.1 现象:模型在验证集 mIoU 很高,上线后满屏误报
原因:验证集和实际航拍数据分布不一致,最常见的是高度和光照差异。训练用的是 50 米顺光,上线飞的是 80 米逆光,尺度变了、对比度变了,模型直接懵。
解决:训练集必须覆盖实际上线的高度和光照区间,做不到就做测试时增广,推理时对同一张图做多尺度、多亮度推理再融合。另外验证集要按飞行架次划分,不能随机划分,否则同一架次的相似图同时进训练和验证,指标虚高。
5.2 现象:小目标(远处落石)分割边缘糊成一团
原因:下采样倍数太高,SegFormer 的 1/4 特征图对小于 8 像素的目标已经丢信息了。
解决:训练时用更高的输入分辨率,或者用带高分辨率分支的网络;推理时滑窗 patch 别太大,512 比 1024 对小目标更友好。另一个手段是在损失里加边界损失,专门惩罚边缘预测错。
5.3 现象:轨道被预测成障碍物,或者障碍物被轨道吃掉
原因:轨道和某些障碍物(比如灰色落石、锈色机具)颜色接近,模型分不清。
解决:标注时把轨道单独设一类,让模型显式学轨道特征;数据里补充轨道被遮挡、被阴影覆盖的样本;后处理阶段用轨道先验,比如轨道区域内的预测才保留,区域外降权。
5.4 现象:训练 loss 正常下降,但验证集 mIoU 卡在 0.3 不动
原因:类别极度不平衡,背景占 95% 以上,模型学会了全预测背景就能拿到很低的 loss。
解决:换 Dice 或 Focal 损失;对背景像素下采样;检查掩膜里小类像素总数,如果某类总像素不到几千,考虑合并类别或补数据。别硬训,数据不够模型再大也没用。
5.5 现象:推理速度慢,边缘设备跑不动
原因:模型太大,或者滑窗 stride 太小导致重复计算太多。
解决:换轻量 backbone,或者对模型做量化(INT8);滑窗 stride 从 0.5 提到 0.75;批处理时把多块拼成一个 batch 送进去,比逐块推理快很多。实测 batch=8 比 batch=1 在边缘盒子上能快 2~3 倍。
6. 进阶技巧:用伪标签和主动学习把标注成本压下来
铁路数据标注贵,一张图标 5 分钟,一千张就是 80 多个小时。我一般用伪标签加主动学习这套组合拳:先用少量标注数据训一版基线,拿它去推理未标注数据,把高置信度的预测当伪标签,人工只复核低置信度和模型分歧大的样本。这样标注量能压到原来的三分之一。
具体操作上,置信度阈值别设太高,0.7 就行,太高伪标签太少没意义;复核时优先看模型熵高的区域,那些是模型最不确定的地方,标一张顶十张。伪标签训练时给伪标签样本降权,比如损失乘 0.5,避免错误标签带偏模型。迭代两三轮,mIoU 通常能涨 5~10 个点。
验证方法上,别只看 mIoU,铁路场景更该看的是「漏报率」和「误报率」这两个业务指标。我习惯在验证集上按障碍物面积分档统计,小目标漏报率单独看,因为小目标才是铁路最危险的。最后留一句我的习惯:每次换数据源或换飞行参数,先拿 50 张新数据跑一遍推理,肉眼过一遍再决定要不要重训,别等上线了才发现模型不认识新场景。希望帮到你。
本文还有配套的精品资源,点击获取