最近在做一个基于C#的目标检测项目,需要将最新的YOLO26模型集成到现有系统中。与之前使用的YOLOv5相比,YOLO26采用了端到端无NMS(非极大值抑制)的推理方式,输出格式有了显著变化。本文将详细介绍如何通过C#调用Microsoft.ML.OnnxRuntime和OpenCvSharp来实现YOLO26模型的目标检测功能。
YOLO26是Ultralytics团队推出的最新一代目标检测模型,相比YOLOv5,它最大的特点是采用了端到端的推理方式,直接输出最终检测结果,省去了传统YOLO系列模型中的NMS后处理步骤。这种设计使得模型输出更加简洁,推理速度也有所提升。
首先需要准备开发环境,我使用的是Visual Studio 2022和.NET 6.0框架。需要安装以下NuGet包:
可以通过NuGet包管理器控制台安装这些包:
bash复制Install-Package Microsoft.ML.OnnxRuntime
Install-Package OpenCvSharp4
Install-Package OpenCvSharp4.runtime.win
Install-Package OpenCvSharp4.Extensions
YOLO26的官方实现提供了将PyTorch模型转换为ONNX格式的功能。我使用的是Ultralytics提供的yolo26m.pt预训练模型,转换命令如下:
python复制from ultralytics import YOLO
# 加载预训练模型
model = YOLO('yolo26m.pt')
# 导出为ONNX格式
model.export(format='onnx', dynamic=False, imgsz=640)
转换完成后会得到一个yolo26m.onnx文件,这就是我们将在C#中使用的模型文件。需要注意的是,YOLO26的ONNX模型输入输出格式与YOLOv5有所不同:
在C#中加载ONNX模型需要使用Microsoft.ML.OnnxRuntime库。首先创建一个推理会话:
csharp复制using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
// 加载ONNX模型
var sessionOptions = new SessionOptions();
sessionOptions.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
var session = new InferenceSession("yolo26m.onnx", sessionOptions);
// 获取输入输出信息
var inputMeta = session.InputMetadata;
var outputMeta = session.OutputMetadata;
YOLO26模型的输入需要经过特定的预处理。我们需要将输入图像调整为640x640大小,并进行归一化处理:
csharp复制using OpenCvSharp;
public Tensor<float> PreprocessImage(Mat image)
{
// 原始图像尺寸
int originalHeight = image.Height;
int originalWidth = image.Width;
// 计算缩放比例
float scale = Math.Min(640f / originalWidth, 640f / originalHeight);
// 缩放图像
Mat resized = new Mat();
Cv2.Resize(image, resized, new Size(originalWidth * scale, originalHeight * scale));
// 创建填充后的图像
Mat padded = new Mat(640, 640, MatType.CV_8UC3, new Scalar(114, 114, 114));
resized.CopyTo(padded[new Rect(
(640 - resized.Width) / 2,
(640 - resized.Height) / 2,
resized.Width,
resized.Height)]);
// 转换为张量并归一化
var inputTensor = new DenseTensor<float>(new[] { 1, 3, 640, 640 });
for (int y = 0; y < 640; y++)
{
for (int x = 0; x < 640; x++)
{
var pixel = padded.At<Vec3b>(y, x);
inputTensor[0, 0, y, x] = pixel[2] / 255f; // R
inputTensor[0, 1, y, x] = pixel[1] / 255f; // G
inputTensor[0, 2, y, x] = pixel[0] / 255f; // B
}
}
return inputTensor;
}
YOLO26的输出坐标需要转换回原始图像坐标系。我编写了一个专门的坐标转换函数:
csharp复制public (int x, int y, int w, int h) ScaleCoordinatesOfYolo26(
float xl, float yl, float xr, float yr,
int originalWidth, int originalHeight,
int inputSize = 640)
{
// 计算缩放比例
float scale = Math.Min((float)inputSize / originalWidth, (float)inputSize / originalHeight);
// 计算填充尺寸
int padX = (int)((inputSize - originalWidth * scale) / 2);
int padY = (int)((inputSize - originalHeight * scale) / 2);
// 映射回原始图像坐标
int scaledX = (int)((xl - padX) / scale);
int scaledY = (int)((yl - padY) / scale);
int scaledW = (int)((xr-xl) / scale);
int scaledH = (int)((yr-yl) / scale);
return (scaledX, scaledY, scaledW, scaledH);
}
YOLO26的输出格式为[1,300,6],我们需要解析这个张量并过滤低置信度的检测结果:
csharp复制public class DetectionResult
{
public int ClassId { get; set; }
public string Label { get; set; }
public float Confidence { get; set; }
public Rect BoundingBox { get; set; }
public (int x, int y, int w, int h) OriginalCoordinates { get; set; }
}
public List<DetectionResult> ProcessDetectionsOfYolo26(
DenseTensor<float> output,
int originalWidth,
int originalHeight,
float confidenceThreshold = 0.8f)
{
var detections = new List<DetectionResult>();
// 解析输出张量 (1, 300, 6)
int numDetections = output.Dimensions[1];
for (int i = 0; i < numDetections; i++)
{
// 提取对象置信度
float objectConfidence = output[0,i,4];
// 获取最高概率类别
int classId = Convert.ToInt32(output[0, i, 5]);
if (objectConfidence > confidenceThreshold)
{
// 提取边界框坐标
float pXl = output[0, i, 0];
float pYl = output[0, i, 1];
float pXr = output[0, i, 2];
float pYr = output[0, i, 3];
// 映射回原始图像坐标
var (x, y, w, h) = ScaleCoordinatesOfYolo26(
pXl, pYl, pXr, pYr,
originalWidth, originalHeight);
// 创建边界框
Rect boundingBox = new Rect(x, y, w, h);
detections.Add(new DetectionResult
{
ClassId = classId,
Label = CocoLabels[classId],
Confidence = objectConfidence,
BoundingBox = boundingBox,
OriginalCoordinates = (x, y, w, h)
});
}
}
return detections;
}
将上述组件组合起来,形成完整的推理流程:
csharp复制public void RunDetection(string imagePath)
{
// 加载图像
Mat image = Cv2.ImRead(imagePath);
int originalHeight = image.Height;
int originalWidth = image.Width;
// 预处理
var inputTensor = PreprocessImage(image);
// 准备输入
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("images", inputTensor)
};
// 运行推理
using var results = session.Run(inputs);
var output = results.First().AsTensor<float>();
// 处理结果
var detections = ProcessDetectionsOfYolo26(
output as DenseTensor<float>,
originalWidth,
originalHeight,
0.5f); // 置信度阈值
// 绘制结果
foreach (var detection in detections)
{
Cv2.Rectangle(image, detection.BoundingBox, Scalar.Red, 2);
Cv2.PutText(image,
$"{detection.Label}: {detection.Confidence:F2}",
new Point(detection.BoundingBox.X, detection.BoundingBox.Y - 5),
HersheyFonts.HersheySimplex,
0.5,
Scalar.Green,
1);
}
// 显示结果
Cv2.ImShow("Detection Results", image);
Cv2.WaitKey(0);
}
在实际使用中,我发现以下几点可以显著提高推理性能:
csharp复制var sessionOptions = new SessionOptions();
sessionOptions.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
sessionOptions.EnableMemoryPattern = true;
sessionOptions.ExecutionMode = ExecutionMode.ORT_PARALLEL;
批量处理:如果可能,尽量一次处理多张图像。YOLO26模型支持批量输入,可以显著提高吞吐量。
异步处理:对于实时视频流处理,可以使用异步管道来避免阻塞主线程。
YOLO26的输出格式与YOLOv5不同,直接使用YOLOv5的后处理代码会导致错误。主要区别在于:
在坐标转换过程中,容易忽略填充(padding)的影响。正确的做法是先减去填充部分,再除以缩放比例。我最初犯的错误是直接除以缩放比例,导致检测框位置偏移。
YOLO26的置信度分数分布与YOLOv5有所不同。经过测试,我发现0.5-0.7之间的阈值效果较好。过高的阈值会导致漏检,而过低的阈值会增加误检。
在使用OpenCvSharp和ONNX Runtime时,需要注意及时释放资源。特别是Mat对象和InferenceSession对象,应该使用using语句或在finally块中释放。
将上述代码稍作修改,就可以应用于视频流实时检测:
csharp复制public void ProcessVideo(string videoPath)
{
using var capture = new VideoCapture(videoPath);
using var window = new Window("Real-time Detection");
Mat frame = new Mat();
while (capture.Read(frame))
{
var detections = DetectObjects(frame);
DrawDetections(frame, detections);
window.ShowImage(frame);
if (Cv2.WaitKey(1) == 27) break; // ESC键退出
}
}
在我的测试环境中(i7-11800H, RTX 3060),YOLO26m模型的推理性能如下:
如果需要检测特定类别的对象,可以基于YOLO26进行自定义训练:
python复制from ultralytics import YOLO
# 加载基础模型
model = YOLO('yolo26m.pt')
# 自定义训练
model.train(data='custom_dataset.yaml', epochs=100, imgsz=640)
对于更复杂的应用场景,可以考虑将YOLO26与其他模型集成:
对于生产环境部署,可以考虑以下优化措施:
在实际项目中集成YOLO26模型的过程中,我总结了以下几点经验:
模型选择:YOLO26相比YOLOv5在保持精度的同时,推理速度有所提升,特别是端到端的输出格式简化了后处理流程。
预处理一致性:确保推理时的预处理与训练时完全一致,包括归一化方式和填充策略。
后处理优化:YOLO26的后处理比YOLOv5简单,但仍需注意坐标转换的准确性。
阈值调整:不同的应用场景需要不同的置信度阈值,需要通过验证集来确定最佳值。
资源管理:特别是在长时间运行的应用程序中,要注意及时释放资源,避免内存泄漏。
这个项目让我深刻体会到,将最新的计算机视觉模型集成到C#应用程序中是完全可行的。Microsoft.ML.OnnxRuntime和OpenCvSharp的组合提供了强大的工具链,使得.NET开发者也能充分利用最先进的深度学习模型。