简介这是一份基于C#与Onnx Runtime部署YOLOv8水表读数检测的完整源码包面向需要对仪表读数进行自动化识别的上位机开发者或视觉算法工程师。资源围绕水表数字区域定位、读数识别与结果输出展开工程结构可直接复用适合接入现有C#桌面应用或工业检测流程。压缩包共344个文件总大小约355.27MB核心包括onnx模型、C#源码与项目配置文件以及大量依赖DLL另有少量PNG/JPG样本图片与说明文档便于直接查看检测效果并联调运行。目前已有355人学习下载。通过源码可掌握YOLOv8模型在C#侧加载、预处理、推理和结果解析的完整链路同时提供水表读数检测项目从模型输出到应用层展示的实现思路对希望将深度学习模型落地到Windows桌面场景的开发者很有帮助。 做水表读数识别这个项目最早是被客户一句“你们能不能拿手机拍一下直接出数字”给问住的。传统的视觉方案水表读数基本靠人眼蹲在井盖边上抄或者用机械式脉冲远传表。现在想用摄像头实时拍直接输出表盘读数而且要准、要快、还能集成到现有的上位机系统里。这个项目标题“C# Onnx yolov8 水表读数检测 源码.rar”里面的每个词其实已经把技术路线定死了C#做上位机ONNX做模型运行时YOLOv8做检测网络。这篇文章我就按自己实际做过的一版方案把整个链路从头到尾拆开讲包括怎么选型、怎么训模型、怎么导ONNX、C#端怎么写、踩了哪些坑。这个项目适合谁看呢一个是做工业视觉上位机的工程师想尝试把深度学习模型塞进WinForm或WPF项目里另一个是刚接触YOLOv8部署搞不清pt、ONNX、ONNX Runtime之间关系的人。不管你是打算照着重做一套还是只想知道这套东西靠不靠谱我觉得这篇都能给你省不少时间。1. 项目整体设计与思路拆解1.1 标题拆解一条链路三段技术标题里最值钱的其实是“Onnx”这个词它决定了整个项目能不能在C#里跑起来。YOLOv8训练出来的默认权重是PyTorch格式的也就是.pt文件C#程序没法直接加载它。ONNX相当于一个中间格式把PyTorch的模型结构和权重打包成一个通用的计算图描述然后C#端用ONNX Runtime这个库去读取它、执行推理。整体链路是PyTorch训练YOLOv8 - 导出ONNX模型 - C#上位机加载ONNX Runtime - 调用摄像头或图片 - 推理得到检测框 - 排序拼接成读数 - 展示或上报。C#在这个场景里的作用是“壳”负责所有工程化的事图像采集、界面展示、通讯上报、逻辑控制。YOLOv8负责“眼”把水表表盘里的各个数字位框出来并识别出具体数字。ONNX是这两者之间的胶水解决语言隔离的问题。1.2 为什么不是别的方案组合有人会问为什么不用TensorFlow Lite、OpenVINO或者干脆用Python写服务、C#走HTTP调用每种方案我都考虑过。直接用Python写服务C#通过Socket或HTTP拿结果写起来确实快但多一层网络通讯就多一层不稳定。工业现场对实时性要求高摄像头一秒拍好几帧来回序列化传输的成本吃不消。OpenVINO对Intel CPU优化很猛但水表检测后期大概率要部署到NVIDIA Jetson或者瑞芯微RK3588这类边缘设备上OpenVINO换平台就废了。ONNX Runtime是跨平台的Windows、Linux、ARM都能跑换设备时模型文件不用动C#代码改动也很少。所以选择C#直调ONNX Runtime本质上是把模型部署的兼容性和上位机开发的效率放在同一个进程里解决。推理库以NuGet包的形式存在加个引用就行部署的时候把对应DLL拷过去省心。1.3 需要用到GPU吗这是很多新手问得最多的问题。YOLOv8n这个nano版本大约300万参数输入640x640的单帧图片在普通Intel i5上跑CPU推理大约需要80到150毫秒能到每秒10帧左右。如果是YOLOv8sCPU可能要300毫秒以上那就明显卡了。水表读数这个场景说实话不存在高速运动目标一秒拍个两三张完全够用。所以纯CPU也能顶得住。但我建议如果要有余量还是用ONNX Runtime GPU版。GTX 1660Ti这种老卡也能把YOLOv8s的推理压到20毫秒以内。而且GPU版本只需要在NuGet里换一个包代码不用改一行。在文章后面的章节我会说这个坑在哪有几个人就是在这挂的。2. 水表读数识别的核心难点2.1 不能只做“检测”还要做“识别”YOLOv8本身是目标检测网络输出的是目标的类别和坐标框。水表读数这个场景里如果把“检测”和“识别”拆成两步先检测出表盘区域再对表盘里的数字做OCR也可以。但实际操作下来这种两步方案在C#里集成太麻烦还得再引入一个OCR引擎。更省事的做法是让YOLOv8直接输出数字框和数字类别。也就是把水表字轮里的每一个数字位当成一个独立目标去训练。类别就是0到9加上小数点的话就是11类。这样推理完做一次按X坐标排序把类别对应的数字依次拼起来就是完整读数了。这个方法看着粗暴但实际效果很好。因为水表字轮的数字区域特征明显数字间有间隔每个数字是个独立的矩形目标边框清晰非常契合目标检测任务。YOLOv8对这种小而规整的目标识别能力很强。2.2 字轮常见的鬼畜情况真正做水表识别绕不开这几个头疼的问题。第一个是半字。机械水表在换位时数字会出现“一半是3一半是4”的中间状态标注时这种情况最纠结。我建议数据集里一定要包含至少10%的半字样本并且标注遵循“视觉上更接近谁就标谁”的原则。模型见过足够多的半字样本后对临界状态就不那么敏感了。第二个是反光。很多水表装在潮湿阴暗的管道井里补光灯一照表盘玻璃反光数字区域白花花一片。这里有两个解决方向采集图像时把光源角度打斜一点不要让光直射进表盘另外做数据增强时增加随机亮度、随机加高光块的操作模拟反光。第三个是表盘倾斜和透视。摄像头不可能每次都正对表盘有些斜着拍字轮数字会有透视形变。常规的目标检测对轻微的透视鲁棒性还行但如果角度太夸张就得在预处理里加仿射变换校正或者训练集里大量加入随机旋转和透视增强。2.3 数据集的采集与标注水表图像这种实际工程数据网上找不到现成的公开数据集只能自己采集。我当时的方法是做一个简易拍照支架固定摄像头找了几十户不同品牌的水表每个表从不同角度、不同光照条件拍几十张照片。一共凑了大概8000张。标注用LabelImg格式输出成YOLO格式的txt文件。8000张图看起来多但对YOLOv8来说也就是刚够用。如果采集不了那么多就用图像增强来凑旋转、平移、缩放、色彩抖动、模糊、噪声、随机裁剪都能把有效样本量放大。我实测下来增强后的数据量建议不低于15000张mAP50能从0.85提到0.95以上这个提升非常明显。3. 从训练到ONNX的转换细节3.1 训练时要注意什么用YOLOv8训练自己的数据集命令其实不复杂。我用的Ultralytics库配置文件里设置nc1110个数字加小数点设置训练轮数为300轮。关键在于输入尺寸训练时我用了640x640因为水表字轮数字目标是中小目标分辨率太低会丢失细节。如果条件允许用960x960训练推理精度会更高但推理速度会相应慢一半。训练完成之后就是导出ONNXyolo export modelbest.pt formatonnx opset12 simplifyTrueOpenCV的dnn模块和ONNX Runtime对算子的支持范围不同opset版本选12相对稳妥。simplifyTrue会用onnx-simplifier清理计算图里冗余节点导出的模型体积更小、推理更快。3.2 pt转ONNX踩过的坑第一个坑是动态轴。YOLOv8导出ONNX默认是固定batch和固定尺寸的如果你想支持动态输入尺寸要在导出命令里加上dynamicTrue。但在C#端我强烈不建议开动态输入因为动态维度会让后处理代码复杂很多而且推理速度会变慢。固定640x640省心省力。第二个坑是NumPy版本和onnx-simplifier的兼容性。我在Windows上导出的时候因为NumPy版本太高simplify时报错。解决办法是降级NumPy到1.21或者干脆不用simplifyONNX Runtime本身也会做图优化差别不大。第三个坑是int8量化。热搜词里有“.onnx量化int8”这里多说一句。ONNX Runtime的dynamic quantization对Transformer类模型友好对YOLOv8这种卷积网络支持有限。想真正压到int8加速又不想明显掉精度要把模型导出为QDQ格式的ONNX然后用onnxruntime的量化工具进行离线量化还要准备校准数据集。这个流程比较繁琐我在单路水表项目的实测中统一用FP16半精度来跑GPU推理精度不掉、速度翻倍比折腾int8省事得多。3.3 ONNX模型的验证导出的ONNX模型别直接丢给C#去试先在自己的电脑上用Python快速验证一遍。用onnxruntime-gpu库跑同一张测试图看输出的检测框坐标和识别数字是否正确。这一步能提前过滤掉一大半“C#端跑不出结果”的诡异问题因为ONNX模型本身有没有毛病Python这边两分钟就能测完。4. C#调用ONNX Runtime的完整实现4.1 工程环境与NuGet包C#项目我用的是.NET Framework 4.7.2因为很多工业现场的老上位机还挂在这里。如果你是新项目直接用.NET 6或.NET 8的WinForm也一样NuGet包是通用的。需要引用的包Microsoft.ML.OnnxRuntime 或 Microsoft.ML.OnnxRuntime.GpuOpenCvSharp4 和 OpenCvSharp4.runtime.win用于图像预处理这里有个大坑Microsoft.ML.OnnxRuntime和Microsoft.ML.OnnxRuntime.Gpu不能同时安装否则会出现DLL加载冲突。选GPU版的时候记得把CPU版先卸载干净。另外GPU版对CUDA和cuDNN的版本有严格要求10.x版本还不算麻烦1.16之后的版本对CUDA 12的要求比较苛刻装错版本会在运行时直接报DllNotFoundException。4.2 C#推理主流程代码下面是核心的推理代码结构我直接给一个简化的可运行框架using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; public class YoloV8Detector { private InferenceSession _session; private int _inputWidth 640; private int _inputHeight 640; private string[] _classNames { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, dot }; public YoloV8Detector(string modelPath) { var options new SessionOptions(); options.AppendExecutionProvider_CPU(); _session new InferenceSession(modelPath, options); } public ListDetection Detect(Mat image) { // 1. letterbox缩放到640x640 var resized LetterBox(image, _inputWidth, _inputHeight); // 2. BGR转RGB、归一化、构造tensor var inputTensor Preprocess(resized); // 3. 推理 var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) }; using (var results _session.Run(inputs)) { var output results.First().AsTensorfloat(); // output shape: [1, 84, 8400] return PostProcess(output, image.Width, image.Height); } } }预处理里最关键的resize方式必须是letterbox也就是保持原始宽高比填充到640x640不是直接拉伸。直接拉伸会让水表数字变形对模型的精度打击非常大。填充部分用灰色值114这是YOLO系列训练时就用的默认填充色。4.3 后处理输出张量解析与NMSYOLOv8的输出和YOLOv5有一个不同的地方YOLOv5的输出是三个尺度的特征图每个尺度一个分支YOLOv8导出ONNX后是单输出维度是[1, 4类别数, 8400]其中8400是全部尺度候选框的总数。拿我的模型来说就是[1, 15, 8400]。解析逻辑private ListDetection PostProcess(Tensorfloat output, int originalW, int originalH) { var detections new ListDetection(); int rows output.Dimensions[2]; // 8400 int cols output.Dimensions[1]; // 15 var data output.ToArray(); // 调整为 (rows, cols) 视角处理 for (int i 0; i rows; i) { var row new float[cols]; for (int j 0; j cols; j) { row[j] data[j * rows i]; // 注意内存布局是列优先 } float cx row[0], cy row[1], w row[2], h row[3]; // 找到最大类别得分 float maxScore 0f; int maxCls -1; for (int j 4; j cols; j) { if (row[j] maxScore) { maxScore row[j]; maxCls j - 4; } } if (maxScore 0.5f) { // 转回原始图像坐标 float x (cx - w / 2) / _inputWidth * originalW; float y (cy - h / 2) / _inputHeight * originalH; float boxW w / _inputWidth * originalW; float boxH h / _inputHeight * originalH; detections.Add(new Detection(x, y, boxW, boxH, maxScore, _classNames[maxCls])); } } // 按x坐标排序得到从左到右的读数字符 return SortAndNMS(detections); }这里最容易出错的地方是内存布局。ONNX Runtime输出的Tensor是按C行优先存储还是按列优先实测下来ONNX Runtime的Tensor内存排布是标准的C连续存储但YOLOv8的ONNX输出维度是[1, 15, 8400]实际上你要拿到的每行数据是8400个候选框中的某一个也就是data[j * rows i]这个索引方式很多新手把它写反导致检测框完全错乱。后处理的NMS非极大值抑制YOLOv8官方已经不需要像YOLOv5那样做多标签NMS了因为它的head输出本身已经通过DFL解耦了类别和框。而我们这个场景每个候选框只需要保留最高类别分数然后做一个简单的按分数降序、按IoU去重的逻辑就行。整个后处理代码不超过50行。5. 上位机工程化的实操细节5.1 扫码枪触发事件怎么实现热搜词里有“C#扫码枪触发事件”这个在水表读数场景里是个高频需求。有些抄表员不看屏幕直接拿扫码枪扫一下表箱上的条码系统就自动拍一张表盘图并识别读数同时把这个读数绑定到条码编号上。扫码枪本质上是个模拟键盘输入的HID设备扫出来的条码会以键盘敲击的方式输入到当前焦点控件。所以触发事件的核心逻辑就是在窗体上放一个隐藏的TextBox给它焦点监听KeyDown事件。扫码枪的条码内容会以极快的速度逐字符输入最后以回车结尾。判断到回车键时就认为一次扫码结束了此时取TextBox里的完整内容然后触发拍照识别。还有一些老款扫码枪不带回车后缀那就需要指定前缀或超时判断。更稳的做法是直接调用PosEx或BarcodeReader的SDK但大多数场景键盘模拟方式已经够用。如果画面上的按钮抢了焦点也没关系只要保证那个隐藏TextBox一直能被重新聚焦即可。private void txtScanner_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { string code txtScanner.Text.Trim(); if (code.Length 0) { Task.Run(() CaptureAndDetect(code)); } txtScanner.Clear(); e.Handled true; } }这里有个细节扫码枪输入速度极快KeyDown事件是逐个字符触发的但Enter只有在最后一个字符之后出现。所以不要在每个字符的KeyDown里做逻辑只在Enter出现时一次性处理。同时拍照识别要丢到后台线程去别卡UI。5.2 循环采集和UI刷新卡顿问题热搜词里有“C#循环数据采集和ui刷新卡顿”这是上位机开发的经典问题。我见过太多人在循环里调用Camera.Read()然后直接把结果赋值给PictureBox最后UI线程被图像解码和绘制拖死拖拽窗体都卡。正确做法是生产者和消费者模式。摄像头采集线程把Mat帧放到一个并发队列里UI线程只用定时器每隔50ms取最新的一帧显示。推理线程也从同一个队列里取帧但推理结果用事件通知UI去更新。实际实现如下private ConcurrentQueueMat _frameQueue new ConcurrentQueueMat(); private CancellationTokenSource _cts new CancellationTokenSource(); private void CaptureLoop() { while (!_cts.IsCancellationRequested) { using (var frame _camera.Read()) { if (_frameQueue.Count 2) { // 丢弃旧帧保证延迟 _frameQueue.TryDequeue(out _); } _frameQueue.Enqueue(frame.Clone()); } } } private void DetectionLoop() { while (!_cts.IsCancellationRequested) { if (_frameQueue.TryDequeue(out Mat frame)) { var result _detector.Detect(frame); // 触发事件更新UIInvoke只负责更新结果文本 OnDetectCompleted?.Invoke(result, frame.Id); } else { Thread.Sleep(10); } } }核心原则是UI线程只做赋值和绘制最轻量的内容所有耗时工作全部扔到后台Task或者独立线程。对于水表识别这种低频场景队列里保留最新帧就够了不需要堆积历史帧防止内存暴涨。对于识别结果文本的更新主界面用BeginInvoke而不是Invoke。BeginInvoke是异步的不会因为UI线程繁忙而阻塞后台线程。5.3 采集拍照瞬间的曝光与补光还有一个容易忽略但非常影响识别率的是图像质量。水表在管道井里环境光往往很差摄像头最好带白名单补光灯也就是只发出特定波长的光配上对应的滤光片能滤掉大部分环境光干扰。在软件层面对于USB摄像头可以通过OpenCvSharp设置曝光值_capture.Set(VideoCaptureProperties.Exposure, -5); _capture.Set(VideoCaptureProperties.Gain, 30); _capture.Set(VideoCaptureProperties.Brightness, 100);对于工业相机如果是海康或大华直接用它们的SDK设置曝光和增益。如果识别率一直提不上去可以先调曝光把图像调亮再谈模型优化。图像整体偏暗的情况下再好的模型也是白搭。6. 实测过程中踩过的典型坑与排查记录6.1 常见问题速查表问题现象可能原因解决方法推理时报DllNotFoundException缺少VC运行库或CUDA/cuDNN版本不匹配安装对应版本的CUDA和cuDNN把dll拷到exe目录检测结果只有框没有数字模型类别数配置错误后处理索引错位核对导出时nc值检查输出维度解析识别准确率忽高忽低预处理不一致例如RGB/BGR通道顺序或归一化方式不对确保C#端与训练时的预处理完全一致检测框重叠特别多NMS阈值太低或没做NMS提高IoU阈值到0.45左右检查后处理逻辑数字顺序混乱排序键错了用框中心点X坐标排序不要用框左侧X坐标因为有些数字贴得近UI卡到无法操作推理或图像采集占用UI线程全部迁移到后台TaskUI线程只做结果赋值显卡报错但CPU一切正常onnxruntime-gpu包和驱动/CUDA版本不兼容先跑一遍onnxruntime自带的GPU示例确认环境没问题再看代码识别速度太慢只有2FPS输入尺寸太大或没有用GPU推理检查是否加载了GPU版NuGet包确认ExecutionProvider是CUDA6.2 半字判决和阈值调节经验半字的判决是这个项目里精度提升空间最大的地方。我实测下来置信度阈值设在0.4到0.5之间比较合适。太高了半字状态会被过滤掉导致漏检太低了会把背景上的污渍误判成数字。如果采集图像质量稳定可以把阈值卡在0.45这个值是综合很多表型测试出来的。另外关于小数点要不要独立识别这个看具体业务。水表读数一般只取整数部分小数点很多时候不需要。但如果项目要求精确到小数那就必须要10个数字加小数点的11类模型而且小数点目标特别小标注不能漏。还有一个取巧的办法不管小数点单独用一个OpenCV的传统图像处理步骤去检测红针或小数位也能减少模型压力。6.3 精度不够时先加图还是先调参不少新手遇到识别率低第一反应是调训练轮数或者换大模型。我的实际感受是YOLOv8对小目标的识别能力已经很成熟问题往往出在数据而不是模型。如果误检多先看训练集里负样本够不够多如果漏检多先看对应角度的正样本图够不够多。当时第一版模型误检很高把表盘的刻度条纹当成了数字。我没有立刻改网络结构而是采集了一批不含任何数字的表盘背景图标成空图加入训练集并提高背景图的采样权重。重新训练后误检率大幅下降。再比如反光导致漏检就往训练集里加入更多强反光样本并且用自适应直方图均衡化增强图像后重训了一次效果提升比任何调参都明显。6.4 边缘设备扩展方向这个项目的可扩展性其实很强。YOLOv8的ONNX模型可以直接在瑞芯微RK3588上转换为RKNN格式只要在C#侧换成RKNN Toolkit的API就行模型和业务逻辑都不用动。或者用Linux环境下的.NET 6跑同一个代码推理引擎还是ONNX Runtime。这也是当初坚持用ONNX作为中间格式的原因它不会把上层应用锁死在某一家的推理框架里。另外工程上可以考虑把水表读数结果结合MES系统或水务平台的上报接口实现数据自动入库。再进一步还能接入异常检测逻辑比如连续多次识别结果不一致时自动抓拍告警提醒人工复核。这些都是在现有代码框架上很容易扩展的部分。最后说一点个人体会水表读数检测这个项目看起来模型是核心其实真正花时间最多的地方是数据采集、预处理一致性和C#工程化。模型用YOLOv8这种成熟框架就够了没必要自己改网络结构。如果你能把标注样本覆盖到足够多的表型、角度和光照环境再在C#侧把队列和线程模型处理干净整个系统的识别率和稳定性都会非常能打。这个项目做完不仅在产线上跑得稳还能沉淀成一套可复用的“工业表计读数识别”模板下次遇到电表、气表、压力表改改数据重新训练就能直接套上去。本文还有配套的精品资源点击获取