一、前言
本篇总结C#端使用yolo10的onnx文件做模型推理,主要使用Microsoft.ML.OnnxRuntime.Gpu这个库。需要注意的是Microsoft.ML.OnnxRuntime 和 Microsoft.ML.OnnxRuntime.Gpu 这2库只装1个就行,CPU就装前者,反之后者。然后需要注意系统安装的CUDA版本、CUDNN、OnnxRuntime这3者的版本一致,可在这里查询 NVIDIA - CUDA | onnxruntime
这里使用的是 Microsoft.ML.OnnxRuntime.Gpu 版本 1.15.1版本
CUDA 11.8 和 Cudnn 8.5.0
二、代码
使用vs2022平台 debug x64模式,注意需要将图片进行 letterbox居中填充预处理,以及将Mat转为Tensor,数据排布需要转换,详看 letterBox 和 matToTensor,yolo10输出不需要使用nms,输出矩阵(300,6),直接置信度筛除
//Form.cs void inferDemo() { string model_path = @"model/yolov10x.onnx";string device = "GPU"; //CPU GPU float conf_thr = 0.15f;DefectYolo yoloDet = new DefectYolo(model_path = model_path, conf_thr = conf_thr, device = device); //只需一次初始化string img_path = @"model/demo.png";bool show_draw = true;yoloDet.inferImg(img_path, show_draw = show_draw);}
//DefectYolo.csusing System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;using OpenCvSharp;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using static OpenCvSharp.LineIterator;
using System.Collections;
using System.Web.Compilation;
using System.IO;
using System.Security.Claims;namespace yolo10onnx
{class DefectYolo{private int model_height=0, model_width=0;private int ch = 3;private float conf_thr = 0.15f;float[] floatArray;private InferenceSession session;IDisposableReadOnlyCollection<DisposableNamedOnnxValue> result_infer;public DefectYolo(string model_path, float conf_thr, string device = "GPU" ){//初始化模型var sessionOptions = new SessionOptions();if (device.Contains("GPU")){try{sessionOptions.AppendExecutionProvider_CUDA(); //只需要安装 Microsoft.ML.OnnxRuntime.GPU , 然后 onnxruntime 版本和 CUDA cudnn版本都要对好}catch (Exception ex){MessageBox.Show("模型初始化失败!GPU调用发生错误:" + ex.Message);}}else{sessionOptions.AppendExecutionProvider_CPU();}//根据onnx文件路径实例化一个推理对象session = new InferenceSession(model_path, sessionOptions);//session.Run 第一次很慢,先预热var inputMeta = session.InputMetadata;foreach (var input in inputMeta){int[] model_shape = input.Value.Dimensions;model_height = model_shape[2];model_width = model_shape[3];}DenseTensor<float> zeroT = getRandomTensor();List<NamedOnnxValue> input_ontainer = new List<NamedOnnxValue>(); ;//将 input_tensor 放入一个输入参数的容器,并指定名称input_ontainer.Add(NamedOnnxValue.CreateFromTensor("images", zeroT ));result_infer = session.Run(input_ontainer);floatArray = new float[model_height * model_width * ch];}private DenseTensor<float> getRandomTensor(){int[] shape = new int[] { 1, 3, model_height, model_width };float[] values = new float[1 * 3 * model_height * model_width]; // 根据需要填充数据Array.Clear(values, 0, values.Length); DenseTensor<float> tensor = new DenseTensor<float>(values, shape);return tensor;}private Mat letterBox(Mat img, ref Tuple<float, float> ratio, ref float dw, ref float dh,Tuple<int, int> newShape = null, bool auto = false, bool scaleFill = false, bool scaleup = true, bool center = true, int stride = 32){if (newShape == null){newShape = new Tuple<int, int>(640, 640); // Default shape (640, 640)}Size shape = img.Size(); // current shape [height, width]// Scale ratio (new / old)float r = Math.Min(newShape.Item1 / (float)shape.Height, newShape.Item2 / (float)shape.Width);if (!scaleup) // only scale down, do not scale up (for better val mAP){r = Math.Min(r, 1.0f);}// Compute paddingratio = new Tuple<float, float>(r, r); // width, height ratiosSize newUnpad = new Size((int)Math.Round(shape.Width * r), (int)Math.Round(shape.Height * r));dw = newShape.Item2 - newUnpad.Width;dh = newShape.Item1 - newUnpad.Height; // wh paddingif (auto) // minimum rectangle{dw = dw % stride;dh = dh % stride;}else if (scaleFill) // stretch{dw = 0.0f;dh = 0.0f;newUnpad = new Size(newShape.Item2, newShape.Item1);ratio = new Tuple<float, float>(newShape.Item2 / (float)shape.Width, newShape.Item1 / (float)shape.Height); // width, height ratios}if (center){dw /= 2; // divide padding into 2 sidesdh /= 2;}if (!shape.Equals(newUnpad)) // resize{img = img.Resize(newUnpad, interpolation: InterpolationFlags.Linear);}int top = (int)Math.Round(dh - 0.1f);int bottom = (int)Math.Round(dh + 0.1f);int left = (int)Math.Round(dw - 0.1f);int right = (int)Math.Round(dw + 0.1f);// Add border (padding)Scalar borderColor = new Scalar(114, 114, 114); // Color for the padding (similar to [114, 114, 114])img = img.CopyMakeBorder(top, bottom, left, right, BorderTypes.Constant, borderColor);return img;}private List<float[]> filterResult(float[] pred_array, Tuple<float, float> ratio , float x_offset, float y_offset){List<float[]> pred_l = new List<float[]>();int inter = 6;for (int i = 0; i < pred_array.Length; i += inter){float conf_v = pred_array[i + 4];if (conf_v > conf_thr){ float xmin = (pred_array[i] - x_offset) / ratio.Item1;float ymin = (pred_array[i + 1] - y_offset) / ratio.Item2;float xmax = (pred_array[i + 2] - x_offset) / ratio.Item1;float ymax = (pred_array[i + 3] - y_offset) / ratio.Item2;pred_l.Add( new float[] { xmin, ymin, xmax, ymax, conf_v, pred_array[i + 5] } );}}return pred_l;}public void boxLabel(Mat im, float[] pred_arr, Scalar color = default(Scalar), Scalar txtColor = default(Scalar), string label = "" ){int lw = 1;if (color == default(Scalar))color = new Scalar(0, 255, 255); // Default color (yellow)if (txtColor == default(Scalar))txtColor = new Scalar(0, 0, 0); // Default text color (black)// Convert float box coordinates to integerPoint p1 = new Point((int)pred_arr[0], (int)pred_arr[1] );Point p2 = new Point((int)pred_arr[2], (int)pred_arr[3] );// Draw the rectangleCv2.Rectangle(im, p1, p2, color, lw, LineTypes.AntiAlias);if (!string.IsNullOrEmpty(label)){// Font thickness and size calculationint tf = Math.Max(lw - 1, 1); // Font thicknessSize textSize = Cv2.GetTextSize(label, HersheyFonts.HersheySimplex, lw / 3.0, tf, out _);int textWidth = textSize.Width;int textHeight = textSize.Height;// Check if the label can fit outside the rectanglebool outside = p1.Y - textHeight -3 >= 0;Point labelPos;Rect labelRect;if (outside){// Label fits outside the boxlabelPos = new Point(p1.X, p1.Y - textHeight-3 );labelRect = new Rect(p1.X, labelPos.Y, textWidth, textHeight + 3);}else{// Label fits inside the boxlabelPos = new Point(p1.X, p1.Y + textHeight+3 );labelRect = new Rect(p1.X, labelPos.Y- textHeight, textWidth, textHeight+3 );}// Draw the background rectangle for the labelCv2.Rectangle(im, labelRect, color, -1, LineTypes.AntiAlias);// Draw the label text\if (outside){Cv2.PutText(im, label, new Point(p1.X, labelPos.Y + textHeight + 1), HersheyFonts.HersheySimplex, lw / 3.0, txtColor, tf, LineTypes.AntiAlias);}else{Cv2.PutText(im, label, new Point(p1.X, labelPos.Y - 1), HersheyFonts.HersheySimplex, lw / 3.0, txtColor, tf, LineTypes.AntiAlias);}}}private void visResult(Mat img , List<float[]> pred_list ,bool show_draw ){if (show_draw){for (int i=0; i< pred_list.Count ;i++){float[] pred_target = pred_list[i];float conf = pred_target[4];int cls_id = (int)pred_target[5];string label_str = string.Format("{0}-{1:F2}", cls_id, conf);boxLabel(img , pred_target, new Scalar(),new Scalar() ,label_str);}Cv2.ImShow("img", img);Cv2.WaitKey();Cv2.DestroyAllWindows();}}public void inferImg(string image_path , bool show_draw=false){DateTime t0 = DateTime.Now;Mat src = Cv2.ImRead(image_path); // Mat mat_image = new Mat();Tuple<float, float> ratio = new Tuple<float, float>(0.0f,0.0f) ;float dw=0.0f;float dh=0.0f;mat_image = letterBox(src,ref ratio , ref dw , ref dh);Tensor<float> tensor = matToTensor(mat_image); //new DenseTensor<float>(input_image, new[] { 1, 3, 896, 896 });List<NamedOnnxValue> input_ontainer = new List<NamedOnnxValue>(); ;//将 input_tensor 放入一个输入参数的容器,并指定名称input_ontainer.Add(NamedOnnxValue.CreateFromTensor("images", (DenseTensor<float>)tensor ));// tensor));result_infer = session.Run(input_ontainer);// 将输出结果转为DisposableNamedOnnxValue数组DisposableNamedOnnxValue[] results_onnxvalue = result_infer.ToArray();Tensor<float> result_tensors = results_onnxvalue[0].AsTensor<float>();float[] det_result_array = result_tensors.ToArray();List<float[]> pred_list = filterResult(det_result_array , ratio, dw , dh);DateTime t1 = DateTime.Now;MessageBox.Show( "推理耗时:" + (t1 - t0).TotalMilliseconds + "ms"); //200ms左右visResult( src , pred_list , show_draw);}private Tensor<float> matToTensor(Mat mat_image){for (int y = 0; y < model_height; y++){for (int x = 0; x < model_width; x++){// 获取当前像素的 BGR 值Vec3b pixel = mat_image.At<Vec3b>(y, x);// 获取当前像素的 BGR 值byte b = pixel.Item0; // 蓝色通道byte g = pixel.Item1; // 绿色通道byte r = pixel.Item2; // 红色通道// 计算在 result 数组中的索引int index = y * model_width + x;// 按照要求的顺序排列floatArray[index] = r/ 255.0f; // R通道floatArray[index + model_height * model_width] = g / 255.0f; // G通道floatArray[index + 2 * model_height * model_width] = b / 255.0f; // B通道}}Tensor<float> tensor = new DenseTensor<float>(floatArray, new[] { 1, ch, model_height, model_width });return tensor;}}}
三、效果
GPU的 onnx runtime启动时间比较慢,但是之后的每次推理就很快了。