资讯动态

Penn-Fudan数据集实战:FCN全卷积网络行人分割从数据准备到调参避坑

发布时间:2026/10/16 9:15:07 来源:尧图企业网站定制
简介这份资源围绕全卷积网络FCN在Penn-Fudan Database上的行人检测与分割实践展开面向已具备CNN基础、希望深入图像分割的深度学习学习者与研究者。内容涵盖FCN端到端像素级预测原理、以VGG或ResNet为骨干叠加反卷积上采样层、跳跃连接融合浅层与深层特征等关键设计并配套Penn-Fudan数据集的图像与分割掩模训练流程可用于智能监控、自动驾驶等场景的语义分割入门与复现。资源包共533个文件以340张png图像与掩模、172个txt标注说明为主另含6个py训练脚本、4个npy指标文件、4个xml配置及若干模型权重与工程文件压缩包约184.68MB目录结构便于按数据、代码、结果分块查阅。目前已有504人学习下载适合需要完整跑通FCN分割流程、对照实验参数与评估指标的读者参考。1. 从行人分割说起为什么 Penn-Fudan 是 FCN 落地的第一块试金石行人分割这件事听起来像是自动驾驶或者安防监控里的高端需求但真正动手做的时候大多数人卡在第一步找不到一个干净、标注完整、规模适中的数据集来验证全卷积网络FCN到底能不能跑通。Penn-Fudan Database 就是在这个场景下被反复提起的——它包含 170 张图像、345 个行人实例每张图都有像素级掩码标注覆盖了校园、街道、室内等多种场景。规模不大但足够让你把 FCN 的完整链路走一遍数据加载、标签对齐、模型前向、损失回传、掩码可视化。这个数据集最适合两类人一是刚接触语义分割、想用最小成本理解 FCN 编解码结构的开发者二是已经用过 DeepLab 或 U-Net但想回头补上“全卷积”这个基础概念的人。它的标注质量足够高图像分辨率统一为 256×256行人姿态多样遮挡和重叠情况都有覆盖不会像某些玩具数据集那样跑出来的结果好看但没参考价值。下面我会从数据准备、模型搭建、训练调参到避坑排查把这条链路拆开讲清楚。2. 数据准备与标签对齐把 Penn-Fudan 转成 FCN 能吃的格式2.1 原始数据结构与读取方式Penn-Fudan 的原始发布格式通常是每张图像对应一个标注文件标注以 PNG 掩码或文本坐标形式存在。常见做法是先把所有图像和掩码按文件名配对然后统一缩放到网络输入尺寸。这里有一个容易被忽略的点掩码的像素值不是 0/1 二值而是每个行人实例有独立 ID背景为 0。如果你直接拿来做二分类分割需要先把多实例掩码合并成前景/背景二值图。我一般会先写一个校验脚本确认图像和掩码的数量、尺寸、命名规则是否一致。下面这段代码做三件事遍历目录、配对文件、检查尺寸是否匹配。import os import cv2 import numpy as np def check_pairing(img_dir, mask_dir): img_files sorted(os.listdir(img_dir)) mask_files sorted(os.listdir(mask_dir)) # 文件名去掉扩展名后应一一对应 img_stems [os.path.splitext(f)[0] for f in img_files] mask_stems [os.path.splitext(f)[0] for f in mask_files] assert img_stems mask_stems, 图像与掩码文件名不匹配 for stem in img_stems: img cv2.imread(os.path.join(img_dir, stem .jpg)) mask cv2.imread(os.path.join(mask_dir, stem .png), 0) if img.shape[:2] ! mask.shape[:2]: print(f尺寸不一致: {stem}, img{img.shape[:2]}, mask{mask.shape[:2]}) else: print(fOK: {stem}, shape{img.shape[:2]}, mask唯一值{np.unique(mask)}) # 调用示例 check_pairing(./PennFudanPed/PNGImages, ./PennFudanPed/PedMasks)逻辑说明cv2.imread第二个参数设为 0 表示以灰度模式读取掩码避免三通道干扰。np.unique(mask)能快速看出掩码里有哪些像素值正常情况下应该是 0 和若干个正整数每个行人一个 ID。如果出现 255 或浮点值说明标注格式和预期不符需要先做归一化或阈值处理。参数方面图像目录和掩码目录的路径根据实际解压位置调整。如果掩码是彩色 PNG读取后还要做通道合并或取单通道否则后续转二值会出错。2.2 转二值掩码与数据增强策略确认配对无误后下一步是把多实例掩码转成二值前景掩码。做法很简单所有大于 0 的像素置为 1背景保持 0。但这里有个坑——有些标注文件里行人之间会有 1 像素的间隙直接二值化后这些间隙会变成背景导致训练时模型学到错误的边界。常见做法是先做一次形态学闭运算把间隙填上再转二值。def to_binary_mask(mask, kernel_size3): # 多实例转二值 binary (mask 0).astype(np.uint8) # 闭运算填补小间隙 kernel np.ones((kernel_size, kernel_size), np.uint8) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return closed # 数据增强随机水平翻转 随机裁剪 def augment(img, mask): if np.random.rand() 0.5: img cv2.flip(img, 1) mask cv2.flip(mask, 1) # 随机裁剪到 224x224 h, w img.shape[:2] if h 224 and w 224: top np.random.randint(0, h - 224) left np.random.randint(0, w - 224) img img[top:top224, left:left224] mask mask[top:top224, left:left224] return img, mask逻辑说明cv2.morphologyEx的MORPH_CLOSE先膨胀后腐蚀能填掉小孔洞但不会明显改变行人轮廓。kernel_size一般取 3 或 5太大容易把相邻行人粘在一起。数据增强部分水平翻转对行人分割是安全的因为行人左右对称随机裁剪则要求原图尺寸大于裁剪尺寸Penn-Fudan 原图 256×256裁到 224×224 刚好留出一点边缘变化。提示如果掩码里不同行人的 ID 有重叠区域标注错误闭运算后可能出现粘连。建议在转二值前先检查每个 ID 的连通域数量超过 1 的说明标注有碎片需要手动清理或丢弃该样本。2.3 数据集封装与批处理PyTorch 的Dataset和DataLoader是标准做法但 Penn-Fudan 样本量小批大小不宜过大。我一般设 batch_size4 或 8配合num_workers2足够。注意collate_fn要能处理不同尺寸的掩码——如果做了随机裁剪同一批里图像尺寸可能不一致需要统一 resize 到固定尺寸再拼接。from torch.utils.data import Dataset, DataLoader import torch class PennFudanDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.transform transform self.stems [os.path.splitext(f)[0] for f in sorted(os.listdir(img_dir))] def __len__(self): return len(self.stems) def __getitem__(self, idx): stem self.stems[idx] img cv2.imread(os.path.join(self.img_dir, stem .jpg)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, stem .png), 0) mask to_binary_mask(mask) if self.transform: img, mask self.transform(img, mask) # 统一 resize 到 224x224 img cv2.resize(img, (224, 224)) mask cv2.resize(mask, (224, 224), interpolationcv2.INTER_NEAREST) img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask torch.from_numpy(mask).long() return img, mask # 使用示例 dataset PennFudanDataset(./PNGImages, ./PedMasks, transformaugment) loader DataLoader(dataset, batch_size4, shuffleTrue, num_workers2)逻辑说明permute(2, 0, 1)把 HWC 转成 CHW符合 PyTorch 卷积输入格式。掩码用INTER_NEAREST插值避免引入非整数标签。归一化除以 255 是常规操作但如果你用预训练骨干网络要换成对应的均值和标准差。参数方面batch_size根据显存调整4 在 6GB 显存下跑 FCN 8 倍下采样没问题。num_workers在 Windows 上建议设 0否则容易卡在共享内存。3. 全卷积网络搭建从 VGG 骨干到跳级连接3.1 为什么 FCN 要用全卷积替换全连接传统 CNN 做分类时最后几层是全连接层输出固定长度的向量。但分割任务要求输出和输入同尺寸的掩码全连接层会把空间信息压扁没法恢复。FCN 的核心思想就是用卷积层替换全连接层让网络在任意尺寸输入下都能输出对应的空间响应图。具体做法是把 VGG16 的最后三个全连接层换成 1×1 卷积通道数分别设为 4096、4096、类别数。这样输出的特征图保留了空间维度再通过上采样恢复到原图尺寸。Penn-Fudan 只有行人和背景两类所以最后一层 1×1 卷积输出通道为 2。但实际训练时我一般设成 2 通道后接 softmax或者 1 通道接 sigmoid两者等价。用 2 通道的好处是方便扩展到多类。3.2 跳级连接与上采样参数FCN-8s 是常用版本它把 pool4 和 pool3 的特征图通过跳级连接融合进来再一次性上采样 8 倍。相比 FCN-32sFCN-8s 的边界更精细但计算量稍大。Penn-Fudan 图像尺寸小FCN-8s 完全跑得动。import torch.nn as nn import torch.nn.functional as F from torchvision import models class FCN8s(nn.Module): def __init__(self, n_class2): super().__init__() vgg models.vgg16(pretrainedTrue) features list(vgg.features.children()) # 编码器VGG16 前 5 个卷积块 self.conv1 nn.Sequential(*features[0:5]) # 64 self.conv2 nn.Sequential(*features[5:10]) # 128 self.conv3 nn.Sequential(*features[10:17]) # 256 self.conv4 nn.Sequential(*features[17:24]) # 512 self.conv5 nn.Sequential(*features[24:31]) # 512 # 替换全连接为 1x1 卷积 self.fc6 nn.Conv2d(512, 4096, 1) self.fc7 nn.Conv2d(4096, 4096, 1) self.score_fr nn.Conv2d(4096, n_class, 1) # 跳级连接得分层 self.score_pool4 nn.Conv2d(512, n_class, 1) self.score_pool3 nn.Conv2d(256, n_class, 1) # 上采样层 self.upscore2 nn.ConvTranspose2d(n_class, n_class, 4, stride2, padding1) self.upscore4 nn.ConvTranspose2d(n_class, n_class, 4, stride2, padding1) self.upscore8 nn.ConvTranspose2d(n_class, n_class, 16, stride8, padding4) def forward(self, x): # 编码 c1 self.conv1(x) c2 self.conv2(c1) c3 self.conv3(c2) c4 self.conv4(c3) c5 self.conv5(c4) # 全卷积替换 f6 F.relu(self.fc6(c5)) f7 F.relu(self.fc7(f6)) score self.score_fr(f7) # 跳级融合 up2 self.upscore2(score) score_pool4 self.score_pool4(c4) score_pool4 score_pool4[:, :, 5:5up2.size(2), 5:5up2.size(3)] fuse4 up2 score_pool4 up4 self.upscore4(fuse4) score_pool3 self.score_pool3(c3) score_pool3 score_pool3[:, :, 9:9up4.size(2), 9:9up4.size(3)] fuse3 up4 score_pool3 up8 self.upscore8(fuse3) # 裁剪到输入尺寸 return up8[:, :, 31:31x.size(2), 31:31x.size(3)]逻辑说明ConvTranspose2d的stride和padding决定上采样倍数。upscore2是 2 倍upscore4再 2 倍upscore8是 8 倍。跳级连接时pool4 和 pool3 的得分图尺寸比上采样结果大需要裁剪对齐。裁剪偏移量 5、9、31 是根据 VGG16 各层感受野和池化累积算出来的不同输入尺寸下可能需要微调。参数方面n_class2对应行人和背景。如果显存不够可以把fc6和fc7的通道数从 4096 降到 1024精度损失不大但显存占用明显下降。3.3 损失函数与类别不平衡处理Penn-Fudan 里行人像素占比通常不到 20%背景占大多数。直接用交叉熵损失会让模型倾向于预测背景。常见做法是给前景像素加权权重设为背景的 5 到 10 倍。或者用 Dice Loss它对类别不平衡不敏感。class WeightedCrossEntropy(nn.Module): def __init__(self, weightNone): super().__init__() self.weight weight def forward(self, pred, target): # pred: [B, C, H, W], target: [B, H, W] return F.cross_entropy(pred, target, weightself.weight) # 使用前景权重设为 8 weights torch.tensor([1.0, 8.0]).cuda() criterion WeightedCrossEntropy(weightweights)逻辑说明weight张量的长度等于类别数索引 0 是背景索引 1 是行人。权重值需要根据实际前景占比调整如果前景占比 10%权重设 9 左右比较合适。训练时观察 loss 曲线如果 loss 下降但 IoU 不升说明权重可能过大模型在过度补偿前景。注意加权交叉熵的权重不要设得太极端否则模型会把背景像素也预测成前景导致召回率高但精确率低。建议先用权重 5 跑一轮看验证集 IoU 再调。4. 训练调参与验证Penn-Fudan 上的实操参数4.1 优化器选择与学习率策略FCN 微调时编码器部分通常用较小的学习率解码器和跳级连接用较大的学习率。常见做法是分参数组VGG 骨干 lr1e-4新增的 1×1 卷积和上采样层 lr1e-3。优化器用 SGD 加动量 0.9或者 Adam。Penn-Fudan 样本少Adam 收敛更快但最终精度可能略低于调好的 SGD。# 分参数组 backbone_params list(model.conv1.parameters()) list(model.conv2.parameters()) \ list(model.conv3.parameters()) list(model.conv4.parameters()) \ list(model.conv5.parameters()) head_params list(model.fc6.parameters()) list(model.fc7.parameters()) \ list(model.score_fr.parameters()) list(model.score_pool4.parameters()) \ list(model.score_pool3.parameters()) list(model.upscore2.parameters()) \ list(model.upscore4.parameters()) list(model.upscore8.parameters()) optimizer torch.optim.SGD([ {params: backbone_params, lr: 1e-4}, {params: head_params, lr: 1e-3} ], momentum0.9, weight_decay1e-4) # 学习率衰减每 20 个 epoch 降一半 scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5)逻辑说明weight_decay设 1e-4 防止过拟合Penn-Fudan 只有 170 张图不加正则化很容易过拟合。StepLR的step_size根据总 epoch 数调整一般训练 50 到 80 个 epoch 足够。参数方面如果显存允许batch_size 可以提到 8学习率相应乘以 1.5 左右。但 Penn-Fudan 样本少batch_size 太大反而梯度不稳定。4.2 验证指标与可视化检查分割任务不能只看 loss要看 IoU 和像素准确率。IoU 对前景更敏感像素准确率容易被背景拉高。我一般两个都算但以 IoU 为准。def compute_iou(pred, target, n_class2): ious [] pred pred.argmax(dim1) # [B, H, W] for cls in range(n_class): pred_inds pred cls target_inds target cls intersection (pred_inds target_inds).sum().item() union (pred_inds | target_inds).sum().item() if union 0: ious.append(float(nan)) else: ious.append(intersection / union) return np.nanmean(ious) # 训练循环中每 5 个 epoch 验证一次 model.eval() with torch.no_grad(): for img, mask in val_loader: img, mask img.cuda(), mask.cuda() pred model(img) iou compute_iou(pred, mask) print(fVal IoU: {iou:.4f})逻辑说明argmax(dim1)把 2 通道输出转成类别索引。nanmean忽略没有该类别的样本避免除零。Penn-Fudan 验证集一般从 170 张里划 30 张剩下的训练。可视化检查同样重要。把原图、真实掩码、预测掩码并排画出来能直观看出模型是漏检还是误检。常见问题是边界模糊这通常是上采样倍数不够或跳级连接没对齐。4.3 训练轮次与早停策略Penn-Fudan 上 FCN-8s 通常 40 到 60 个 epoch 收敛。如果验证 IoU 连续 10 个 epoch 不升就可以停了。早停能省时间也能防止过拟合。我一般会保存验证 IoU 最高的模型权重而不是最后一个 epoch 的。best_iou 0.0 patience 10 wait 0 for epoch in range(80): model.train() for img, mask in train_loader: img, mask img.cuda(), mask.cuda() optimizer.zero_grad() pred model(img) loss criterion(pred, mask) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() val_iou evaluate(model, val_loader) if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), best_fcn8s.pth) wait 0 else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break逻辑说明patience10表示连续 10 个 epoch 没提升就停。torch.save保存的是state_dict加载时用model.load_state_dict(torch.load(...))。注意保存路径不要带中文否则在某些环境下会报错。参数方面如果训练 loss 还在降但验证 IoU 平了说明过拟合可以加 dropout 或增大 weight_decay。如果两者都平了但 IoU 不高可能是学习率太小或跳级连接有问题。5. 避坑与排查Penn-Fudan 跑 FCN 时最容易翻车的五个点5.1 掩码读取后全是 0 或全是 1现象np.unique(mask)只返回[0]或[0, 255]没有中间 ID。原因通常是掩码文件是调色板模式或 16 位 PNGcv2.imread默认按 8 位读取导致数值截断。解决用cv2.imread(path, cv2.IMREAD_UNCHANGED)读取然后检查位深。如果是 16 位除以 256 或做归一化。调色板模式则要转成灰度再处理。5.2 上采样后尺寸对不上现象RuntimeError: The size of tensor a (X) must match the size of tensor b (Y)。原因是跳级连接时裁剪偏移量算错或者输入图像尺寸不是 32 的倍数。FCN 经过 5 次池化下采样 32 倍输入尺寸最好是 32 的倍数。Penn-Fudan 原图 256×256 没问题但如果做了随机裁剪到 224×224224 不是 32 的倍数上采样后就会差几个像素。解决裁剪尺寸改成 256 或 192或者在 forward 最后用F.interpolate强制对齐到输入尺寸。5.3 训练 loss 不降或变成 NaN现象loss 从第一个 epoch 就很大或者几个 batch 后变成 NaN。原因可能是学习率太大、权重初始化不当、或者输入图像没有归一化。解决先把学习率降到 1e-5 跑几个 batch确认 loss 能降再逐步调大。检查输入是否除以 255如果用了预训练 VGG还要按 ImageNet 的均值和标准差归一化。另外ConvTranspose2d的权重初始化如果用了默认的 Kaiming在某些配置下会输出过大值可以手动改成xavier_uniform_。5.4 验证 IoU 高但可视化结果很差现象验证集 IoU 0.85 以上但画出来的掩码边界模糊、行人粘连。原因是 Penn-Fudan 里行人像素占比低背景预测对了就能拉高 IoU。解决单独算前景 IoU或者用 Dice 系数。可视化时把预测掩码叠加到原图上用半透明红色显示能更清楚看出边界问题。如果边界确实差检查跳级连接的裁剪偏移是否对齐或者把 FCN-8s 换成 FCN-16s 再对比。5.5 显存溢出或训练速度异常慢现象batch_size4 就 OOM或者每个 epoch 要跑十几分钟。原因可能是fc6和fc7的 4096 通道太占显存或者num_workers设太大导致数据加载阻塞。解决把fc6和fc7通道降到 1024显存能省一半以上。num_workers在 Windows 上设 0Linux 上设 2 到 4。另外如果用了pin_memoryTrue但没配 CUDA反而会拖慢速度。6. 进阶技巧用 FCN 做迁移学习与多数据集验证Penn-Fudan 只有 170 张图单独训练容易过拟合。一个实用技巧是先在 COCO 或 PASCAL VOC 上预训练 FCN再用 Penn-Fudan 微调。PyTorch 的torchvision里有预训练的 FCN ResNet50可以直接加载然后把最后一层改成 2 类。这样即使 Penn-Fudan 样本少也能在 20 个 epoch 内收敛到不错的 IoU。from torchvision.models.segmentation import fcn_resnet50 # 加载预训练 FCN model fcn_resnet50(pretrainedTrue) # 替换分类头为 2 类 model.classifier[4] nn.Conv2d(512, 2, 1) # 辅助分类器也改 model.aux_classifier[4] nn.Conv2d(256, 2, 1) # 只微调分类头和最后两个卷积块 for name, param in model.named_parameters(): if classifier in name or aux_classifier in name or layer4 in name: param.requires_grad True else: param.requires_grad False optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4)逻辑说明fcn_resnet50的classifier是一个 Sequential索引 4 是最后的 1×1 卷积。替换后输出通道变成 2。冻结大部分骨干只微调高层能显著减少训练时间和显存占用。filter(lambda p: p.requires_grad, ...)只把需要更新的参数传给优化器避免更新被冻结的层。参数方面微调学习率设 1e-4 到 5e-5比从头训练小一个量级。如果显存够可以把layer3也解冻但 Penn-Fudan 上提升有限。另一个技巧是交叉验证。Penn-Fudan 只有 170 张划 30 张验证可能波动大。用 5 折交叉验证每折取 34 张验证能更稳定地评估模型。但训练时间会变成 5 倍适合最后调参阶段用。验证方法上除了 IoU还可以算边界 F1 分数。具体做法是对预测掩码和真实掩码分别做边缘提取然后算匹配率。Penn-Fudan 的行人边界清晰边界 F1 能反映模型对轮廓的还原能力。如果 IoU 高但边界 F1 低说明模型只学到了大致区域没抓住细节。我自己的习惯是每次换数据集或改网络结构都先跑一个 5 epoch 的小实验确认 loss 能降、IoU 能升再开完整训练。这样能避免跑了半天才发现参数设错。从那以后我每次动 FCN 的跳级连接或上采样参数都强制走一遍这个短跑验证省了不少后悔药。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑