C# yolo10使用onnx推理

一、前言

本篇总结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),直接置信度筛除

cs 复制代码
    //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);
       
        }
cs 复制代码
//DefectYolo.cs


using 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 padding
            ratio = new Tuple<float, float>(r, r);   // width, height ratios
            Size 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 padding

            if (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 sides
                dh /= 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 integer
            Point 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 rectangle
            Cv2.Rectangle(im, p1, p2, color, lw, LineTypes.AntiAlias);

            if (!string.IsNullOrEmpty(label))
            {
                // Font thickness and size calculation
                int tf = Math.Max(lw - 1, 1); // Font thickness
                Size 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 rectangle
                bool outside = p1.Y - textHeight -3 >= 0;

                Point labelPos;
                Rect labelRect;
                if (outside)
                {
                    // Label fits outside the box
                    labelPos = new Point(p1.X, p1.Y - textHeight-3 );
                     labelRect = new Rect(p1.X, labelPos.Y, textWidth, textHeight + 3);
                }
                else
                {
                    // Label fits inside the box
                    labelPos = 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 label
                
              
                Cv2.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启动时间比较慢,但是之后的每次推理就很快了。