ARTICLE DETAIL

资讯详情

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

C#部署LDC边缘检测模型:ONNX Runtime推理与调优实战

C#部署LDC边缘检测模型:ONNX Runtime推理与调优实战 简介面向C#开发者与深度学习部署者的边缘检测工程示例以轻量级密集卷积神经网络LDC为核心结合ONNX Runtime实现了在.NET环境中的推理流程。该网络在计算效率与检测精度之间取得平衡具有轻量级结构和高精度特点能快速完成边缘检测特别适合CPU推理或资源受限的嵌入式、边缘设备可用于实时场景。压缩包共68个文件大小约29MB除完整C#源码工程解决方案、项目文件、窗体及公共类外还包含360p、1080p、2160p三种输入分辨率的预训练ONNX模型以及OpenCvSharp、ONNX Runtime依赖库、测试图片和配置文件解压后可用Visual Studio直接加载调试。资源目前已有168人学习适合作为快速上手的参考基线。项目完整演示了从模型加载、图像预处理、推理到边缘概率图后处理与可视化的闭环流程公共模块封装了图像缩放、归一化、结果绘制等逻辑便于移植到自有图像处理管线通过切换不同分辨率模型用户还能直观评估输入尺寸对边缘细节与检测耗时的影响是理解C#调用ONNX模型、落地边缘检测任务的不错案例。1. C# Onnx 跑轻量级密集卷积网络 LDC边缘检测的落地第一步如果你在做一个 C# 上位机项目需要在工业相机、普通 USB 摄像头甚至一张静态图上实时提取边缘又不想把 OpenCV 的 Canny 阈值反复调到怀疑人生那直接上一套训练好的神经网络边缘检测模型是更省事的路。C# Onnx 推理组合的好处是模型是 ONNX 格式推理走 Onnx Runtime不绑死任何深度学习框架LDC 这种轻量级密集卷积网络模型文件通常只有几 MB在 CPU 上跑一次前向也就是十几毫秒到几十毫秒的量级比 PiDiNet 这类稍重的模型更适合工控机部署。这篇笔记我会按自己的落地习惯从模型导出讲到 C# 推理代码、参数调优和踩坑记录给想把这个方案接进自己项目的人一条能直接照着走的路。2. 理解 LDC轻量级密集卷积网络凭什么做边缘检测2.1 Dense Connection 的价值低参数量下的梯度流动边缘检测不是简单算梯度。像 Canny 这类经典算子对光照、噪声、纹理太敏感换个场景阈值就要重调。神经网络的思路是把边缘检测当成一个密集预测任务输入一张图输出一张同尺寸的概率图每个像素代表它属于边缘的可能性。要做到这个效果网络必须同时保留浅层的细粒度纹理和深层的语义轮廓而 LDC 这类轻量级密集卷积网络的核心就是靠 Dense Connection 把这两者串起来。Dense Connection 的意思是在一个 Dense Block 内部每一层卷积的输入都拼接上前面所有层的输出。这样做的好处首先是特征复用网络不需要在每一层重新学一遍低级特征参数总量明显下降。其次是梯度流动路径短反向传播时梯度能直接从最后一层流向第一层训练收敛更稳。对于边缘检测这种逐像素预测任务浅层特征和深层语义同样重要密集连接天然适合做多尺度特征融合。我最初接触 LDC 时把它和 HED、PiDiNet 放在一起对比过。HED 是典型的早融合方案通过多个侧输出层做监督PiDiNet 则是用像素差分卷积来显式建模边缘方向。LDC 选择了一条更简单的路把 Dense Block 堆起来最后接一个 1x1 卷积把特征压成单通道边缘概率图。它的优势不在刷新精度榜单而在模型体积和推理速度的平衡。实测下来同样输入尺寸下 LDC 的参数量往往只有 VGG16 主干网络的十分之一左右非常适合 CPU 部署。2.2 LDC 与 Prewitt、Canny 的本质区别学习型边缘 vs 手工算子很多人会用 Prewitt 算子来验证边缘检测原理两个 3x3 的卷积核分别响应横向和纵向梯度。Canny 在此基础上加了高斯平滑、非极大值抑制和双阈值滞后连接。这类手工算子的共性是梯度大的地方就认为是边缘但没办法区分“有意义的轮廓”和“纹理噪声”。比如一张满是树叶的照片Canny 会输出密密麻麻的纹理边缘而神经网络边缘检测模型经过数据集训练后能学会把树叶纹理压下去、把树干轮廓保留下来。LDC 的输入端是原始 RGB 图像输出端是 0 到 1 的连续概率图。它不需要手动设定卷积核所有边缘特征都是从训练数据里学出来的。这里有个容易被忽略的细节模型的训练数据集决定了它的“审美”。如果训练数据以室内物体为主那么它在室外场景上的表现就可能打折。所以你在 C# 里部署 LDC 时最好先确认模型的训练数据和你业务场景是否接近否则后处理调参救不回来。从部署角度看LDC 和 Prewitt 还有一个本质区别Prewitt 是固定权重任何语言几行代码就能实现而 LDC 的权重是训练出来的必须以神经网络格式分发。ONNX 在这里起到的作用就是模型打包标准。你可以把 PyTorch、TensorFlow 训练好的 LDC 结构导出成 ONNX 文件C# 项目里通过 Onnx Runtime 加载。这也就是标题里“C# Onnx”这个组合的由来。2.3 拿到模型第一步从 PyTorch 导出 ONNX 的关键参数大多数开源 LDC 实现是用 PyTorch 训练的你要把它用到 C# 里第一步是导出 ONNX。这一步看着简单实际上很容易导出后跑不通。下面是一段我常用的导出脚本关键点都写在注释里。import torch model LDC(num_classes1).eval() # 假设模型只输出单通道边缘概率图 state_dict torch.load(ldc_edge.pth, map_locationcpu) model.load_state_dict(state_dict) dummy_input torch.randn(1, 3, 512, 512) # 固定输入尺寸1张图、3通道、512x512 torch.onnx.export( model, dummy_input, ldc_edge.onnx, input_names[input], output_names[output], dynamic_axes{ input: {0: batch}, output: {0: batch} }, opset_version12, do_constant_foldingTrue ) print(export done)这段代码里dummy_input的形状必须和模型前向期望的输入一致。很多模型在 forward 里写死了宽高你换成 256x256 反而报错所以导出前先用这个尺寸跑一次model(dummy_input)确认能通过。dynamic_axes只把 batch 维设为动态是因为 Onnx Runtime 对固定尺寸的优化更激进如果你希望 C# 侧可以传任意尺寸可以把宽高也加进去但会增加首次推理的耗时。导出完成后先用onnxruntime在 Python 侧验证一遍确认输出形状是[1, 1, 512, 512]而不是[1, 512, 512, 1]。这个通道顺序问题直接关系到 C# 后处理代码怎么写。Python 侧验证通过后再进入 C# 工程。3. 在 C# 侧搭建 Onnx Runtime 推理工程3.1 工程结构与 NuGet 包C# 调用 ONNX 最直接的方式是用Microsoft.ML.OnnxRuntime这个 NuGet 包它封装了原生推理引擎不需要你手动 P/Invoke。我一般的解决方案结构是这样EdgeDetector/ ├── EdgeDetector.csproj └── EdgeDetector/ ├── LdcInference.cs ├── ImagePreprocessor.cs ├── ImagePostprocessor.cs ├── Models/ │ └── ldc_edge.onnx └── Program.cs创建项目时目标框架我建议选 .NET 6 或 .NET 8不要选 .NET Framework 除非你的项目被迫留在老环境。Onnx Runtime 官方 NuGet 包对 .NET Core/.NET 5 的支持更积极版本更新也快。EdgeDetector.csproj里需要加这几项Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeExe/OutputType TargetFrameworknet8.0/TargetFramework AllowUnsafeBlockstrue/AllowUnsafeBlocks Platformsx64/Platforms /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.ML.OnnxRuntime Version1.16.3 / PackageReference IncludeSystem.Drawing.Common Version8.0.0 / /ItemGroup ItemGroup None UpdateModels\ldc_edge.onnx CopyToOutputDirectoryPreserveNewest / /ItemGroup /Project这里有个选择System.Drawing.Common是 Windows 上处理 Bitmap 的老牌库简单但性能一般。如果要做实时视频流我会改用 OpenCvSharp4但为了让新手能少引入一个依赖下面代码我仍用System.Drawing.Common演示你后续按需替换就行。3.2 推理器核心代码Session 创建、输入输出检查推理器对外只暴露两个方法LoadModel和Predict。加载模型时Onnx Runtime 会解析整个计算图并把算子调度到对应执行逻辑上所以 Session 对象建议复用不要每张图都重新 new 一个。using System; using System.Collections.Generic; using System.Drawing; using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class LdcInference : IDisposable { private InferenceSession _session; private int _inputHeight; private int _inputWidth; public LdcInference(string modelPath, int inputHeight 512, int inputWidth 512) { _inputHeight inputHeight; _inputWidth inputWidth; _session new InferenceSession(modelPath); Console.WriteLine(模型输入节点:); foreach (var input in _session.InputMetadata) { Console.WriteLine($ {input.Key}: {string.Join(,, input.Value.Dimensions)}); } Console.WriteLine(模型输出节点:); foreach (var output in _session.OutputMetadata) { Console.WriteLine($ {output.Key}: {string.Join(,, output.Value.Dimensions)}); } } public float[] Predict(Bitmap bitmap) { // 预处理、张量转换、推理、后处理由下面逐步完善 return null; } public void Dispose() { _session?.Dispose(); } }代码里加载完模型立刻打印输入输出节点信息这是排查问题的第一道关卡。很多模型导出的输入名不是input而是images、x之类输出名也可能是output、logits、edge。你要在构造DenseTensor时用_session.InputMetadata.Keys里的实际名字而不是凭感觉猜。Dimensions 数组里也会明确写出模型期望的维度如果它显示[1,3,512,512]而你传入的是[1,512,512,3]后处理出来的边缘图大概率是坏的。3.3 图像预处理Bitmap 转张量HWC 到 CHW归一化神经网络输入要求连续内存的张量C# 里最常见的做法是把 Bitmap 的像素数据复制到 float 数组里再装进DenseTensorfloat。顺序上有一点特别容易错Bitmap 的 Scan0 内存布局一般是 BGR 或 RGB 按行排列也就是 HWC而 PyTorch 模型要求的是 CHW。所以你要手动转换维度和通道顺序。public static float[] Preprocess(Bitmap src, int targetHeight, int targetWidth) { using var resized new Bitmap(src, new Size(targetWidth, targetHeight)); var rect new Rectangle(0, 0, targetWidth, targetHeight); var bmpData resized.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); int stride bmpData.Stride; byte[] raw new byte[stride * targetHeight]; System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, raw, 0, raw.Length); resized.UnlockBits(bmpData); float[] result new float[3 * targetHeight * targetWidth]; int index 0; for (int c 0; c 3; c) { for (int h 0; h targetHeight; h) { for (int w 0; w targetWidth; w) { int offset h * stride w * 3; byte b raw[offset]; byte g raw[offset 1]; byte r raw[offset 2]; float value (c 0) ? r : (c 1) ? g : b; result[index] value / 255.0f; } } } return result; }这段代码先把图像缩放到模型输入尺寸然后锁定内存直接读像素字节。stride不等于 width 乘以 3系统为了内存对齐可能多出几个字节所以每行偏移一定要用h * stride。通道顺序上我读出来的变量名是b、g、r但赋值给哪个通道取决于你用的模型训练时的通道顺序。如果模型是 OpenCV 训练出来的通常是 BGR如果是 PyTorch 标准 ImageNet 训练通常是 RGB。这里的判断逻辑我放到第 5 章避坑部分详细展开。3.4 后处理张量转 Bitmap 边缘图模型输出是一个一维数组长度是batch * channels * height * width。你要把它重新组织成一张灰度图并做一次可选的反向归一化。public static Bitmap Postprocess(float[] output, int height, int width) { var bmp new Bitmap(width, height, PixelFormat.Format24bppRgb); var rect new Rectangle(0, 0, width, height); var bmpData bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); int stride bmpData.Stride; byte[] raw new byte[stride * height]; for (int h 0; h height; h) { for (int w 0; w width; w) { int srcIndex h * width w; // 单通道输出 float value output[srcIndex]; byte pixel (byte)Math.Clamp(value * 255.0f, 0, 255); int dstIndex h * stride w * 3; raw[dstIndex] pixel; raw[dstIndex 1] pixel; raw[dstIndex 2] pixel; } } System.Runtime.InteropServices.Marshal.Copy(raw, 0, bmpData.Scan0, raw.Length); bmp.UnlockBits(bmpData); return bmp; }这一段把单通道的概率值映射到 0-255生成灰度边缘图。srcIndex的计算前提是输出张量形状为[1,1,height,width]是连续的。如果你的输出是[1,height,width]那要把srcIndex改成h * width w之前先确认模型输出维度否则图像会错位成条纹或雪花。后处理的阈值问题我还没加先保留概率值调试时更直观。后面调参数阶段再决定是做二值化还是保留灰度。4. 边缘检测推理的三个必调参数与调优顺序4.1 推理尺寸LDC 的输入分辨率与感受野很多 LDC 结构图里写着 Dense Block但没告诉你它实际感受野多大。推理尺寸直接决定边缘的粗细和断裂程度。输入尺寸越大小结构保留得越多但耗时近似平方上涨。我常用的一组对照是 256、512、1024256x256速度最快单帧 CPU 可以在 10ms 内完成但细边缘容易变成断线适合实时预览。512x512速度与效果平衡多数开源模型训练时也常用这个尺寸。1024x1024能画得很细但 CPU 推理时间可能到 200ms 以上只适合离线处理。需要强调推理尺寸不是越大越好。边缘检测模型里的 Dense Block 下采样倍数固定如果输入太大输出层感受野覆盖不到某些细结构反而会在边缘附近产生双影。我一般先按模型训练时的尺寸走再通过torch.onnx.export的dummy_input固定下来C# 侧不做随机缩放。4.2 归一化常数0-255、0-1 还是 ImageNet 均值方差这是最容易翻车的地方。PyTorch 模型训练时通常把输入张量标准化到[0,1]或者减去 ImageNet 均值再除以方差。ONNX 模型本身不记录归一化参数所以你在 C# 里看到的输入要求只能靠推测。第 3 章代码里写的是value / 255.0f这只适合训练时分母是 255 的模型。如果训练时用的是transform.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225])那么预处理就要改成float value (c 0) ? r : (c 1) ? g : b; result[index] (value / 255.0f - mean[c]) / std[c];判断方法很简单导出 ONNX 前在 Python 里用一张已知图片分别用 0-1 和 ImageNet 归一化喂进模型比较输出边缘的强弱。然后在 C# 侧复现相同的公式。如果输出边缘明显变淡十有八九是归一化没配对。4.3 输出阈值与形态学去噪模型输出是连续概率值通常在 0 到 1 之间但不会严格等于 1。要得到清晰的单像素边缘必须做阈值处理。我一般会先看灰度边缘图的直方图确定边缘像素和背景像素的分界点。常见做法是设定一个 0.2 到 0.5 之间的阈值把低于阈值的像素置 0高于阈值的保留原值或置 255。阈值之后还可能存在离散噪点。我习惯再用一次形态学开运算先腐蚀后膨胀把孤立点消掉。这一步在 C# 里可以直接用 OpenCvSharp 的Cv2.MorphologyEx不需要自己写。要注意的是开运算的核大小不要超过 3x3否则会把真正的细边缘一起抹掉。public static Bitmap ApplyThresholdAndDenoise(Bitmap edge, float threshold) { // 这里用 OpenCvSharp 做示例因为 System.Drawing.Common 不含形态学算子 using var mat OpenCvSharp.Extensions.BitmapConverter.ToMat(edge); using var gray mat.CvtColor(OpenCvSharp.ColorConversionCodes.BGR2GRAY); using var binary new OpenCvSharp.Mat(); Cv2.Threshold(gray, binary, threshold * 255.0, 255, OpenCvSharp.ThresholdTypes.Binary); using var kernel Cv2.GetStructuringElement(OpenCvSharp.MorphShapes.Ellipse, new OpenCvSharp.Size(3, 3)); Cv2.MorphologyEx(binary, binary, OpenCvSharp.MorphTypes.Open, kernel); return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(binary); }threshold的建议取值范围我放在下面的表里。注意这里我把阈值乘了 255是因为前面Cv2.Threshold是作用在 0-255 灰度图上的不是 0-1 概率图。4.4 参数速查表我把上面几节涉及的参数整理成一张表方便你直接抄参数项建议值说明输入尺寸512x512速度与效果平衡CPU 单帧约 30-50ms缩放策略直接 Resize拉伸会导致边缘变形但实现最简单通道顺序按训练框架PyTorch 通常 RGBOpenCV 训练通常 BGR归一化0-1 或 ImageNet必须与训练时一致输出阈值0.2-0.5先用直方图观察再定形态学核3x3 Ellipse过大丢失细边缘运行库OnnxRuntime CPU可换 GPU 版 NuGet 包这张表不是金科玉律但能覆盖绝大多数模型。我每次接到一个新的 ONNX 边缘检测模型都先按这个表跑通然后再一项项调。调的顺序建议是先固定输入尺寸再查归一化最后调阈值。其中归一化错了后面调阈值意义不大。5. C# Onnx 推理最常见的 5 个坑5.1 坑 1模型加载就崩报 DllNotFoundException现象new InferenceSession(modelPath)抛出DllNotFoundException提示找不到onnxruntime.dll或某个原生依赖。原因Microsoft.ML.OnnxRuntimeNuGet 包只在包目录下包含对应平台的原生库如果你的项目PlatformTarget是AnyCPU运行时可能加载失败另外释放模式没有把runtimes/win-x64/native下的文件复制到输出目录。解决把项目平台设为 x64并在 csproj 里加Platformsx64/Platforms。如果还报错手动检查输出目录里onnxruntime.dll是否存在。不要直接把 x86 和 x64 的原生库混在同一目录Windows 的 DLL 搜索顺序会让你非常痛苦。5.2 坑 2输入张量名称猜错推理时维度冲突现象代码里写了input但模型里叫images运行时抛出InvalidArgument提示输入维度不匹配或找不到输入名。原因ONNX 文件里的输入输出名并不是模型类名而是导出脚本里input_names参数指定。即使指定了有些框架导出时会额外加上伪节点。解决严格用_session.InputMetadata.Keys读取实际名称。代码里我已经在构造函数中打印过这些信息部署时不要跳过这步。输入名称正确后再检查维度。如果维度显示[1,3,512,512]但你DenseTensorfloat的维度是[1,512,512,3]需要回到预处理部分改 HWC 到 CHW 的顺序。5.3 坑 3边缘图变成“噪点云”而不是清晰的线现象输出边缘图上的轮廓很粗边缘内外全是斑点完全看不出物体边界。原因绝大多数情况是 BGR 和 RGB 通道顺序混用。OpenCV 读图后连续内存是 BGR而 PyTorch 训练库很多默认用 RGB。如果你在 C# 侧读 Bitmap 时按 RGB 赋值但模型是在 OpenCV 数据管线里训练的那卷积核学到的是反色通道特征边缘响应自然乱七八糟。解决把预处理代码里的通道映射改成(c 0) ? r : (c 1) ? g : b的反向即第一个通道取 B。我习惯先跑一张已知边界清晰的图像比如黑白棋盘格如果输出边缘正常说明通道顺序对如果输出边界像多了两层虚影就反转通道顺序再试。5.4 坑 4输出全黑或全白概率值范围异常现象后处理生成的边缘图全黑或者整个画面近乎全白只有很淡的轮廓。原因输出张量不是你想象中的概率值。可能模型输出的是 logits未经过 Sigmoid范围在 -5 到 5 之间也可能输出张量里保存的是两个通道比如一个是边缘概率一个是内部区域概率。全黑时如果 logits 是负数你直接乘以 255 得到的像素是 0全白时正数 logits 会溢出成 255。解决在 Python 侧用 onnxruntime 打印输出的数值范围。如果输出范围明显超出 0-1就要在 C# 后处理前加一步Sigmoid激活函数。float activation 1.0f / (1.0f (float)Math.Exp(-value));然后才做阈值或灰度映射。另外如果你发现输出维度是[1,2,height,width]先检查是否第二个通道才是边缘图或者两个通道分别对应边缘和背景需要做 softmax 后取边缘通道。提示确认输出结构最直接的方式是打印output.Value.Shape不要靠猜。5.5 坑 5GPU 包报 CUDA 错误CPU 反而正常现象换成Microsoft.ML.OnnxRuntime.Gpu后调用InferenceSession直接抛CUDA error: the provided PTX was compiled with an incompatible version之类的异常。原因GPU 版本的 Onnx Runtime 对 CUDA 和 cuDNN 版本有严格要求NuGet 包只是运行时外壳还要你本机装有匹配的显卡驱动和 CUDA 运行时。很多人只看显卡能用就装了最新驱动没装对应 CUDA Toolkit。解决如果你不是专门做 GPU 推理优化CPU 版其实够用。LDC 这类轻量级模型在 CPU 上的性能已经不错省去 CUDA 环境维护成本。如果必须用 GPU先查 Onnx Runtime 官方文档里对应版本要求的 CUDA 版本装好后用InferenceSession打印SessionOptions.AppendExecutionProvider_CUDA(0)的执行提供器列表确认 CUDA 真正被启用而不是 fallback 到 CPU。6. 再进一步LDC 边缘检测的实时化与量化6.1 用 ONNX Runtime 自带 API 做 INT8 量化LDC 模型虽然轻但在低端工控机上仍可能想象力不足。常见做法是把 FP32 模型量化成 INT8。C# 侧可以直接用Microsoft.ML.OnnxRuntime.Quantization命名空间里的工具但更可控的方式是在 Python 侧用onnxruntime.quantization先量化好再让 C# 加载量化后的 ONNX 文件。量化需要一份校准数据集不需要标注只要有几十到几百张和业务场景相近的图像。校准时会统计每层激活值的范围然后用整数 8 位近似浮点计算。量化后模型体积能降到四分之一速度可能提升 1.5 到 3 倍但边缘检测这类对细节敏感的任务量化后细边缘容易出现断裂。我做量化后一定会做一个 A/B 测试同一张图分别用 FP32 和 INT8 推理对比边缘像素占比和检测出的碎线段数量。from onnxruntime.quantization import quantize_static, QuantType from onnxruntime.quantization import CalibrationMethod quantize_static( model_inputldc_edge.onnx, model_outputldc_edge_int8.onnx, calibration_datasetMyCalibrationData(), # 你的数据加载器 quant_formatQuantType.QInt8, per_channelTrue, calibration_methodCalibrationMethod.MinMax, )这段代码里per_channelTrue对边缘检测结果影响很大。按通道量化比按张量量化更精细保留了每个卷积核的数值分布差异边缘细节保留得更好。CalibrationMethod.MinMax是最简单的如果模型输出明显漂移可以换成Entropy方法。6.2 用 DirectShow/UVC 摄像头回调接实时边缘检测C# 里接 USB 摄像头实时边缘检测我一般分两条路老式 DirectShow 方案用AForge.NET或OpenCvSharp的 VideoCapture新式 UVC 方案直接用 MediaFoundation。很多工业相机上实际走的是 UVC 协议回调里拿到的是 YUY2 或 MJPG 帧要先转成 RGB 才能喂给 LDC。OpenCvSharp 的VideoCapture会在内部线程里持续回调每来一帧就丢给 LDC 推理然后把边缘图画到 PictureBox 上。需要注意推理耗时小于帧间隔时还好如果推理慢于帧率输出会出现明显延迟。我常用一个双缓冲队列摄像头线程往队列里塞原始帧推理线程从队列里取最新的帧处理取不到就丢弃保证界面始终显示最新边缘结果。using var capture new VideoCapture(0); using var frame new Mat(); while (true) { capture.Read(frame); if (frame.Empty()) continue; using var bitmap BitmapConverter.ToBitmap(frame); float[] edge _inference.Predict(bitmap); using var edgeBmp LdcPostprocess.Postprocess(edge, _inputHeight, _inputWidth); pictureBox.Image edgeBmp; Thread.Sleep(1); }这段代码没有处理摄像头回传像素格式实际中量产后你会遇到 UVC 回调里区分多个摄像头的问题解决办法是枚举VideoCapture的索引或用设备路径匹配。这些与 LDC 本身无关但会让你的实时边缘检测管线真正可用。6.3 验证边缘检测效果不能只看“看起来像”最后一个进阶技巧是建立量化验证指标。边缘检测不像分类任务有准确率常见做法是算边缘像素占比、连通区域数量、单像素宽度比例。我自己的习惯是准备一套十张左右的真实业务图分别用 Canny 和 LDC 跑一遍对比边缘像素占比的稳定性。如果 LDC 在同一场景不同亮度下边缘像素占比波动很大说明模型过拟合亮度特征需要加数据增强重新训练或者回到第 4 章把归一化参数检查一遍。我还会在跑验证时把边缘图和原图做一下叠加直接观察边缘是否贴合物体轮廓。这一步用 C# 的Graphics.DrawImage就能叠不需要 OpenCV。如果边缘比真实轮廓粗了两个像素以上我习惯做一次腐蚀操作收缩边缘。腐蚀的迭代次数一般 1 到 2 次迭代太多会让边缘消失。在这些验证做完之前我不会轻易把模型接到产线上。这也是我屡次翻车后养成的习惯先跑通再量化再验证最后才上实时管线。希望这份笔记能帮你在自己的 C# Onnx 边缘检测项目里少走几步弯路。本文还有配套的精品资源点击获取
返回列表