一、前言
本篇总结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启动时间比较慢,但是之后的每次推理就很快了。