C#调用ONNX实现GroundingDINO工业部署方案
简介本资源是面向C#开发者与计算机视觉初学者的开放世界目标检测实践项目基于ONNX Runtime在Windows平台实现GroundingDINO算法部署解决传统目标检测模型难以泛化至未知类别与复杂场景的痛点适用于智能监控、工业质检、自定义物体识别等实际应用开发。压缩包共336个文件含55个运行依赖DLL、14个核心C#源码.cs、1个Visual Studio解决方案.sln、1个ONNX模型文件及30个XML配置与29个TXT说明文档辅以PDB调试符号、NuGet包.nupkg和跨平台运行时库.so/.dylib/.dll整体达903.14MB结构完整开箱即编译。已有465人学习下载提供可直接运行的Demo工程、分层组织的代码模块含预处理、推理、后处理全流程、详细依赖管理方案及ONNX模型加载与输出解析示例大幅降低C#调用视觉大模型的技术门槛。1. C# 调用 ONNX 实现 GroundingDINO为什么在工业质检和上位机场景里它比 Python 更稳你可能刚在 Python 里跑通了 GroundingDINO 的 PyTorch 版本但一接到产线需求——“把模型集成进现有 C# 上位机系统实时处理工控相机视频流延迟压到 80ms 以内不许弹 CMD 窗口”——就卡住了。这不是理论问题而是工程落地的硬约束Python 的 GIL、DLL 冲突、GPU 上下文切换开销、以及 Windows 服务环境下 pip 包管理的脆弱性在真实产线里会直接导致检测帧率跳变、内存泄漏或服务崩溃。而这份C# Onnx GroundingDINO.sln源码正是为这类场景设计的它绕过 Python 解释器层用 .NET 6 ONNX Runtime C# API 直接加载量化后的.onnx模型所有预处理图像缩放、归一化、文本 tokenization、推理、后处理NMS、box 拆包、caption 匹配全部在托管代码中完成最终输出ListGroundingResult对象可无缝注入 WinForms/WPF/MAUI 界面或 Modbus TCP 数据帧。它不追求 SOTA 指标但保证在 i5-8300H GTX 1050 Ti 的嵌入式工控机上以 640×480 输入稳定维持 22 FPS更关键的是它把onnxruntime.dll作为本地依赖嵌入packages/避免运行时 DLL 版本错配——这点在某新能源汽车电池壳体生产线的实际部署中让质检系统上线周期从 3 周压缩到 2 天。2. ONNX Runtime C# API 与 GroundingDINO 模型结构的对齐逻辑GroundingDINO 的核心难点不在视觉分支而在多模态对齐它需要将用户输入的自然语言描述如“左侧凸起的焊缝缺陷”编码为文本特征并与图像特征做 cross-attention。ONNX 模型导出时官方通常提供两个文件groundingdino_swinb_cogcoor.onnx主干和text_encoder.onnx文本编码器但这份 C# 源码做了关键改造——它将文本编码逻辑固化进 ONNX 图仅保留单个.onnx文件输入张量名为images和input_ids输出为pred_boxes,pred_logits,pred_caption。这种设计规避了 C# 中调用多个 ONNX 模型时的 session 同步难题也简化了 tokenization 流程。2.1 文本预处理从 string 到 input_ids 的零依赖实现Python 版本依赖 transformers 库做分词但在 C# 中引入 NuGet 包Microsoft.ML.Transforms会带来额外依赖链。本项目采用轻量级方案内置BertTokenizer的 C# 移植版位于Onnx Demo/Utils/BertTokenizer.cs仅支持 WordPiece 分词不加载 vocab.json而是将groundingdino_vocab.txt编译进资源Properties/Resources.resx。关键逻辑如下// Onnx Demo/Utils/BertTokenizer.cs public static int[] Tokenize(string text, int maxLen 256) { var tokens new Liststring { [CLS] }; foreach (var word in SplitBySpace(text.ToLower())) { var subwords WordPieceTokenize(word); tokens.AddRange(subwords); } tokens.Add([SEP]); // 截断并填充 var ids new int[maxLen]; for (int i 0; i Math.Min(tokens.Count, maxLen); i) { ids[i] VocabMap.GetValueOrDefault(tokens[i], 0); // 0 为 [UNK] } return ids; }注意VocabMap是静态字典初始化时从嵌入资源读取groundingdino_vocab.txt每行一个 token共 30522 项。SplitBySpace仅按空格切分不处理标点——这与原始 GroundingDINO 的 tokenizer 一致因训练时 caption 均已清洗。若需支持中文需替换为jieba的 C# 移植版本项目未包含但Onnx Demo/Plugins/目录预留了接口。2.2 ONNX Runtime Session 配置GPU 加速与内存复用的关键参数Onnx Demo/Inference/OnnxRunner.cs中的CreateSessionOptions()方法决定了性能上限。默认配置会触发 CPU fallback必须显式启用 CUDA// Onnx Demo/Inference/OnnxRunner.cs private SessionOptions CreateSessionOptions() { var options new SessionOptions(); options.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_EXTENDED; options.IntraOpNumThreads Environment.ProcessorCount / 2; // 防止线程争抢 // 关键启用 CUDA EP且指定 GPU 设备 ID if (IsCudaAvailable()) { options.AppendExecutionProvider_CUDA(0); // 使用第 0 块 GPU options.AddConfigEntry(session.load_model_format, ORT); // 强制 ORT 格式 options.AddConfigEntry(cuda.memcpy_async, 1); // 启用异步内存拷贝 } // 内存复用避免每次推理都分配新 buffer options.AddConfigEntry(session.use_env_allocators, 1); return options; }提示IsCudaAvailable()通过 P/Invoke 调用cudaGetDeviceCount判断而非依赖OnnxRuntime.GpuNuGet 包的自动探测——后者在某些工控机驱动版本下会返回 false 正值。session.use_env_allocators开启后ONNX Runtime 会复用内部内存池实测在连续 1000 帧推理中GC 压力下降 63%。2.3 输入张量构造图像预处理的 SIMD 加速实现GroundingDINO 要求输入为CHW格式、float32、归一化至[0,1]。C# 原生Bitmap转float[]极慢本项目使用System.Numerics.Vectorfloat实现批量归一化// Onnx Demo/Preprocess/ImageProcessor.cs public static float[] ToFloatArray(Bitmap bmp, int targetWidth 640, int targetHeight 480) { var resized ResizeBilinear(bmp, targetWidth, targetHeight); // 双线性缩放 var data resized.LockBits(new Rectangle(0, 0, targetWidth, targetHeight), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { var bytes new byte[targetWidth * targetHeight * 3]; Marshal.Copy(data.Scan0, bytes, 0, bytes.Length); // SIMD 加速一次处理 4 个像素R,G,B,R var output new float[targetWidth * targetHeight * 3]; for (int i 0; i bytes.Length; i 3) { // BGR - RGB 顺序调整 归一化bytes[i] 是 Bbytes[i1] 是 Gbytes[i2] 是 R output[i / 3 * 3 0] bytes[i 2] / 255.0f; // R output[i / 3 * 3 1] bytes[i 1] / 255.0f; // G output[i / 3 * 3 2] bytes[i 0] / 255.0f; // B } return output; } finally { resized.UnlockBits(data); } }参数说明targetWidth/targetHeight必须与 ONNX 模型的dynamic_axes一致本项目为640×480。若模型支持动态尺寸需在model.onnx的input_shape中修改[-1,3,H,W]并重新导出——但 C# ONNX Runtime 对动态轴支持有限建议固定尺寸。3. 推理结果解析与开放世界检测的业务适配GroundingDINO 的输出是pred_boxes[N,4]、pred_logits[N,1]和pred_caption[N]字符串数组其中N是检测框数量。但工业场景不关心“猫”或“狗”而关注“焊缝气孔”、“壳体划痕”等特定缺陷。本项目通过CaptionMatcher类实现语义映射3.1 Caption 后处理基于编辑距离的缺陷关键词匹配Onnx Demo/Postprocess/CaptionMatcher.cs不直接使用pred_caption而是将其与预设的缺陷词典做模糊匹配// Onnx Demo/Postprocess/CaptionMatcher.cs private static readonly string[] DefectKeywords { 气孔, 裂纹, 划痕, 凹坑, 凸起, 错边 }; public static string MatchDefect(string caption, float threshold 0.7f) { var cleanCaption Regex.Replace(caption, [^a-zA-Z\u4e00-\u9fa5], ).Trim(); if (string.IsNullOrEmpty(cleanCaption)) return unknown; var scores DefectKeywords.Select(k FuzzyStringMatch.Ratio(cleanCaption, k) // 使用 FuzzyStringMatch NuGet 包 ).ToArray(); int bestIdx Array.IndexOf(scores, scores.Max()); return scores[bestIdx] threshold ? DefectKeywords[bestIdx] : unknown; }注意FuzzyStringMatch采用 Ratcliff-Obershelp 算法对中英文混合 caption如 “left side weld porosity”比 Levenshtein 更鲁棒。threshold0.7f是产线实测阈值——低于此值视为噪声避免误报。3.2 结果结构化生成符合 OPC UA 或 Modbus 协议的数据包检测结果最终要喂给 PLCGroundingResult类设计为可序列化// Onnx Demo/Models/GroundingResult.cs public class GroundingResult { public float XMin { get; set; } // 归一化坐标 [0,1] public float YMin { get; set; } public float XMax { get; set; } public float YMax { get; set; } public float Confidence { get; set; } public string DefectType { get; set; } // 经 CaptionMatcher 映射后的结果 public int PixelArea (int)((XMax - XMin) * 640 * (YMax - YMin) * 480); // 像素面积用于尺寸过滤 }实际业务中常需过滤小目标如PixelArea 50或低置信度Confidence 0.45f// Onnx Demo/Program.cs var results runner.RunInference(bitmap, 左侧凸起的焊缝缺陷); var validResults results .Where(r r.Confidence 0.45f r.PixelArea 50) .Select(r new { Type r.DefectType, Area r.PixelArea, Position new { r.XMin, r.YMin, r.XMax, r.YMax } }) .ToList();提示PixelArea计算假设输入图像为640×480若实际分辨率不同需在ImageProcessor.cs中传入scaleFactor参数校正。4. 模型量化与 INT8 推理在无独立 GPU 的工控机上提速 3.2 倍产线设备常为无独显的 Atom 或 J 系列 CPU此时 FP32 推理耗时超 300ms/帧。本项目提供quantized/groundingdino_swinb_cogcoor_int8.onnx由 ONNX Runtime 的onnxruntime-tools量化工具生成但需手动修复 C# 兼容性问题。4.1 量化前的模型修正移除不支持的 OP原始 Swin Transformer 中的Softmax和LayerNormalization在 INT8 模式下易出错。Onnx Demo/Tools/QuantizeHelper.cs提供预处理// Onnx Demo/Tools/QuantizeHelper.cs public static void FixModelForQuantization(string modelPath) { var model Model.Load(modelPath); // 替换 LayerNormalization 为等效的 Scale Bias foreach (var node in model.Graph.Nodes.Where(n n.OpType LayerNormalization)) { var scaleNode model.Graph.AddNode(Scale, Scale_ node.Name, new NodeAttributes { [scale] 1.0f }); // ... 插入 Scale 和 Bias 节点删除原 LayerNorm } model.Save(modelPath.Replace(.onnx, _fixed.onnx)); }4.2 C# 中加载 INT8 模型的特殊配置INT8 模型需禁用某些优化否则会触发InvalidArgument异常// Onnx Demo/Inference/OnnxRunner.cs private SessionOptions CreateInt8SessionOptions() { var options CreateSessionOptions(); // 复用基础配置 options.GraphOptimizationLevel GraphOptimizationLevel.ORT_DISABLE_ALL; // 关闭图优化 options.AddConfigEntry(ep.cpu.arena_extend_strategy, kSameAsRequested); // 防止内存越界 return options; }实测对比i5-8300H, 16GB RAM模型类型平均推理耗时内存占用准确率 dropFP32286 ms1.2 GB—INT889 ms0.7 GB1.2% mAP0.5关键参数ep.cpu.arena_extend_strategy是 Intel CPU 上 INT8 的必填项缺失会导致AccessViolationException。准确率下降来自量化误差但对工业质检可接受——产线验证中1.2% 的漏检率仍低于人工目检的 3.5%。5. 工业部署技巧静默运行、日志隔离与热更新模型在 Windows 服务或上位机后台进程中不能出现控制台窗口或弹窗。Onnx Demo/Program.cs的入口点做了三重加固5.1 静默启动与进程优先级锁定// Onnx Demo/Program.cs static void Main(string[] args) { // 隐藏控制台窗口适用于 WinForms/WPF 托管进程 if (Environment.UserInteractive) SetConsoleCtrlHandler(null, true); // 锁定 CPU 亲和性避免调度抖动 Process.GetCurrentProcess().ProcessorAffinity (IntPtr)0x00000001; // 绑定到核心 0 // 设置高优先级但不过度抢占 Process.GetCurrentProcess().PriorityClass ProcessPriorityClass.AboveNormal; var runner new OnnxRunner(quantized/groundingdino_swinb_cogcoor_int8.onnx); // ... 启动检测循环 }5.2 日志写入独立文件避免干扰主程序所有 ONNX Runtime 日志包括 GPU 初始化失败信息被重定向到logs/onnx_runtime.log// Onnx Demo/Logging/OnnxLogger.cs public class OnnxLogger : IDisposable { private readonly StreamWriter _writer; public OnnxLogger() { Directory.CreateDirectory(logs); _writer new StreamWriter(logs/onnx_runtime.log, true) { AutoFlush true }; OnnxRuntime.OnnxRuntime.SetLogger(new CustomLogger(_writer)); } private class CustomLogger : OnnxRuntime.Logging.Logger { private readonly StreamWriter _writer; public CustomLogger(StreamWriter writer) _writer writer; public override void Log(LogLevel level, string message) _writer.WriteLine($[{DateTime.Now:HH:mm:ss.fff}] {level}: {message}); } }5.3 模型热更新无需重启服务即可切换 ONNX 文件当产线新增缺陷类型需更新模型时OnnxRunner支持ReloadModel(string newPath)// Onnx Demo/Inference/OnnxRunner.cs public void ReloadModel(string newPath) { lock (_lock) { _session?.Dispose(); _session new InferenceSession(newPath, _sessionOptions); // 重建输入/输出元数据缓存 _inputMeta _session.InputMetadata.Values.First(); _outputMeta _session.OutputMetadata; } }操作步骤将新.onnx文件复制到quantized/目录调用runner.ReloadModel(quantized/new_model.onnx)。实测热更新耗时 120ms期间旧模型继续服务无缝切换。本文还有配套的精品资源点击获取