HED边缘检测实战:从VGG16到多任务学习的深度学习流水线

发布时间:2026/10/10 7:15:42
HED边缘检测实战:从VGG16到多任务学习的深度学习流水线 简介HED_edgeDetect 是一份面向计算机视觉初学者与深度学习实践者的边缘检测资源包围绕 HEDHypercolumns for Edge Detection这一基于卷积神经网络的端到端边缘检测方法展开可用于理解多尺度特征融合、预训练与微调两阶段训练流程并直接对图像进行边缘预测。压缩包为 rar 格式共 3 个文件整体约 2KB包含 1 个 Python 脚本、1 个 prototxt 网络结构定义文件与 1 个 sh 预训练权重下载脚本分别对应推理代码、模型部署配置与权重获取环节结构精简便于快速上手。目前已有 1240 人学习下载说明该资源在边缘检测入门实践中具有一定参考价值。借助这份资料读者可以加载预训练模型完成图像边缘检测也可在此基础上调整参数、替换数据集以适配特定场景同时结合代码理解 HED 从粗到细的边缘捕获机制为后续研究或工程应用提供可复用的起点。1. HED 边缘检测为什么它比 Canny 更适合深度学习流水线如果你做过传统图像处理大概率写过 Canny高斯滤波、Sobel 求梯度、非极大值抑制、双阈值滞后连接一套下来边缘是有了但阈值调起来像玄学换一张光照不同的图就得重来。HEDHolistically-Nested Edge Detection换了个思路——用深度学习把边缘检测变成一个端到端的像素级二分类问题输入原图输出一张概率边缘图不再需要手工设阈值。这个方案适合谁做图像分割预处理、工业缺陷检测、遥感地物提取、医学影像轮廓勾画的从业者尤其是已经在用 PyTorch 或 TensorFlow 搭模型、想把边缘检测模块嵌进现有流水线的人。它解决的核心痛点是传统算子对噪声和光照敏感、参数不可学习、多尺度边缘融合靠人工设计。HED 用 VGG16 做骨干在五个阶段各接一个侧输出层最后融合成一张精细边缘图训练时用加权交叉熵处理正负样本极度不平衡的问题。下面从原理到代码把这条路径拆开讲清楚。2. HED 的网络结构与多尺度侧输出机制2.1 为什么选 VGG16 做骨干而不是 ResNetHED 原论文用的是 VGG16不是因为它精度最高而是因为它的结构规整——五个卷积块每块后面接池化特征图尺寸依次减半天然形成五个尺度。边缘检测需要同时捕捉粗轮廓和细纹理浅层特征图分辨率高能定位像素级边缘深层特征图感受野大能判断这个边缘是不是语义上有意义的边界。ResNet 的残差连接虽然训练更稳但跳跃连接会让侧输出的特征提取变得不那么直接需要额外设计旁路。我一般会建议如果你只是复现 HED 做边缘检测VGG16 足够如果你要把边缘检测作为多任务学习的一个分支那用 ResNet 或 EfficientNet 做共享骨干更划算。VGG16 的五个卷积块输出通道分别是 64、128、256、512、512。HED 在每个块的最后一个卷积层后面接一个侧输出分支结构是卷积核 1×1、输出通道 1、然后上采样到原图尺寸。五个侧输出各自算损失最后用一个融合层通常是 1×1 卷积或加权求和合并成最终输出。这种深监督deep supervision的设计让浅层侧输出也能收到梯度不会因为链太长而梯度消失。2.2 侧输出层的具体实现与参数设置侧输出层的代码不复杂但有几个参数容易翻车。下面是我常用的 PyTorch 实现片段import torch import torch.nn as nn import torch.nn.functional as F from torchvision import models class HEDSideOutput(nn.Module): def __init__(self, in_channels, mid_channels16): super().__init__() # 先降维再输出减少参数量避免过拟合 self.conv1 nn.Conv2d(in_channels, mid_channels, 1) self.conv2 nn.Conv2d(mid_channels, 1, 1) self.relu nn.ReLU(inplaceTrue) def forward(self, x, target_size): x self.relu(self.conv1(x)) x self.conv2(x) # 双线性上采样到原图尺寸不用转置卷积避免棋盘伪影 x F.interpolate(x, sizetarget_size, modebilinear, align_cornersFalse) return x class HED(nn.Module): def __init__(self): super().__init__() vgg models.vgg16(pretrainedTrue).features # 按 VGG16 的池化位置切分五个阶段 self.stage1 vgg[:4] # conv1_1, conv1_2, pool1 前 self.stage2 vgg[4:9] # conv2_1, conv2_2 self.stage3 vgg[9:16] # conv3_1~3 self.stage4 vgg[16:23] # conv4_1~3 self.stage5 vgg[23:30] # conv5_1~3 self.side1 HEDSideOutput(64) self.side2 HEDSideOutput(128) self.side3 HEDSideOutput(256) self.side4 HEDSideOutput(512) self.side5 HEDSideOutput(512) # 融合层把五个侧输出拼接后做 1x1 卷积 self.fuse nn.Conv2d(5, 1, 1) def forward(self, x): size x.shape[2:] f1 self.stage1(x) f2 self.stage2(f1) f3 self.stage3(f2) f4 self.stage4(f3) f5 self.stage5(f4) s1 self.side1(f1, size) s2 self.side2(f2, size) s3 self.side3(f3, size) s4 self.side4(f4, size) s5 self.side5(f5, size) # 拼接五个侧输出融合成最终边缘图 fused self.fuse(torch.cat([s1, s2, s3, s4, s5], dim1)) return [s1, s2, s3, s4, s5], fused逻辑说明HEDSideOutput先用 1×1 卷积把通道降到 16再降到 1这样比直接 1×1 从 512 降到 1 更稳参数量从 512 降到 512×1616虽然多了但训练更平滑。上采样用bilinear而不是ConvTranspose2d因为转置卷积在边缘检测里容易产生棋盘格伪影边缘图会多出周期性噪声。align_cornersFalse是 PyTorch 的推荐设置避免尺寸对齐时的偏移。参数说明mid_channels默认 16如果数据集小少于 1000 张可以降到 8如果边缘特别细比如遥感影像里的道路可以升到 32。融合层用 1×1 卷积而不是直接相加是因为五个侧输出的数值范围可能不一致1×1 卷积可以学习加权系数。训练时五个侧输出和融合输出都算损失推理时只用融合输出。2.3 损失函数加权交叉熵与正负样本不平衡边缘检测有个天然问题一张图里边缘像素通常只占 5% 到 10%剩下全是背景。如果用普通交叉熵模型会倾向于全预测背景准确率看着高但边缘全丢。HED 原论文用了加权交叉熵正样本边缘权重高负样本权重低。具体实现def hed_loss(preds, fused, target, pos_weight10.0): # preds: 五个侧输出的列表fused: 融合输出 # target: 二值边缘图1 表示边缘0 表示背景 criterion nn.BCEWithLogitsLoss(pos_weighttorch.tensor([pos_weight])) loss 0.0 for p in preds: loss criterion(p, target) loss criterion(fused, target) return loss / (len(preds) 1)pos_weight10.0是经验值意思是正样本的损失权重是负样本的 10 倍。如果你的数据集边缘特别稀疏比如只占 2%可以调到 20如果边缘较粗占 15% 以上降到 5 也行。注意BCEWithLogitsLoss内部已经做了 sigmoid所以模型输出不要加 sigmoid 层否则会双重激活导致梯度消失。我见过有人在这里翻车训练 loss 一直不降查了半天发现是模型里多写了一个nn.Sigmoid()。提示pos_weight不是越大越好。设成 50 以上时模型会疯狂预测边缘背景区域出现大量假阳性边缘图看起来像雪花屏。建议从 10 开始根据验证集上的 F1 分数微调。3. 从 BSDS500 数据到可训练 HED 的完整流程3.1 数据准备BSDS500 的标注格式与转换BSDS500 是边缘检测的经典数据集500 张自然图像每张有 5 到 10 个人工标注的边缘图。原始标注是 MATLAB 的.mat格式每个像素的值是多个标注者投票的比例范围 0 到 1。训练 HED 时需要把它转成二值图通常取阈值 0.5大于 0.5 算边缘否则算背景。转换脚本如下import scipy.io as sio import numpy as np import cv2 import os def mat_to_binary(mat_path, save_path, threshold0.5): # BSDS500 的 groundTruth 结构groundTruth[0][0][0] 是边界图 mat sio.loadmat(mat_path) gt mat[groundTruth] # 取第一个标注者的边界图也可以取平均 boundary gt[0][0][0][0][0] # 形状 (H, W)值域 0~1 binary (boundary threshold).astype(np.uint8) * 255 cv2.imwrite(save_path, binary) # 批量转换 root BSDS500/data/groundTruth/train save_root BSDS500/edges/train os.makedirs(save_root, exist_okTrue) for f in os.listdir(root): if f.endswith(.mat): mat_to_binary(os.path.join(root, f), os.path.join(save_root, f.replace(.mat, .png)))逻辑说明gt[0][0][0][0][0]这个索引看起来吓人是因为 MATLAB 的嵌套结构在 scipy 里变成了多层数组。实际调试时可以先print(gt.shape)和print(gt[0][0].shape)确认。取第一个标注者还是取平均我一般取平均后再二值化这样边缘更稳定不会因为某个标注者的偏好导致边缘偏移。平均的写法是np.mean([gt[0][i][0][0][0] for i in range(gt.shape[1])], axis0)。参数说明threshold0.5是常用值如果你希望边缘更细可以提到 0.7如果希望召回更多边缘降到 0.3。但注意训练时目标图太细会导致正样本过少pos_weight要相应调大。3.2 训练循环与学习率策略HED 的训练和普通分割网络类似但有几个细节VGG16 骨干用预训练权重初始化侧输出层随机初始化。学习率用分段衰减初始 1e-3每 10 个 epoch 降 10 倍。优化器用 SGD 动量 0.9比 Adam 更稳因为边缘检测对权重更新幅度敏感Adam 的自适应学习率容易在后期震荡。import torch.optim as optim from torch.utils.data import DataLoader model HED().cuda() # 骨干用预训练权重学习率小一些侧输出层学习率大一些 backbone_params list(model.stage1.parameters()) list(model.stage2.parameters()) \ list(model.stage3.parameters()) list(model.stage4.parameters()) \ list(model.stage5.parameters()) side_params list(model.side1.parameters()) list(model.side2.parameters()) \ list(model.side3.parameters()) list(model.side4.parameters()) \ list(model.side5.parameters()) list(model.fuse.parameters()) optimizer optim.SGD([ {params: backbone_params, lr: 1e-4}, {params: side_params, lr: 1e-3} ], momentum0.9, weight_decay5e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) for epoch in range(30): model.train() for img, edge in train_loader: img, edge img.cuda(), edge.cuda() preds, fused model(img) loss hed_loss(preds, fused, edge) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上算 F1 model.eval() with torch.no_grad(): # 验证逻辑省略核心是取 fused 输出阈值 0.5 二值化后算 F1 pass逻辑说明骨干学习率 1e-4侧输出 1e-3是因为骨干已经预训练好了只需要微调侧输出是随机初始化的需要更快学习。weight_decay5e-4是 VGG 系列常用的正则化强度如果你发现过拟合严重训练 F1 远高于验证 F1可以提到 1e-3。StepLR每 10 个 epoch 降 10 倍30 个 epoch 足够收敛。BSDS500 只有 200 张训练图数据增强很关键随机裁剪 256×256、水平翻转、颜色抖动能把有效数据量扩大 10 倍以上。3.3 推理与后处理从概率图到二值边缘模型输出的是 logits经过 sigmoid 后得到 0 到 1 的概率图。推理时不需要五个侧输出只用融合输出。后处理有两个关键步骤非极大值抑制NMS和阈值化。NMS 是为了让边缘变细因为模型输出的边缘往往有 2 到 3 像素宽。简单做法是沿梯度方向做局部极大值抑制但更实用的方法是先阈值化再用形态学细化cv2.ximgproc.thinning。import cv2 import numpy as np import torch def infer(model, img_path, threshold0.5): model.eval() img cv2.imread(img_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 标准化和训练时一致 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) inp (img_rgb / 255.0 - mean) / std inp torch.from_numpy(inp).permute(2, 0, 1).float().unsqueeze(0).cuda() with torch.no_grad(): _, fused model(inp) prob torch.sigmoid(fused).squeeze().cpu().numpy() # 阈值化 binary (prob threshold).astype(np.uint8) * 255 # 细化让边缘变单像素宽 binary cv2.ximgproc.thinning(binary) return prob, binary逻辑说明标准化参数用的是 ImageNet 的均值和方差因为 VGG16 预训练权重是在 ImageNet 上训的。如果你从头训练可以用自己数据集的统计量。threshold0.5是默认值但实际部署时建议在验证集上扫一遍 0.3 到 0.7选 F1 最高的。cv2.ximgproc.thinning需要 opencv-contrib 包安装命令是pip install opencv-contrib-python。如果不想装 contrib可以用简单的形态学腐蚀代替但细化效果差一些。注意推理时输入图像尺寸最好和训练时一致。如果训练用 256×256 裁剪推理时整图输入模型对超大图的边缘响应会变弱因为感受野固定。常见做法是滑窗推理再拼接或者把图缩放到 512×512 以内。4. 避坑与排查HED 训练和部署中的 5 个血泪教训4.1 现象训练 loss 震荡不降验证 F1 卡在 0.3原因最常见的是pos_weight设得太大模型在正负样本之间反复横跳。另一个可能是学习率太高侧输出层的 1e-3 对某些数据集偏大。还有一个隐蔽原因数据标注的二值化阈值不对如果阈值 0.5 导致边缘太细正样本比例低于 1%模型很难学到。解决先把pos_weight降到 5学习率降到 5e-4观察 loss 是否平滑下降。如果还不行检查标注图的边缘像素占比用np.mean(target 0)算一下正常应该在 5% 到 15% 之间。低于 3% 就降低二值化阈值到 0.3或者用形态学膨胀把边缘加粗一个像素。4.2 现象推理边缘图出现棋盘格伪影原因上采样用了转置卷积ConvTranspose2d且kernel_size和stride不匹配。转置卷积在放大倍数不是整数倍时会在特征图之间插入不均匀的零导致周期性伪影。解决把上采样换成F.interpolate(modebilinear)这是最稳的方案。如果非要用转置卷积确保kernel_size能被stride整除比如kernel_size4, stride2并且加上output_padding0。但边缘检测里我强烈建议用双线性插值伪影问题直接消失。4.3 现象模型对细边缘不敏感粗轮廓还行原因VGG16 的深层特征图分辨率太低stage5 的输出尺寸只有原图的 1/16细边缘在池化过程中丢了。侧输出虽然上采样回原图但信息已经损失了。解决两个方向。一是把 stage1 和 stage2 的侧输出权重调高融合层不要用均匀的 1×1 卷积而是给浅层侧输出更大的初始权重。二是换骨干用空洞卷积dilated convolution代替部分池化保持分辨率。常见做法是把 VGG16 的 stage4 和 stage5 的池化层去掉换成 dilation2 和 dilation4 的卷积这样深层特征图分辨率提高 4 倍细边缘响应明显改善。4.4 现象多卡训练时 loss 不收敛单卡正常原因BCEWithLogitsLoss的pos_weight在多卡 DataParallel 下没有正确同步每张卡上的正样本比例不同导致梯度方向不一致。另一个可能是 batch size 变大后等效学习率没调SGD 的更新步长相对变小。解决多卡训练时把pos_weight设成固定值而不是根据每批数据动态计算。如果用了DataParallel建议换成DistributedDataParallel梯度同步更准确。学习率按卡数线性缩放单卡 1e-34 卡就设 4e-3但 warmup 阶段要拉长到 5 个 epoch避免初期震荡。4.5 现象部署到 TensorRT 后边缘图变模糊原因TensorRT 默认把F.interpolate的bilinear模式转成最近邻插值或者融合了某些算子导致精度下降。另一个可能是 FP16 推理时sigmoid 前的 logits 范围被压缩小数值边缘被舍入成零。解决导出 ONNX 时显式指定opset_version11以上并且用torch.onnx.export的dynamic_axes固定输入尺寸。TensorRT 构建引擎时把F.interpolate层标记为precisionFP32不要用 FP16。如果必须用 FP16在 sigmoid 前乘一个温度系数比如 2.0把 logits 范围拉开减少舍入误差。5. 进阶技巧用 HED 做多任务边缘检测与损失比例调参如果你已经把 HED 跑通了下一步大概率是想把它嵌进多任务网络——比如同时做边缘检测和语义分割或者边缘检测和深度估计。这时候绕不开一个问题多个 loss 之间的比例怎么调我试过几种方案说一个最实用的。第一种是手动权重。边缘 loss 和分割 loss 直接相加权重比 1:1 或 1:10。缺点是边缘 loss 通常比分割 loss 大一个数量级因为边缘是像素级二分类分割是像素级多分类量纲不同。我一般会先把边缘 loss 除以pos_weight做归一化再和分割 loss 相加。第二种是用不确定性加权uncertainty weighting。给每个任务一个可学习的方差参数loss 变成loss / (2 * sigma^2) log(sigma)。这样模型自动学出任务权重不需要手工调。代码实现class MultiTaskLoss(nn.Module): def __init__(self, num_tasks2): super().__init__() # 可学习的 log 方差初始为 0 self.log_sigma nn.Parameter(torch.zeros(num_tasks)) def forward(self, losses): # losses: 每个任务的 loss 列表 total 0.0 for i, loss in enumerate(losses): precision torch.exp(-self.log_sigma[i]) total precision * loss self.log_sigma[i] return total逻辑说明log_sigma初始为 0对应 sigma1precision1初始权重相等。训练过程中如果某个任务的 loss 一直很大模型会增大log_sigma降低该任务的权重避免它主导梯度。这个方法的代价是多了两个参数但省去了手工调权重的麻烦。注意log_sigma要用nn.Parameter注册优化器要包含它学习率可以设成和其他参数一样。第三种是梯度归一化GradNorm。计算每个任务 loss 对共享层参数的梯度范数然后动态调整权重让所有任务的梯度范数接近。这个方法效果最好但实现复杂适合任务差异特别大的场景。我一般先用不确定性加权如果效果不理想再上 GradNorm。验证多任务效果时不要只看单个任务的指标。边缘检测看 F1分割看 mIoU但还要看两个任务是否互相促进。如果边缘 F1 涨了但分割 mIoU 掉了说明边缘任务在抢容量。这时候可以试试把边缘侧输出只接在浅层深层共享给分割减少任务冲突。最后说一个我踩过的坑多任务训练时数据增强要一致。边缘图和分割 mask 必须用同样的随机裁剪和翻转参数否则边缘和分割区域对不上模型学出来的边缘是错位的。我习惯把图像、边缘图、分割 mask 打包成一个字典在 Dataset 的__getitem__里一次性做增强返回三个同步的张量。这个习惯帮我省了很多调试时间。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询