拓冰建站拓冰建站
首页 / 资讯中心 / 正文

HED边缘检测模型:深度学习下的整体嵌套边缘检测实战解析

简介HED层次边缘检测模型的Python实现资源围绕模型提供完整源码与说明适用于计算机视觉初学者、算法工程师及深度学习爱好者解决图像边缘检测中的精细定位与多尺度特征提取问题也可作为课堂案例或毕业设计参考。压缩包内共2个文件一个Python源代码脚本用于模型构建与推理一个Markdown说明文档用于梳理原理与使用方式整个资源包仅2KB轻量小巧方便快速下载与阅读。目前已有456人学习浏览适合作为理解全卷积网络、多尺度侧边输出层以及组合损失函数设计的入门实例。通过该案例读者可掌握HED模型的网络结构搭建、数据预处理与增强思路、训练参数配置和边缘后处理流程同时结合文档理解Canny/Sobel等传统算法与深度学习的差异为后续研究语义分割、轮廓检测等像素级视觉任务打下扎实基础。1. HED 边缘检测模型为什么传统算子到了深度学习时代反而被重新审视边缘检测是计算机视觉最底层的任务几乎所有后续任务——目标检测、语义分割、特征匹配——都依赖边缘图质量。传统 Sobel、Canny 算子跑了几十年深度学习之后HEDHolistically-Nested Edge Detection整体嵌套边缘检测成了少数把边缘检测纳入端到端可训练框架、且原版 python 源码至今仍有参考价值的模型。你手里的 zip 压缩包解压后就是一个完整工程模型定义、数据加载、训练推理脚本都在里面。核心思路是用单条全卷积网络同时输出多个不同感受野尺度的边缘图再融合成最终结果。适合想把边缘检测接到业务里或者想研究深度监督机制的工程师。2. HED 的原理与工程运行环境2.1 侧输出层、融合层与深度监督HED 的“嵌套”语义HED 的核心贡献在“嵌套”这两个字。它不是像 FCN 那样只在最后一层做一次上采样而是在 VGG16 骨干的 5 个 stage 上分别引出侧输出side output。VGG16 的卷积层天然分成 5 个 stage感受野逐层扩大前两个 stage 捕获局部纹理和短边缘后面三个 stage 逐步感知物体轮廓和语义边界。HED 每个 stage 之后接一个 1x1 卷积形成侧输出头再上采样到输入尺寸得到与输入同分辨率的边缘概率图。另一个关键设计是深度监督每个侧输出层都要与真值图计算损失梯度直达浅层避免 VGG 微调时浅层更新太慢。最后融合层把 5 张侧输出图沿通道拼起来经过 1x1 卷积输出最终边缘概率图融合层也有独立损失参与训练。总损失就是 5 个侧输出损失与融合损失的加权和。这个思路后来被深度估计、显著性检测等任务大量借用HED 是深度监督机制最典型的工程样本。类别平衡交叉熵的权重逻辑损失函数是这份源码包中最值得单独阅读的文件。边缘像素在一张图里占比通常低于 10%直接用普通交叉熵会导致模型把几乎全部像素判为非边缘。类别平衡的做法是统计当前样本中正负像素数量按比例把两类损失分别加权L -β Σ p_i log(p_i) - (1-β) Σ (1-p_i) log(1-p_i)其中 β |Y-| / (|Y| |Y-|)正样本越少它的权重反而越高。类别平衡不改变模型结构只调整 loss 的加权系数实现成本几乎为零。在自定义数据集上重新训练之前应当自己实现一遍这个 loss不要直接套用 torch.nn.BCEWithLogitsLoss否则薄边缘召回率会明显下滑。2.2 Python 环境准备依赖版本与源码解包拿到 HED-python源码.zip 这样的压缩包大概率是从论文主页或 GitHub 仓库打包来的。原版实现基于 Caffe 和 C但社区移植版本主体是 Python PyTorch。以下以 PyTorch 版本为例符合当前 python 环境的主流用法也方便在训练和推理之间切换。依赖项版本建议用途Python3.8 ~ 3.10解释器PyTorch1.10 ~ 2.0模型定义与训练torchvision与 PyTorch 匹配提供 VGG16 预训练权重OpenCV4.5图像读取、形态学后处理NumPy1.21数组操作tqdm任意训练进度显示Linux 下的安装顺序我一般是这样conda create -n hed python3.9 conda activate hed pip install torch torchvision opencv-python numpy tqdm unzip HED-边缘检测模型-python源码.zip -d hed_project cd hed_project提示GPU 显存小于 6G 时训练输入尺寸建议从 512x512 降到 384x384或者启用源码里的随机裁剪策略。解压后标准工程的目录大致长这样hed_project/ ├── dataset.py # 数据加载与边缘标签预处理 ├── model.py # HED 网络结构定义 ├── train.py # 训练入口 ├── inference.py # 单张图片推理 ├── loss.py # 类别平衡交叉熵损失 ├── config.py # 全局参数 └── utils.py # 可视化与边缘评估辅助函数按这个结构代码阅读顺序应当是 loss.py → model.py → dataset.py → train.py最后看 inference.py。很多人的习惯是从 model 直接开始但 HED 的 loss 是它区别于普通分割网络最重要的地方建议先读 loss。2.3 模型定义VGG16 特征层切分与侧输出头打开 model.py核心工作是切分 torchvision 的 VGG16 features 层。在 torchvision 的 vgg16 实现中features 是 Sequential 容器0~4 是 conv1 层、5~9 是 conv2 层、10~16 是 conv3、17~23 是 conv4、24~30 是 conv5。切分时用索引而不是层名# model.py 关键代码 from torchvision import models import torch.nn as nn class HED(nn.Module): def __init__(self, pretrainedTrue): super().__init__() vgg models.vgg16(pretrainedpretrained) self.stage1 nn.Sequential(*list(vgg.features.children())[:5]) self.stage2 nn.Sequential(*list(vgg.features.children())[5:10]) self.stage3 nn.Sequential(*list(vgg.features.children())[10:17]) self.stage4 nn.Sequential(*list(vgg.features.children())[17:24]) self.stage5 nn.Sequential(*list(vgg.features.children())[24:31]) # 每个 stage 后接的侧输出头 self.side1 self._side_head(64) self.side2 self._side_head(128) self.side3 self._side_head(256) self.side4 self._side_head(512) self.side5 self._side_head(512) self.fuse nn.Conv2d(5, 1, kernel_size1, biasFalse) # 侧输出头与融合层做小方差初始化 for m in [self.side1, self.side2, self.side3, self.side4, self.side5, self.fuse]: if isinstance(m, nn.Conv2d): nn.init.normal_(m.weight, std0.01) if m.bias is not None: nn.init.constant_(m.bias, 0) def _side_head(self, in_ch): # 先降通道再输出 1 通道边缘图 return nn.Sequential( nn.Conv2d(in_ch, 128, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(128, 1, 1), ) def forward(self, x): h1 self.stage1(x) h2 self.stage2(h1) h3 self.stage3(h2) h4 self.stage4(h3) h5 self.stage5(h4) s1 self.side1(h1) s2 self.side2(h2) s3 self.side3(h3) s4 self.side4(h4) s5 self.side5(h5) up nn.Upsample(sizex.shape[2:], modebilinear, align_cornersFalse) s1, s2, s3, s4, s5 up(s1), up(s2), up(s3), up(s4), up(s5) fuse_out self.fuse(torch.cat([s1, s2, s3, s4, s5], dim1)) return [s1, s2, s3, s4, s5, fuse_out]代码里两个细节值得注意。第一侧输出头第一层卷积用 3x3 是为了融合局部上下文信息只用 1x1 做通道压缩容易丢掉空间邻域细节第二上采样这里用固定双线性插值原论文用的是可学习反卷积但从工程经验看双线性插值在训练稳定性上更好恢复出的边缘在视觉上也更整齐。如果接手的老源码里是反卷积并且训练 loss 出现周期性抖动优先检查它的初始化方式。3. 数据准备与训练脚本从 BSDS500 到自定义数据集3.1 BSDS500 数据格式与真值图预处理原版 HED 工程训练集用 BSDS500这是伯克利的分割与边缘检测标准数据集200 张训练图之外还有 100 张验证和 200 张测试。每张图有多个标注者手工标注边缘训练时合并成一张二值图。dataset.py 做的事是图片读取、resize、转 Tensor、随机裁剪、输出 batch看起来简单实际上有几个关键细节。第一边缘真值图必须是二值图1 代表边缘、0 代表背景不能把语义分割图直接当作边缘标签使用。第二resize 真值图时不能用双线性插值因为插值会在边缘周围产生介于 0 到 1 之间的灰色像素训练时这样的软标签会让模型对边缘位置不敏感。如果你从标注工具导出的是带颜色边缘的 PNG要把颜色通道合并取任一通道最大值再二值化。# dataset.py 核心流程 import cv2, torch from glob import glob from torch.utils.data import Dataset class HEDDataset(Dataset): def __init__(self, image_dir, edge_dir, img_size512): self.image_paths sorted(glob(image_dir /*.jpg)) self.edge_paths sorted(glob(edge_dir /*.png)) self.img_size img_size def __getitem__(self, idx): img cv2.imread(self.image_paths[idx]) img cv2.resize(img, (self.img_size, self.img_size)) edge cv2.imread(self.edge_paths[idx], cv2.IMREAD_GRAYSCALE) # 真值图 resize 必须用最近邻避免插值灰边 edge cv2.resize(edge, (self.img_size, self.img_size), interpolationcv2.INTER_NEAREST) _, edge cv2.threshold(edge, 127, 1, cv2.THRESH_BINARY) img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 edge torch.from_numpy(edge).float().unsqueeze(0) return img, edge这个版本没有做 VGG 的 ImageNet 统计量减均值更常见的做法是在模型 forward 内部对输入做标准化。如果发现模型第一次前向 loss 就特别大十有八九是输入分布不对。3.2 类别平衡损失实现为什么不用 DiceLossloss.py 是这类任务的点睛之笔。很多人第一反应是用 Dice Loss 或 Focal Loss 替代交叉熵因为边缘检测天然是极度类别不平衡问题。但从我使用的对比看HED 原版 loss 的效果更稳定。Dice Loss 对区域重叠比例敏感不太适配多尺度侧输出加总 loss 的设定Focal Loss 在边缘这种细线目标上效果也不错但参数 gamma 需要针对每个数据集重新调普适性不如原版。# loss.py类别平衡交叉熵 import torch import torch.nn.functional as F def class_balanced_sigmoid_ce(pred, target): pred pred.view(-1) target target.view(-1) pos (target 0.5).float() neg (target 0.5).float() pos_count torch.clamp(pos.sum(), min1) neg_count torch.clamp(neg.sum(), min1) beta pos_count / (pos_count neg_count) loss -beta * F.binary_cross_entropy_with_logits( pred, target, reductionnone) * pos \ - (1 - beta) * F.binary_cross_entropy_with_logits( pred, target, reductionnone) * neg return loss.mean()训练循环里要对 6 个输出分别算 lossfrom loss import class_balanced_sigmoid_ce side_losses [class_balanced_sigmoid_ce(s, target) for s in side_outputs] fuse_loss class_balanced_sigmoid_ce(fuse, target) total_loss sum(side_losses) fuse_loss total_loss.backward()这里我习惯给侧输出加权重系数比如 5 个侧输出每个乘 0.4融合层乘 1.0避免浅层 loss 过多干扰深层特征微调。实测在自定义小数据集上这样能明显压缩训练震荡。3.3 训练参数设置、显存占用与启动命令config.py 里的关键参数整理成表适合对照自己的显存环境改参数原论文推荐8G 显存建议说明batch_size104显存不足先降 batchbase_lr1e-61e-6微调 VGG 保持低学习率lr_decay0.1/10000 iter0.9/epoch按 epoch 衰减更方便img_size512384减小分辨率可省显存weight_decay2e-42e-4防止尾部过拟合启动命令python train.py --data_dir ./data --config config.py --device cuda训练时观察 loss 曲线正常情况在前 5 个 epoch 内总 loss 会从 0.5 附近缓慢下降到 0.2 以下。如果发现 fuse loss 下降但 side loss 不动说明侧输出头的梯度被融合层抑制常见解法是把侧输出的权重系数提高。这个现象在多层监督模型里经常出现HED 尤其明显。更现实的做法是让融合层学习率降到侧输出的一半。4. 推理阶段从图像到整洁边缘图的完整流程4.1 单张图推理脚本与后处理细节训练完成后进入推理。把模型前向输出的 0~1 概率图直接保存成图片边缘是模糊的灰带直接给下游使用效果不好。需要加一个轻量后处理流程高斯模糊消除孤立噪点再用 Canny 双阈值做细化和二值化。# inference.py import cv2, numpy as np, torch from model import HED def post_process(edge_prob, low_thresh0.3, high_thresh0.7): edge (edge_prob * 255).astype(np.uint8) blur cv2.GaussianBlur(edge, (3, 3), 0) edges cv2.Canny(blur, low_thresh * 255, high_thresh * 255) return edges def main(img_path, weight_path, devicecuda): model HED(pretrainedFalse) model.load_state_dict(torch.load(weight_path, map_locationdevice)) model.to(device).eval() img cv2.imread(img_path) orig_h, orig_w img.shape[:2] resized cv2.resize(img, (512, 512)) tensor torch.from_numpy(resized.transpose(2, 0, 1)).float().unsqueeze(0) tensor tensor / 255.0 tensor tensor.to(device) with torch.no_grad(): _, fuse model(tensor) fuse fuse.squeeze().cpu().numpy() fuse cv2.resize(fuse, (orig_w, orig_h)) fusion_edge post_process(fuse) cv2.imwrite(fusion_edge.png, fusion_edge)值得检查两个点。第一是通道顺序cv2 读图得到 BGRHED 原始工程按 BGR 训练推理也用 BGR两边一致就不会出问题。第二是 resize 方向模型输入是 512x512输出也是 512x512但最终要的是原图分辨率的边缘图必须先把 fuse 概率图 resize 回原分辨率再做后处理而不是先提取边缘再放大后者会放大锯齿和断裂。4.2 多尺度推理聚合提升边缘召回率的标准技巧HED 测试阶段常见技巧是多尺度推理。把输入图像按 0.5、0.75、1.0、1.25、1.5 倍缩放分别推理再把每张 512x512 的输出对齐回原尺寸取均值。不同尺度下模型感受到的上下文不同融合后的边缘图对粗细边缘的覆盖更均衡。def multiscale_inference(model, img, scales[0.5, 0.75, 1.0, 1.25, 1.5]): h, w img.shape[:2] acc np.zeros((h, w), dtypenp.float32) for s in scales: scaled cv2.resize(img, (int(w * s), int(h * s))) tensor torch.from_numpy(scaled.transpose(2, 0, 1)).float().unsqueeze(0) / 255.0 tensor tensor.to(device) with torch.no_grad(): _, fuse model(tensor) fuse fuse.squeeze().cpu().numpy() fuse cv2.resize(fuse, (w, h)) acc fuse return acc / len(scales)多尺度推理的耗时账要算清楚单张 512x512 在 2080Ti 上单尺度约 10ms五尺度约 50ms。视频级应用建议只保留 0.8、1.0、1.2 三个尺度或者直接单尺度。边缘检测对耗时敏感程度比检测任务高因为它很多时候只是预处理模块。4.3 用 5 个侧输出做可视化排查HED 模型的 5 个侧输出是很好的调试入口。正常训练完成的模型s1、s2 应偏纹理细节s4、s5 偏语义边界。如果某两个相邻侧输出的结果几乎一模一样说明其中一个 stage 的侧输出头没有学到有效特征如果所有侧输出都特别脏大概率是数据标签问题而不是模型。for idx, s in enumerate(side_outputs): s_img (s.squeeze().cpu().numpy() * 255).astype(np.uint8) cv2.imwrite(fside_{idx 1}.png, s_img)在自定义数据集上训练时建议关注侧输出之间的差异程度差异越大多尺度融合的收益越好差异太小说明模型退化成单一尺度单输出模型。这个检查三分钟就能完成但能避免很多弯路。5. 边缘模型的实际应用与优化技巧5.1 边缘图后处理与下游任务衔接边缘图很少是最终产品更多时候是中间特征。比如文字检测前的版面分析HED 输出边缘图之后常见做法是先用连通域分析滤掉小碎片再用形态学闭运算把同一条边缘的断裂连接起来。具体参数按图像分辨率设定512x512 输入下我常用 50 像素作为面积下限。# 连通域碎片过滤 def filter_small_components(edge_img, min_area50): n, labels, stats, _ cv2.connectedComponentsWithStats(edge_img, 8) result np.zeros_like(edge_img) for i in range(1, n): if stats[i, cv2.CC_STAT_AREA] min_area: result[labels i] 255 return result如果断裂过多可以先膨胀一次再腐蚀一次再执行过滤。5.2 半精度推理与轻量骨干替换PyTorch 推理时使用半精度几乎零成本model.half() tensor tensor.half() with torch.no_grad(): _, fuse model(tensor)半精度对 ODS 指标的影响通常在 0.005 以内显存减半速度提升 1.3~1.8 倍。计算力再紧张就把 VGG16 换成 MobileNetV3 重训模型体积从 230MB 降到 30MB 左右F1 掉 1~2 个点但推理速度能上一个量级。5.3 自定义数据集上要盯住的三件事最后是自定义数据上最容易翻车的三点。边缘标签的厚度要一致太粗学出双线边界太细让背景梯度占主导。正负样本比例先统计边缘占比低于 3% 时优先提高输入分辨率而不是盲目换 loss。不要冻结预训练 VGG 的骨干只是把学习率压低即可。验证阶段把 ODS、OIS、AP 三个指标写进脚本一起输出不同版本之间的对比才有依据。指标计算完成后再按业务场景固定阈值整套 HED 边缘检测链路就算完整跑通。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门