简介这份资源面向计算机视觉与自动驾驶方向的学习者与开发者围绕「如何用深度学习完成人行道检测」这一实际问题展开。内容涵盖卷积神经网络特征提取、VGG/ResNet/YOLO/Faster R-CNN等预训练模型的迁移学习、COCO与PASCAL VOC格式的数据集标注、基于TensorFlow/Keras/PyTorch的训练流程以及Focal Loss、Smooth L1 Loss与Adam等损失函数和优化器的选择并涉及mAP、召回率等评估指标与调优思路。压缩包共32个文件约33.95MB包含6个Python源码、8个xml标注文件、7篇pdf参考文献、2个html页面及md说明文档等源码、数据与文献相互配套便于对照复现。目前已有923人学习下载适合希望从零搭建检测模型或借助SidewalkDetection项目理解完整工程链路的中级读者参考。1. 人行道检测为什么它比车道线检测更容易翻车做自动驾驶感知的同行大多有个共识车道线检测已经卷到能发顶会了但人行道检测至今仍是很多量产方案里的短板。原因不复杂——车道线是结构化、高对比度、有强先验的细长目标而人行道是弱纹理、边界模糊、被树荫和积水反复切割的语义区域。你在城区跑一圈就会发现同一段人行道在上午十点和下午四点可能是两个完全不同的颜色分布模型上午学到的特征下午就失效了。人行道检测sidewalk detection在深度学习语境下本质是一个二分类或三分类的语义分割任务把图像像素划分为人行道、非人行道有时加一类不确定。它服务的下游任务很明确——可行驶区域判定、盲道与无障碍通行评估、机器人配送路径规划、市政巡检里的占道识别。适合谁做做移动机器人导航的、做低速无人配送车的、做街景语义理解的以及想拿一个看起来简单但坑很深的任务练语义分割工程能力的开发者。这一篇不讲论文综述只讲我踩过的坑和能直接复现的路径。2. 从任务定义到数据人行道检测的选型逻辑2.1 语义分割、实例分割还是边缘检测先想清楚输出给谁用很多人一上来就纠结用 DeepLab 还是 SegFormer其实更该先问下游要的是这块区域能不能走还是人行道的精确边界在哪。如果是前者语义分割输出一张概率图阈值化后取连通域就够如果是后者比如要做路沿石识别或高精度地图标注边缘检测或实例分割更合适。我一般按这个顺序决策下游需求推荐输出形式典型损失函数备注可通行区域判定二值语义分割BCE Dice最快落地标注成本最低边界/路沿提取边缘概率图加权 BCE需要额外边缘标注多类路面人行道/盲道/非机动车道多类语义分割CE Dice类别不平衡要处理单条人行道实例计数实例分割Mask R-CNN 系列标注成本高慎选选型理由很实际语义分割的标注是逐像素涂色一个人一天能标 20 到 40 张实例分割要描多边形同样时间只能标 5 到 10 张。人行道这种大面积连续区域实例分割的收益远不如语义分割。2.2 公开数据集怎么挑以及自采数据的三个硬指标公开数据集里Cityscapes 有人行道类别类别 id 为 1但它的标注偏欧洲城市树荫遮挡少、路面干净直接拿来训国内场景会明显掉点。Mapillary Vistas 覆盖更杂但人行道边界标注一致性一般。我的做法是用 Cityscapes 做预训练权重初始化再用自采数据微调。自采数据必须盯住三个硬指标缺一个后面都要返工分辨率下限短边不低于 720p。人行道边界在低分辨率下会糊成一团模型学不到有效梯度。光照覆盖同一路段至少覆盖晴天正午、阴天、逆光傍晚三个时段。只采一种光照模型就是晴天模型。遮挡比例树荫、停车、积水、施工围挡这四类遮挡每类至少占总样本的 5%。低于这个比例模型遇到遮挡就瞎猜。常见做法是用行车记录仪或手机固定在推车上以 5 到 10 帧每秒抽帧避免连续帧高度冗余。抽帧后按 8:1:1 划分训练、验证、测试且必须按路段划分而不是按帧随机划分——按帧随机划分会导致同一路段的相邻帧同时出现在训练和测试集指标虚高得离谱这是血泪经验。2.3 标注规范边界像素到底算谁的人行道标注最容易扯皮的是边界。我的规范是以路沿石内侧为界路沿石本身标为非人行道树荫遮挡下如果人眼能推断出人行道延续就继续标为人行道并在标注工具里打一个遮挡标签位积水覆盖区域如果能看到水下是铺装路面标为人行道。标注工具用 labelme 或 CVAT 都行导出为 PNG 掩码像素值 0 为非人行道、1 为人行道、255 为忽略区域边界模糊、无法判断。忽略区域在损失里不参与计算能显著减少标注噪声带来的训练震荡。3. 用轻量语义分割模型跑通最小闭环3.1 环境与数据目录约定先约定目录结构后面所有脚本都基于它sidewalk_det/ ├── data/ │ ├── images/ # 原图 jpg │ ├── masks/ # 掩码 png像素值 0/1/255 │ ├── train.txt # 每行一个样本名不含扩展名 │ ├── val.txt │ └── test.txt ├── configs/ │ └── base.yaml └── train.py环境用 PyTorch 2.x 加 segmentation-models-pytorch一条命令装齐pip install torch torchvision segmentation-models-pytorch albumentations opencv-python pyyaml选 segmentation-models-pytorch 的理由是它把编码器ResNet、EfficientNet、MiT和解码器Unet、FPN、DeepLabV3解耦换骨干只改一个字符串做消融实验特别省事。3.2 数据集类与增强别让增强把边界搞没了import cv2 import numpy as np import torch from torch.utils.data import Dataset import albumentations as A class SidewalkDataset(Dataset): def __init__(self, root, split_file, img_size512, trainTrue): self.root root self.names [l.strip() for l in open(split_file) if l.strip()] self.img_size img_size self.train train # 训练增强几何变换用最近邻插值保护掩码边界 self.tf_train A.Compose([ A.RandomResizedCrop(size(img_size, img_size), scale(0.5, 1.0)), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.7), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit20, p0.5), A.MotionBlur(blur_limit5, p0.2), ]) # 验证只做 resize保证指标可比 self.tf_val A.Compose([A.Resize(img_size, img_size)]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img cv2.imread(f{self.root}/images/{name}.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(f{self.root}/masks/{name}.png, cv2.IMREAD_GRAYSCALE) tf self.tf_train if self.train else self.tf_val out tf(imageimg, maskmask) img, mask out[image], out[mask] img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 # 255 忽略区域映射为 255训练时单独处理 mask torch.from_numpy(mask).long() return img, mask逻辑说明RandomResizedCrop 的 scale 下限设 0.5是为了模拟远处人行道在画面中占比很小的情形这对模型尺度鲁棒性帮助很大。MotionBlur 模拟运动模糊占比控制在 0.2太多会让边界梯度消失。掩码的 255 忽略值在增强后必须保持所以几何变换统一用最近邻插值albumentations 对 mask 默认就是最近邻。参数说明img_size 设 512 是精度和显存的平衡点A100 上 batch 16 占用约 9GB如果只有 8GB 显存降到 384 或把 batch 降到 8。brightness_limit 不要超过 0.4否则会把傍晚样本压成白天破坏光照多样性。3.3 损失函数与训练循环import torch.nn as nn import torch.nn.functional as F class DiceBCELoss(nn.Module): def __init__(self, ignore_index255): super().__init__() self.ignore_index ignore_index def forward(self, logits, target): # 构造有效掩码忽略区域不参与任何损失 valid (target ! self.ignore_index) target_clean target.clone() target_clean[~valid] 0 # BCE 部分 bce F.binary_cross_entropy_with_logits( logits, target_clean.float(), reductionnone) bce (bce * valid).sum() / valid.sum().clamp(min1) # Dice 部分 prob torch.sigmoid(logits) prob prob * valid tgt target_clean.float() * valid inter (prob * tgt).sum(dim(1, 2)) union prob.sum(dim(1, 2)) tgt.sum(dim(1, 2)) dice 1 - (2 * inter 1e-6) / (union 1e-6) return bce dice.mean()逻辑说明BCE 负责逐像素梯度Dice 负责整体区域重叠度两者相加能缓解人行道正负样本极度不平衡的问题人行道通常只占画面 20% 到 40%。忽略区域的处理是关键——先把 255 置零再乘 valid 掩码保证既不贡献梯度也不污染 Dice 统计。参数说明ignore_index 必须和标注规范里的 255 一致。Dice 的平滑项 1e-6 防止空掩码时除零。如果发现训练早期 loss 震荡剧烈把 Dice 权重降到 0.5等 BCE 稳定后再加回来。训练循环里我固定用 AdamW学习率 3e-4余弦退火到 1e-6训练 60 个 epoch。验证指标只看两个mIoU 和边界 F1用形态学膨胀 3 像素后算边界带的 F1。只看 mIoU 会被大面积背景稀释边界 F1 才反映人行道边缘质量。4. 推理部署与后处理从概率图到可通行区域4.1 滑窗推理与多尺度融合整图直接 resize 到 512 会丢失远处人行道细节我的做法是滑窗加多尺度。窗口 512、步长 384重叠 128每个窗口在 0.75、1.0、1.25 三个尺度各推一次概率图取平均。import torch import numpy as np torch.no_grad() def sliding_inference(model, img, window512, stride384, scales(0.75, 1.0, 1.25)): model.eval() H, W img.shape[:2] prob_sum np.zeros((H, W), dtypenp.float32) count np.zeros((H, W), dtypenp.float32) for s in scales: nh, nw int(H * s), int(W * s) img_s cv2.resize(img, (nw, nh)) for y in range(0, max(nh - window, 0) 1, stride): for x in range(0, max(nw - window, 0) 1, stride): patch img_s[y:ywindow, x:xwindow] if patch.shape[0] window or patch.shape[1] window: continue t torch.from_numpy(patch).permute(2,0,1).float().unsqueeze(0) / 255.0 p torch.sigmoid(model(t))[0,0].cpu().numpy() # 映射回原图坐标 ys, xs int(y/s), int(x/s) ye, xe int((ywindow)/s), int((xwindow)/s) p cv2.resize(p, (xe-xs, ye-ys)) prob_sum[ys:ye, xs:xe] p count[ys:ye, xs:xe] 1 return prob_sum / np.maximum(count, 1)逻辑说明多尺度融合能同时照顾近处大目标和远处小目标。重叠区域取平均而不是取最大是为了抑制单窗口的偶发误检。count 矩阵保证边缘区域不被零除。参数说明stride 设 window 的 0.75 倍是经验值再小收益递减、耗时线性增长。scales 里 1.25 那档在显存紧张时可以去掉对 mIoU 影响约 0.5 个点。4.2 后处理阈值、连通域与形态学概率图直接二值化会有大量椒盐噪声。我的后处理顺序是先高斯模糊核 5平滑再阈值 0.5 二值化然后开运算去小噪点最后保留面积大于画面 1% 的连通域。def postprocess(prob, min_area_ratio0.01): prob cv2.GaussianBlur(prob, (5, 5), 0) binary (prob 0.5).astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) n, labels, stats, _ cv2.connectedComponentsWithStats(binary, 8) h, w binary.shape out np.zeros_like(binary) for i in range(1, n): if stats[i, cv2.CC_STAT_AREA] min_area_ratio * h * w: out[labels i] 1 return out逻辑说明高斯模糊抑制孤立高概率点开运算去掉细碎毛刺连通域过滤掉远处误检的小块。min_area_ratio 设 1% 是防止把远处真实人行道误删——远处人行道在画面里可能只占 2% 到 3%。参数说明阈值 0.5 不是万能逆光和阴影场景下模型置信度整体偏低可以自适应降到 0.4但要用验证集确认边界 F1 不下降。形态学核不要超过 7否则会把窄人行道整条抹掉。5. 人行道检测的避坑与排查清单5.1 指标虚高按帧随机划分的陷阱现象验证集 mIoU 0.85一上路实测惨不忍睹。原因同一路段的相邻帧高度相似按帧随机划分导致训练集和验证集共享大量近重复样本。解决严格按路段划分划分前用感知哈希对帧去重汉明距离小于 5 的帧只保留一帧。5.2 树荫下整片漏检现象模型在树荫覆盖区域把人行道判成非人行道边界 F1 掉 20 个点。原因训练集树荫样本不足且树荫下纹理和阴影被模型当成非路面的强特征。解决补充树荫样本到总样本 10% 以上增强里加随机遮挡CoarseDropout模拟斑驳光影损失里对阴影区域不额外加权避免模型学到阴影即负样本的捷径。5.3 边界像素抖动导致下游路径规划震荡现象可通行区域边缘逐帧跳动规划出的路径左右摇摆。原因分割边界本身有 1 到 3 像素抖动逐帧独立推理没有时序约束。解决推理端加时序平滑用指数移动平均对概率图做跨帧融合系数 0.6 到 0.7或者对二值结果做 5 帧中值滤波。注意平滑系数太大会导致真实边界变化响应延迟转弯处尤其明显。5.4 逆光场景概率整体偏低现象傍晚逆光时整图概率被压到 0.3 到 0.4固定阈值 0.5 导致大面积漏检。原因逆光下人行道和背景对比度下降模型输出置信度整体下移。解决训练时加入逆光样本并做直方图均衡化增强推理时用局部自适应阈值以画面下半部分概率的 60 分位数作为基准动态调整。这个改动在验证集上边界 F1 能回升 8 到 10 个点。5.5 小目标远处人行道被连通域过滤误删现象远处人行道面积小于 1% 被后处理删掉导致可通行区域在远处断裂。原因min_area_ratio 设得过大或者滑窗步长太大导致远处窗口覆盖不足。解决对画面上半部分单独用更小的 min_area_ratio0.3%滑窗在远处用更小步长或者直接对上半部分做一次整图推理再和滑窗结果融合。6. 把边界 F1 从 0.72 拉到 0.85 的一个具体技巧前面所有环节里对边界质量提升最明显的不是换模型而是边界带加权损失。做法是在计算 BCE 时对掩码做形态学膨胀和腐蚀各 3 像素两者相减得到边界带边界带像素的损失权重乘 5。def boundary_weighted_bce(logits, target, ignore_index255, band3, weight5.0): valid (target ! ignore_index) tgt target.clone() tgt[~valid] 0 tgt tgt.float() # 用最大池化近似膨胀最小池化近似腐蚀 k 2 * band 1 dil F.max_pool2d(tgt.unsqueeze(1), k, 1, band).squeeze(1) ero -F.max_pool2d(-tgt.unsqueeze(1), k, 1, band).squeeze(1) edge (dil - ero).clamp(0, 1) w 1.0 weight * edge loss F.binary_cross_entropy_with_logits(logits, tgt, reductionnone) loss (loss * w * valid).sum() / (w * valid).sum().clamp(min1) return loss逻辑说明膨胀减腐蚀得到边界带边界带权重放大让模型把梯度集中到边缘。用池化近似形态学是为了在 GPU 上批量计算比逐样本调 OpenCV 快一个数量级。参数说明band 设 3 对应约 3 像素边界带太大超过 5会让模型过度关注边缘而忽略区域内部mIoU 反而下降。weight 设 5 是验证集上扫出来的3 到 8 之间都有效超过 10 训练不稳定。这个技巧配合前面的 DiceBCELoss 一起用边界 F1 从 0.72 提到 0.85mIoU 只掉了 0.3 个点非常划算。我自己的习惯是每次改损失或增强先只跑 10 个 epoch 看边界 F1 曲线曲线前 5 个 epoch 不抬头就说明方向错了别硬跑 60 个 epoch 浪费时间。人行道检测这活儿模型结构不是瓶颈数据划分、边界处理和光照覆盖才是决定上限的东西。希望帮到你。本文还有配套的精品资源点击获取