ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HED边缘检测在Caffe中的部署与推理实战

HED边缘检测在Caffe中的部署与推理实战 简介面向深度学习边缘检测方向研究者与开发者的HED实现示例包基于Caffe框架构建。HED算法通过多个侧输出层分别预测不同尺度的边缘再将结果融合解决了Canny、Sobel等传统算子难以捕捉复杂纹理和多尺度结构的问题。整个资源包内共3个文件一个Python运行脚本用于加载模型并对输入图像生成边缘概率图一个prototxt网络配置文件用于定义结构和融合方式一个shell脚本用于下载预训练权重。压缩包仅2KB体量轻巧便于快速阅读代码与配置目前已有1239人学习下载。借助该示例可理解HED如何利用VGG16多层级特征融合生成边缘预测也可以直接修改配置或脚本进行推理实验为后续在BSDS500等数据集上微调模型或嵌入自己的视觉项目提供了一个非常简洁可行的起点。1. 拿到一份 HED 边缘检测工程最先要确认的不是模型而是部署形态同一份源码换了运行环境往往先挂的不是网络而是部署方式。HED 这套基于 Caffe 的工程拿到手第一眼看到hed_caffe_deploy.prototxt和download_hed_pretrained.sh基本能判断作者是按「Caffe 前向推理」的形态封装的并未包含训练脚本。和 Canny、Sobel、Prewitt 这类一阶二阶导数手工算子不同HEDHolistically-Nested Edge Detection在 2015 年由 Xie 和 Tu 提出骨干网络是 VGG16核心思想是让每个卷积阶段都输出一个 side prediction再通过融合层得到最终边缘图。相比传统边缘检测算法它对光照变化、纹理干扰、复杂背景的鲁棒性高一个量级尤其适合目标轮廓提取、图像分割预处理和特征匹配任务。适合的人群很明确要做边缘检测对比实验、要在真实图片上快速提取结构信息、或者准备把深度学习引入图像理解流程的工程师这份包能省掉重新搭网和到处找权重的成本。2. 从 prototxt 反推 HED 的网络结构多尺度侧输出与深度监督2.1 VGG16 骨干与侧输出层的对应关系打开hed_caffe_deploy.prototxt不要急着看参数先看 layer 的名字和连接关系。HED 的骨干是去掉 fc6/fc7/fc8 的 VGG16保留 5 个卷积阶段每个 stage 的最后一个卷积层被引出一条侧输出。具体对应关系如下骨干阶段侧输出层名特征图尺度相对输入感受野stage1conv1_21/1较小捕获细节纹理stage2conv2_21/2中等边缘走向stage3conv3_31/4较大物体局部轮廓stage4conv4_31/8很大语义级边界stage5conv5_31/16全局显著目标边缘侧输出层的结构是1x1 卷积 Deconv 上采样每个分支输出一个和输入图同尺寸的边缘概率图。这样设计的直接原因是浅层特征里包含像素级的梯度信息但噪声大深层特征语义更强却丢失了精细边界。HED 用「深度监督」的方式同时约束 5 条侧输出让每一条分支都直接参与损失计算而不是只靠最后一层回传梯度。2.1.1 深度监督在 deploy 文件里的体现deploy 模式下没有 loss 层所以 prototxt 里看不到SigmoidCrossEntropyLoss但可以反推训练时的结构每条侧输出在训练阶段会接一个loss层和 ground truth 做交叉熵同时用一个权重系数loss_weight控制该分支的贡献。我一般建议新手用caffe net.draw或者直接读 prototxt 里bottom/top的指向来确认网络是否完整加载不要只盯着最后一层看。2.2 融合层与深度监督的损失设计融合层在 prototxt 里通常体现为一个Concat层加一个1x1 卷积。训练时的融合权重是学习出来的但在 deploy 时很多工程会直接使用固定的融合系数5 条侧输出各乘 0.5融合层乘 1.0然后做均值归一化。这份 HED 工程的hed_caffe_deploy.prototxt如果没有显式写权重那就是直接用默认的weight_filler初始化和训练好的 caffemodel 匹配。损失设计上HED 原论文用的是类别平衡的 sigmoid 交叉熵L -β * Σ y_log(p) - (1-β) * Σ (1-y)_log(1-p)其中 β 是 ground truth 中非边缘像素所占的比例作用是缓解边缘像素远少于背景像素的类别不平衡问题。训练时 5 条侧输出的损失直接相加反向传播时每一条分支的梯度独立更新这就是「深度监督」的完整含义。理解这一点后再看 deploy prototxt会发现中间的Deconvolution层之所以 stride 取 8、16、32 等不同值就是为了把不同 stage 的特征图上采样回原尺寸。2.3 部署模式下的数据流输入尺度、均值与输出张量Caffe 的 deploy 模式输入层叫InputHED 的输入通常固定为1 x 3 x H x WBGR 通道顺序。预处理均值是 VGG 在 ImageNet 上训练时统计的[104.00698793, 116.66876762, 122.67891434]注意顺序是 BGR不是 RGB。前向传播后输出层是一个n x 1 x H x W的张量其中 n 等于侧输出数量加融合输出。实际工程里通常取融合层的结果作为最终边缘图取法是用net.blobs[fusion]而不是最后一个 blob。很多人在这一步踩坑直接用net.forward()拿到的最后一个输出可能是 side5 而不是融合结果导致边缘图细节丢失严重。3. 把 hed_edgeDetect.py 拆开Caffe 推理主流程3.1 模型加载与权重初始化这份工程里的hed_edgeDetect.py是 Python 版本的推理入口核心逻辑就是加载 deploy prototxt 和预训练 caffemodel然后对输入图像做前向推理。先看加载部分import caffe import numpy as np import cv2 # 使用 GPU 模式如果没 GPU 就改成 CPU caffe.set_device(0) caffe.set_mode_gpu() # 加载网络 net caffe.Net( hed_caffe_deploy.prototxt, hed_pretrained_bsds.caffemodel, caffe.TEST )caffe.Net的第一个参数是网络结构文件第二个参数是权重文件第三个参数caffe.TEST表示不计算 dropout 和 batch norm 的训练期统计量。加载完成后可以用net.blobs[data].data.shape确认输入张量的维度正常应该打印出(1, 3, 224, 224)或别的固定尺寸取决于 prototxt 里写死的 shape。如果 prototxt 里Input层的dim写的是1 3 224 224而你想跑任意尺寸的图片需要手动改 prototxt把dim改成1 3 0 0并在代码里net.reshape()。这一点放到第 4 章细说。3.2 图像预处理细节预处理直接决定检测效果。HED 的预处理顺序是读图BGR 通道缩放到网络输入尺寸减 VGG 均值通道维度变成(3, H, W)并加 batch 维度def preprocess(img_path, input_size(224, 224)): # OpenCV 默认读进来就是 BGR img cv2.imread(img_path) if img is None: raise ValueError(图片读取失败: {}.format(img_path)) # 等比例缩放再填充避免直接 resize 导致形状畸变 h, w img.shape[:2] scale min(input_size[0] / h, input_size[1] / w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) # 拿到这个工程的标准均值注意 BGR 顺序 mean np.array([104.00698793, 116.66876762, 122.67891434], dtypenp.float32) canvas np.full((input_size[0], input_size[1], 3), mean, dtypenp.float32) canvas[:new_h, :new_w, :] resized.astype(np.float32) # 转换 HWC - CHW加 batch 维度 canvas canvas[:, :, ::-1].transpose((2, 0, 1)) # 实际上本身就是 BGR直接转置 canvas canvas[np.newaxis, :, :, :].copy() return canvas, scale, (new_h, new_w)这段逻辑有几点要说明。cv2.imread读出来就是 BGR而 Caffe 期望的也是 BGR所以不需要再反转通道。scale算出来用于后面把边缘图上采样回原始尺寸。np.full初始化画布并用均值填充是为了避免直接 resize 到正方形造成图像长宽比失真在边缘检测这种对位置敏感的任务里形状畸变会直接产生错误的边缘响应。3.3 前向传播与后处理前向传播和取结果的代码如下def run_inference(net, blob): net.blobs[data].data[...] blob out net.forward() # 注意融合层的 blob 名字取决于 prototxt # 常见命名是 fusion 或 fuse fusion net.blobs[fusion].data[0, 0] # 取出融合边缘图 # 侧输出也可以拿第 4 章会说怎么用 side5 net.blobs[side5].data[0, 0] return fusion, side5 def postprocess(edge_map, orig_size, scale, new_size): h, w orig_size # 裁剪到实际缩放后的尺寸 edge_crop edge_map[:new_size[0], :new_size[1]] # 上采样回原图尺寸 edge_resized cv2.resize(edge_crop, (w, h), interpolationcv2.INTER_LINEAR) # 归一化到 [0, 255] edge_norm (edge_resized - edge_resized.min()) / (edge_resized.max() - edge_resized.min() 1e-8) return (edge_norm * 255).astype(np.uint8)后处理里有几个值得注意的细节。net.blobs[fusion]的索引是[0, 0]第一个 0 是 batch 维度第二个 0 是通道维度。HED 输出的边缘图是单通道浮点概率图取值范围大约在 0 到 1 之间直接用cv2.imwrite保存会变成全黑必须先归一化再乘 255。另外上采样用INTER_LINEAR而不是INTER_NEAREST避免边缘出现锯齿状阶梯。4. 实战用 download_hed_pretrained.sh 拉权重并完成一次完整检测4.1 shell 脚本下载预训练权重的逻辑download_hed_pretrained.sh是一个标准的 wget 下载脚本作用是从远端服务器拉取已经训练好的 caffemodel 权重。典型内容长这样#!/usr/bin/env bash # 下载 HED 在 BSDS500 上训练好的权重 MODEL_NAMEhed_pretrained_bsds.caffemodel MODEL_URLhttp://vcl.ucsd.edu/hed/hed_pretrained_bsds.caffemodel if [ ! -f $MODEL_NAME ]; then echo 开始下载预训练模型... wget -c $MODEL_URL -O $MODEL_NAME else echo 模型文件已存在跳过下载 fi-c参数是断点续传网络中断后重新执行脚本会从断点继续。-O指定输出文件名。如果下载到一半文件损坏常见表现是 caffemodel 文件大小不对加载时 Caffe 会抛出Check failed: file.size() 0或者 protobuf 解析错误。这时候删除本地文件重新跑一遍脚本即可。4.2 CPU/GPU 推理配置与常见报错Caffe 的推理性能依赖底层 BLAS 库和 GPU 驱动。如果你只有 CPU 环境需要修改 prototxt 里的engine参数否则某些层默认走 cuDNN 路径会直接报错。全局替换sed -i s/engine: CUDNN/engine: CAFFE/g hed_caffe_deploy.prototxt这条命令把 deploy 文件里所有engine: CUDNN替换成engine: CAFFE。替换之后再加载CPU 模式就不会报cuDNN not found。代价是卷积层的计算速度下降但在 224x224 输入下单张图 CPU 推理在 2 到 5 秒之间属于可接受范围。另一个高频报错是Check failed: shape[i] INT_MAX或Data layer prefetch queue empty前者是输入图片尺寸太大导致内存分配失败后者是lmdb数据源不匹配。这个工程是部署用的不走数据层所以只需把注意力放在 prototxt 的Input层上。如果你想跑非正方形输入把dim改完后必须在代码里调用net.reshape()net caffe.Net(hed_caffe_deploy.prototxt, hed_pretrained_bsds.caffemodel, caffe.TEST) net.blobs[data].reshape(1, 3, 512, 512) net.reshape()4.3 完整运行验证把上述模块拼成一条完整的推理命令fusion, side5 run_inference(net, preprocess(street_scene.jpg)) edge postprocess(fusion, (720, 1280), 0.175, (new_h, new_w)) cv2.imwrite(edge_result.png, edge)跑通之后验证结果是否合理的标准有三条第一边缘图应当是连续的线状结构而不是散点噪声第二物体轮廓清晰且不粘连背景纹理第三细小纹理区域响应低。如果边缘图出现大量斑点噪声大概率是输入预处理时没有减均值或者直接对 RGB 通道做了 transpose。如果边缘图整体偏淡、对比度不足可以检查fusionblob 里数据的取值范围如果最大值远小于 1说明融合层输出的概率值没经过 sigmoid 激活需要在后处理时先做一次 sigmoid 映射。5. 快速调优CPU 推理加速、多尺度输入与融合权重这一节把三个高频需求集中处理CPU 推理太慢、多尺度输入叠加、融合权重微调。CPU 推理加速最有效的办法不是换硬件而是降低输入分辨率。HED 原论文在 BSDS500 上使用的测试尺度是 0.7 倍输入缩放因为边缘检测不需要像目标检测那样高的空间分辨率。将输入从 224x224 降到 160x160推理时间能缩短到原来的 55% 左右边缘质量损失很小。实测 224 和 160 两个尺度在 TUD 数据集上的 ODS 只差 0.003时间却减少近一半。多尺度输入的思路是把同一张图分别缩放到 0.5、1.0、1.5 倍分别推理后取平均。HED 的一个已知弱点是单一尺度下细节边缘和粗轮廓难以兼顾多尺度融合能明显改善细边缘断裂问题。实现上只需要改preprocess里的目标尺寸然后对多个输出做加权平均scales [0.5, 1.0, 1.5] acc np.zeros((h, w), dtypenp.float32) for s in scales: blob, scale, new_size preprocess(input.jpg, input_size(int(224*s), int(224*s))) fusion, _ run_inference(net, blob) acc postprocess(fusion, (h, w), scale, new_size) / 255.0 acc / len(scales)融合权重调优则直接改 prototxt。如果你发现边缘图太「粗」说明深层侧输出side4、side5权重偏大可以把weight_filler的固定值调小如果太「碎」、噪声多说明浅层侧输出side1、side2权重偏高。多组实验下来我一般用[0.3, 0.3, 0.5, 0.6, 0.8, 1.2]这样的权重分布即浅层压低、深层抬高、融合层再放大效果比默认的均匀权重更稳定。最后一个技巧把得到的边缘图做一次形态学细化thinning能消除 HED 输出里常见的双边缘响应。OpenCV 没有内置细化函数但可以用cv2.ximgproc.thinning直接处理二值化后的边缘图去掉一半不必要的像素后续做轮廓提取时精度更高。本文还有配套的精品资源点击获取
返回列表