ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# WinForms与YoloV8的电池表面缺陷检测实战指南

C# WinForms与YoloV8的电池表面缺陷检测实战指南 简介面向工业视觉检测场景的C# WinForms Demo源码包专为需要将工业相机与YOLOv8深度学习模型结合的开发者准备。借助Baumer SDK或本地图像输入调用YOLOv8n ONNX模型完成电池检测识别并在界面实时绘制检测框与置信度适合作为产线视觉方案的前期验证与二次开发基础。代码结构简洁清晰相机采集部分以接口形式封装可轻松替换为Basler、Daheng或OpenCV等常见相机接口方便快速迁移到不同硬件环境也能帮助理解YOLO模型在WinForms下的调用流程。压缩包共144个文件约63.71MB核心包含48个dll运行库、15个cs源码文件、1个onnx模型文件、4个exe可执行程序以及12个png界面资源、若干xml配置与资源文件工程结构完整目录层次直观便于阅读、调试和按需裁剪。目前已有147人学习下载既可运行体验完整效果也可直接参考其封装方式接入自己的视觉检测需求。1. 电池检测为什么要用WinForms YoloV8一个工位的故事说到电池检测许多人第一反应是用Halcon或传统视觉算法去做边缘、找圆、量尺寸。但如果是表面划痕、极耳缺损、焊缝不均匀这类特征传统算法的维护成本会让人发疯。一个常见做法是用C#上位机把工业相机拿到的图像或者直接把本地图像文件夹里的图丢给YoloV8深度学习模型做检测识别。WinForms在这里不是噱头它是快速把模型落地到产线工位的实用壳子接相机、显示实时画面、保存检测日志几小时就能搭完。这篇文章就把这条链路讲透包括相机选型、模型导出、C#推理代码以及那些不调试几次根本发现不了的坑。适合有C#基础但没怎么碰过深度学习模型的上位机工程师。2. 先把这套系统拆开方案选型与数据流设计在动手写代码之前先看看整条数据流。这套系统的核心是工业相机或本地图像作为输入源YoloV8作为检测引擎WinForms作为交互外壳。对你来说YoloV8其实是一个黑匣子只需要知道它输入一个640x640的BGR或RGB图像输出一批候选框坐标和类别。如果你仔细观察训练和推理会发现它导出为ONNX后和C#的关系只是一次函数调用而已。C#上位机里没有多少深度学习的痕迹但图像预处理和后处理阶段几乎全是C#代码。2.1 为什么上位机选C# WinForms而不是WPF或CC#上位机是工业现场最主流的开发方式原因有三一是与相机SDK官方支持好Basler、海康都提供.NET接口二是WinForms控件成熟部署简单Win7/10工控机都能跑三是调试周期短。WPF界面漂亮但工业工控机配置普遍不高DataGrid和PictureBox在高帧率场景下并不一定友好C部署深度学习那套会拖慢开发除非你本身就是算法工程师否则没必要。对于电池检测这种单工位离线或在线检测WinForms足够。如果你以后要多个工位统一管理再把检测服务拆成C#后台服务用TCP或Modbus输出结果也不迟。第一步用WinForms把单机跑通风险最小。这个判断放到今天依然成立很多做工业视觉集成的人最初都试过用WPF做炫酷界面最后为了稳定性和现场可维护性还是老老实实退回WinForms。2.2 工业相机与镜头选型先定分辨率、接口和帧率选型顺序是先看被测物尺寸、最小缺陷、安装距离再反推相机和镜头。以一块18650电池外壳长度检测为例视野范围要覆盖65mm最小缺陷假设0.3mm按一个缺陷至少占3个像素的要求横向分辨率需要65/0.3*3≈650像素。但为了留裕量我们通常用500万像素黑白相机比如Basler的500万像素GigE相机。接口优先选GigE或USB3因为线缆和接口卡都比较常见。生产线上运动速度高还要看相机帧率、曝光和增益避免拖影。镜头选型有一个核心公式焦距f 工作距离 * 传感器靶面宽 / 视野宽。比如工作距离200mm靶面1/2.5英寸约5.7mm宽视野65mm焦距约17.5mm选一个16mm或25mm的镜头再权衡视野。电池表面有反光记得加偏振滤光镜或调低曝光。这是“工业相机镜头选型”里最基础的判断方法。现场常见的一个问题是只看相机分辨率而忽略镜头靶面导致最后图像边缘发黑或清晰度不够。选型步骤关键参数示例值1. 确定视野被测物最大外形65mm2. 确定最小缺陷缺陷尺寸0.3mm3. 反推分辨率缺陷占3像素500万像素4. 选接口传输距离、带宽GigE / USB35. 选帧率产线节拍30fps6. 选镜头工作距离、靶面16mm / 25mm2.3 YoloV8模型从训练到ONNX导出的过程如果你不是算法工程师也不需要从头写网络结构。YoloV8是Ultralytics推出的目标检测框架训练入口和导出都封装好了。第一步是用Labelme标电池图片再通过labelme2yolo转成YOLO的txt格式。热词“labelme标注用于yolov8”指的就是这个流程。标注时按你的业务需求定义类别比如defect、normal_side、battery。缺陷不细化的唯一后果是后续不好分析漏检原因所以尽量把划痕、凹坑、脏污分开。训练命令通常是yolo train modelyolov8s.pt databattery.yaml epochs100 imgsz640 device0 batch16参数说明model是基础权重data是数据集配置文件epochs是训练轮数imgsz是训练分辨率device0是用第一张GPU。训练完会得到runs/train/exp/weights/best.pt。接下来导出yolo export modelruns/train/exp/weights/best.pt formatonnx opset12导出后得到一个单文件onnx这个文件就是C#端要加载的模型。上面命令里的imgsz640决定了C#端预处理必须也是640训练时不要用其他尺寸否则部署时尺寸对不上识别率下降是小事坐标全错才是大坑。导出以后先花一分钟确认输入输出形状不要急着写代码import onnxruntime as ort sess ort.InferenceSession(battery.onnx) for i in sess.get_inputs(): print(i.name, i.shape, i.type) for o in sess.get_outputs(): print(o.name, o.shape, o.type)逻辑说明这段脚本用onnxruntime读取模型元数据。输入通常叫images形状是[1,3,640,640]输出形状可能是[1,84,8400]或[1,8400,84]这决定了后面C#端的解析代码怎么写。如果输出是[1,8400,84]可以少做一次转置如果是[1,84,8400]解析时要注意维度的顺序。2.4 数据流与线程模型相机回调、本地图像和UI更新C#端要做的事不是简单地把相机SDK的图片直接塞给模型而是一个线程分工。常见做法是开一个采集线程相机回调里拿到原始帧后转成Bitmap放入一个带锁的队列或ConcurrentQueue检测线程从队列里取出最新帧转成Tensor执行推理推理结果再通过WinForms的Invoke或委托更新到PictureBox。这里首先要明确不要直接在相机回调里跑推理因为相机SDK的回调频率高一卡就会丢帧或触发相机内部缓冲区溢出。也不要在UI线程中跑推理界面会假死产线工人通常会直接关进程。热词“c#委托和事件”在这里非常有用相机SDK的ImageGrabbed事件WinForms的Invoke都可以用委托来封装既避免跨线程冲突又方便把检测结果带过去。比如把结果丢给一个自定义事件UI订阅后刷新业务解耦。本地图像流程比相机流程简单它没有回调竞争只需要把图片文件加载成Bitmap然后走相同的预处理和推理管线。两条路径最终在预处理入口汇合这也是把代码写成公共方法而不是各写一份的原因。3. 在WinForms里跑通检测相机回调、本地图像与ONNX Runtime落地从这一章开始给的是可以直接抄的代码路径。前提是已经装好Visual StudioNuGet包安装了OpenCvSharp4、Microsoft.ML.OnnxRuntime、Basler Pylon SDK或对应相机SDK。工程建议使用.NET Framework 4.7.2或.NET 6/8都可以但SDK兼容性要提前确认。下面按照“相机取流→本地图像→推理→画框”顺序写。3.1 用Basler相机SDK取流的最简代码Basler工业相机是市面上常见选择官方Pylon SDK提供C#示例。第一步连接相机并启动取流using Basler.Pylon; Camera camera new Camera(); // 空字符串表示枚举第一台相机 camera.Open(); camera.Parameters[PLCamera.PixelFormat].SetValue(PLCamera.PixelFormat.Mono8); camera.Parameters[PLCamera.Width].SetValue(1280); camera.Parameters[PLCamera.Height].SetValue(1024); camera.ImagesGrabbed OnImagesGrabbed; camera.GrabStrategy GrabStrategy.OneByOne; camera.StartGrabbing();逻辑说明Camera是Pylon的核心对象Open()之后必须要有参数设置比如分辨率、像素格式。ImagesGrabbed事件是相机每采集到一帧就触发StartGrabbing开始循环取流。GrabStrategy.OneByOne表示逐帧处理避免帧堆积。如果你用的是海康相机接口变成了MVS SDK里的MvUserDefineOutputInfo和RegisterImageCallBackEx但线程模型和队列设计是一样的。回调函数里要做的是把非托管图像转成Bitmap然后丢给队列而不是直接去推理。最小代码private ConcurrentQueueBitmap frameQueue new ConcurrentQueueBitmap(); private void OnImagesGrabbed(object sender, ImageGrabbedEventArgs e) { using (IGrabResult grabResult e.GrabResult) { if (!grabResult.GrabSucceeded) return; Bitmap bitmap new Bitmap(grabResult.Width, grabResult.Height, PixelFormat.Format8bppIndexed); // 这里必须设置灰度调色板否则画出来是全黑 ColorPalette palette bitmap.Palette; for (int i 0; i 256; i) { palette.Entries[i] Color.FromArgb(i, i, i); } bitmap.Palette palette; BitmapData bmpData bitmap.LockBits( new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); IntPtr ptr bmpData.Scan0; // 从 grabResult 复制像素到 ptr int stride bmpData.Stride; // 使用 Marshal.Copy 或 unsafe 指针逐行拷贝 bitmap.UnlockBits(bmpData); frameQueue.Enqueue(bitmap); } }说明IGrabResult是相机返回的一帧用using包裹是为了确保非托管内存释放。Bitmap是WinForms基础图像类后续OpenCvSharp和ONNX Runtime都围绕它做转换。这里最容易漏的是灰度调色板漏掉以后相机显示全黑但模型依然能推理会让人误以为相机坏了。如果乱释放容易踩到后面说的AccessViolation坑。3.2 本地图像批量加载与LetterBox预处理对于没有相机或者做验证的场景本地图像是最快的入口。做法是遍历文件夹里的jpg/png逐个做预处理再推理。预处理的核心是LetterBox保持宽高比缩放到640x640多余部分填充灰色通常114。这是YoloV8训练时采用的预处理方式不能直接拉伸否则目标变形。用OpenCvSharp实现public static Mat Preprocess(Mat image, int inputSize, out float scale, out int padX, out int padY) { scale Math.Min((float)inputSize / image.Width, (float)inputSize / image.Height); int newW (int)(image.Width * scale); int newH (int)(image.Height * scale); Mat resized new Mat(); Cv2.Resize(image, resized, new Size(newW, newH)); Mat canvas new Mat(inputSize, inputSize, MatType.CV_8UC3, new Scalar(114, 114, 114)); padX (inputSize - newW) / 2; padY (inputSize - newH) / 2; Rect roi new Rect(padX, padY, newW, newH); resized.CopyTo(canvas[roi]); resized.Dispose(); return canvas; }逻辑说明scale取宽高较小的缩放比保证图像完整放进640x640的canvas里roi把缩略图居中贴到灰色画布。为什么中间padding而不是直接resize因为模型训练时的标注框也是在居中后的坐标上生成的推理后要还原坐标必须记住这个偏移量。所以函数返回三个值缩放比scale、左上角偏移padX/padY。后面画框时需要把这些变量原样带回。读取本地图像也是用OpenCvSharp直接读Mat mat Cv2.ImRead(D:\battery_images\00001.jpg, ImreadModes.Color); Mat rgb new Mat(); Cv2.CvtColor(mat, rgb, ColorConversionCodes.BGR2RGB); Mat inputMat Preprocess(rgb, 640, out float scale, out int padX, out int padY);说明Cv2.ImRead默认读成BGR而YoloV8训练时通常使用RGB所以要先转通道。有些版本在导出ONNX时会把预处理同时导出如果导出图包含preprocess信息则C#端不需要再做CvtColor但保险起见还是显式转换一次避免不同模型行为不一致。3.3 用ONNX Runtime跑YoloV8推理并解析输出ONNX Runtime在C#中的用法很简单。先加载模型创建Session每次都把Mat转成TensorRun一次。难点不在调用而在于YoloV8的输出格式。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using System.Collections.Generic; using System.Linq; InferenceSession session new InferenceSession(battery.onnx); int inputSize 640; float confThreshold 0.3f; float nmsThreshold 0.4f; public ListRect RunInference(Mat image) { Mat inputMat Preprocess(image, inputSize, out float scale, out int padX, out int padY); Mat rgb new Mat(); Cv2.CvtColor(inputMat, rgb, ColorConversionCodes.BGR2RGB); rgb.ConvertTo(rgb, MatType.CV_32FC3, 1.0 / 255.0); DenseTensorfloat inputTensor new DenseTensorfloat(new[] { 1, 3, inputSize, inputSize }); // 把rgb数据按 CHW 布局填入 tensor // 做法遍历 Mat将像素值赋给 inputTensor[0, c, y, x] for (int y 0; y inputSize; y) { for (int x 0; x inputSize; x) { Vec3b pixel rgb.AtVec3b(y, x); inputTensor[0, 0, y, x] pixel[0]; inputTensor[0, 1, y, x] pixel[1]; inputTensor[0, 2, y, x] pixel[2]; } } var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) }; var results session.Run(inputs); var output results.First().AsTensorfloat(); ListRect boxes new ListRect(); Listfloat scores new Listfloat(); Listint classes new Listint(); // 输出形状 [1,84,8400] 或 [1,8400,84] int numAttrs output.Dimensions[1] 84 ? 84 : output.Dimensions[2]; int numAnchors output.Dimensions[1] 84 ? output.Dimensions[2] : output.Dimensions[1]; for (int i 0; i numAnchors; i) { int cxIndex i * numAttrs 0; int cyIndex i * numAttrs 1; int wIndex i * numAttrs 2; int hIndex i * numAttrs 3; float scoreBase 0; int bestClass 0; // 兼容两种输出形状 float cx, cy, w, h; if (output.Dimensions[1] 84) { cx output[0, 0, i]; cy output[0, 1, i]; w output[0, 2, i]; h output[0, 3, i]; for (int j 4; j 84; j) { if (output[0, j, i] scoreBase) { scoreBase output[0, j, i]; bestClass j - 4; } } } else { cx output[0, i, 0]; cy output[0, i, 1]; w output[0, i, 2]; h output[0, i, 3]; for (int j 4; j 84; j) { if (output[0, i, j] scoreBase) { scoreBase output[0, i, j]; bestClass j - 4; } } } if (scoreBase confThreshold) continue; int left (int)((cx - padX) / scale); int top (int)((cy - padY) / scale); int width (int)(w / scale); int height (int)(h / scale); boxes.Add(new Rect(left, top, width, height)); scores.Add(scoreBase); classes.Add(bestClass); } // NMS 去重 int[] indices; Cv2.Dnn.NMSBoxes(boxes, scores, confThreshold, nmsThreshold, out indices); ListRect result new ListRect(); foreach (int idx in indices) { result.Add(boxes[idx]); } return result; }逻辑说明必须对全部分支做循环把x、y、w、h和类别分数从输出里拆出来置信度高于阈值才保留。反算坐标时padding是左上角偏移scale是预处理里的缩放比。如果不做这一步框会整体偏到右下角这是最常见的翻车点之一。输出形状有两种先判断Dimensions[1]是不是84再决定索引顺序一段代码兼容两种格式。参数说明confThreshold一般控制在0.25-0.5电池缺陷检测建议0.3起步nmsThreshold是IoU阈值默认0.4如果框重叠太严重可以调到0.3。不要为了所有缺陷都能检出来就把置信度设到0.01那样误报全回来了。3.4 把结果画到WinForms上双缓冲与Invoke检测结果需要回到界面。为了让显示不闪烁PictureBox设置DoubleBuffered或者用自定义控件。先把检测结果Bitmap准备好再一次性赋值给PictureBox.Image。样式简单也好维护private void UpdateUi(Bitmap resultBitmap) { if (pictureBox1.InvokeRequired) { pictureBox1.Invoke(new Action(() UpdateUi(resultBitmap))); return; } pictureBox1.Image?.Dispose(); pictureBox1.Image resultBitmap; }画框在Bitmap上直接用Graphics就好using (Graphics g Graphics.FromImage(resultBitmap)) { SizeF stringSize new SizeF(); using (Pen pen new Pen(Color.Red, 2)) { foreach (Rect box in detectedBoxes) { g.DrawRectangle(pen, box); g.DrawString(defect, new Font(Microsoft YaHei, 12), Brushes.Red, box.Left, box.Top - 20); } } }说明InvokeRequired用于判断当前线程是否是UI线程如果是相机回调线程或推理线程必须通过Invoke回到UI线程来更新。否则会出现“线程间操作无效”的报错。为什么用Invoke而不是直接赋值因为WinForms控件的句柄是在UI线程创建的跨线程直接访问是不安全的行为偶尔不报错但现场跑几天就会出问题。复制Bitmap时要注意释放旧图避免内存只升不降。通常在UpdateUi里把上一次的结果图Dispose掉。如果检测频率高于UI刷新频率更新UI时只保留最新一帧不要用队列把结果全部排起来否则界面会越画越慢。4. 电池检测的常见问题与避坑记录现象、原因、解决办法下面这些条目全部来自实际调试中遇到过的问题有的隐藏很深不翻车几次很难发现。4.1 AccessViolationException相机SDK回调与C#释放顺序现象程序跑一段时间或退出时抛“System.AccessViolationException: Attempted to read or write protected memory”或“c0000005”错误有时在相机回调里有时在关闭窗体时。原因Pylon的非托管C底层与C#垃圾回收生命周期不同步。最常见是相机对象还没停止取流就Dispose或者回调函数里直接操作了已经释放的IGrabResult。还有一个隐蔽问题相机回调线程里访问了已经Dispose的Bitmap。解决退出时先停止抓取再释放相机对象顺序必须固定为StopGrabbing() - Close() - Dispose()。回调中不要调用UI只做数据拷贝。调试时把相机SDK升级到与.NET运行时匹配的版本比如**.NET Framework 4.7.2对应老版本Pylon.NET 6用新版本Pylon**混用就会出现间歇性AccessViolation。在释放相机之前先让采集线程退出并Join确保没有回调还在执行。private void Form_FormClosing(object sender, FormClosingEventArgs e) { camera.StopGrabbing(); camera.Close(); camera.Dispose(); }逻辑说明这个顺序看起来简单但很多人的代码是在FormClosing里直接Dispose没有先Stop导致传图线程还在跑底层SDK已经释放资源于是下次回调访问非法内存。严格按顺序写基本能规避大多数AccessViolation。4.2 模型推理结果为空检测框完全不出来现象本地图片能加载推理也不报错但解析了所有anchor没有一个超过置信度阈值。程序不崩溃就是一张空图。原因要么输入图像预处理不对要么输出解析时索引用错。YoloV8的输出有几种形状取决于导出时的opset和模型结构如果用了非标准导出要特别小心维度顺序。更常见的坑是放缩后忘记减padding坐标算出来在图像外面被NMS直接滤掉。解决先不跑NMS把原始score打印出来看最大值是多少。如果最大score是0.001那基本是预处理问题如果是0.6那可能是阈值太高或NMS参数问题。快速排查预处理可以随便挑一张标注过的图把letterbox后的图保存下来肉眼确认目标没有拉伸和损坏。float maxScore 0; for (int i 0; i numAnchors; i) { for (int j 4; j 84; j) { if (output[0, j, i] maxScore) maxScore output[0, j, i]; } } Console.WriteLine($Max score: {maxScore});逻辑说明这段代码直接把所有anchor的最大分类分数打出来。如果你发现maxScore只有0.0001说明网络输入和训练时不一致常见错误是忘了做除以255的归一化或者把BGR当RGB输入。如果maxScore有0.5但仍然没有框说明后处理里置信度阈值设得比它高上调maxScore对应的阈值即可。4.3 相机取流高帧率导致界面卡死或内存暴涨现象相机30fps跑起来一会儿界面就卡任务管理器里内存只升不降。原因相机回调速度快UI刷新跟不上而且队列里Bitmap没有及时释放。另一个常见原因是Bitmap对象使用了PixelFormat.Format8bppIndexed但没有设置调色板导致每次LockBits都失败重试。如果相机分辨率是1200万像素每帧8MB队列里堆积几秒就是几百MB内存。解决放弃每帧都显示采用“最新帧”策略。检测线程每次只取队列里的最新一帧把旧帧Dispose掉UI更新频率限制在15fps左右保证人眼够用。具体处理是循环里先TryDequeue到没有新帧为止只处理最后一帧。如果CPU占用还高先把图像显示缩小检测结果只在高清画面上画框不要每帧都处理整幅图。while (frameQueue.TryDequeue(out Bitmap stale)) { stale.Dispose(); // 丢弃旧帧 } if (frameQueue.TryDequeue(out Bitmap latest)) { // 处理最新帧 }逻辑说明TryDequeue调用一次就取出一个元素这里用while把所有旧帧先清空只留下最后取出的latest。这个技巧能让检测永远追着最新图像跑而不是在一个过期帧上反复推理。现场如果相机帧率不高也可以不用这个优化但保留它没有坏处。4.4 电池表面反光导致漏检和误检现象亮面电池外壳上划痕时有时无甚至反光区域经常被当成缺陷。模型在训练集上表现不错一到现场就变“近视眼”。原因工业相机的镜头和光源角度没调好过曝区域没有信息可提取。深度学习模型可以学会一部分反光纹理但根治还是要靠光学方案。电池外壳通常是不锈钢或镀镍反光强烈如果使用点光或者直射光高光区域会把缺陷完全盖住。解决加偏振片或者把光源改成低角度环形光相机曝光时间调低别让高光区域饱和。同时清理训练样本把反光干扰单独作为一类收集起来重新训练。如果你发现同一张图片在不同曝光下检测结果差异很大基本可以断定是光学问题而不是模型问题。这个方向不要乱调参数光学解决了模型精度会立刻提升。4.5 训练集和现场场景不一致模型精度差现象在标注好的测试集上mAP有0.9现场来一批新电池就乱框。漏检、误检同时存在。原因训练数据大多是实验室在固定光源、固定角度下拍的现场来料方向、型号、曝光条件都和训练时不同。深度学习模型对数据分布很敏感训练集里都是正光位照片现场出现侧光位就识别不准。解决采集至少2-3天的现场图像用Labelme重标后加入训练集特别关注正样本和负样本的配比背景样本太少会导致误报。也可以把“良品”单独设为一个class模型更容易收敛。在算法侧yolov8训练时打开数据增强参数真实场景图像偏暗时可以适当加大hsv_h、hsv_v和degrees让模型见过更多变化。每次换型后保留老数据避免灾难性遗忘。5. 检测上产线前的加速与模型迭代GPU、多相机与数据闭环在产线上“能跑通”和“稳定跑”是两回事。这一章写的是把检测速度拉起来、多相机并行以及模型快速迭代的具体做法。5.1 切换ONNX Runtime的GPU Provider把检测延迟压下去如果是用CPU推理一个640x640的模型单帧延迟大约在60-150ms看你的工控机。如果产线节拍要求每帧200ms内出结果CPU可能勉强够但如果要更高帧率需要配置CUDAExecutionProvider。ONNX Runtime的C# API切换到GPU只需要在创建Session时增加一行SessionOptions options new SessionOptions(); options.AppendExecutionProvider_CUDA(0); InferenceSession session new InferenceSession(battery.onnx, options);逻辑说明AppendExecutionProvider_CUDA(0)指定使用第0块显卡。要注意电脑必须装NVIDIA显卡驱动和对应的CUDA版本/cuDNNONNX Runtime各版本对CUDA版本要求不一致常见做法是装CUDA 12.x配套ONNX Runtime 1.15。如果你在CPU机器上调用这行代码运行时还是会退回CPU不会报错但性能没有变化。要确认是否生效可以检查session.GetProfiling()或者直接打印执行时间。另一个容易忽略的点是TensorRT EP。在NVIDIA显卡上AppendExecutionProvider_Tensorrt有可能比CUDA EP更快尤其对同一个模型反复推理但要先经过TensorRT的engine编译首次推理可能要几十秒。如果模型要频繁替换我一般只用CUDA EP而不用TensorRT省得现场等着急。GPU显存不够时可以把batch设为1ONNX Runtime每次只处理一帧显存占用会小很多。5.2 多相机同时检测用队列和独立Session避免互相拖累如果工位有上下两个相机比如一个看电芯正面、一个看极耳最笨的做法是每个相机一个线程各跑一个Session。这样互不干扰但是两个GPU推理会排队。另一种做法是把图像统一放入一个BlockingCollection由检测线程逐帧取出并决定跑哪个模型。每个相机维护自己的ModelContext里面有自己的Session和预处理参数避免全局变量污染。多个相机共用一个Session时ONNX Runtime内部的并行执行会有锁竞争两个相机可能会互相等待十几毫秒这在高速产线上很致命。我的习惯是每个相机单独Session并且把Session加载放在初始化阶段不要在每帧里new对象。多线程调用同一个Session时如果报错优先检查是否有静态变量被多线程同时写。核心代码示意private void MultiCameraGrabLoop() { while (continueRunning) { if (frameQueue.TryTake(out var frame, 100) frame.CameraId 0) { var boxes sessionForCamera0.Run(frame.Mat); uiDispatcher.Invoke(() ShowBoxes(boxes)); } else if (frameQueue.TryTake(out frame, 100) frame.CameraId 1) { var boxes sessionForCamera1.Run(frame.Mat); uiDispatcher.Invoke(() ShowBoxes(boxes)); } } }逻辑说明TryTake等待100ms超时就继续循环避免结束后线程卡死。每个模型的Session需要预先加载完毕后才能进入循环。如果在两路相机之间切换注意frameQueue里存入的Mat必须深拷贝因为相机SDK会复用缓冲区不拷贝会导致前一帧数据被下一帧覆盖。5.3 数据闭环从Labelme标注到yolov8训练以及模型更新策略模型不是训一次就结束真正上线后一定会遇到新材料、新缺陷。数据闭环是工人点击“误检”按钮把当前帧保存到指定目录算法工程师定期把这些图拉回来用Labelme标注重新训练再导出新的onnx覆盖掉工位上的模型文件。整个过程不需要改C#代码只替换onnx文件即可。训练阶段需要看yolov8模型训练参数含义lr0是初始学习率epochs是总轮数batch是批量大小imgsz是训练分辨率patience是早停阈值。电池项目常见设置是先用epochs100跑如果val_loss还在下降就加50轮。batch大小受显存限制一般8或16都行。要严格使用相同的imgsz训练是640导出和部署也要是640。如果模型在验证集上有很高的召回率但现场误报多应该上调置信度阈值或者增加负样本来压制假阳性。yolo train modelbest.pt databattery.yaml epochs150 imgsz640 batch16 patience20 lr00.01参数说明patience20表示20个epoch内val_loss没有改善就停止训练防止过拟合。lr0选0.01是官方默认值如果你发现loss震荡可以降到0.001。每次训练完先跑一下验证集确认误检/漏检比例有没有变化再导出onnx替换。6. 怎么验证模型没有白训用检验数据说服现场主管模型上线前建议先用本地图像做一次盲测。准备三类图正常电池、带缺陷电池、其他随机物体。确保这些图是训练时没有见过的。跑一轮记录检测耗时、漏检数、误检数。给现场主管看的时候不要只说“感觉挺准”直接列一个表格样本类型数量检出数误检数平均耗时正常电池3027385ms缺陷电池2018086ms如果正常电池被误检成缺陷说明阈值太敏感把confThreshold往上调一档如果缺陷漏检优先补光学和样本不要无脑调阈值。然后保留一批“验收图”每次模型迭代后在验收图上跑一遍回归测试一下上次的误检是否又回来了。这一批图我本地打了标签在C#端写一个简单的BatchTest窗体直接把检测结果输出到txt和标注文件对比这个工具只要半小时就能写完但它能把“玄学”变成可交付的结论。我的习惯是上线后继续每个月存一批现场图不管有没有缺陷都存。两个月后回看很多当时觉得是噪音的样板现在是关键的负样本。工业检测是一个数据不断积累的过程这套C#WinFormsYoloV8的链路好处就是模型替换成本极低。希望帮到你。本文还有配套的精品资源点击获取
返回列表