尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C#工业视觉实战:基于OnnxRuntime部署DAViD深度估计模型

C#工业视觉实战:基于OnnxRuntime部署DAViD深度估计模型 1. 项目概述当C#遇上实时深度估计最近在做一个工业视觉检测的项目客户要求在不更换现有工控机Windows系统.NET环境的前提下实现对产品表面微小凹陷的深度进行实时、非接触式测量。传统的激光位移传感器方案成本高且安装空间受限。这时基于单目RGB图像的深度估计技术就成了一个极具吸引力的选项。在众多模型中DAViD以其在复杂纹理和弱纹理区域都能保持良好一致性的特点进入了我的视野。然而DAViD官方通常提供PyTorch或TensorFlow的模型如何将其无缝集成到以C#为核心的工业上位机软件中就成了一个必须解决的核心工程问题。OnnxRuntime的出现恰好为这个难题提供了一个优雅的桥梁。它允许我们将训练好的模型转换为ONNX格式然后在C#环境中高效地加载和推理省去了部署Python环境或调用外部进程的麻烦真正实现了“模型即服务”的嵌入式集成。这个项目就是关于如何将DAViD深度估计模型通过OnnxRuntime部署到C#应用中的完整实践记录。2. 技术选型与方案设计思路2.1 为什么是OnnxRuntime在C#中调用深度学习模型常见的路径有几种通过Python.NET或IronPython进行互操作、将模型封装为gRPC/HTTP服务、或者使用像TensorFlow.NET这样的原生绑定库。前两者引入了额外的复杂性和网络延迟不适合对实时性要求高的本地应用后者则对特定框架依赖性强生态和性能优化可能不及专为推理设计的运行时。OnnxRuntimeORT是一个跨平台的高性能推理引擎专门为ONNX格式的模型优化。它的C# API (Microsoft.ML.OnnxRuntime) 成熟稳定由微软官方维护与.NET生态集成度极高。选择ORT的核心理由有三点性能优异ORT内置了大量算子优化支持CPU、GPUCUDA、DirectML、TensorRT等多种执行提供程序Execution Provider, EP能充分发挥硬件算力。对于工业场景我们可以根据工控机是否有独立显卡来灵活选择EP最大化推理速度。格式统一ONNX作为一种开放的模型格式成为了PyTorch、TensorFlow、PaddlePaddle等主流框架之间互操作的“中间语言”。这意味着无论你的DAViD模型最初来自哪个框架都可以先转换为ONNX再被C#应用统一调用极大地降低了模型部署的框架耦合度。部署简洁最终交付物只需要模型文件.onnx和OnnxRuntime的动态库DLL无需在客户现场安装Python或复杂的深度学习框架真正做到了开箱即用减少了运维成本。2.2 DAViD模型特点与转换考量DAViDDense Affine-invariant Visual Descriptor是一种用于单目深度估计的深度学习模型。它通常采用编码器-解码器结构编码器如ResNet提取图像的多尺度特征解码器则逐步上采样并融合这些特征最终输出与输入图像尺寸对应的深度图每个像素值代表估计的深度或视差。在准备将其部署到C#前我们需要对原始模型进行转换和分析输入输出明确模型的输入张量Tensor形状和数据类型。例如输入通常是[1, 3, H, W]的浮点型RGB图像经过归一化输出是[1, 1, H, W]或[1, H, W]的深度图。动态维度工业应用中输入图像尺寸可能不固定。在导出ONNX模型时可以考虑将高度和宽度维度设置为动态的如[1, 3, -1, -1]以增加模型的灵活性。但需注意这可能会牺牲一些优化机会且需要确保预处理和后处理逻辑能适应可变尺寸。后处理模型输出的深度图往往是归一化后的相对值或视差值。我们需要在C#端根据相机内参和模型训练时的设定将其转换为有物理意义的绝对深度值例如毫米。这部分后处理逻辑需要与模型训练代码对齐并同样在C#中实现。注意模型转换是部署的第一步也是最容易出错的一步。务必使用与训练时相同或兼容的框架版本进行转换并在Python环境中先用ONNX Runtime验证转换后的模型推理结果与原始框架一致这一步的验证能避免后续C#调试中很多棘手的问题。3. 环境搭建与核心依赖配置3.1 创建C#项目与安装NuGet包我使用的是Visual Studio 2022和.NET 6或更高版本的控制台应用或WPF/WinForms项目模板。项目创建后通过NuGet包管理器安装以下核心库Microsoft.ML.OnnxRuntime这是核心的推理库。通常安装稳定版本即可例如Microsoft.ML.OnnxRuntime。如果需要GPU加速则根据硬件选择NVIDIA GPU安装Microsoft.ML.OnnxRuntime.Gpu。它依赖于本地的CUDA和cuDNN环境部署时需要确保目标机器上已安装相应版本的CUDA运行时。AMD/Intel GPU 或 任何支持DirectX 12的GPU安装Microsoft.ML.OnnxRuntime.DirectML。这是Windows平台上一个通用性更好的GPU加速方案对驱动版本要求相对宽松。OpenCvSharp4和OpenCvSharp4.runtime.win用于图像加载、颜色空间转换、缩放等预处理操作。它比System.Drawing更擅长处理计算机视觉任务且API与Python版的OpenCV类似方便算法移植。System.Numerics.Tensors可选但推荐在处理多维数组数据时这个库提供了比基础数组更高效和易用的TensorT类型与OnnxRuntime的输入输出数据结构契合度更高。安装命令示例包管理器控制台Install-Package Microsoft.ML.OnnxRuntime Install-Package Microsoft.ML.OnnxRuntime.DirectML # 如果使用DirectML EP Install-Package OpenCvSharp4 Install-Package OpenCvSharp4.runtime.win3.2 模型文件与运行时库的部署策略开发完成后我们需要将应用发布给最终用户。除了生成的可执行文件.exe和依赖的.NET运行时还需要额外处理模型文件 (.onnx)将其作为嵌入资源Embedded Resource打包进程序集或者更常见的放在应用程序根目录的特定子文件夹如Models/下。我倾向于后者因为更新模型时无需重新编译和发布整个应用只需替换文件即可。OnnxRuntime原生库Microsoft.ML.OnnxRuntimeNuGet包会自动将对应平台如win-x64的本地动态库onnxruntime.dll等复制到项目的输出目录。在发布时务必确保这些DLL随主程序一起被拷贝。如果使用GPU版本还需要确保目标机器上有正确的CUDA或DirectX驱动。一个健壮的目录结构可能如下所示YourApp.exe YourApp.dll (其他依赖项) onnxruntime.dll (CPU版核心库) directml.dll (如果使用DirectML EP) cudnn64_8.dll (如果使用CUDA EP需额外部署) Models/ david_depth_estimation.onnx4. 核心代码实现与推理流程拆解4.1 图像预处理从Bitmap到模型输入张量模型的输入通常要求是归一化后的[Batch, Channel, Height, Width]格式的浮点张量。在C#中我们需要将常见的Bitmap或图像文件转换为这个格式。using OpenCvSharp; using System.Numerics.Tensors; public static DenseTensorfloat PreprocessImage(string imagePath, int targetHeight, int targetWidth) { // 1. 使用OpenCV读取图像 using Mat img Cv2.ImRead(imagePath, ImreadModes.Color); if (img.Empty()) throw new ArgumentException(Could not read image.); // 2. 转换颜色空间 BGR - RGB (如果模型是在RGB上训练的) Cv2.CvtColor(img, img, ColorConversionCodes.BGR2RGB); // 3. 调整尺寸至模型预期输入大小 using Mat resized new Mat(); Cv2.Resize(img, resized, new Size(targetWidth, targetHeight)); // 4. 将图像数据转换为浮点型并归一化 (例如除以255.0再减去均值除以标准差) // 假设模型要求归一化到[0,1] resized.ConvertTo(resized, MatType.CV_32FC3, 1.0 / 255.0); // 5. 将OpenCV Mat数据转换为Channel-first的DenseTensor // OpenCV Mat数据布局是Height, Width, Channel (HWC) // 我们需要转换为Channel, Height, Width (CHW) var dimensions new[] { 1, 3, targetHeight, targetWidth }; var inputTensor new DenseTensorfloat(dimensions); // 手动进行HWC - CHW的转换和赋值 unsafe { float* src (float*)resized.Data; for (int c 0; c 3; c) { for (int h 0; h targetHeight; h) { for (int w 0; w targetWidth; w) { // 计算源数据(HWC)和目标数据(CHW)的索引 int srcIndex h * targetWidth * 3 w * 3 c; int dstIndex c * targetHeight * targetWidth h * targetWidth w; inputTensor[dstIndex] src[srcIndex]; } } } } return inputTensor; }实操心得预处理是精度保障的关键。务必与模型训练时的预处理流程完全一致包括裁剪方式、归一化系数mean/std、颜色通道顺序。一个常见的错误是OpenCV默认读取为BGR顺序而很多公开模型是在RGB顺序上训练的忽略这一点会导致结果完全错误。4.2 初始化推理会话与执行推理创建InferenceSession是核心步骤在这里我们可以配置执行提供程序EP以利用硬件加速。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class DavidDepthEstimator : IDisposable { private InferenceSession _session; private readonly int _inputHeight; private readonly int _inputWidth; private readonly string _inputName; public DavidDepthEstimator(string modelPath, bool useGpu false) { // 配置Session选项 SessionOptions options new SessionOptions(); if (useGpu) { // 方式一使用DirectML (Windows通用GPU) // options.AppendExecutionProvider_DML(deviceId: 0); // 方式二使用CUDA (NVIDIA GPU) // options.AppendExecutionProvider_CUDA(deviceId: 0); // 为了示例这里启用CPU。实际使用时取消注释上述一行。 // 注意需要安装对应的NuGet包如Microsoft.ML.OnnxRuntime.DirectML } // 默认使用CPU。可以设置线程数等优化选项。 options.IntraOpNumThreads Environment.ProcessorCount; // 使用所有逻辑核心 options.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; // 创建推理会话 _session new InferenceSession(modelPath, options); // 获取模型输入信息假设只有一个输入 var inputMeta _session.InputMetadata.First(); _inputName inputMeta.Key; var shape inputMeta.Value.Dimensions; // 假设形状为 [1, 3, H, W] _inputHeight (int)shape[2]; _inputWidth (int)shape[3]; } public float[,] EstimateDepth(string imagePath) { // 1. 预处理图像 var inputTensor PreprocessImage(imagePath, _inputHeight, _inputWidth); // 2. 准备输入容器 var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(_inputName, inputTensor) }; // 3. 执行推理 using IDisposableReadOnlyCollectionDisposableNamedOnnxValue results _session.Run(inputs); // 4. 获取输出假设只有一个输出且名为“output” var outputTensor results.First().AsTensorfloat(); var outputShape outputTensor.Dimensions.ToArray(); // 例如 [1, 1, H, W] // 5. 将输出张量转换为便于处理的二维数组深度图 int outHeight (int)outputShape[2]; int outWidth (int)outputShape[3]; float[,] depthMap new float[outHeight, outWidth]; for (int h 0; h outHeight; h) { for (int w 0; w outWidth; w) { // 计算在张量中的索引 int index h * outWidth w; depthMap[h, w] outputTensor.GetValue(index); } } return depthMap; } public void Dispose() { _session?.Dispose(); } }4.3 后处理与深度图可视化模型输出的depthMap是归一化的值。为了得到有意义的深度我们需要进行后处理。public static (float[,] physicalDepth, Bitmap visualization) PostprocessDepth(float[,] rawDepthMap) { int height rawDepthMap.GetLength(0); int width rawDepthMap.GetLength(1); float[,] physicalDepth new float[height, width]; // 1. 假设模型输出是视差disparity需要根据基线baseline和焦距focal_length转换为深度 // 深度 Z (focal_length * baseline) / disparity // 这里假设 rawDepthMap 已经是经过缩放和偏移的视差图。 // 具体公式需要根据DAViD模型训练时的设定来调整。 float focal 800.0f; // 示例焦距像素单位 float baseline 0.1f; // 示例基线米 float scale 1000.0f; // 将米转换为毫米 float minDepth float.MaxValue; float maxDepth float.MinValue; // 计算物理深度并找到范围用于后续归一化可视化 for (int h 0; h height; h) { for (int w 0; w width; w) { float disparity rawDepthMap[h, w]; // 防止除零 if (Math.Abs(disparity) 1e-6) disparity 1e-6f; float depth (focal * baseline) / disparity; physicalDepth[h, w] depth * scale; // 单位毫米 minDepth Math.Min(minDepth, physicalDepth[h, w]); maxDepth Math.Max(maxDepth, physicalDepth[h, w]); } } // 2. 创建可视化位图灰度图或伪彩色图 Bitmap visBitmap new Bitmap(width, height); float range maxDepth - minDepth; if (range 1e-6) range 1.0f; for (int h 0; h height; h) { for (int w 0; w width; w) { // 将深度值归一化到0-255 float normalized (physicalDepth[h, w] - minDepth) / range; int intensity (int)(normalized * 255); intensity Math.Clamp(intensity, 0, 255); // 使用灰度 Color color Color.FromArgb(intensity, intensity, intensity); // 或者使用伪彩色例如Jet色图这里简化用灰度 visBitmap.SetPixel(w, h, color); } } return (physicalDepth, visBitmap); }5. 性能优化与内存管理实战5.1 会话与张量复用频繁创建和销毁InferenceSession和DenseTensor会带来不必要的开销。在高频调用的场景下如处理视频流必须进行复用。public class OptimizedDepthEstimator : IDisposable { private InferenceSession _session; private DenseTensorfloat _inputTensor; // 预分配输入张量内存 private FixedBufferOnnxValue _fixedInput; // 使用FixedBufferOnnxValue避免每次创建NamedOnnxValue private string _inputName; private string _outputName; public OptimizedDepthEstimator(string modelPath) { // ... 初始化_session获取_inputName, _outputName ... var inputMeta _session.InputMetadata.First(); _inputName inputMeta.Key; var shape inputMeta.Value.Dimensions; _inputTensor new DenseTensorfloat(shape); _fixedInput FixedBufferOnnxValue.CreateFromTensor(_inputTensor); } public float[,] RunInference(Mat preprocessedMat) { // 1. 将预处理好的Mat数据直接填充到预分配的_inputTensor中 FillTensorFromMat(preprocessedMat, _inputTensor); // 2. 准备输入复用FixedBufferOnnxValue var inputs new[] { _fixedInput }; var inputNames new[] { _inputName }; // 3. 运行推理指定输出名称 using var results _session.Run(inputNames, inputs, new[] { _outputName }); // 4. 获取输出... var outputTensor results[0].AsTensorfloat(); // ... 后续处理 } private unsafe void FillTensorFromMat(Mat mat, DenseTensorfloat tensor) { // 高效的HWC - CHW转换和填充逻辑直接操作内存指针 // ... (实现略参考上文预处理部分) } }5.2 异步推理与流水线设计对于需要处理连续帧的应用可以将图像预处理、推理、后处理放在不同的线程或任务中形成流水线以隐藏I/O和计算延迟。public class PipelineDepthProcessor { private BlockingCollectionMat _preprocessQueue new BlockingCollectionMat(10); private BlockingCollectionDenseTensorfloat _inferenceQueue new BlockingCollectionDenseTensorfloat(5); private CancellationTokenSource _cts; public void StartPipeline() { _cts new CancellationTokenSource(); var token _cts.Token; // 预处理线程 Task.Factory.StartNew(() { while (!token.IsCancellationRequested) { // 从摄像头或文件获取图像... Mat rawFrame GetNextFrame(); Mat processed Preprocess(rawFrame); _preprocessQueue.Add(processed, token); } }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default); // 推理线程 Task.Factory.StartNew(() { using var estimator new OptimizedDepthEstimator(model.onnx); while (!token.IsCancellationRequested) { if (_preprocessQueue.TryTake(out Mat frame, 100, token)) { var tensor estimator.PreprocessToTensor(frame); // 快速转换 _inferenceQueue.Add(tensor, token); frame.Dispose(); // 及时释放Mat内存 } } }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default); // 后处理与结果消费线程 Task.Factory.StartNew(() { while (!token.IsCancellationRequested) { if (_inferenceQueue.TryTake(out DenseTensorfloat tensor, 100, token)) { var depthMap estimator.RunInference(tensor); // 假设RunInference接受张量 var result PostprocessDepth(depthMap); DisplayOrSaveResult(result); // 注意tensor由推理线程管理此处不释放 } } }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } public void StopPipeline() { _cts?.Cancel(); } }6. 工业场景集成与上位机应用6.1 与PLC及运动控制器的数据交互在自动化产线上深度估计的结果往往需要反馈给PLC可编程逻辑控制器来控制机械臂或分拣机构。C#上位机可以通过多种方式与PLC通信OPC UA这是工业4.0标准协议跨平台、安全性高。可以使用如OPCFoundation.NetStandard.Opc.Ua等库实现客户端将计算出的深度、缺陷坐标等数据写入PLC的标签Tag。Modbus TCP一种广泛使用的串行通信协议。可以使用NModbus4等库将深度信息映射到保持寄存器Holding Registers供PLC读取。西门子S7协议针对西门子PLC可以使用S7NetPlus库进行高效的数据交换。核心是将深度图的分析结果如最大凹陷深度、位置坐标转换为PLC能理解的整型或浮点型数据并按照约定的通信协议和地址进行定时或触发式写入。6.2 结果可视化与用户界面设计对于操作员而言一个直观的界面至关重要。在WPF或WinForms中我们可以实时深度图显示将PostprocessDepth生成的Bitmap绑定到Image控件并实时更新。可以使用伪彩色如Jet色图来增强深度差异的视觉感知。深度剖面分析在图像上绘制一条线实时显示该线上各点的深度剖面曲线帮助判断凹陷的轮廓。阈值报警与标注设置深度阈值当检测到超过阈值的凹陷时在图像上用红色框标出该区域并在界面上弹出报警信息或记录日志。参数配置面板提供界面供用户调整模型置信度阈值、深度换算参数、感兴趣区域ROI等使系统更具适应性。// WPF中简单的图像绑定示例 public partial class MainWindow : Window { private DavidDepthEstimator _estimator; private System.Timers.Timer _processTimer; public MainWindow() { InitializeComponent(); _estimator new DavidDepthEstimator(Models/david.onnx, useGpu: true); _processTimer new System.Timers.Timer(100); // 10 FPS _processTimer.Elapsed ProcessFrame; } private void ProcessFrame(object sender, System.Timers.ElapsedEventArgs e) { // 从相机抓取一帧 using var frame GrabFrameFromCamera(); if (frame null) return; // 在后台线程处理 Task.Run(() { var depthMap _estimator.EstimateDepth(frame); var (physicalDepth, visBitmap) PostprocessDepth(depthMap); // 分析结果例如找到最小深度点最凹陷处 FindMinDepthPoint(physicalDepth, out int x, out int y, out float depthValue); // 更新UI必须在UI线程上执行 Dispatcher.Invoke(() { DepthImageView.Source ConvertBitmapToImageSource(visBitmap); DepthValueLabel.Content $深度: {depthValue:F2} mm; // 在图像控件上绘制一个十字标记在(x,y)处 DrawMarker(x, y); }); }); } }7. 常见问题排查与性能调优记录7.1 推理速度慢达不到实时要求检查执行提供程序EP首先确认是否正确启用了GPU加速。在代码中输出InferenceSession的SessionOptions配置或使用NVIDIA Nsight Systems等工具查看GPU利用率。如果GPU未使用可能是驱动问题、CUDA/cuDNN版本不匹配或者未安装对应的Microsoft.ML.OnnxRuntime.Gpu包。输入尺寸过大DAViD模型输入分辨率直接影响计算量。如果原始图像很大可以考虑在保持长宽比的情况下先缩放到一个合理的尺寸如512x512再进行推理。在模型导出时固定输入尺寸比动态尺寸能获得更好的图优化效果。会话选项优化var options new SessionOptions(); options.EnableCpuMemArena true; // 启用CPU内存竞技场对重复推理有优化 options.EnableProfiling false; // 发布版本关闭性能分析 options.ExecutionMode ExecutionMode.ORT_SEQUENTIAL; // 或 ORT_PARALLEL 根据模型结构测试 options.InterOpNumThreads 1; // 如果模型没有并行子图设为1 options.IntraOpNumThreads Environment.ProcessorCount; // 设置算子内部并行线程数批处理Batching如果应用场景允许如同时处理多个工位的图像将多张图片拼成一个批次如[4, 3, H, W]进行推理通常能显著提升GPU的利用率从而提高吞吐量。但这会增加单次推理的延迟需要权衡。7.2 深度估计结果不准确或异常预处理/后处理不一致这是最常见的原因。务必逐像素比对Python端预处理后输入给模型的数据和C#端预处理后输入给ORT的数据是否完全一致包括RGB/BGR顺序、归一化数值、裁剪和缩放算法。同样对模型输出的后处理公式也必须严格对齐。模型转换问题使用ONNX的onnxruntimePython包加载转换后的模型用相同的输入数据运行推理对比结果与原始框架PyTorch的输出。如果差异较大需要检查转换命令torch.onnx.export的参数如opset_version、是否设置了dynamic_axes等。数值精度确保C#中使用的是float单精度浮点数与模型的数据类型通常是FP32匹配。某些量化模型可能是INT8则需要不同的处理方式。内存越界在手动进行HWC到CHW转换或从张量中读取数据时仔细检查索引计算确保没有超出数组边界。这类错误有时不会立即抛出异常而是导致读取到错误的内存数据产生乱码结果。7.3 内存泄漏与资源释放OnnxRuntime中的许多对象都实现了IDisposable接口必须及时释放。InferenceSession是最重要的资源应在应用生命周期结束时或不再需要时调用Dispose()。最好将其封装在using语句或类的Dispose方法中。DisposableNamedOnnxValuesession.Run()返回的集合中的对象也是需要释放的。使用using块包裹Run方法调用可以确保结果被正确释放。FixedBufferOnnxValue如果使用了它也需要在适当的时候释放不过它通常与输入张量生命周期绑定。DenseTensor和Mat虽然它们不是来自ORT但大对象频繁创建也会导致GC压力。尽量复用这些对象如前面优化章节所述。一个简单的检查方法是使用任务管理器或性能计数器观察进程的私有工作集内存在长时间运行或反复执行推理循环后内存是否持续增长。如果增长很可能是上述对象没有正确释放。7.4 在无GPU或弱GPU环境下的部署对于只有集成显卡或老旧GPU的工控机DirectML EP可能比CUDA EP更稳定且兼容性更好。如果GPU性能实在太差甚至不如CPU可以强制使用CPU EP。// 显式指定仅使用CPU var options new SessionOptions(); // 不添加任何GPU EP默认就是CPU // 可以进一步配置CPU的并行策略 options.AppendExecutionProvider_CPU(); // 显式指定CPU虽然不是必须在这种情况下为了达到可接受的帧率可能需要在算法层面做出妥协例如使用更轻量级的深度估计模型如MobileDepth或者降低输入图像的分辨率。同时充分利用CPU的多核并行能力设置IntraOpNumThreads也变得尤为重要。
返回列表