深度学习边缘检测实战:HED与PiDiNet模型解析与推理指南
简介这是一份基于深度学习的边缘检测完整工程集成了Python源码、预训练模型与配套数据集面向计算机视觉相关专业的在校生或从业者可支撑毕业设计、课程设计、初期项目演示等场景。压缩包共34个文件主要为可执行的Python脚本实现HED、PiDiNet等主流边缘检测网络、.pth模型权重、jpg/png测试图片与xml标注文件并附带md/txt说明文档用于快速上手指引整体大小8.72MB结构清晰便于查找。项目代码经验证可稳定运行解压后以英文命名路径即可直接使用既适合新手熟悉深度学习边缘检测的完整流程也支持在此基础上扩展其他功能或换用自定义数据训练。目前已有67人浏览学习对于想通过完整项目案例快速掌握边缘检测模型训练与推理细节的读者而言是一份兼具实用性和参考价值的资料。1. 为什么是深度学习边缘检测从 HED 到 PiDiNet最近在整理毕设项目时我一直在处理一类看似简单但实际很容易翻车的任务边缘检测。用 Canny 时sigma 和高低阈值调了一晚上换一张图就失效后来切换到基于深度学习的 HED 和 PiDiNet才真正理解了什么叫把边缘先验交给卷积网络去学。这个项目包正好同时给了两套 python 实现、预训练模型和一批用于梯度检查的测试数据集适合课程设计、大作业或者想快速验证边缘检测效果的人直接上手。它解决的核心问题不是“能不能跑”而是“如何在一张真实图片上稳定地得到层级清晰的边缘响应”这也是我接下来要拆解的重点。解压后第一件事就是把项目路径改成英文否则 torch.load 在 Windows 上很容易因为中文路径报错。2. 工程结构与数据集组织先看懂文件再谈训练2.1 解压后先做什么路径与目录规划项目包解压后里面有一堆散装文件hed_edge.py、pidinet.py、pipNet 目录、image2edge-main 目录、table5_pidinet.pth以及若干 jpg/png 测试图。第一次见这个结构会觉得乱但拆开看并不复杂。# 建议先整理成标准结构避免后续参数写错路径 mkdir -p edge_project/{models,data,input,output} mv hed_edge.py pidinet.py edge_project/ mv pipNet image2edge-main edge_project/ mv table5_pidinet.pth edge_project/models/ mv *.jpg *.jpeg *.png edge_project/input/ 2/dev/null || true这段命令的作用是先建一层标准目录把代码、模型权重和测试图片分开。很多开源项目跑不通不是模型问题而是路径写死、大小写不一致。2/dev/null || true是为了在没有任何匹配图片时也不会因为 mv 报错而中断后续操作。整理完后项目路径建议保持英文纯小写。项目中同时出现pipNet和image2edge-main两个目录容易让人误以为要分别配置两套环境。实际上pipNet一般是 PiDiNet 的作者代码仓库结构image2edge-main则是包装后的推理工程最终调用入口还是pidinet.py。我拆项目时习惯先看readme.txt再对比两个目录里的README.md确认哪个是原始论文代码、哪个是二次封装。2.2 数据集与测试图片怎么放这个包没有把完整训练集打进去而是放了一批检查用的图片比如white_pic1_002.jpg、white_pic1_009.jpg、img00000000.png、6.jpg、7.jpeg等。这类图片的特点是既有强边缘也有弱纹理适合快速验证模型有没有把细节吃掉。文件类型在项目中的作用建议放置位置hed_edge.pyHED 模型推理/训练入口项目根目录pidinet.pyPiDiNet 模型推理/训练入口项目根目录table5_pidinet.pthPiDiNet 预训练权重models/debug_out_*.png之前跑出来的边缘输出样例仅对照不参与计算*.jpg/jpeg/png测试输入图input/注意debug_out_1.png、debug_out_7.png这类文件是别人跑出来的结果不是输入数据。如果直接把它们当作数据集喂给模型会在预处理阶段产生通道数不一致的问题因为部分图片是四通道 PNG。训练自己的数据集时图片要统一转成 RGB 三通道并保证缩放到同一尺寸。2.3 模型权重加载table5_pidinet.pth 到底是什么table5_pidinet.pth对应 PiDiNet 论文中 Table 5 的配置通常是基于 ImageNet 预训练 backbone 后在 BSDS500 或类似边缘数据集上微调的结果。加载它不是torch.load一行命令就完事还要注意权重字典里的 key 是否带module.前缀。import torch ckpt torch.load(models/table5_pidinet.pth, map_locationcpu) print(type(ckpt)) if isinstance(ckpt, dict): print(list(ckpt.keys())[:5])这段代码用来先检查权重文件的结构。map_locationcpu是为了在任何显卡环境下都能只读不加载先看它是裸 state_dict 还是包装过的 checkpoint。如果你发现 key 以module.开头说明是用 DataParallel 保存的加载到单卡模型时要 strip 掉前缀否则load_state_dict会直接报 mismatch。一个小技巧是把打印出来的 key 列表和模型的state_dict().keys()做差集能立刻定位是缺层还是多前缀。这一章我特意先讲文件结构是因为后面所有代码都要基于这份目录约定来写。如果路径不对后面每一步都会踩雷。3. hed_edge.py 与 pidinet.py 核心参数解读3.1 HED 模型的前向流程HEDHolistically-Nested Edge Detection的思路是在 VGG16 的每个 stage 输出一个侧边边缘预测再把多尺度结果融合成最终边缘图。hed_edge.py里一般会定义一个带多个 side output 的网络前向时得到 5 个侧输出和 1 个 fuse 输出。# 伪代码级别的 HED 前向封装便于理解参数 class HED(nn.Module): def __init__(self, pretrainedFalse): super().__init__() # VGG16 前四层卷积块作为共享特征提取 self.block1 nn.Sequential(...) self.block2 nn.Sequential(...) self.block3 nn.Sequential(...) self.block4 nn.Sequential(...) self.block5 nn.Sequential(...) # 每个 block 接一个 1x1 卷积出侧边输出 self.side1 nn.Conv2d(64, 1, kernel_size1) def forward(self, x): s1 self.side1(self.block1(x)) # 真实实现里还有上采样到原尺寸、sigmoid、按通道加权融合等步骤 fuse torch.cat([s1, ...], dim1) return fuse这段代码里需要重点关注的是卷积层的 padding 和 stride。HED 的侧边输出要求分辨率与输入一致所以每个 maxpool 之前要保持 padding1、kernel_size3最后再通过双线性插值恢复尺寸。如果你在训练时发现 loss 一直在 0.69 左右不动先检查是否忘了对输出做 sigmoid 和 BCEWithLogitsLoss 混用否则数值范围不对。3.2 PiDiNet 的关键参数PiDiNet 是论文 Pixel Difference Networks for Efficient Edge Detection 的模型核心是用像素差分卷积替代普通卷积能在低算力设备上保持高帧率。pidinet.py里最重要的两个参数是c和s分别控制通道数和下采样尺度。from pidinet import PiDiNet model PiDiNet( in_ch3, out_ch1, c16, # 初始通道数越小越轻量 s2, # 下采样次数2 表示 1/4 分辨率 pretrainedNone )c16表示第一个阶段的卷积通道数后续阶段会按倍数放大s2表示网络内部做了两次下采样。这两个参数直接决定模型参数量。如果只是做 256x256 推理c16, s2足够如果输入分辨率达到 512 以上建议把s提高到 3 以降低显存占用但边缘细节会变粗。调参时先跑一遍 forward 看参数总量再决定是否用更大模型。这里把 HED 和 PiDiNet 放在一起对比能更清楚各自的边界对比维度HEDPiDiNet特征提取VGG16 多阶段像素差分卷积 轻量 encoder侧边输出每个 stage 一个融合输出为主参数量约 14M 量级更小适合边缘设备适合场景追求高精度GPU 充足实时推理、CPU/移动端这个表格不是要让大家二选一而是按场景选型。毕设里如果要做对比实验两个模型正好能构成“大模型 vs 轻量模型”的对照组课程设计里如果只用 CPU 跑直接选 PiDiNet 会更省心。3.3 输入归一化与输出后处理边缘检测模型的前处理比分类模型更敏感。HED 和 PiDiNet 在 ImageNet 预训练时都用固定的均值方差加载权重后不能随意改归一化方法。import cv2 import numpy as np import torch mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) def preprocess(img_path, size256): img cv2.imread(img_path) if len(img.shape) 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) else: img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (size, size), interpolationcv2.INTER_LINEAR) img img.astype(np.float32) / 255.0 img (img - mean) / std img torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0) return img这里的做法是先把 BGR 转 RGB因为 PyTorch 预训练权重接收的是 RGB 顺序然后缩放到 256x256用 float32 和 1/255 归一化最后减均值除方差。注意不要先transpose再resize否则得到的是错位的通道。输出后一般取 sigmoid 后的单通道图再乘 255 保存为 uint8这样查看结果时才不会是一片纯黑或纯灰。4. 推理实战与典型坑环境、显存、图片预处理4.1 环境选择与依赖安装这个项目以 PyTorch 为主依赖项一般包括 torch、torchvision、opencv-python、numpy、pillow。安装时不要一股脑装最新版先看本地 Python 版本和 CUDA 版本。python -c import sys; print(sys.version) nvidia-smi pip install torch torchvision opencv-python numpynvidia-smi的右上角能看到当前驱动支持的 CUDA 版本PyTorch 的 CUDA 运行时只要小于等于这个版本就能用。如果机器上没有 NVIDIA 显卡就直接装 CPU 版 torchmap_locationcpu推理同样能跑只是速度慢很多。这个项目不是训练大模型CPU 推理一张 256x256 图大约 2 到 5 秒完全可以接受。4.2 单张图片推理的完整流程下面这段代码把前处理、推理、后处理串起来同时也解决了debug_out_*.png这类文件被误读的问题。import torch import cv2 import numpy as np def infer_one(model, img_path, size256): model.eval() x preprocess(img_path, size) # 见上一章定义 with torch.no_grad(): logits model(x) if isinstance(logits, (list, tuple)): logits logits[-1] # HED 取 fuse 输出 prob torch.sigmoid(logits) prob prob[0, 0].cpu().numpy() # 去掉 batch 和 channel prob (prob * 255).astype(np.uint8) return cv2.cvtColor(prob, cv2.COLOR_GRAY2BGR) result infer_one(model, input/6.jpg) cv2.imwrite(output/6_edge.jpg, result)这里有一个容易被忽略的参数isinstance(logits, (list, tuple))。很多边缘检测模型前向会返回多个 side output如果直接拿去sigmoid会把整个列表当成一个 tensor 处理报运行时错误。取logits[-1]是拿最后一个融合输出这是 HED 最常见的做法PiDiNet 有些实现返回一个 torch.Tensor有些返回 dict需要根据pidinet.py里 forward 的返回值调整。4.3 显存不足与推理速度的取舍如果显卡显存只有 4Gsize512输入直接把 CUDA out of memory。可以先在 CPU 上跑一次确认显存占用也可以在代码里动态分档size 320 if torch.cuda.is_available() else 256这段代码的意思是根据环境决定输入尺寸GPU 显存够时用 320 拿更多边缘细节CPU 时用 256 减少等待。还有更激进的做法是用torch.cuda.empty_cache()清理缓存但这对正在计算的 tensor 无效只对碎片缓存有效。真正要降显存优先减小 batch size 和输入分辨率这两项是显存占用的大头。4.4 典型坑路径中文、灰度图维度、BN 的 eval 模式路径中文 torch.load、cv2.imread 在 Windows 上遇到中文路径会有各种编码问题项目说明里特别强调解压后改成英文路径这是最省事的解法。灰度图维度 有些输入图片本身就是单通道直接cv2.resize后会变成 HxW随后permute报错。我在 4.2 的preprocess里先判断len(img.shape) 2然后cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)。BN 的 eval 模式 PiDiNet 结构里有 BatchNorm如果model.train()状态下推理BN 会利用当前 batch 的统计量导致输出边缘图出现明显的棋盘状噪声。加载权重后必须调用model.eval()。这条坑排除了我最早调试时 90% 的怪问题。提示如果 output 图边缘非常碎第一反应不是调模型而是检查输入尺寸是否过大。图像从 512 直接缩到 128小结构会变成像素级噪声保持 256 到 320 之间通常最稳。5. 用 PiDiNet 做批量后处理与评估5.1 批量推理与结果命名当输入目录里有几十张测试图时不能一张张手动运行。下面这个脚本遍历input/下的图片固定命名规则输出到output/。import os from glob import glob size 256 os.makedirs(output, exist_okTrue) for img_path in sorted(glob(input/*.jpg) glob(input/*.jpeg) glob(input/*.png)): name os.path.splitext(os.path.basename(img_path))[0] edge infer_one(model, img_path, sizesize) cv2.imwrite(foutput/{name}_edge_{size}.png, edge)这段脚本里sorted(glob(...))是为了让输出顺序和输入顺序一致方便批量比对。命名里带上_size参数避免同一张图跑了不同分辨率后互相覆盖。注意文件名最终不要带中文不然后续做表格统计或打包上传时会出现奇怪的编码问题。5.2 边缘图质量验证的关键检查项如果手头有 ground truth可以用 ODS固定阈值下的最优 F-measure和 OIS每张图单独最优阈值评估边缘结果。没有 ground truth 时至少要做一次人工检查边缘线是否连续、是否出现大量背景纹理、细线是否断裂。检查项常见正常表现异常表现边缘连续性目标轮廓完整断裂成短线段背景响应白色区域干净出现点状或网状纹理分辨率变化256 和 320 结果趋势一致128 出现方块感看到异常时优先把preprocess里size从 256 改到 320 重新跑如果问题消失说明模型没坏是输入分辨率不够。如果 320 下仍然断裂再考虑是不是模型加载时少了module.前缀处理。5.3 用半精度和 TorchScript 加速在推理阶段GPU 上可以用半精度减少显存占用。加上torch.cuda.amp后我的 RTX 显卡把 256x256 的推理速度提升了大约 30%。CPU 上没有收益甚至更慢所以该技巧只建议有 CUDA 的环境使用。with torch.no_grad(): with torch.cuda.amp.autocast(): logits model(x.half() if x.is_cuda else x)这段代码的原理是把输入切成 FP16 前向输出仍然是 FP32 范围内的数值。使用前要确认模型权重也是 FP16 或能在前向时转换否则会报类型不匹配。如果采集到稳定的流水线还可以用torch.jit.trace把 PiDiNet 冻结成 TorchScript在 C 部署场景里直接调用这是把毕设项目落地成工程 demo 的一个常见加速技巧。本文还有配套的精品资源点击获取