新闻详情

新闻详情

首页 / 资讯中心 / 详情

C#上位机结合ONNX Runtime与YOLOv8实现仪表指针读数识别

发布时间:2026/10/2 2:54:26来源:尧图网络
C#上位机结合ONNX Runtime与YOLOv8实现仪表指针读数识别
简介这是一套基于C#与ONNX Runtime的YOLOv8仪表指针检测源码项目面向需要在.NET环境中落地目标检测的开发者适用于工业仪表自动读数、巡检图像分析等场景。项目包含完整的Visual Studio解决方案与工程目录可直接打开编译运行并附有ONNX模型、示例图片及依赖说明便于理解模型加载、图像预处理、推理执行与结果解析的完整链路。整个压缩包共351个文件大小约355MB文件类型以dll、cs、xml、onnx、nupkg、config等为主cs为C#源码onnx为推理模型dll与nupkg为运行时依赖及NuGet包xml与config用于参数配置pdb提供调试符号方便断点跟踪。同时包含onnxruntime原生库便于跨平台部署。已有404人学习参考通过阅读源码可快速掌握YOLOv8在C#环境的调用方式以及仪表指针定位的工程实现思路适合作为目标检测应用开发和模型部署的学习样本。1. C# Onnx YOLOv8 仪表指针检测上位机里最难啃的一段是“读数”做仪表读数项目的人应该都有同感模型训练出来不是终点把模型塞进 C# 上位机、稳定读出一个指针角度才是终点。我拆过一套 C#WinForm ONNX Runtime YOLOv8 的仪表指针检测源码输入一张 96×96 或 320×320 的表盘图输出指针目标框再结合表盘中心把角度换算成实际读数。这套东西解决的是燃气表、电表、老旧机械表这类“摄像头拍一下就能自动抄表”的场景也适合作为 C# 上位机接入深度模型的入门模板。适合谁C# 后端或上位机开发、做视觉方案选型的工程师以及想把 YOLOv8 真的跑进 Windows 程序的初学者。2. 从训练到导出把 YOLOv8 模型变成 C# 能用的 ONNX 文件2.1 导出前先想清楚输入尺寸96 不是瞎选的YOLOv8 官方默认训练尺寸是 640×640但仪表指针检测这个场景很特殊——指针是细长目标表盘本身结构固定不需要那么高的分辨率去区分多种物体。指针检测更看重“位置准”而不是“类别多”所以常见做法是把训练和导出尺寸统一压到 320×320 甚至 96×96。这套源码里的 ONNX 模型输入我一般就按 1×3×96×96 来设计有些机器上跑 320×320 也行只是 CPU 推理会慢不少。96×96 不是玄学是实测结果对一块普通指针表盘96 分辨率刚好能保留刻度线的边缘指针和背景的对比也足够让模型学出指针尖端位置。再低到 64指针细的就要丢了。导出前的另一个决定是类别数。这套源码只检测“指针”一类所以模型输出通道是 54 个框坐标 1 个类别置信度。如果后面你想顺带检测表盘或者刻度数字重新训练成多类输出通道会变成 4N后处理代码就得跟着改。2.2 导出命令opset 和 simplify 是两处关键训练完拿到 best.pt用 ultralytics 自带的 export 方法转 ONNX 就行。我用的命令如下from ultralytics import YOLO model YOLO(best.pt) model.export( formatonnx, opset12, simplifyTrue, dynamicFalse, imgsz96 )这段代码的逻辑是加载训练权重导出成 ONNX。opset12 是为了兼容老版本 onnxruntimeC# 那边引用的 NuGet 包在 1.17.x、1.18.x 上都能直接认如果默认导出的 opset 是 17在老版本运行时会出现算子不支持的报错所以我会主动指定 12。simplifyTrue 会调用 onnxsim 把计算图做常量折叠、删掉训练残留的节点导出后的文件往往能小 5%10%这部分压缩的收益不大但能减少运行时解析算子的时间。dynamicFalse 表示固定输入尺寸。如果你不想写死 96可以改成 dynamicTrue后面 C# 构造 Tensor 的 batch 和宽高就灵活些代价是部分 CPU 平台在动态 shape 下推理速度会打折扣。这里有个容易忽略的细节导出时的 imgsz 必须和训练时一致或成倍数关系。如果训练用的 96导出时写 640模型虽然能导出成功但推理精度会明显下降因为模型是按 96 分辨率学到的特征尺度。2.3 导出后先用 Netron 和探针代码确认输出结构很多人在 C# 里翻车是因为没先确认自己导出的 ONNX 到底输出什么 shape。用 Netron 打开 onnx 文件能看到输入名、输出名和各维度的值。光看还不够我习惯用一小段 Python 探针把输入输出的 shape 打出来import onnxruntime as ort sess ort.InferenceSession(best.onnx) for inp in sess.get_inputs(): print(input:, inp.name, inp.shape) for out in sess.get_outputs(): print(output:, out.name, out.shape)预期输出类似input: images [1, 3, 96, 96] output: output0 [1, 5, 8400]这段探针代码的价值在于它确定了 C# 端的两个关键参数——输入 Tensor 的名字要叫 images输出 Tensor 的 shape 是 1×5×8400。YOLOv8 是三尺度检测96 分辨率下的特征图是 12×12、6×6、3×3三种尺度各自对应 80×80、40×40、20×20 位置加起来正好 8400 个候选框。如果你导出的模型是 640 输入输出会变成 1×5×8400 还是 1×5×25200看 Netron 最稳妥。后面 C# 后处理里的行列遍历都要以这个探针结果为准。2.4 导出后再简化一次手动剪掉不必要算子导出时已经开了 simplify但如果你在 Netron 里看到很多 Resize 和 Mul 节点说明简化不够彻底。可以再用命令行工具过一遍python -m onnxsim best.onnx best_sim.onnx这一步实际上是把模型里原样透传的 Reshape、Transpose 合并掉对 C# 端的 onnxruntime 解析更友好。做完之后把 best_sim.onnx 复制到 C# 项目的输出目录建议命名为 model.onnx别带中文路径Windows 下 onnxruntime 对中文路径有些不稳定的兼容问题。3. 在 C# 里接住 ONNX 输出输入拼装、NMS 与坐标解码3.1 C# 工程的项目配置和模型加载C# 端我推荐用 Microsoft.ML.OnnxRuntime 的 NuGet 包加上 OpenCvSharp4.Windows 做图像解码和预处理。这两个包在项目属性里统一选 x64避免 AnyCPU 在运行时出现程序集不匹配。加载模型的代码如下using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; var sessionOptions new SessionOptions(); sessionOptions.LogSeverityLevel OrtLoggingLevel.ORT_ERR; sessionOptions.SetSessionGraphOptimizationLevel(GraphOptimizationLevel.ORT_ENABLE_ALL); using var session new InferenceSession(model.onnx, sessionOptions);这里 SetSessionGraphOptimizationLevel 开的是 ORT_ENABLE_ALL让 onnxruntime 在加载时做算子融合和内存规划优化。LogSeverityLevel 建议设成 ORT_ERR只输出错误信息避免调试时控制台被 onnxruntime 的日志刷屏。如果你要跑 CUDA需要另外安装 Microsoft.ML.OnnxRuntime.Gpu 包并调用 AppendExecutionProvider_CUDA但配套的 cudnn 版本必须和包要求的严格一致否则运行时会报找不到指定模块第一版先把 CPU 跑通更重要。3.2 Mat 到 DenseTensor像素搬运是 C# 接入的大头onnxruntime 的输入不是 OpenCV 的 Mat而是要一个 1×3×96×96 的 DenseTensor 。难的不是 API而是把 BGR 的 HWC 像素转成 RGB 的 CHW 布局。我用过两种方式一种是直接用 Mat.GetArray 取灰度或三通道数据再手动循环另一种是配合 OpenCvSharp 的 Split 做通道分离。这里给出一个通用的手动搬运版本public static DenseTensorfloat MatToTensor(Mat mat, int targetSize 96) { Mat resized new Mat(); Cv2.Resize(mat, resized, new Size(targetSize, targetSize), 0, 0, InterpolationFlags.Linear); Cv2.CvtColor(resized, resized, ColorConversionCodes.BGR2RGB); var tensor new DenseTensorfloat(new[] { 1, 3, targetSize, targetSize }); var data tensor.Buffer.Span; int height resized.Rows; int width resized.Cols; int channels resized.Channels(); unsafe { byte* ptr (byte*)resized.DataPointer; for (int y 0; y height; y) { byte* rowPtr ptr y * resized.Step(); for (int x 0; x width; x) { int pixelOffset x * channels; int index y * width x; data[index] rowPtr[pixelOffset 0] / 255f; // R data[targetSize * targetSize index] rowPtr[pixelOffset 1] / 255f; // G data[2 * targetSize * targetSize index] rowPtr[pixelOffset 2] / 255f; // B } } } return tensor; }这里有个坑onnxruntime 的 DenseTensor 内存布局要求连续Buffer.Span 的索引顺序是 NCHW所以写入时先填第一个通道R再填 G、B。我上面用 index 直接定位避免每次像素都做多维索引性能会好很多。Cv2.Resize 的 InterpolationFlags.Linear 对应 Python 端 PIL 的 BILINEAR如果你的模型在 Python 里精度正常、在 C# 端漏检优先看这一步的插值方式是否一致。归一化这里直接除以 255f符合 YOLOv8 训练时的前处理不要额外做 mean/std 标准化YOLOv8 的预处理只有缩放和归一化到 0-1。3.3 后处理置信度过滤、NMS 与坐标还原拿到推理结果后第一步把输出张量读出来。输出 shape 是 1×5×8400表示 8400 个候选框每列的前 4 个元素是中心点 x、中心点 y、宽 w、高 h已经解码到输入图像坐标系不需要再做 anchor 解码第 5 个元素是类别置信度。这部分和 YOLOv5 有区别别套旧代码。过滤代码如下var output session.Run(new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) }).First().AsTensorfloat(); var dims output.Dimensions; // [1, 5, 8400] int numBoxes (int)dims[2]; float confThreshold 0.25f; var boxes new ListRectF(); var scores new Listfloat(); for (int i 0; i numBoxes; i) { float conf output[0, 4, i]; if (conf confThreshold) continue; float cx output[0, 0, i]; float cy output[0, 1, i]; float w output[0, 2, i]; float h output[0, 3, i]; float x1 cx - w / 2f; float y1 cy - h / 2f; boxes.Add(new RectF(x1, y1, w, h)); scores.Add(conf); }这段代码的关键点有两个一是 output 的索引是 [batch, attr, anchor]不要写成 [anchor, attr]二是 cx/cy/w/h 是模型输入图坐标系下的坐标尺寸是 96后面要把它们放大回原图放大系数就是原图宽高除以 96。过滤完的置信度数组还要过一遍 NMS。C# 端没有现成的 NMS可以用一个最简单的循环实现按分数从高到低排序选一个框删掉所有和它 IoU 大于 0.45 的框重复直到为空。指针检测框通常只有一到两个NMS 的负担很小直接用两层循环即可不用引入额外依赖。坐标还原的常见做法float scaleX originalWidth / 96f; float scaleY originalHeight / 96f; RectF scaledBox new RectF(box.X * scaleX, box.Y * scaleY, box.Width * scaleX, box.Height * scaleY);到这里C# 端已经能稳定拿到指针的矩形框。但这个框只是“包围指针的最小矩形”矩形四边不是都贴合指针直接取中心点当指针位置误差很大下一步还得在框内做直线拟合来找指针方向。4. 指针到读数最小外接矩形、角度映射与量程标定4.1 拿到指针框之后怎么找指针线检测框只是告诉你“指针在哪”不代表指针方向。这里我常用的做法是把检测框区域裁出来转灰度做二值化再找所有非零像素点用 OpenCV 的 FitLine 拟合一条直线。指针是细长条拟合出来的直线方向就是指针方向。核心代码Mat roi new Mat(gray, new Rect((int)box.X, (int)box.Y, (int)box.Width, (int)box.Height)); Mat thresh new Mat(); Cv2.Threshold(roi, thresh, 60, 255, ThresholdTypes.Binary); var points new ListPoint(); for (int y 0; y thresh.Rows; y) { for (int x 0; x thresh.Cols; x) { if (thresh.Atbyte(y, x) 0) { points.Add(new Point(x, y)); } } } Vec4f line Cv2.FitLine(points, DistanceTypes.L2, 0, 0.01, 0.01); float vx line[0], vy line[1]; float angle (float)(Math.Atan2(vy, vx) * 180.0 / Math.PI);这段代码里阈值 60 是按“指针是深色、表盘是浅色”假设写的实际部署时建议用 Otsu 自动阈值替换固定值因为不同表计的底色和光照差异很大。FitLine 的四个参数里DistanceTypes.L2 表示欧氏距离拟合后面联系数 0、0.01、0.01 是数值稳定性和精度设置保持默认即可。角度算出来是 -180° 到 180° 的笛卡尔角度但指针有方向性同样的直线可能是 30° 也可能是 -150°需要结合框中心相对表盘中心的位置来确定唯一的指针朝向。判断方法是把拟合直线上的两个点投影到表盘中心两侧取离表盘中心更远的那个方向作为指针指向。4.2 角度补偿与量程映射指针角度算出来后下一步是把角度转成物理读数。最常见的表盘是弧形的量程 0270°比如压力表、气压表。这套源码里的读数公式是线性映射float value minScale (pointerAngle - startAngle) / (endAngle - startAngle) * (maxScale - minScale);其中 startAngle、endAngle 是量程起点和终点对应的指针角度minScale、maxScale 是表盘刻度的最小值和最大值。这里有两个前提表盘量程是线性的且指针绕表盘中心旋转。对工业压力表、温度表基本成立对带有非线性刻度的仪表就需要分段线性映射把每个大刻度对应的角度和读数录入数组指针角度落在哪两段之间就用哪一段做插值。最容易翻车的是角度环绕问题。假设量程从 350° 开始、转 270° 后停在 260° 的位置实际上指针跨过了 0° 线直接拿 350 和 260 相减会得到负的区间读数变成负数。解决方式很简单先对角度做归一化把 startAngle 平移到 0再对 360 取模。float normalized (pointerAngle - startAngle 360f) % 360f; float range (endAngle - startAngle 360f) % 360f; float value minScale normalized / range * (maxScale - minScale);这段代码比上一版更稳因为它把角度差强制限制在 0360不会出现跨零导致的跳变。实际使用中我会额外加一个判断如果 normalized 大于 range就说明指针已经超出了有效量程范围这时候要么报警要么把读数钳到 maxScale。4.3 标定文件把表盘中心和量程端点固化下来不同表计的安装位置、摄像头角度不同表盘中心在图像里的坐标也不同所以这套源码把标定参数放在外部配置文件里避免每次改代码。我一般用 JSON 存这些参数{ meterCenterX: 640, meterCenterY: 480, startAngle: 30, endAngle: 300, minScale: 0, maxScale: 10, threshold: 60, useOtsu: true }表盘中心可以离线标定一次用一张表盘正面图手动点出圆心和指针转动的起始、终止刻度角度。startAngle 和 endAngle 的获取方式是在标定界面里先拖一条线到量程起点刻度再拖一条到终点刻度程序自动算两条线相对于表盘中心的角度。摄像头位置动过之后标定参数要重新采一遍这是仪表项目上线时必须走的流程。5. 避坑/常见问题5 个跑过才知道的坑5.1 现象一运行就报 BadImageFormatException程序加载 onnxruntime.dll 或 OpenCvSharp 的 native 库时直接抛 BadImageFormatException或者提示“未能加载文件或程序集”。这是在开发机上最常见的第一道坎。原因通常是项目目标平台是 AnyCPU但 onnxruntime 的 native 库只有 x64 版或者本机装的是 x86 依赖。OpenCvSharp4.Windows 默认跟着目标平台走AnyCPU 下会优先找 x86 的库两边一冲突就崩。解决把 C# 工程的目标平台固定在 x64同时在项目属性里把“首选 32 位”关掉。还有一步常常漏掉确认 onnxruntime.dll 真的被复制到了输出目录。NuGet 包通常会自动拷贝 x64 版本但如果输出目录里同时存在多个版本的 onnxruntime.dll运行时会加载到旧版本出现一些奇怪的算子不支持报错。检查 \bin\x64\Release 下的 dll 文件保留一个版本。5.2 现象输出张量的 shape 和网上教程对不上网上的 YOLOv8 ONNX 例子大多是 1×84×8400 或 1×84×25200你导出的模型输出却是 1×5×8400或者反过来。原因YOLOv8 的输出第四维是 4类别数。网上例子的 84 是 COCO 80 类加 4 个框参数你的自定义模型只有指针一个类所以是 5。另一个可能是 ultralytics 新版导出时默认附加了 NMS 或 end2end 处理输出变成了 1×6×N。解决先用 Netron 看清楚输出依赖。如果看到输出后面跟着 NMS 节点说明导出的是端到端模型shape 一般是 1×6×N前 4 个是框坐标第 5 个是置信度第 6 个是类别 ID后处理逻辑完全不同。建议 export 时不要加 NMS 相关参数保持标准输出让 C# 端自己控制 NMS 阈值可调性更强。5.3 现象同一个模型在 Python 里精度很好到 C# 端指针漏检或误检把 best.pt 在 Python 里用 Ultralytics 跑测试图片检测框很稳放到 C# 端同一张图就检不到指针或者框偏了。原因排查按三个顺序来第一前处理不一致。Python 端用 PIL/OpenCV 读图是 BGR转成 RGB 再送入模型C# 端如果忘了 CvtColor颜色通道反了某些表盘背景下指针会被背景吞掉。第二缩放插值方式不同。C# 的 InterpolationFlags.Linear 和 Python 的 INTER_LINEAR 等价但如果你在 C# 用了 Nearest细指针就容易被重采样丢掉。第三归一化方式不同。YOLOv8 用 1/255 归一化有人会把图像先转成 0 到 1 的 float又做一次 0-1 标准化数据分布就不对了。解决把 C# 端预处理输出和 Python 端预处理输出各自存成二进制文件逐字节比对。这一步看着笨但能一锤定音找到差异比盲调置信度省时间。5.4 现象同一张图多次推理结果不一样连续在同一张图上跑推理有时能检出指针有时检不出甚至框的位置在抖动。原因十有八九是复用了同一个 DenseTensor上一帧的数据还没被完全读取下一帧就开始往里面写。ONNX Runtime 的 Run 是异步的不Run 是同步的但如果你在多个线程里共用同一个 session 和同一个输入 buffer就会产生读写竞争。仪表检测上位机往往同时跑摄像头采集线程和 UI 显示线程两个线程同时调 session.Run结果就会出现不确定性。解决给每个推理请求单独 new 一个 DenseTensorsession 可以共享但输入 Tensor 不能共享。如果对性能有要求至少用 ThreadStatic 或队列把输入输出隔离。5.5 现象单独跑 CPU 正常换成 GPU 版后直接崩溃C# 工程换成 Microsoft.ML.OnnxRuntime.Gpu启动时程序闪退事件查看器里是“找不到指定的模块”。原因onnxruntime-gpu 的 NuGet 包版本和本机 CUDA、cuDNN 版本不匹配。onnxruntime 的每个版本都对应特定的 CUDA 和 cuDNN 版本要求装错了就是起不来没有任何回旋余地。解决先看 onnxruntime-gpu 包说明里写的 CUDA 版本再检查本机环境变量。如果你的部署机器是 Windows且不想装一套 CUDA 全家桶建议继续用 CPU 推理96×96 输入的模型在普通 i5 上单帧也就十几毫秒足够仪表秒级刷新。GPU 版适合批量处理大量图片不适合时钟周期要求严格的实时指针检测。6. 验证闭环用 100 张图把推理精度量化出来6.1 校验工具把人工角度和推理角度对齐改完代码不算完我这个项目里的习惯是建一个最小化的校验工具。准备一个测试集每张原始表盘图对应一份 JSON 标注里面记录人工标注的指针角度。C# 端跑一遍推理把结果和人工标注比对double totalError 0; int count 0; foreach (var sample in testSamples) { var mat Cv2.ImRead(sample.ImagePath, ImreadModes.Color); var angle GetPointerAngle(mat); // 推理角度 double label sample.GroundTruthAngle; // 人工标注角度 double diff Math.Abs(angle - label); if (diff 180) diff 360 - diff; // 角度差取最短弧 totalError diff; count; } double meanError totalError / count; Console.WriteLine($MAE {meanError:F2} deg, count {count});这段代码里GetPointerAngle 需要把检测框 直线拟合 角度归一化全部包进去这样校验的是完整链路而不是某一环节。角度差必须取 360° 以内的最短弧否则在跨零点的地方误差会虚高比如实际差 2° 会被算成 358°。如果平均误差在 0.5° 以内对大多数工业仪表来说读出的数值误差已经能控制在最小刻度的 1/4 以内。6.2 误差统计与阈值调整顺序拿到 MAE 之后如果精度不达标不要急着改模型或换算法。我的调整顺序是现象优先调整项说明偶尔漏检MAE 偏高把输入分辨率从 96 提到 160 或 320分辨率提高后指针特征更清晰代价是推理耗时增加框有抖动但都能检到调 NMS 的 IoU 阈值从 0.45 降到 0.3指针是细长目标多个候选框 IoU 偏低默认阈值会留住多个重叠框角度误差稳定但整体偏移重新标定 startAngle/endAngle而不是调模型偏移大概率是表盘中心点位标偏了某一种表盘型号大面积失败单型号再采集 2030 张图补训练一个模型覆盖多种表盘是坑型号花纹差异大时建议按型号分组最后说一个实战习惯我现在拿到任何新表计都会先跑一遍上面的校验循环把 100 张图的 MAE 打印出来再决定要不要调参。这个动作已经被我写成了固定流程没有一次例外。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于MCP与Docker的智能体记忆复盘系统hindsight工程实践 2026/10/2 5:50:34

基于MCP与Docker的智能体记忆复盘系统hindsight工程实践

1. 项目缘起:为什么“事后复盘”值得被单独做成一个项目“hindsight”这个词本身很有意思,字面意思是“后见之明”,也就是事后才明白过来的那种洞察。把这个词拿来做一个技术项目的名字,指向性其实非常明确:它要解决的…

阅读更多 →
5G NR中pi/2 BPSK与QPSK调制原理及实测星座图解析 2026/10/2 5:50:33

5G NR中pi/2 BPSK与QPSK调制原理及实测星座图解析

1. 这不是教科书里的抽象图,而是5G NR物理层实测信号的“指纹”你打开示波器或频谱仪,看到的不是教科书上那个完美对称、四点均匀分布的QPSK星座图——它可能歪斜、抖动、边缘模糊,甚至在I/Q平面上拉出一条条细长的“尾巴”。而当你把同样的设…

阅读更多 →
MCP+A2A组合实战:多智能体系统生产级部署的坑与解法 2026/10/2 5:50:27

MCP+A2A组合实战:多智能体系统生产级部署的坑与解法

先说个背景:这是《使用MCP和A2A设计多智能体AI系统》的第六篇。前几篇我们把MCP Server从零搭到了能跑,也把A2A协议里的AgentCard、Task模型、消息格式逐条拆过。但说实话,“能跑”和“能用在生产环境”之间,隔着的不是协议文档&a…

阅读更多 →
当一切交由 Agent,用户不再打开你的后台,SaaS 还值钱吗 2026/10/2 5:50:27

当一切交由 Agent,用户不再打开你的后台,SaaS 还值钱吗

1. 一个正在发生的静默转变过去半年,我跟不少做 SaaS 的朋友聊天,话题总会绕到同一个焦虑上:客户越来越不爱打开我们的后台了。以前产品经理天天盯着 DAU、页面停留时长、功能点击率,现在这些指标在很多场景下正在集体失效。原因不…

阅读更多 →
Element必填星号不显示?表单校验规则与prop配置详解 2026/10/2 5:50:27

Element必填星号不显示?表单校验规则与prop配置详解

1. 为什么表单校验总在"必填"这一关翻车做过后台管理系统的前端,十有八九都被"必填校验"折腾过。不是字段没配规则,而是用户根本不知道这个字段要填——等提交的时候被拦截,弹出一串红字,体验已经扣分了。真正…

阅读更多 →
开源模块化模拟驾驶支架DIY:铝型材搭建与调校全记录 2026/10/2 5:50:27

开源模块化模拟驾驶支架DIY:铝型材搭建与调校全记录

折腾装备这件事,我一直有个执念:能自己搭的,绝不买成品。前阵子想上一套模拟驾驶支架,看了一眼成品价格,再看了一眼自己钱包,果断决定走开源方案。最后相中了openrig——一套以开源图纸为核心的模块化装备机…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉