前言
根据YoLo运用学习3所说,实现在C#环境使用YoLo模型进行AI推理检测,最常见有两种:
1、python训练好YoLo的模型pt格式,将其转化为onnx格式,通过C#的相关库去加载并推理onnx格式并训练好的YoLo模型
2、直接通过python训练好YoLo模型后直接去识别检测图片,将定位、置信阈值等信息传入Halcon,进行显示。
目前AI推理检测所记录为第一种:加载图片->将图片预处理(图片缩放,图片bgr值转成onnx格式的输入张量)转成输入张量->将输入张量创建输入容器,运行推理->将推理结果绘画到winform的pictureBox。
流程图如下:
一、C#环境使用YoLo模型进行AI推理检测
1.加载图片
代码如下(示例):
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "图文件(*.*)|*.jpg;*.png;*.jpeg;*.bmp";
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
//获取用户选择推断的图像路径
imgUr = openFileDialog.FileName;
//把图像显示到图像控件上面
bitmap = new Bitmap(openFileDialog.FileName);
//绑定图像到控件显示
pic_result.Image = bitmap;
}
2.图片预处理
说明:将图片缩放并归一化,将图片BGR\RGB值转成onnx格式输入张量。
代码如下(示例):
/// <summary>
/// OpenCV默认使用 BGR 通道顺序
/// </summary>
public static DenseTensor<float> BitmapToOnnxInput(Bitmap pic, int[] inputT)
{
#region 图片缩放成目标张量的宽高
//图片宽高
float picW = pic.Width;
float picH = pic.Height;
//目标张量宽高(640,640)
float goalW = inputT[3];
float goalH = inputT[2];
if(picW > goalW || picH > goalH)
{
//图片缩放
zoom = (goalW / picW) < (goalH / picH) ? (goalW / picW) : (goalH / picH);
picW = picW * zoom;
picH = picH * zoom;
}
#endregion
#region 缩放后重新创建一个图片和张量
//创建一个空张量
float[,,] tempData = new float[inputT[1], inputT[2], inputT[3]];
//创建一个缩放后图
Bitmap bitmap = new Bitmap(pic, (int)picW, (int)picH);
#endregion
//将缩放后的图片bgr值归一化到[0,1]范围
for(int y = 0; y < bitmap.Height; y++)
{
for(int x = 0; x < bitmap.Width; x++)
{
Color pixe1 = bitmap.GetPixel(x, y);
//将BGR值归一化到[0,1]范围
tempData[0, y, x] = pixe1.B / 255.0f; //B通道
tempData[1, y, x] = pixe1.G / 255.0f; //G通道
tempData[2, y, x] = pixe1.R / 255.0f; //R通道
}
}
//创建一个数组
float[] unfoldData = new float[inputT[1]*inputT[2]*inputT[3]];
//将tempData的数据填充到unfoldData数组中
Buffer.BlockCopy(tempData, 0, unfoldData, 0, unfoldData.Length * sizeof(float));
return new DenseTensor<float>(unfoldData, inputT);
}
3.加载onnx模型并推理
说明:使用InferenceSession从模型文件加载ONNX模型并推理
安装:Microsoft.ML.OnnxRuntime库、Microsoft.ML.OnnxRuntime.Gpu库
在CPU上运行
using Microsoft.ML.OnnxRuntime;
string modelPath = "path/to/your/model.onnx";
var session = new InferenceSession(modelPath);
在GPU上运行
using Microsoft.ML.OnnxRuntime;
//需使用SessionOptions配置GPU设备
string modelPath = "path/to/your/model.onnx";
int gpuDeviceId = 0; // GPU设备ID,通常为0
var sessionOptions = SessionOptions.MakeSessionOptionWithCudaProvider(gpuDeviceId);
var session = new InferenceSession(modelPath, sessionOptions);
代码如下(示例):
//构建要模型文件的路径
var modelPath = Path.Combine(rootPath, "best.onnx");
var session = new InferenceSession(modelPath);
//输入名称:images
string inputName = session.InputNames.First();
//输入张量:[1,3,640,640]
int[] inputTensor = session.InputMetadata[inputName].Dimensions;
//提取当前检测模型标签类型数组
var modelMessage = session.ModelMetadata.CustomMetadataMap;
if (modelMessage.Keys.Contains("names"))
{
//将标签字符串分割并放入string类型数组内
labels = SplitLabels(modelMessage["names"]);
}
else
{
labels = [];
}
var tensor = BitmapToOnnxInput(bitmap, inputTensor);
//创建输入容器
var inputs = new List<NamedOnnxValue> {
NamedOnnxValue.CreateFromTensor(inputName, tensor)
};
//运行推理
var results = session.Run(inputs);
//处理结果
outputTensor = results.First().AsTensor<float>();
分割模型标签类型字符串成string类型数组方法
private string[] SplitLabels(string labels)
{
//temp1: 0: '马叔', 1: '彪哥', 2: '玉芬', 3: '桂英'
string temp1 = labels.Replace("{", "").Replace("}", "");
string[] temp2 = temp1.Split(',');
string[] temp3 = new string[temp2.Length];
for (int i = 0; i < temp2.Length; i++)
{
int start = temp2[i].IndexOf(':') + 3;
int end = temp2[i].Length - 1;
temp3[i] = temp2[i].Substring(start, end - start);
}
return temp3;
}
4.数据后处理
说明:将输出张量outputTensor进行处理,解析检测框、类型和置信度等信息放到一个集合
代码如下(示例):
private List<float[]> GetOutputValue(Tensor<float> outputTensor)
{
List<float[]> filteredResults = new List<float[]>();
//解析张量,将张量转换为数组
float[] dataArray = outputTensor.ToArray();
//批量大小
int batchSize = outputTensor.Dimensions[0];
//每个检测框的属性数量
int numDetections = outputTensor.Dimensions[1];
//检测框的数量
int outputSize = outputTensor.Dimensions[2];
//遍历检测结果
for(int i = 0; i < batchSize; i++) //遍历批量
{
for(int j = 0; j < numDetections; j++) //遍历检测框
{
//计算当前检测框的起始索引
int startIndex = i * outputSize * numDetections + j * numDetections;
float[] result = new float[6];
result[0] = dataArray[startIndex + 0]; // x:边界框中心点的 x 坐标
result[1] = dataArray[startIndex + 1]; // y:边界框中心点的 y 坐标
result[2] = dataArray[startIndex + 2]; // w:边界框的宽度
result[3] = dataArray[startIndex + 3]; // h:边界框的高度
result[4] = dataArray[startIndex + 4]; // 置信度
result[5] = (int)dataArray[startIndex + 5]; // 类别ID
filteredResults.Add(result);
}
}
return filteredResults;
}
5.图片属性值缩放还原和反归一化
目前主要参数:
//解析检测框、置信度、类型等信息放入一个集合
List<float[]> reArray = GetOutputValue(outputTensor);
代码如下(示例):
//imgWidth 和 imgHeight:原始图像的宽度和高度
public void RestorePoint(ref List<float[]> data, Bitmap bitmap)
{
if (data.Count > 0)
{
if (data[0].Length >= 4) // 确保每个数组至少有4个元素(边界框信息)
{
for (int i = 0; i < data.Count; i++)
{
// 反归一化中心点坐标
// x_center * bitmap.Width
// y_center * bitmap.Height
//缩放还原
data[i][0] = data[i][0] / zoom;
data[i][1] = data[i][1] / zoom;
// 反归一化宽度和高度
//width * bitmap.Width
//height * bitmap.Height
//缩放还原
data[i][2] = data[i][2] / zoom;
data[i][3] = data[i][3] / zoom;
}
}
}
}
6.将解析到的信息的集合绘制成处理后图片
说明:将解析到信息的集合绘制成bitmap类型图片
目前主要参数:
//解析检测框、置信度、类型等信息放入一个集合
List<float[]> reArray = GetOutputValue(outputTensor);
使用System.Drawing类型
private Bitmap DrawDetectionsBySysDraw(Bitmap bitmap, List<float[]> dates, string[] labels)
{
Bitmap rePic = new Bitmap(bitmap.Width, bitmap.Height);
Graphics G = Graphics.FromImage(rePic);
//将原始图片绘制到新Bitmap上
G.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height);
//设置绘图质量
G.SmoothingMode = SmoothingMode.AntiAlias;
G.InterpolationMode = InterpolationMode.HighQualityBicubic;
G.PixelOffsetMode = PixelOffsetMode.HighQuality;
//定义字体和画笔
Font font = new Font("Arial", 12);
Brush brush = Brushes.Red;
Pen pen = new Pen(Color.Red, 2);
//遍历检测框并绘制
for (int i = 0; i < dates.Count; i++)
{
float[] detection = dates[i];
//string label = labels[i];
//解析检测框坐标和置信度
float x = detection[0]; //边界框中心点的 x 坐标
float y = detection[1]; //边界框中心点的 y 坐标
float width = detection[2]; //边界框的宽度
float height = detection[3]; //边界框的高度
float confidence = detection[4]; //置信度
//绘制检测框
G.DrawRectangle(pen, x, y, width, height);
//绘制标签和置信度
string text = $"{confidence * 100:0.00}%";
SizeF textSize = G.MeasureString(text, font);
//绘制文本背景
G.FillRectangle(Brushes.Blue, x, y - textSize.Height - 5, textSize.Width + 10, textSize.Height + 10);
//绘制文本
G.DrawString(text, font, brush, x + 5, y - textSize.Height - 5);
}
return rePic;
}
使用OpenCvSharp类型
public Bitmap DrawDetectionsByOpenCV(Bitmap bitmap, List<float[]> dates, string[] labels)
{
// 将Bitmap转换成OpenCvSharp的Mat对象
Mat image = BitmapConverter.ToMat(bitmap);
//遍历dates集合,绘制检测框和标签
for (int i = 0; i < dates.Count; i++)
{
float[] detection = dates[i];
//解析检测框坐标和置信度
float x = detection[0]; //边界框中心点的 x 坐标
float y = detection[1]; //边界框中心点的 y 坐标
float width = detection[2]; //边界框的宽度
float height = detection[3]; //边界框的高度
// 绘制矩形框
Cv2.Rectangle(image, new OpenCvSharp.Point(x, y), new OpenCvSharp.Point(x + width, y + height), Scalar.Red, 2);
// 绘制标签
Cv2.PutText(image, labels[1], new OpenCvSharp.Point(x, y - 10), HersheyFonts.HersheyScriptSimplex, 0.5, Scalar.Red, 1);
}
//将绘制后的Mat对象转换回Bitmap
Bitmap resultBitmap = BitmapConverter.ToBitmap(image);
return resultBitmap;
}
总结
目前未能实现成功,实现模型视觉识别。但需进一步调试,目前问题有两个。
一个是检测框使用C#中winform显示都会在最左上角,一个是检测框显示个数、置信度显示不对。原因:有可能:1、推理yolo模型代码有问题 2、图片属性还原或绘制代码有误,需跟进。