在C#中使用YOLO(You Only Look Once)目标检测算法,通常有几种不同的实现方式,包括使用OpenCV、ONNX Runtime或ML.NET等框架。下面为你介绍两种常见的实现方法:
方法一:使用ONNX Runtime部署YOLOv5模型
这种方法的优势在于实现简单,且性能优化较好。
具体步骤
- 准备YOLOv5模型
- 首先,从YOLOv5官方仓库(https://github.com/ultralytics/yolov5 )下载预训练模型。
- 然后,将PyTorch模型转换为ONNX格式:
python export.py --weights yolov5s.pt --include onnx
创建C#项目并安装NuGet包
- 新建一个.NET Core控制台应用程序。
- 通过NuGet包管理器安装以下组件:
- Microsoft.ML.OnnxRuntime
- System.Drawing.Common(用于图像处理)
编写C#代码
下面是一个完整的示例代码:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
namespace YoloV5OnnxRuntime
{
class Program
{
// 检测类别名称
private static readonly string[] classNames = new[] {
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
"fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
"skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",
"tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
"potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone",
"microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear",
"hair drier", "toothbrush"
};
static void Main(string[] args)
{
// 模型路径和输入图像路径
string modelPath = "yolov5s.onnx";
string imagePath = "test.jpg";
// 加载模型
using var session = new InferenceSession(modelPath);
// 加载并预处理图像
var (inputTensor, originalWidth, originalHeight) = LoadImage(imagePath);
// 准备输入参数
var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", inputTensor) };
// 运行模型推理
using var results = session.Run(inputs);
// 处理输出结果
var outputTensor = results.First().AsTensor<float>();
var predictions = ProcessOutput(outputTensor, originalWidth, originalHeight);
// 绘制检测结果
DrawPredictions(imagePath, predictions, "output.jpg");
Console.WriteLine("检测完成,结果已保存至 output.jpg");
}
// 加载并预处理图像
private static (DenseTensor<float> tensor, int width, int height) LoadImage(string imagePath)
{
using var image = Image.FromFile(imagePath);
int originalWidth = image.Width;
int originalHeight = image.Height;
// 调整图像大小为模型输入尺寸(通常为640x640)
var resizedImage = ResizeImage(image, 640, 640);
// 图像转张量
var tensor = new DenseTensor<float>(new[] { 1, 3, 640, 640 });
var mean = new[] { 0.0f, 0.0f, 0.0f }; // 均值
var std = new[] { 1.0f, 1.0f, 1.0f }; // 标准差
using (var bitmap = new Bitmap(resizedImage))
{
for (int y = 0; y < bitmap.Height; y++)
{
for (int x = 0; x < bitmap.Width; x++)
{
var pixel = bitmap.GetPixel(x, y);
// 注意:图像通道顺序为BGR(OpenCV默认),而模型可能需要RGB
tensor[0, 0, y, x] = (pixel.R / 255.0f - mean[0]) / std[0];
tensor[0, 1, y, x] = (pixel.G / 255.0f - mean[1]) / std[1];
tensor[0, 2, y, x] = (pixel.B / 255.0f - mean[2]) / std[2];
}
}
}
return (tensor, originalWidth, originalHeight);
}
// 调整图像大小
private static Image ResizeImage(Image image, int width, int height)
{
var destRect = new Rectangle(0, 0, width, height);
var destImage = new Bitmap(width, height);
destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);
using (var graphics = Graphics.FromImage(destImage))
{
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
using (var wrapMode = new System.Drawing.Imaging.ImageAttributes())
{
wrapMode.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
}
}
return destImage;
}
// 处理模型输出
private static List<Prediction> ProcessOutput(Tensor<float> output, int originalWidth, int originalHeight)
{
const float confidenceThreshold = 0.5f;
const float nmsThreshold = 0.4f;
var predictions = new List<Prediction>();
// 解析模型输出
for (int i = 0; i < output.Length / 85; i++)
{
float confidence = output[0, i, 4];
if (confidence >= confidenceThreshold)
{
// 找到概率最高的类别
int classId = 0;
float maxClassProbs = 0;
for (int c = 0; c < 80; c++)
{
float classProbs = output[0, i, 5 + c];
if (classProbs > maxClassProbs)
{
maxClassProbs = classProbs;
classId = c;
}
}
float score = confidence * maxClassProbs;
if (score >= confidenceThreshold)
{
// 获取边界框坐标
float cx = output[0, i, 0];
float cy = output[0, i, 1];
float w = output[0, i, 2];
float h = output[0, i, 3];
// 转换为左上角和右下角坐标
float x1 = (cx - w / 2) / 640.0f * originalWidth;
float y1 = (cy - h / 2) / 640.0f * originalHeight;
float x2 = (cx + w / 2) / 640.0f * originalWidth;
float y2 = (cy + h / 2) / 640.0f * originalHeight;
predictions.Add(new Prediction
{
ClassId = classId,
Score = score,
BBox = new RectangleF(x1, y1, x2 - x1, y2 - y1)
});
}
}
}
// 非极大值抑制
return NonMaxSuppression(predictions, nmsThreshold);
}
// 非极大值抑制
private static List<Prediction> NonMaxSuppression(List<Prediction> predictions, float threshold)
{
var result = new List<Prediction>();
// 按置信度降序排序
predictions = predictions.OrderByDescending(p => p.Score).ToList();
while (predictions.Count > 0)
{
var best = predictions[0];
result.Add(best);
predictions.RemoveAt(0);
// 移除重叠度高的边界框
predictions = predictions.Where(p => IoU(best.BBox, p.BBox) < threshold).ToList();
}
return result;
}
// 计算交并比
private static float IoU(RectangleF a, RectangleF b)
{
float areaA = a.Width * a.Height;
if (areaA <= 0) return 0;
float areaB = b.Width * b.Height;
if (areaB <= 0) return 0;
float minX = Math.Max(a.Left, b.Left);
float minY = Math.Max(a.Top, b.Top);
float maxX = Math.Min(a.Right, b.Right);
float maxY = Math.Min(a.Bottom, b.Bottom);
float intersectionWidth = maxX - minX;
float intersectionHeight = maxY - minY;
if (intersectionWidth <= 0 || intersectionHeight <= 0)
return 0;
float intersectionArea = intersectionWidth * intersectionHeight;
return intersectionArea / (areaA + areaB - intersectionArea);
}
// 绘制预测结果
private static void DrawPredictions(string inputImagePath, List<Prediction> predictions, string outputImagePath)
{
using var image = Image.FromFile(inputImagePath);
using var graphics = Graphics.FromImage(image);
// 设置绘图质量
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
// 绘制边界框和标签
foreach (var prediction in predictions)
{
var bbox = prediction.BBox;
var label = $"{classNames[prediction.ClassId]}: {prediction.Score:F2}";
// 绘制边界框
using var pen = new Pen(Color.FromArgb(255, 255, 0, 0), 2);
graphics.DrawRectangle(pen, bbox.X, bbox.Y, bbox.Width, bbox.Height);
// 绘制标签背景
using var font = new Font("Arial", 10, FontStyle.Bold);
using var brush = new SolidBrush(Color.FromArgb(255, 255, 0, 0));
using var textBrush = new SolidBrush(Color.White);
var textSize = graphics.MeasureString(label, font);
var textBackground = new RectangleF(bbox.X, bbox.Y - textSize.Height, textSize.Width, textSize.Height);
graphics.FillRectangle(brush, textBackground);
graphics.DrawString(label, font, textBrush, bbox.X, bbox.Y - textSize.Height);
}
// 保存结果图像
image.Save(outputImagePath, ImageFormat.Jpeg);
}
}
// 预测结果类
public class Prediction
{
public int ClassId { get; set; }
public float Score { get; set; }
public RectangleF BBox { get; set; }
}
}
方法二:使用ML.NET部署YOLOv5模型
ML.NET是微软的跨平台机器学习框架,也可用于部署YOLO模型。
具体步骤
准备工作与方法一相同
- 下载并转换YOLOv5模型。
- 创建C#项目并安装必要的NuGet包:
- Microsoft.ML
- Microsoft.ML.OnnxTransformer
编写C#代码
下面是使用ML.NET的示例代码:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.OnnxTransformer;
using Microsoft.ML.Trainers;
namespace YoloV5MLNet
{
class Program
{
// 检测类别名称(同上)
private static readonly string[] classNames = new[] { ... }; // 同上,省略
static void Main(string[] args)
{
string modelPath = "yolov5s.onnx";
string imagePath = "test.jpg";
// 创建MLContext
var mlContext = new MLContext();
// 定义模型输入输出架构
var data = new List<ImageInputData> { new ImageInputData { ImagePath = imagePath } };
var dataView = mlContext.Data.LoadFromEnumerable(data);
// 定义数据转换管道
var pipeline = mlContext.Transforms.LoadImages(outputColumnName: "image", imageFolder: "", inputColumnName: nameof(ImageInputData.ImagePath))
.Append(mlContext.Transforms.ResizeImages(outputColumnName: "image", imageWidth: 640, imageHeight: 640, inputColumnName: "image"))
.Append(mlContext.Transforms.ExtractPixels(outputColumnName: "images", inputColumnName: "image", interleavePixelColors: true, offsetImage: 0))
.Append(mlContext.Transforms.ApplyOnnxModel(
shapeDictionary: new Dictionary<string, int[]>()
{
{ "images", new[] { 1, 3, 640, 640 } },
{ "output", new[] { 1, 25200, 85 } }
},
inputColumnNames: new[] { "images" },
outputColumnNames: new[] { "output" },
modelFile: modelPath));
// 训练管道(这里只是为了创建预测引擎)
var model = pipeline.Fit(dataView);
// 创建预测引擎
var predictor = mlContext.Model.CreatePredictionEngine<ImageInputData, ImagePrediction>(model);
// 进行预测
var prediction = predictor.Predict(new ImageInputData { ImagePath = imagePath });
// 处理输出结果
var originalImage = Image.FromFile(imagePath);
var predictions = ProcessOutput(prediction.Output, originalImage.Width, originalImage.Height);
// 绘制检测结果
DrawPredictions(imagePath, predictions, "output.jpg");
Console.WriteLine("检测完成,结果已保存至 output.jpg");
}
// 图像处理和结果解析方法(与ONNX Runtime版本相同)
private static List<Prediction> ProcessOutput(float[] output, int originalWidth, int originalHeight)
{
// 与ONNX Runtime版本中的ProcessOutput方法相同
// ...
}
// 非极大值抑制和IoU计算方法(同上)
private static List<Prediction> NonMaxSuppression(List<Prediction> predictions, float threshold)
{
// ...
}
private static float IoU(RectangleF a, RectangleF b)
{
// ...
}
// 绘制预测结果(同上)
private static void DrawPredictions(string inputImagePath, List<Prediction> predictions, string outputImagePath)
{
// ...
}
}
// 数据模型类
public class ImageInputData
{
[LoadColumn(0)]
public string ImagePath { get; set; }
}
public class ImagePrediction
{
[ColumnName("output")]
public float[] Output { get; set; }
}
// 预测结果类(同上)
public class Prediction
{
public int ClassId { get; set; }
public float Score { get; set; }
public RectangleF BBox { get; set; }
}
}
性能优化建议
使用GPU加速:若要提升检测速度,可安装CUDA版本的ONNX Runtime,这样就能利用GPU进行推理。
模型量化:可以将FP32模型转换为INT8量化模型,以此减小模型体积并提高推理速度。
批量处理:如果需要处理大量图像,可以考虑使用批量输入来提高吞吐量。
异步推理:对于实时应用场景,建议使用异步API来避免阻塞主线程。
通过上述方法,你可以在C#环境中实现YOLO目标检测功能。ONNX Runtime方法更为直接,而ML.NET方法则能更好地与微软生态系统集成。你可以根据自己的具体需求选择合适的实现方式。