1. 项目概述:C#上位机与YOLO实时检测的工业级实现
在工业自动化和智能检测领域,实时目标检测系统正成为生产线上的"智能眼睛"。我最近完成了一个基于C# WinForm和YOLOv8的金属零件缺陷检测系统,部署在某汽车零部件制造商的质检工位上。这个项目让我深刻体会到,要构建一个真正可用的实时检测系统,不仅需要掌握算法原理,更需要解决工程化落地中的各种"魔鬼细节"。
这套技术栈的核心优势在于:C# WinForm提供了快速开发工业上位机界面的能力,而YOLO算法通过ONNX Runtime实现了跨平台的高效推理。两者结合既能满足工厂对可视化操作界面的需求,又能提供接近实时的检测性能(在RTX 3060显卡上达到45FPS)。下面我将分享从模型选型到性能优化的全流程实战经验。
2. 技术选型与架构设计
2.1 为什么选择C# + YOLO组合
在评估了Python+Qt、C++/MFC等多种方案后,我们最终选择C# WinForm作为上位机框架,主要基于以下考量:
- 工业现场操作人员更习惯Windows环境,WinForm控件丰富且易于培训
- 相比Python,C#编译型语言特性更适合资源受限的工控机环境
- 通过P/Invoke可以方便调用C++编写的性能敏感模块
- Visual Studio提供的窗体设计器能快速构建符合工控标准的HMI界面
YOLOv8的选取则考虑了:
- 相比v5/v7版本,v8的精度-速度平衡更适合工业场景
- 完善的Python训练生态与ONNX导出支持
- 原生支持分类、检测、分割多种任务,扩展性强
2.2 系统架构设计
我们的解决方案采用分层架构:
code复制[硬件层]
工业相机/PLC → 工控机(GPU) → 显示器
[软件层]
采集模块(OpenCVSharp) → 推理引擎(ONNX Runtime) → 业务逻辑(C#) → UI呈现(WinForm)
[数据流]
M12相机 → RTSP流 → 帧缓冲 → 预处理 → YOLO推理 → 结果可视化 → MES系统对接
关键设计决策:
- 采用双缓冲队列隔离采集与推理线程
- 使用ONNX作为中间表示,避免环境依赖问题
- 将检测逻辑封装为DLL,便于后续升级模型
- 设计心跳机制监控各模块健康状态
3. 核心实现细节
3.1 环境搭建与依赖管理
通过NuGet管理关键包依赖:
xml复制<PackageReference Include="OpenCvSharp4" Version="4.8.0" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.8.0" />
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu" Version="1.15.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
特别要注意的是,必须匹配CUDA/cuDNN版本:
- 对于RTX 30系列显卡,推荐组合:
- CUDA 11.7
- cuDNN 8.5
- ONNX Runtime 1.15+
- 安装时使用以下命令验证环境:
bash复制nvidia-smi # 查看GPU状态
nvcc --version # 检查CUDA
3.2 视频采集优化实践
工业场景下的视频采集有特殊要求:
csharp复制// 使用OpenCV的VideoCapture配置工业相机参数
var capture = new VideoCapture();
capture.Set(VideoCaptureProperties.FourCC, FourCC.MJPG); // MJPEG压缩节省带宽
capture.Set(VideoCaptureProperties.FrameWidth, 1920);
capture.Set(VideoCaptureProperties.FrameHeight, 1080);
capture.Set(VideoCaptureProperties.Fps, 30);
// 硬件触发模式配置(适合同步生产线)
if (useHardwareTrigger) {
capture.Set(VideoCaptureProperties.Trigger, 1);
capture.Set(VideoCaptureProperties.TriggerDelay, 100);
}
常见工业相机协议支持:
- GigE Vision: 通过OpenCV的DSHOW后端支持
- USB3 Vision: 需要厂商SDK(如Basler的Pylon)
- Camera Link: 通常需要帧抓取卡配套驱动
3.3 ONNX模型处理技巧
从YOLOv8导出ONNX时推荐参数:
python复制# Ultralytics YOLOv8导出命令
yolo export model=yolov8n.pt format=onnx imgsz=640 opset=12 simplify=True
关键参数说明:
opset=12:确保支持最新算子simplify=True:应用onnx-simplifier优化计算图imgsz=640:根据相机分辨率调整,不是越大越好
模型加载优化代码:
csharp复制var sessionOptions = new SessionOptions();
sessionOptions.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
sessionOptions.AppendExecutionProvider_CUDA(0); // 优先使用GPU
// 启用TensorRT加速(需单独安装TensorRT)
sessionOptions.RegisterCustomOpLibraryV2("tensorrt.dll");
sessionOptions.EnableOrtCustomOps();
// 对于多模型场景,启用内存共享
sessionOptions.AddSessionConfigEntry("memory.enable_memory_arena_sharing", "1");
3.4 多线程架构实现
工业级应用必须考虑线程安全和资源竞争:
csharp复制// 双缓冲队列实现
public class FrameBuffer : IDisposable
{
private Mat _currentFrame;
private readonly object _lockObj = new();
private long _frameCounter = 0;
public void UpdateFrame(Mat frame)
{
lock (_lockObj) {
_currentFrame?.Dispose();
_currentFrame = frame.Clone();
Interlocked.Increment(ref _frameCounter);
}
}
public (Mat frame, long seq) GetCurrentFrame()
{
lock (_lockObj) {
return (_currentFrame?.Clone(), _frameCounter);
}
}
public void Dispose() {...}
}
// 采集线程
async Task CaptureLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
using var frame = new Mat();
if (!_capture.Read(frame) || frame.Empty()) continue;
_frameBuffer.UpdateFrame(frame);
await Task.Delay(1, token); // 适度让出CPU
}
}
// 推理线程
async Task InferenceLoop(CancellationToken token)
{
var warmUpFrames = 5; // 预热帧数
while (!token.IsCancellationRequested)
{
var (frame, seq) = _frameBuffer.GetCurrentFrame();
if (frame == null) continue;
using (frame) // 确保资源释放
{
if (warmUpFrames-- > 0) continue; // 跳过前几帧
var results = _detector.Run(frame);
UpdateUI(results, seq);
}
await Task.Delay(_inferenceInterval, token);
}
}
3.5 检测结果后处理
YOLOv8的输出解析需要特别注意:
csharp复制public class DetectionResult
{
public Rect BoundingBox { get; set; }
public float Confidence { get; set; }
public int ClassId { get; set; }
public string ClassName => _classNames[ClassId];
}
private List<DetectionResult> ParseOutput(OrtValue output, float confThreshold = 0.5f)
{
var results = new List<DetectionResult>();
var dimensions = output.GetTensorTypeAndShape().Shape;
// YOLOv8输出格式为 [1,84,8400] 其中84=xywh+conf+80classes
var outputArray = output.GetTensorDataAsSpan<float>();
for (int i = 0; i < dimensions[2]; i++) // 遍历8400个预测
{
var conf = outputArray[4 + i * dimensions[1]];
if (conf < confThreshold) continue;
// 解析类别
int classId = 0;
float maxClsConf = 0;
for (int j = 0; j < _classNames.Length; j++) {
var clsConf = outputArray[5 + j + i * dimensions[1]];
if (clsConf > maxClsConf) {
maxClsConf = clsConf;
classId = j;
}
}
// 计算实际坐标
float x = outputArray[0 + i * dimensions[1]] - outputArray[2 + i * dimensions[1]] / 2;
float y = outputArray[1 + i * dimensions[1]] - outputArray[3 + i * dimensions[1]] / 2;
var rect = new Rect((int)x, (int)y,
(int)outputArray[2 + i * dimensions[1]],
(int)outputArray[3 + i * dimensions[1]]);
results.Add(new DetectionResult {
BoundingBox = rect,
Confidence = conf * maxClsConf,
ClassId = classId
});
}
// 应用NMS
return ApplyNMS(results, 0.45f);
}
4. 工业场景下的性能优化
4.1 内存管理黄金法则
在长期运行的工业应用中,内存泄漏会导致系统崩溃:
csharp复制// 安全释放模式示例
public class SafeMat : IDisposable
{
private Mat _mat;
private bool _disposed = false;
public SafeMat(Mat mat) => _mat = mat;
public static implicit operator Mat(SafeMat m) => m._mat;
public void Dispose()
{
if (_disposed) return;
_mat?.Dispose();
_mat = null;
_disposed = true;
GC.SuppressFinalize(this);
}
~SafeMat() => Dispose();
}
// 使用示例
using var frame = new SafeMat(capture.QueryFrame());
using var resized = new SafeMat(new Mat());
Cv2.Resize(frame, resized, new Size(640, 640));
4.2 推理加速技巧
通过以下方法在RTX 3060上实现了3倍加速:
- TensorRT加速:
csharp复制var trtOptions = new OrtTensorRTProviderOptions();
trtOptions.UpdateOptions(new Dictionary<string, string> {
["device_id"] = "0",
["trt_fp16_enable"] = "1",
["trt_engine_cache_enable"] = "1",
["trt_engine_cache_path"] = "./trt_cache"
});
sessionOptions.AppendExecutionProvider_TensorRT(trtOptions);
- 动态批处理:
csharp复制// 在模型导出时开启动态批处理
# Python导出命令
yolo export ... batch=1,4,8 # 支持1/4/8三种批大小
// C#端根据负载动态调整
if (_pendingFrames.Count >= 4) {
var batchInput = PrepareBatchInput(4);
var batchResults = session.Run(batchInput);
ProcessBatchResults(batchResults);
}
- 混合精度推理:
csharp复制sessionOptions.AddSessionConfigEntry("session.enable_fp16_arithmetic", "1");
sessionOptions.AddSessionConfigEntry("session.enable_fp16_storage", "1");
4.3 实时性保障策略
确保稳定帧率的技巧:
- 跳帧策略:
csharp复制var lastProcessedSeq = 0L;
while (!token.IsCancellationRequested)
{
var (frame, seq) = _frameBuffer.GetCurrentFrame();
if (seq <= lastProcessedSeq) continue; // 跳过已处理帧
// ...处理逻辑...
lastProcessedSeq = seq;
}
- 动态频率调整:
csharp复制// 根据处理耗时动态调整推理间隔
var sw = Stopwatch.StartNew();
ProcessFrame(frame);
sw.Stop();
_inferenceInterval = Math.Clamp(
(int)(_inferenceInterval * (sw.ElapsedMilliseconds / _targetFrameTime)),
10, 100);
- GPU-CPU负载平衡:
csharp复制// 当GPU利用率超过90%时,降级到CPU处理
var gpuUtil = GetGpuUtilization(); // 通过NVML获取
if (gpuUtil > 90) {
sessionOptions.AppendExecutionProvider_CPU(0);
sessionOptions.DisableMemPattern();
}
5. 工业部署实战经验
5.1 常见问题排查指南
| 故障现象 | 可能原因 | 解决方案 |
|---|---|---|
| 画面卡顿 | GPU内存不足 | 降低模型分辨率或使用tiny版本 |
| 检测框偏移 | 预处理未归一化 | 确保输入数据归一化到0-1范围 |
| 内存持续增长 | 未释放ORTValue | 使用using包裹所有推理输出 |
| 首次推理慢 | 未预热模型 | 启动时用空白图运行5-10次推理 |
| 检测漏框 | NMS阈值过高 | 调整iou_threshold到0.3-0.5 |
5.2 产线部署注意事项
- 环境隔离:
bash复制# 使用独立conda环境
conda create -n yolo_env python=3.8
conda activate yolo_env
pip install onnxruntime-gpu==1.15.1 opencv-python==4.8.0.74
- 服务化部署:
csharp复制// 作为Windows服务运行
public class DetectionService : ServiceBase
{
protected override void OnStart(string[] args)
{
_worker = new Thread(InferenceMain);
_worker.IsBackground = true;
_worker.Start();
}
protected override void OnStop()
{
_cancellationSource?.Cancel();
_worker?.Join(5000);
}
}
- 看门狗机制:
csharp复制// 监控推理线程健康状态
private void WatchdogThread()
{
while (!_supervisorToken.IsCancellationRequested)
{
if (_lastInferenceTime < DateTime.Now.AddSeconds(-5)) {
RestartInferenceEngine();
}
Thread.Sleep(3000);
}
}
5.3 模型更新策略
实现热更新模型而不重启应用:
csharp复制public class ModelSwitcher
{
private InferenceSession _activeSession;
private readonly object _switchLock = new();
public void SwitchModel(string newModelPath)
{
lock (_switchLock) {
var newSession = CreateSession(newModelPath);
var old = Interlocked.Exchange(ref _activeSession, newSession);
old?.Dispose();
}
}
public IDisposable GetSessionHandle()
{
var session = _activeSession;
return new SessionHandle(session, _switchLock);
}
private class SessionHandle : IDisposable
{
private InferenceSession _session;
private object _lock;
public SessionHandle(InferenceSession s, object l) {
_session = s;
_lock = l;
Monitor.Enter(_lock);
}
public void Dispose() {
Monitor.Exit(_lock);
}
}
}
6. 扩展功能实现
6.1 PLC通讯集成
通过S7.NET库与西门子PLC交互:
csharp复制var plc = new Plc(CpuType.S71200, "192.168.0.10", 0, 1);
plc.Open();
// 写入检测结果
if (defects.Count > 0) {
plc.Write("DB1.DBW0", (short)1); // 故障信号
plc.Write("DB1.DBD2", defects[0].ClassId); // 缺陷类型
}
// 读取启动信号
var runSignal = plc.Read("I0.0");
6.2 检测结果数据库存储
使用Entity Framework Core记录检测历史:
csharp复制public class DetectionContext : DbContext
{
public DbSet<InspectionRecord> Records { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite("Data Source=inspections.db");
}
public class InspectionRecord
{
public int Id { get; set; }
public DateTime Timestamp { get; set; }
public string ProductId { get; set; }
public string DefectType { get; set; }
public float Confidence { get; set; }
public byte[] Snapshot { get; set; } // JPEG格式缩略图
}
// 使用示例
using var db = new DetectionContext();
db.Records.Add(new InspectionRecord {
Timestamp = DateTime.Now,
ProductId = Guid.NewGuid().ToString(),
DefectType = result.ClassName,
Confidence = result.Confidence,
Snapshot = CompressImage(frame)
});
db.SaveChanges();
6.3 可视化增强技巧
- 自定义渲染:
csharp复制protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
// 绘制FPS计数器
e.Graphics.DrawString($"FPS: {_currentFps}",
new Font("Arial", 12),
Brushes.Green,
new Point(10, 10));
// 绘制检测热力图
if (_heatmap != null) {
using var heatmapImg = _heatmap.ToBitmap();
e.Graphics.DrawImage(heatmapImg,
new Rectangle(Width - 210, Height - 210, 200, 200));
}
}
- 报警动画效果:
csharp复制private async void PlayAlarmAnimation()
{
for (int i = 0; i < 5; i++) {
this.BackColor = Color.Red;
await Task.Delay(200);
this.BackColor = SystemColors.Control;
await Task.Delay(200);
}
}
7. 性能优化深度解析
7.1 视频解码加速
使用硬件加速解码提升采集性能:
csharp复制// 启用NVIDIA硬解码
var capture = new VideoCapture();
capture.Set(VideoCaptureProperties.CAP_PROP_HW_ACCELERATION,
VideoAccelerationType.D3D11);
// 或者使用FFMPEG解码
capture.Set(VideoCaptureProperties.CAP_PROP_OPENCV_FFMPEG_CAPTURE_OPTIONS,
"hwaccel;cuvid;");
7.2 推理流水线优化
通过异步流水线提升吞吐量:
csharp复制// 三级流水线设计
var preprocessChannel = Channel.CreateBounded<Mat>(5);
var inferenceChannel = Channel.CreateBounded<float[]>(5);
var postprocessChannel = Channel.CreateBounded<Result>(5);
// 预处理线程
async Task PreprocessWorker()
{
await foreach (var frame in preprocessChannel.Reader.ReadAllAsync()) {
using (frame) {
var tensor = Preprocess(frame);
await inferenceChannel.Writer.WriteAsync(tensor);
}
}
}
// 推理线程
async Task InferenceWorker()
{
await foreach (var tensor in inferenceChannel.Reader.ReadAllAsync()) {
var output = _session.Run(tensor);
await postprocessChannel.Writer.WriteAsync(output);
}
}
// 后处理线程
async Task PostprocessWorker()
{
await foreach (var output in postprocessChannel.Reader.ReadAllAsync()) {
var results = Postprocess(output);
UpdateUI(results);
}
}
7.3 模型量化实践
将FP32模型量化为INT8提升速度:
python复制# 使用ONNX Runtime的量化工具
python -m onnxruntime.quantization.preprocess \
--input yolov8n.onnx \
--output yolov8n_quantized.onnx \
--opset 12
python -m onnxruntime.quantization.quantize \
--model yolov8n_quantized.onnx \
--output yolov8n_int8.onnx \
--quant_format QOperator
C#端加载量化模型:
csharp复制var sessionOptions = new SessionOptions();
sessionOptions.AddSessionConfigEntry("session.intra_op_num_threads", "4");
sessionOptions.AddSessionConfigEntry("session.inter_op_num_threads", "2");
sessionOptions.AppendExecutionProvider_CPU(0); // 量化模型建议用CPU
var session = new InferenceSession("yolov8n_int8.onnx", sessionOptions);
8. 异常处理与日志系统
8.1 健壮的错误处理框架
csharp复制public class DetectionEngine
{
private readonly ILogger _logger;
public async Task RunInferenceLoop()
{
while (!_token.IsCancellationRequested)
{
try {
var frame = await GetNextFrameAsync();
var results = await DetectAsync(frame);
await HandleResultsAsync(results);
}
catch (OperationCanceledException) {
_logger.LogInformation("检测任务已取消");
break;
}
catch (Exception ex) {
_logger.LogError(ex, "推理循环异常");
await RecoveryProcedure(); // 恢复流程
}
}
}
private async Task RecoveryProcedure()
{
try {
await Task.Delay(1000); // 等待1秒
// 重置资源
_frameBuffer?.Dispose();
_frameBuffer = new FrameBuffer();
// 重连相机
_capture?.Release();
_capture = new VideoCapture(_cameraIndex);
// 预热模型
await WarmUpModelAsync();
}
catch (Exception ex) {
_logger.LogCritical(ex, "恢复流程失败");
throw; // 向上抛出
}
}
}
8.2 结构化日志实现
使用Serilog记录运行日志:
csharp复制var logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {Message}{NewLine}{Exception}")
.WriteTo.File("logs/detection-.log",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
// 记录带上下文的日志
logger.Information("启动检测引擎,模型: {ModelName}, 分辨率: {Width}x{Height}",
Path.GetFileName(modelPath),
inputWidth,
inputHeight);
// 记录性能指标
logger.Information("推理耗时: {InferenceTime}ms, 预处理: {PreprocessTime}ms",
stopwatch.ElapsedMilliseconds,
preprocessStopwatch.ElapsedMilliseconds);
9. 部署与维护实战
9.1 安装包制作指南
使用Inno Setup创建安装程序:
iss复制[Setup]
AppName=智能检测系统
AppVersion=1.2.0
DefaultDirName={pf}\SmartInspection
DefaultGroupName=智能检测
OutputDir=output
OutputBaseFilename=SmartInspection_Setup
Compression=lzma2
SolidCompression=yes
[Files]
Source: "Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
[Icons]
Name: "{group}\启动检测系统"; Filename: "{app}\SmartInspection.exe"
Name: "{commondesktop}\智能检测系统"; Filename: "{app}\SmartInspection.exe"
[Run]
Filename: "{app}\vc_redist.x64.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "安装VC++运行库..."
Filename: "{app}\cudnn_installer.exe"; Parameters: "/S"; StatusMsg: "安装CUDA组件..."
Filename: "{app}\SmartInspection.exe"; Description: "启动智能检测系统"; Flags: postinstall nowait
9.2 远程监控实现
通过MQTT上报运行状态:
csharp复制var factory = new MqttFactory();
var client = factory.CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("iot.example.com", 1883)
.WithCredentials("inspection", "password123")
.Build();
await client.ConnectAsync(options);
// 定期上报状态
_ = Task.Run(async () => {
while (!_token.IsCancellationRequested) {
var message = new MqttApplicationMessageBuilder()
.WithTopic("inspection/status")
.WithPayload(JsonConvert.SerializeObject(new {
Timestamp = DateTime.Now,
FPS = _currentFps,
Memory = Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024,
GPU = GetGpuUtilization()
}))
.Build();
await client.PublishAsync(message);
await Task.Delay(5000, _token);
}
});
9.3 自动化测试方案
使用Appium进行UI自动化测试:
csharp复制[TestFixture]
public class InspectionUITests
{
private AppiumDriver<WindowsElement> _driver;
[SetUp]
public void Setup()
{
var options = new AppiumOptions();
options.AddAdditionalCapability("app", @"C:\Program Files\SmartInspection\SmartInspection.exe");
options.AddAdditionalCapability("deviceName", "WindowsPC");
_driver = new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), options);
}
[Test]
public void TestDetectionWorkflow()
{
var startBtn = _driver.FindElementByName("开始检测");
startBtn.Click();
Thread.Sleep(5000); // 等待检测
var resultLabel = _driver.FindElementByAccessibilityId("lblResult");
Assert.That(resultLabel.Text, Does.Contain("OK").Or.Contain("NG"));
}
[TearDown]
public void TearDown() => _driver?.Quit();
}
10. 项目演进方向
10.1 模型持续学习方案
设计模型在线更新机制:
python复制# Flask服务接收新样本
@app.route('/upload', methods=['POST'])
def upload_sample():
image = request.files['image']
annotations = request.form['annotations']
# 存储到训练集
save_to_training_set(image, annotations)
# 触发增量训练
if should_retrain(): # 根据样本数量等条件
threading.Thread(target=train_incremental_model).start()
return jsonify(success=True)
def train_incremental_model():
# 加载基础模型
model = YOLO('yolov8n.pt')
# 增量训练
model.train(
data='dataset.yaml',
epochs=10,
imgsz=640,
batch=16,
resume=True, # 增量训练
cache='ram' # 加速训练
)
# 导出新ONNX
model.export(format='onnx', simplify=True)
# 推送到生产环境
upload_to_production('yolov8n_updated.onnx')
10.2 多相机协同方案
csharp复制public class MultiCameraManager
{
private List<CameraWorker> _workers = new();
private ResultAggregator _aggregator = new();
public void AddCamera(string rtspUrl)
{
var worker = new CameraWorker(rtspUrl);
worker.ResultsReady += (s, e) => _aggregator.Process(e.Results);
_workers.Add(worker);
}
public void StartAll()
{
foreach (var w in _workers) {
w.Start();
}
}
}
public class ResultAggregator
{
private Dictionary<int, List<DetectionResult>> _buffer = new();
public void Process(CameraResult result)
{
lock (_buffer) {
if (!_buffer.ContainsKey(result.CameraId)) {
_buffer[result.CameraId] = new List<DetectionResult>();
}
_buffer[result.CameraId] = result.Detections;
// 每收到3个相机结果就触发一次聚合
if (_buffer.Count >= 3) {
var finalResults = MergeResults(_buffer.Values);
OnFinalResultsReady(finalResults);
_buffer.Clear();
}
}
}
}
10.3 边缘计算集成
将部分计算下放到边缘设备:
csharp复制public class EdgeComputingService
{
private readonly UdpClient _udpClient = new(11000);
public async Task StartAsync()
{
while (true) {
var result = await _udpClient.ReceiveAsync();
var detections = ParseEdgeResults(result.Buffer);
// 与本地结果融合
var merged = MergeWithLocalResults(detections);
UpdateUI(merged);
}
}
private List<DetectionResult> ParseEdgeResults(byte[] data)
{
using var ms = new MemoryStream(data);
using var reader = new BinaryReader(ms);
var count = reader.ReadInt32();
var results = new List<DetectionResult>(count);
for (int i = 0; i < count; i++) {
results.Add(new DetectionResult {
X = reader.ReadSingle(),
Y = reader.ReadSingle(),
Width = reader.ReadSingle(),
Height = reader.ReadSingle(),
Confidence = reader.ReadSingle(),
ClassId = reader.ReadInt32()
});
}
return results;
}
}
