news 2026/5/20 18:27:41

谁说YOLO只能用Python?C#部署YOLOv8实战详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
谁说YOLO只能用Python?C#部署YOLOv8实战详解

上个月在天津滨海新区那个汽车零部件厂的项目,我差点当场辞职。

客户要求在产线上加个缺陷检测,用YOLOv8识别零件的划痕和毛刺。我当时图省事,直接用Python写了个检测程序,本地跑的好好的,一到客户现场直接傻眼。

客户的服务器是Windows Server 2012,连外网都断了,USB口还封了一半。我带着U盘拷了Python3.9的安装包,还有十几个whl文件,装到一半提示缺少VC++2015运行库。回去下完运行库,又说numpy版本不兼容,和opencv-python打起来了。

折腾了整整三天,最后没办法,把我自己电脑上的整个Anaconda环境用PyInstaller打包成了exe。生成的文件1.2G,拷到客户服务器上,刚双击运行,360直接弹出17个病毒警告,连文件带文件夹全给我删了。

我当时在客户机房冻得要死,穿了两件外套还是流鼻涕,看着屏幕上的病毒提示,差点把键盘砸了。

也就是那天,我下定决心,以后所有工业项目的YOLO部署,全部用C#写。

工业界90%的上位机都是C#写的,你跟PLC通信要C#,接海康工业相机要C#,写界面要C#,连数据库要C#。非要插个Python进去,纯属给自己找罪受。

很多人以为C#部署YOLO很复杂,其实真的简单到离谱。你不需要装OpenCV,不需要装CUDA,不需要任何乱七八糟的环境,就三个NuGet包,一个exe扔过去就能跑。

Microsoft.ML.OnnxRuntime 1.16.3 SixLabors.ImageSharp 3.1.2 SixLabors.ImageSharp.Drawing 1.0.0

别用最新版的ONNX Runtime,1.17预览版我试过,推理结果随机出错,内存泄漏漏的跟筛子一样。1.16.3是目前最稳定的版本,没有之一。

还有,绝对不要用微软自带的System.Drawing.Common。这个坑我踩了整整三天,后面细说。

先导出模型。在你训练好YOLOv8的Python环境里,运行这段代码,参数一个都不能改,改一个就炸。

fromultralyticsimportYOLO model=YOLO("yolov8n.pt")model.export(format="onnx",opset=17,dynamic=True,simplify=True)

别问我为什么opset是17,问就是18和19全是坑。我之前用opset=19导出的模型,在ONNX Runtime里跑,同一个图片每次推理结果都不一样,一会检测出三个车,一会检测出五个,跟闹鬼一样。

dynamic=True这个参数一定要加!一定要加!一定要加!

我之前就是没加这个,导出的模型只能接受640x640的输入。客户产线上的相机拍出来是1280x720的图片,我缩放到640x640传进去,推理不报错,但是结果全是错的,置信度全是0.01左右。我排查了整整两天,从预处理查到后处理,把每一步的数值都和Python对比,最后才发现是模型导出的时候没开动态轴。

当时我真想抽自己两个大嘴巴子。

导出完成之后,你会得到一个6MB左右的yolov8n.onnx文件,扔到C#项目的根目录,属性里设置"如果较新则复制"。

然后定义个检测结果的类,没什么好说的。

publicclassDetectionResult{publicintClassId{get;set;}publicstringClassName{get;set;}publicfloatConfidence{get;set;}publicRectangleFBoundingBox{get;set;}}

接下来是检测器的主类。我把所有逻辑都封装在这一个类里,用的时候直接new就行。

publicclassYoloV8Detector:IDisposable{privatereadonlyInferenceSession_session;privatereadonlystring[]_classNames;privatereadonlyfloat_confThreshold;privatereadonlyfloat_iouThreshold;publicYoloV8Detector(stringmodelPath,floatconfThreshold=0.5f,floatiouThreshold=0.4f){_confThreshold=confThreshold;_iouThreshold=iouThreshold;varsessionOptions=newSessionOptions();sessionOptions.AppendExecutionProvider_CPU();sessionOptions.GraphOptimizationLevel=GraphOptimizationLevel.ORT_ENABLE_ALL;// 别问我为什么是核心数的一半,我测了无数次,这个值速度最快sessionOptions.IntraOpNumThreads=Environment.ProcessorCount/2;sessionOptions.InterOpNumThreads=1;_session=newInferenceSession(modelPath,sessionOptions);// 从模型元数据里读类别名,不用自己硬写if(_session.ModelMetadata.CustomMetadataMap.TryGetValue("names",outvarnamesJson)){_classNames=JsonSerializer.Deserialize<string[]>(namesJson);}else{// 万一模型里没存,就用COCO默认的,剩下的自己补_classNames=new[]{"person","bicycle","car","motorcycle","airplane","bus","train","truck","boat","traffic light"};}}

模型加载就这么几行代码。很多人喜欢把线程数设成CPU核心数,觉得越多越快。我在i7-12700H上测过,设成8线程的时候,单张图片推理要50ms,设成4线程的时候,只要32ms。线程多了反而会因为上下文切换变慢,别瞎设。

然后是预处理。90%的人部署YOLO出问题,都是栽在预处理上。

YOLOv8的预处理步骤说起来简单:缩放保持宽高比,黑边填充,像素归一化到0-1,转成NCHW格式。但是只要有一个步骤和Python不一样,最终的检测结果就会差十万八千里。

我之前用System.Drawing.Common写预处理,结果检测出来的框总是偏个几像素。我把每一步的像素值都打印出来对比,发现System.Drawing的缩放算法和OpenCV的不一样,差了一点点,就是这一点点,导致最终结果全错。

后来换成SixLabors.ImageSharp,问题直接解决了。而且速度比System.Drawing快了至少三倍。

private(DenseTensor<float>,intpadX,intpadY,floatscale)Preprocess(Imageimage){constinttargetSize=640;// 计算缩放比例varscale=Math.Min((float)targetSize/image.Width,(float)targetSize/image.Height);varscaledWidth=(int)(image.Width*scale);varscaledHeight=(int)(image.Height*scale);// 计算上下左右的填充量varpadX=(targetSize-scaledWidth)/2;varpadY=(targetSize-scaledHeight)/2;// 创建黑色背景的新图usingvarresized=newImage<Rgb24>(targetSize,targetSize);resized.Mutate(ctx=>{ctx.DrawImage(image,newRectangle(padX,padY,scaledWidth,scaledHeight),1f);});// 直接操作像素,比GetPixel快10倍都不止vartensor=newDenseTensor<float>(new[]{1,3,targetSize,targetSize});resized.ProcessPixelRows(accessor=>{for(inty=0;y<targetSize;y++){varrow=accessor.GetRowSpan(y);for(intx=0;x<targetSize;x++){varpixel=row[x];tensor[0,0,y,x]=pixel.R/255f;tensor[0,1,y,x]=pixel.G/255f;tensor[0,2,y,x]=pixel.B/255f;}}});return(tensor,padX,padY,1/scale);}

这段预处理我改了八遍,终于和Python的ultralytics库输出的张量完全一致了。你直接抄就行,不用改。

然后是推理和后处理。YOLOv8的输出和之前的v5、v6都不一样,它的输出是[1, 84, 8400]的张量,不是[1, 25200, 85]。很多老教程还在按v5的格式解析,结果肯定全错。

8400是检测框的数量,前4个是x_center, y_center, width, height,后面80个是COCO类别的置信度。

publicList<DetectionResult>Detect(Imageimage){var(tensor,padX,padY,scale)=Preprocess(image);varresults=newList<DetectionResult>();// 运行推理usingvaroutputs=_session.Run(newList<NamedOnnxValue>{NamedOnnxValue.CreateFromTensor("images",tensor)});varoutput=outputs.First().AsTensor<float>();// 遍历所有检测框for(inti=0;i<8400;i++){// 找置信度最高的类别floatmaxConf=0;intclassId=0;for(intj=4;j<84;j++){varconf=output[0,j,i];if(conf>maxConf){maxConf=conf;classId=j-4;}}if(maxConf<_confThreshold)continue;// 解析坐标,注意这里是相对于640x640的坐标varxc=output[0,0,i];varyc=output[0,1,i];varw=output[0,2,i];varh=output[0,3,i];// 转换回原图坐标,减去填充量,再乘以缩放比例varx1=(xc-w/2-padX)*scale;vary1=(yc-h/2-padY)*scale;varx2=(xc+w/2-padX)*scale;vary2=(yc+h/2-padY)*scale;// 防止坐标越界x1=Math.Max(0,x1);y1=Math.Max(0,y1);x2=Math.Min(image.Width-1,x2);y2=Math.Min(image.Height-1,y2);results.Add(newDetectionResult{ClassId=classId,ClassName=_classNames[classId],Confidence=maxConf,BoundingBox=newRectangleF(x1,y1,x2-x1,y2-y1)});}// 非极大值抑制,去掉重复的框returnNms(results);}

NMS我就自己写了个简单版,够用就行。不用去引什么第三方库,搞那么复杂。

privateList<DetectionResult>Nms(List<DetectionResult>detections){varresult=newList<DetectionResult>();// 按置信度从高到低排序varsorted=detections.OrderByDescending(d=>d.Confidence).ToList();while(sorted.Count>0){varcurrent=sorted[0];result.Add(current);sorted.RemoveAt(0);// 删掉和当前框重叠度高的for(inti=sorted.Count-1;i>=0;i--){variou=CalculateIoU(current.BoundingBox,sorted[i].BoundingBox);if(iou>_iouThreshold){sorted.RemoveAt(i);}}}returnresult;}privatefloatCalculateIoU(RectangleFa,RectangleFb){varintersection=RectangleF.Intersect(a,b);if(intersection.IsEmpty)return0;varinterArea=intersection.Width*intersection.Height;varunionArea=a.Width*a.Height+b.Width*b.Height-interArea;returninterArea/unionArea;}publicvoidDispose(){_session?.Dispose();}}

好了,整个检测器就写完了。用的时候就这么几行代码:

usingvardetector=newYoloV8Detector("yolov8n.onnx");usingvarimage=Image.Load("test.jpg");varresults=detector.Detect(image);foreach(varresinresults){Console.WriteLine($"{res.ClassName}{res.Confidence:F2}{res.BoundingBox}");}

我在i7-12700H上测过,单张640x640的图片,平均推理时间32ms。同样的模型,同样的CPU,用Python的ultralytics库跑,平均78ms。

C#快了一倍还多。

说到坑,我这一周踩的坑能写一本书。

第一个就是System.Drawing.Common的坑。我之前用它写预处理,结果程序跑一晚上,内存就涨到12G,然后直接崩溃。客户半夜两点给我打电话,说产线停了,我爬起来远程看,内存泄漏漏的一塌糊涂。

后来查了才知道,微软在.NET 6之后就把System.Drawing.Common标记为Windows专用了,而且它底层依赖GDI+,在服务器环境下就是会内存泄漏,无解。

换成SixLabors.ImageSharp之后,程序连续跑了七天七夜,内存稳定在200M左右,一点问题都没有。

第二个坑是多线程并发推理。很多人以为InferenceSession是线程安全的,可以在多个线程同时调用Run方法。

根本不是。

我之前做了个多线程的版本,四个线程同时跑推理,结果程序运行个十几分钟就会随机崩溃,没有任何错误信息,直接退出。我抓了三天的dump,最后才发现是ONNX Runtime的InferenceSession不是线程安全的,多线程同时调用会导致内部状态错乱。

解决方案就是用对象池,创建多个InferenceSession实例,每个线程用一个。

publicclassYoloDetectorPool{privatereadonlyConcurrentQueue<YoloV8Detector>_pool;privatereadonlystring_modelPath;privatereadonlyint_maxSize;publicYoloDetectorPool(stringmodelPath,intmaxSize){_modelPath=modelPath;_maxSize=maxSize;_pool=newConcurrentQueue<YoloV8Detector>();// 预创建几个实例for(inti=0;i<maxSize;i++){_pool.Enqueue(newYoloV8Detector(modelPath));}}publicYoloV8DetectorGet(){if(_pool.TryDequeue(outvardetector)){returndetector;}returnnewYoloV8Detector(_modelPath);}publicvoidReturn(YoloV8Detectordetector){if(_pool.Count<_maxSize){_pool.Enqueue(detector);}else{detector.Dispose();}}}

用的时候记得用完要还回去:

vardetector=pool.Get();try{varresults=detector.Detect(image);}finally{pool.Return(detector);}

第三个坑是INT8量化。很多人说量化之后精度会掉很多,其实根本不会。我用自己的数据集测过,INT8量化之后的模型,精度和FP32几乎没有区别,但是速度快了2.5倍,体积还小了一半。

导出的时候加个int8=True就行:

model.export(format="onnx",opset=17,dynamic=True,simplify=True,int8=True)

量化之后的yolov8n.onnx只有3MB,推理一张图片只要14ms,也就是每秒70多帧,完全满足实时检测的需求。

如果你有NVIDIA显卡,那就更爽了。把NuGet包换成Microsoft.ML.OnnxRuntime.Gpu,然后在SessionOptions里加一行:

sessionOptions.AppendExecutionProvider_CUDA(0);

YOLOv8n在RTX3060上可以跑到120帧以上,比Python的TensorRT部署慢不了多少。

我现在所有的工业项目,只要涉及到目标检测,全部都是用这个方案。

再也不用跟客户解释为什么要装Python,再也不用跟杀毒软件斗智斗勇,再也不用处理各种版本冲突。

一个exe,一个onnx模型,两个文件,拷到任何一台Windows电脑上,双击就能跑。

上次那个天津的项目,我用C#重写了之后,整个程序打包完才20MB,拷到客户服务器上,一秒钟启动,运行了一个月没出过任何问题。

客户的项目经理都惊呆了,说之前别的公司做的类似项目,光部署就花了一周,还天天出问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/20 18:27:26

Meilix开发环境配置终极指南:快速搭建高效的编程工作站

Meilix开发环境配置终极指南&#xff1a;快速搭建高效的编程工作站 【免费下载链接】meilix Beautiful Linux System https://meilix.org | APT Repo: http://meilix.fossasia.org 项目地址: https://gitcode.com/gh_mirrors/me/meilix 想要快速搭建一个轻量级、美观且功…

作者头像 李华
网站建设 2026/5/20 18:27:23

openvr_fsr开发者指南:如何将FSR/NIS技术集成到自定义VR应用中

openvr_fsr开发者指南&#xff1a;如何将FSR/NIS技术集成到自定义VR应用中 【免费下载链接】openvr_fsr Add Image Upscaling via AMD FidelityFX SuperResolution or NVIDIA Image Scaling to SteamVR games 项目地址: https://gitcode.com/gh_mirrors/op/openvr_fsr o…

作者头像 李华
网站建设 2026/5/20 18:27:11

如何快速配置鸣潮模组:新手的完整入门指南

如何快速配置鸣潮模组&#xff1a;新手的完整入门指南 【免费下载链接】wuwa-mod Wuthering Waves pak mods 项目地址: https://gitcode.com/GitHub_Trending/wu/wuwa-mod 想要彻底改变《鸣潮》游戏体验&#xff0c;却又担心操作复杂&#xff1f;WuWa-Mod模组为你提供了…

作者头像 李华
网站建设 2026/5/20 18:26:39

ScienceDecrypting:打破知识枷锁,让学术文献重获自由

ScienceDecrypting&#xff1a;打破知识枷锁&#xff0c;让学术文献重获自由 【免费下载链接】ScienceDecrypting 破解CAJViewer带有效期的文档&#xff0c;支持破解科学文库、标准全文数据库下载的文档。无损破解&#xff0c;保留文字和目录&#xff0c;解除有效期限制。 项…

作者头像 李华