尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

HED边缘检测实战:从全卷积网络原理到模型调参 简介这是一份面向计算机视觉初学者与开发者的HED边缘检测模型Python源码案例聚焦利用全卷积网络实现端到端的多尺度边缘检测。压缩包体积仅2KB共2个文件其中hed_edge.py为完整可运行脚本README.md提供模型原理与运行说明适合快速上手复现。案例覆盖FCN架构、多尺度特征图、侧边输出层、组合损失函数设计等关键环节并涉及数据预处理、模型训练与后处理技术可帮助理解HED较Canny、Sobel等传统算法的优势。通过阅读源码还能掌握基于深度学习框架的建模流程、反向传播更新权重、学习率策略设置以及非极大值抑制等实操技巧。已有456人下载学习对希望深入理解边缘检测网络实现细节的读者具有参考价值。1. 从 Canny 到 HED边缘检测为什么还需要深度学习在工业现场做边缘检测第一步不是调模型而是选算法。Canny 快但阈值一旦遇到纹理密集区域就崩边没闭合成环或者把砖缝、布纹全部当成边缘。HEDHolistically-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(pretrainedTrue) # 按 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_size1), nn.Conv2d(128, 1, kernel_size1), nn.Conv2d(256, 1, kernel_size1), nn.Conv2d(512, 1, kernel_size1), nn.Conv2d(512, 1, kernel_size1), ]) 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.4side2局部边缘0.6side3中等轮廓0.8side4大尺度轮廓1.0side5全局边界1.0fusion融合输出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 APIPython 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 tensorboardWindows 上如果 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, interpolationcv2.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], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.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权重保存目录checkpointslr1e-4是因为 VGG 加载了预训练权重如果从零训练要调到 1e-3。优化器常见做法是 Adamweight_decay1e-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.50.3 和 0.7 都试一下看边缘完整度和噪声的平衡点。4. 调参与排错边缘从“能看”到“可用”跑通之后大多数人的第一反应是“怎么效果这么碎”。这很正常。HED 的输出受预处理、损失权重、后处理三重因素影响下面按故障现象给排查方向比对着代码一行行看更有效。4.1 损失不降先检查标签和骨干初始化如果训练到第 5 个 epoch侧边 loss 还在 0.6 附近波动我的排查顺序是第一确认标签是不是 0/1 二值。float 标签可以是 0.0 和 1.0但不能是 0 和 255。第二确认 VGG 加载了预训练权重。有些复现在代码里写models.vgg16(pretrainedFalse)也没报错但收敛速度极慢边缘根本连不成线。一个稳妥做法是把训练拆成两段。先用 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, size512): 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), dtypenp.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 几十毫秒如果下游是实时跟踪且只有 CPUCanny 仍不可替代但分割、检测、缺陷分析里的边缘先验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, size3) nms np.where(np.isclose(edge, mx) (edge 0.3), edge, 0)size3控制细化范围越大线越细但也可能把连续边缘截断。对 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, iterations1) weak.astype(bool) final strong | mask.astype(np.uint8)iterations1控制连接半径只把强边缘周围 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(fprecision{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%。本文还有配套的精品资源点击获取
返回列表