news 2026/9/10 20:08:04

HED边缘检测实战:从全卷积网络原理到模型调参

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HED边缘检测实战:从全卷积网络原理到模型调参

简介:这是一份面向计算机视觉初学者与开发者的HED边缘检测模型Python源码案例,聚焦利用全卷积网络实现端到端的多尺度边缘检测。压缩包体积仅2KB,共2个文件,其中hed_edge.py为完整可运行脚本,README.md提供模型原理与运行说明,适合快速上手复现。案例覆盖FCN架构、多尺度特征图、侧边输出层、组合损失函数设计等关键环节,并涉及数据预处理、模型训练与后处理技术,可帮助理解HED较Canny、Sobel等传统算法的优势。通过阅读源码,还能掌握基于深度学习框架的建模流程、反向传播更新权重、学习率策略设置以及非极大值抑制等实操技巧。已有456人下载学习,对希望深入理解边缘检测网络实现细节的读者具有参考价值。

1. 从 Canny 到 HED:边缘检测为什么还需要深度学习

在工业现场做边缘检测,第一步不是调模型,而是选算法。Canny 快,但阈值一旦遇到纹理密集区域就崩:边没闭合成环,或者把砖缝、布纹全部当成边缘。HED(Holistically-Nested Edge Detection)把这个问题的答案从“梯度阈值”换成了“像素级分类”:用全卷积网络对每个像素输出边缘概率,同时保留深浅层多尺度特征,所以一条栏杆从上到下粗细稳定,也不被小纹理带着跑。案例106 里的 hed_edge.py 用 Python 实现了这样一套完整流程,从数据加载到侧边输出融合,再到后处理,很适合拿来改造成预处理模块。对做图像分割、草图提取、工业缺陷定位的人,这个源码比单纯调 Canny 值得多看两眼。

2. HED 的网络骨架:全卷积、多尺度与侧边输出

要动 hed_edge.py 的参数,先得把模型的骨架说清楚。HED 不是新发明的骨干网络,它借用了 VGG16 的前五个卷积阶段,然后去掉后面的全连接层。原因很直接:边缘检测要对每个像素回答“是不是边缘”,如果最终特征被压成一维向量,空间位置信息就丢了。改成全卷积结构后,输出是和输入相同尺寸的单通道概率图,可以端到端训练,也可以端到端推理。

2.1 去掉全连接:为什么边缘检测必须用全卷积网络

VGG16 原生结构里有两个全连接层,参数量巨大,输入也被固定成 224x224。HED 的典型实现是把 fc6、fc7 卷积化,或者干脆不用它们,只保留 conv1~conv5 作为特征提取器。边缘检测属于像素级预测,和语义分割一样,需要输出和原图对齐的逐像素结果,所以全连接层在这里没有位置。

具体到计算流程:图像归一化后输入网络,依次通过五个卷积阶段。每个阶段后得到一组特征图,低层特征图分辨率高、语义弱,高层特征分辨率低、语义强。HED 不去选择“哪个阶段最好”,而是把每一阶段的输出都变成边缘预测,这构成了多尺度侧边输出。后面看到的五张 side map,其实就是网络在不同感受野下对“边缘”的理解。

2.2 侧边输出层:从细节到语义的一列特征图

侧边输出层的实现非常简洁:每个卷积阶段后接一个 1x1 卷积,把特征图压缩到 1 通道,再经过 sigmoid 得到概率。用 PyTorch 写的话大致长这样(这是常见实现,具体结构以你的 hed_edge.py 为准):

import torch.nn as nn import torchvision.models as models class HEDBackbone(nn.Module): def __init__(self): super(HEDBackbone, self).__init__() vgg = models.vgg16(pretrained=True) # 按 VGG features 索引拆成五个 stage self.stage1 = vgg.features[0:2] # conv1_2 self.stage2 = vgg.features[2:5] # conv2_2 self.stage3 = vgg.features[5:9] # conv3_3 self.stage4 = vgg.features[9:14] # conv4_3 self.stage5 = vgg.features[14:19] # conv5_3 self.side = nn.ModuleList([ nn.Conv2d(64, 1, kernel_size=1), nn.Conv2d(128, 1, kernel_size=1), nn.Conv2d(256, 1, kernel_size=1), nn.Conv2d(512, 1, kernel_size=1), nn.Conv2d(512, 1, kernel_size=1), ]) def forward(self, x): s1 = self.stage1(x) s2 = self.stage2(s1) s3 = self.stage3(s2) s4 = self.stage4(s3) s5 = self.stage5(s4) outs = [] for feat, conv in zip([s1, s2, s3, s4, s5], self.side): out = torch.sigmoid(conv(feat)) outs.append(out) return outs

代码要说明几点:vgg.features[0:2]这种索引依赖 torchvision 的 VGG 实现顺序,PyTorch 版本不同,序列内层索引可能变动;我在自己环境里遇到过features[2:5]升级后变成多一个池化层的情况。报错信息一般是 channel mismatch,调试时打印vgg.features即可。self.side里的卷积不改变空间分辨率,只做通道压缩,所以 side1 是原图的 1/2 分辨率,side5 是原图的 1/16 分辨率,需要在损失和外层上采样时统一尺寸。

2.3 融合层与组合损失:多张“边缘视图”怎么合成一张

五个侧边输出各自都有道理,但直接用任何一张都不够。低层噪声多,高层边缘粗。所以 HED 后面加一个融合层:把五个概率图在通道维拼接,再用 1x1 卷积融合成单通道。训练时,五个侧边输出和融合输出都参与损失计算,形成深度监督。

侧边损失常用类别平衡交叉熵。因为边缘像素在整张图里占比通常不到 5%,普通交叉熵会让模型很快把所有像素预测成背景。类别平衡的办法是给正样本和负样本分别加权,权重由边缘像素比例确定。公式可以写成:

L_side = -beta * Σ(y * log p) - (1-beta) * Σ((1-y) * log(1-p))

其中beta是当前图像里边缘像素占总像素的比例。总损失等于五个侧边损失和融合损失的加权和。有的复现还会对侧边输出加一个总变差平滑项,让边缘线更连续,这相当于给损失函数加正则化,系数我一般取 0.1~0.5。太大了会把边缘糊掉,太小了又压不住噪声点。

下面这个表是我复现时常用的侧边损失权重,供参考:

侧边层特征特点损失权重
side1短线、纹理0.4
side2局部边缘0.6
side3中等轮廓0.8
side4大尺度轮廓1.0
side5全局边界1.0
fusion融合输出1.2

权重和数据集强相关。假如你的图像里多是小零件边缘,把 side1/side2 的权重大一点;如果只关心物体外轮廓,side5 和 fusion 更重要。训练中可以每 5 个 epoch 保存一次所有侧边输出和融合输出的可视化,盯着看比只盯 loss 有用得多。

3. 跑通 hed_edge.py:环境、数据与训练参数

案例106 的目录核心是 hed_edge.py 和 README.md。拿到源码之后先别急着跑训练,先把依赖和数据格式理清。大多数运行失败都不是模型写错,而是环境、路径和预处理三处对不上,这一章按我自己的排查顺序写下来。

3.1 环境准备与依赖安装

建议 Python 3.8 以上,深度学习框架用 PyTorch。推荐 3.8 或 3.9 而不是最新版本,是因为 HED 这类源码大多基于三年前的 torchvision API,Python 3.10 以上偶尔会遇到collections命名冲突。边缘图读写和形态学后处理都依赖 OpenCV,所以这几个包一次装齐。我一般用如下命令:

python -m pip install --upgrade pip pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy pillow tensorboard

Windows 上如果 CUDA 版本不对,可以先把--index-url去掉,直接pip install torch torchvision让 pip 自动选 build。装完做一次快速验证:

import torch, torchvision, cv2 print(torch.__version__, torch.cuda.is_available())

torch.cuda.is_available()输出 True 说明 GPU 可用。HED 的骨干是 VGG16,六个输出,显存占用比常见的分割模型小一截,6GB 显卡跑 512x512、batch_size 4 基本能过。如果是在 Linux 服务器上,记得先nvidia-smi看显存有没有被别人占满,否则一开训练就 OOM,很容易误判成代码问题。

3.2 数据目录与预处理

训练数据建议按下面的目录放:

data/ train/ images/ 001.jpg 002.jpg ... edges/ 001.png 002.png ...

images放原图,edges放单通道二值标注,边缘为白色,背景为黑色。读数据时有一个特别容易错的点:OpenCV 默认按 BGR 读图,而 PyTorch 预训练 VGG 按 RGB 归一化,忘记转通道会导致训练颜色语义错位。下面这段是整合了转通道、缩放、随机翻转和归一化的预处理代码:

import cv2, numpy as np, random def load_pair(img_path, edge_path, size=(512, 512)): img = cv2.imread(img_path) # BGR img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转 RGB edge = cv2.imread(edge_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, size) edge = cv2.resize(edge, size, interpolation=cv2.INTER_NEAREST) if random.random() < 0.5: img = img[:, ::-1] edge = edge[:, ::-1] img = img.astype(np.float32) / 255.0 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) std = np.array([0.229, 0.224, 0.225], dtype=np.float32) img = (img - mean) / std img = img.transpose(2, 0, 1) edge = (edge > 127).astype(np.float32) edge = edge[None, ...] return img, edge

这里INTER_NEAREST是关键。边缘图是二值标签,如果用线性插值缩放,会把 1 像素宽的线变成模糊的灰带,模型会学到“边缘也有深浅”,给后处理加很多麻烦。edge > 127的作用是把标注图中可能的灰色抗锯齿边缘统一变成二值。整段代码的输出格式是(C, H, W),与 PyTorch 模型的输入对齐。如果数据量小,还可以在翻转之外加随机裁剪,但裁剪范围要保证边缘标注同时被裁,否则标签就错位了。

3.3 训练入口与关键参数

如果 hed_edge.py 把模式、数据目录、学习率都做成命令行参数,训练通常这样起:

python hed_edge.py --mode train \ --data_dir ./data/train \ --epochs 40 \ --batch_size 8 \ --lr 1e-4 \ --side_weight 0.4 0.6 0.8 1.0 1.2

如果脚本没写这些参数,就在脚本顶部找配置区,把默认值改成对应的数字。很多复现的源码把参数硬编码在main()开头,直接改默认值也能跑,但不利于记录实验,我会在 README 里额外记一次最佳参数组合,方便换机器重跑。下面给一个参数速查表:

参数作用建议值
--mode训练或推理train / test
--epochs训练轮数30~50
--batch_size单次迭代样本数4~8
--lr初始学习率1e-4
--side_weight五个侧边损失权重0.4~1.2
--save_dir权重保存目录checkpoints

lr=1e-4是因为 VGG 加载了预训练权重,如果从零训练要调到 1e-3。优化器常见做法是 Adam,weight_decay=1e-4,每 10 个 epoch 把学习率乘 0.1。如果用 SGD,初始学习率要降到 5e-5,否则 VGG 微调时 loss 容易爆。side_weight的顺序必须和侧边输出层的顺序一致,否则会出现 side1 的 loss 怎么调都不降的怪现象。

3.4 推理:加载权重输出边缘图

测试阶段不要开梯度,脚本里一般是一个单独分支:

python hed_edge.py --mode test \ --checkpoint checkpoints/best.pth \ --input imgs/demo.jpg \ --output out/demo_edge.png

如果脚本直接输出概率图而没做后处理,得到的结果可能是灰蒙蒙的。核心逻辑实际是这样的:

model.eval() with torch.no_grad(): sides = model(img_tensor) # 5 张概率图 edge_prob = sides[-1].cpu().numpy()[0, 0] cv2.imwrite("out/demo_edge.png", (edge_prob > 0.5).astype(np.uint8) * 255)

model.eval()会关闭 dropout 和 batch norm 的统计更新,这一步忘掉会导致同一张图两次推理结果不同,尤其在 VGG 微调之后。edge_prob的取值在 0~1,得先阈值再写图。不要盯死 0.5,0.3 和 0.7 都试一下,看边缘完整度和噪声的平衡点。

4. 调参与排错:边缘从“能看”到“可用”

跑通之后,大多数人的第一反应是“怎么效果这么碎”。这很正常。HED 的输出受预处理、损失权重、后处理三重因素影响,下面按故障现象给排查方向,比对着代码一行行看更有效。

4.1 损失不降:先检查标签和骨干初始化

如果训练到第 5 个 epoch,侧边 loss 还在 0.6 附近波动,我的排查顺序是:第一,确认标签是不是 0/1 二值。float 标签可以是 0.0 和 1.0,但不能是 0 和 255。第二,确认 VGG 加载了预训练权重。有些复现在代码里写models.vgg16(pretrained=False)也没报错,但收敛速度极慢,边缘根本连不成线。

一个稳妥做法是把训练拆成两段。先用 5e-4 学习率冻结 backbone,只训练侧边输出和融合层 5 个 epoch,这一步把“翻译”层学出来;然后再解冻全网络,用 1e-4 微调 30 个 epoch。冻结 backbone 可以在每个 stage 上设置requires_grad_(False),但注意 batch norm 也要设成 eval 模式,否则前向统计还是会变,效果和解冻前一样不稳定。

4.2 边缘断裂和噪声并存的调参顺序

对 HED 来说,侧边输出就是可视化探针。打印五张 side map,按下面的表排查:

现象优先看哪一层调整方向
边缘断裂side5 或融合输出side5 权重提到 1.5,学习率减半
噪声点密集side1/side2权重降到 0.2,或先做中值滤波
融合输出比 side5 还碎融合层单独把 fusion 学习率乘 2
边缘整体偏粗NMS 阶段不改训练,后处理加大 NMS 力度

不要把五个 side loss 的绝对数值当成全部,它们在不同层本来就有量级差异。我一般比较的是相对变化:哪个 loss 下降明显慢,就优先处理哪个分支。融合层如果不稳定,可以先固定前五个分支的权重,单独训练 fusion 卷积 10 个 epoch,再联合微调,效果比直接端到端一起训更可控。

4.3 推理与训练预处理不一致的坑

推理时最容易犯的错是把图像 resize 成正方形,破坏宽高比,导致本来细长的物体边缘变形。HED 虽然是全卷积网络,但训练数据通常是正方形,推理时用居中 pad 更合理。预处理函数可以这样写:

def preprocess_infer(img_path, size=512): img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] scale = size / max(h, w) nh, nw = int(h * scale), int(w * scale) img = cv2.resize(img, (nw, nh)) canvas = np.zeros((size, size, 3), dtype=np.float32) canvas[:nh, :nw] = img canvas = canvas / 255.0 canvas = (canvas - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) return canvas.transpose(2, 0, 1)[None, ...], (h, w, nh, nw)

推理结束后,要把输出从 512x512 里裁出nh x nw,再 resize 回h x w。我遇到过一个非常隐蔽的问题:训练时cv2.cvtColor(img, cv2.COLOR_BGR2RGB),推理时却忘了写,导致红蓝通道互换,低层边缘响应明显变弱,看起来像模型退化。这种问题比对模型调参难查得多。

提示:推理时最值得检查的三件小事是通道顺序、输入尺寸、是否调用model.eval()。这三处错了,模型本身再准也无济于事。

4.4 显存不足与速度优化

显存不够时,常见做法是减小 batch_size,但 batch 太小会影响到 batch norm 的效果。可以用梯度累积来缓解:

optimizer.zero_grad() scaled_loss = loss / accum_steps scaled_loss.backward() if (step + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()

需要注意的是,梯度累积不会改变 batch norm 的统计量,因为 BN 看到的还是当前 batch 的样本。如果显存只能跑 batch 2,可以先冻结 backbone,只对侧边输出层做高分辨率训练,这样 BN 层需要的样本量更小,效果通常比强行累积更好。训练速度方面,把输入先缩到 320x320 预热一个 epoch,确认整体流程没问题再放大到 512,省下的时间比什么都值得。

4.5 什么时候该用 HED,而不是 Canny

这里放一个简短的对比:

维度CannyHED
原理梯度幅值与方向多尺度端到端学习
调参每张图都要调阈值训练后推理免调参
纹理鲁棒性弱,易被噪声干扰
边缘连续性常断裂高层分支保持完整
速度CPU 毫秒级GPU 几十毫秒

如果下游是实时跟踪且只有 CPU,Canny 仍不可替代;但分割、检测、缺陷分析里的边缘先验,HED 更干净,值得多花一次 GPU 前向。

5. 最后一步:NMS 后处理与边缘评估

5.1 非极大值抑制:删掉多余的胖边缘

模型输出的是概率图,靠近真实边缘的每个像素都会有较高响应,直接阈值会出现“两条平行线”的假象。标准做法是边缘细化:先算梯度方向,再沿方向比较相邻像素,只保留局部最大值。工程上可以先用 scipy 的局部极大值过滤快速逼近:

from scipy.ndimage import maximum_filter import numpy as np edge = edge_map.astype(np.float32) mx = maximum_filter(edge, size=3) nms = np.where(np.isclose(edge, mx) & (edge > 0.3), edge, 0)

size=3控制细化范围,越大线越细,但也可能把连续边缘截断。对 HED 输出,我用 3x3 通常足够,5x5 只适合边缘特别粗的模型。0.3是概率阈值,优先用低阈值保留弱边缘,剩下交给双阈值过滤。

5.2 双阈值与形态学连接

NMS 之后还可能存在小段断裂。做一次双阈值连接是常见工程技巧:

from scipy.ndimage import binary_dilation high, low = 0.6, 0.2 strong = (nms >= high).astype(np.uint8) weak = ((nms >= low) & (nms < high)).astype(np.uint8) mask = binary_dilation(strong, iterations=1) & weak.astype(bool) final = strong | mask.astype(np.uint8)

iterations=1控制连接半径,只把强边缘周围 1 像素的弱边缘带回来;断裂距离较大时可加到 2,但过度膨胀会让边缘变粗。这一步能明显提高边缘连续性,对后续分割任务的帮助比调整训练损失更直观。

5.3 用像素级 F1 做回归对比

评估 HED 常用 ODS/OIS,但在工程调试阶段,像素级 F1 足够用来对比后处理参数。核心代码很短:

pre = (final > 0).astype(np.uint8) gt = (gt_edge > 0).astype(np.uint8) inter = np.logical_and(pre, gt).sum() precision = inter / max(pre.sum(), 1) recall = inter / max(gt.sum(), 1) f1 = 2 * precision * recall / max(precision + recall, 1e-6) print(f"precision={precision:.3f} recall={recall:.3f} f1={f1:.3f}")

这套指标忽略了几像素的容差,数值比论文里的结果要低,但用于比较不同 NMS 阈值、不同 side_weight 组合足够了。正式评估再换成可容忍 0.75 像素误差的匹配算法。建议把 NMS、双阈值和 F1 计算独立成postprocess.py,每次实验都输出预测图、NMS 图、双阈值图三张对比,参数确定后固化下来,后面接分割模型时就不需要在边缘环节反复返工。如果希望把 HED 接入实时管线,把五个侧边输出层中的普通卷积换成深度可分离卷积,量化到 FP16 后 F1 通常只下降 0.5% 左右,但 GPU 推理时间可以再降 30%。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 20:07:59

HoRain云--<AppML> 案例研究 - 应用程序模型

此案例研究演示了如何构建一个完整的 <AppML> 互联网应用程序&#xff0c;具有针对数据库中的若干表进行信息列举、编辑和搜索的功能。 应用程序模型 在本章中&#xff0c;我们将为数据库中的 Customers 表建立一个完整的应用程序模型。 <AppML> 过滤器 如需允许…

作者头像 李华
网站建设 2026/9/10 20:07:16

卡车电气化三条路径,正在重写全球竞争版图

《卡车电动化&#xff0c;全球同速是个伪命题》 ——电池可以全球采购&#xff0c;路线只能本地生长卡车电动化最怕的&#xff0c;是把全球看成一张地图。中国一年卖出十几万辆新能源重卡&#xff0c;欧美合计仍在数千级。速度差&#xff0c;不是技术差&#xff0c;是场景差。中…

作者头像 李华
网站建设 2026/9/10 20:06:58

ITSM软件选型与实施全攻略:功能对比与价值量化

1. ITSM软件行业现状与对比价值ITSM&#xff08;IT服务管理&#xff09;软件市场近年来呈现爆发式增长态势。根据Gartner最新报告&#xff0c;全球ITSM工具市场规模在2023年达到约50亿美元&#xff0c;年复合增长率保持在12%以上。这种增长背后反映的是企业数字化转型过程中对I…

作者头像 李华
网站建设 2026/9/10 20:05:01

Backstage 插件所有权管理实战指南:从 Inner Source 到 Catalog 注册

Backstage 插件所有权管理实战指南&#xff1a;从 Inner Source 到 Catalog 注册 【免费下载链接】backstage Backstage is an open framework for building developer portals 项目地址: https://gitcode.com/GitHub_Trending/ba/backstage 随着 Backstage 实例在全公司…

作者头像 李华
网站建设 2026/9/10 20:05:00

激光加工多物理场耦合仿真指南:从Abaqus增材制造到表面抛光

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华