引言✍️
在人工智能和计算机视觉领域,实时目标检测(Object Detection)是一项核心且应用广泛的技术。YOLO(You Only Look Once)系列算法以其"单次前向传播"的极速推理能力,成为工业界和学术界的热门选择。对于 .NET 开发者而言,如何将强大的 YOLO 模型与熟悉的 .NET 生态结合,快速构建出可部署的应用程序,是一个极具价值的话题。
本文将带你从零开始,使用最新的 .NET 10 和 YOLOv8 模型,一步步搭建一个完整的实时目标检测应用。无论你是刚接触计算机视觉的 .NET 新手,还是希望将 AI 能力集成到现有业务系统中的开发者,这篇指南都将为你提供清晰的路径和可运行的代码。

1. 环境准备与项目创建
首先,确保你的开发环境已就绪。
1.1 安装 .NET 10 SDK
访问 .NET 官方网站 下载并安装最新的 .NET 10 SDK。安装完成后,在命令行中运行以下命令验证安装:
bash
dotnet --version
预期输出应为 10.x.x。
1.2 创建控制台应用
我们从一个简单的控制台应用开始,便于理解和调试。
bash
dotnet new console -n YoloNetDemo
cd YoloNetDemo
1.3 安装必要的 NuGet 包
YOLO 模型推理需要机器学习库的支持。我们将使用 Microsoft.ML 和 Microsoft.ML.OnnxRuntime 来处理 ONNX 格式的 YOLO 模型。
bash
dotnet add package Microsoft.ML
dotnet add package Microsoft.ML.OnnxRuntime
dotnet add package SixLabors.ImageSharp
Microsoft.ML: .NET 的机器学习基础库。Microsoft.ML.OnnxRuntime: 用于高性能推理 ONNX 模型。SixLabors.ImageSharp: 强大的跨平台图像处理库,用于加载和预处理图片。
1.4 项目整体架构
在开始具体实现之前,让我们先了解整个项目的架构设计:
#mermaid-svg-J7oIpDrK0verIB7x{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J7oIpDrK0verIB7x .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J7oIpDrK0verIB7x .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J7oIpDrK0verIB7x .error-icon{fill:#552222;}#mermaid-svg-J7oIpDrK0verIB7x .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J7oIpDrK0verIB7x .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J7oIpDrK0verIB7x .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J7oIpDrK0verIB7x .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J7oIpDrK0verIB7x .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J7oIpDrK0verIB7x .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J7oIpDrK0verIB7x .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J7oIpDrK0verIB7x .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J7oIpDrK0verIB7x .marker.cross{stroke:#333333;}#mermaid-svg-J7oIpDrK0verIB7x svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J7oIpDrK0verIB7x p{margin:0;}#mermaid-svg-J7oIpDrK0verIB7x .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J7oIpDrK0verIB7x .cluster-label text{fill:#333;}#mermaid-svg-J7oIpDrK0verIB7x .cluster-label span{color:#333;}#mermaid-svg-J7oIpDrK0verIB7x .cluster-label span p{background-color:transparent;}#mermaid-svg-J7oIpDrK0verIB7x .label text,#mermaid-svg-J7oIpDrK0verIB7x span{fill:#333;color:#333;}#mermaid-svg-J7oIpDrK0verIB7x .node rect,#mermaid-svg-J7oIpDrK0verIB7x .node circle,#mermaid-svg-J7oIpDrK0verIB7x .node ellipse,#mermaid-svg-J7oIpDrK0verIB7x .node polygon,#mermaid-svg-J7oIpDrK0verIB7x .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J7oIpDrK0verIB7x .rough-node .label text,#mermaid-svg-J7oIpDrK0verIB7x .node .label text,#mermaid-svg-J7oIpDrK0verIB7x .image-shape .label,#mermaid-svg-J7oIpDrK0verIB7x .icon-shape .label{text-anchor:middle;}#mermaid-svg-J7oIpDrK0verIB7x .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J7oIpDrK0verIB7x .rough-node .label,#mermaid-svg-J7oIpDrK0verIB7x .node .label,#mermaid-svg-J7oIpDrK0verIB7x .image-shape .label,#mermaid-svg-J7oIpDrK0verIB7x .icon-shape .label{text-align:center;}#mermaid-svg-J7oIpDrK0verIB7x .node.clickable{cursor:pointer;}#mermaid-svg-J7oIpDrK0verIB7x .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J7oIpDrK0verIB7x .arrowheadPath{fill:#333333;}#mermaid-svg-J7oIpDrK0verIB7x .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J7oIpDrK0verIB7x .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J7oIpDrK0verIB7x .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J7oIpDrK0verIB7x .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J7oIpDrK0verIB7x .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J7oIpDrK0verIB7x .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J7oIpDrK0verIB7x .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J7oIpDrK0verIB7x .cluster text{fill:#333;}#mermaid-svg-J7oIpDrK0verIB7x .cluster span{color:#333;}#mermaid-svg-J7oIpDrK0verIB7x div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J7oIpDrK0verIB7x .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J7oIpDrK0verIB7x rect.text{fill:none;stroke-width:0;}#mermaid-svg-J7oIpDrK0verIB7x .icon-shape,#mermaid-svg-J7oIpDrK0verIB7x .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J7oIpDrK0verIB7x .icon-shape p,#mermaid-svg-J7oIpDrK0verIB7x .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J7oIpDrK0verIB7x .icon-shape .label rect,#mermaid-svg-J7oIpDrK0verIB7x .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J7oIpDrK0verIB7x .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J7oIpDrK0verIB7x .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J7oIpDrK0verIB7x :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 核心代码实现
定义数据模型
图像预处理
模型推理
后处理NMS
坐标映射
开始
环境准备
获取YOLOv8模型
控制台应用
Web API服务
输出检测结果
提供REST接口
保存标注图片
返回JSON结果
图:.NET + YOLOv8 目标检测项目整体架构
2. 获取与准备 YOLO 模型
YOLOv8 提供了预训练模型,并支持导出为 ONNX 格式,便于跨平台部署。
2.1 下载 YOLOv8 ONNX 模型
- 访问 Ultralytics YOLOv8 Releases 页面。
- 找到 Assets 部分,下载名为
yolov8n.onnx的模型文件(这是 Nano 版本,体积小,速度快,适合入门)。 - 将下载的
yolov8n.onnx文件复制到你的项目根目录下,并在 Visual Studio 或 Rider 中,将其"复制到输出目录"属性设置为"如果较新则复制"。
2.2 理解模型输入输出
- 输入 : 一个形状为
[1, 3, 640, 640]的张量,代表一张经过归一化处理的640x640RGB 图像。 - 输出 : 一个形状为
[1, 84, 8400]的张量。其中8400是模型在640x640网格上预测的框数量,84包含每个框的(cx, cy, w, h)坐标以及 80 个 COCO 数据集的类别置信度。
3. 核心代码实现
接下来,我们编写核心的推理和结果处理代码。
3.1 定义数据模型
在 Program.cs 中,首先定义用于表示检测结果和模型配置的类。
csharp
using Microsoft.ML;
using Microsoft.ML.Transforms.Image;
using OnnxRuntime;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace YoloNetDemo;
// 表示一个检测到的物体
public class DetectionResult
{
public string Label { get; set; } = string.Empty;
public float Confidence { get; set; }
public RectangleF BoundingBox { get; set; } // 使用 RectangleF 表示矩形区域
}
// 模型配置和常量
public static class YoloConfig
{
public const int ImageSize = 640;
public const int ClassCount = 80; // COCO 数据集类别数
public static readonly string[] ClassNames = {
"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"
};
}
3.2 实现图像预处理
YOLO 模型要求输入图像被缩放、填充并归一化。
csharp
public static class ImageProcessor
{
// 将 ImageSharp 图像转换为模型所需的张量
public static float[] PreprocessImage(Image<Rgb24> image)
{
// 1. 调整图像大小并保持宽高比 (Letterbox)
var resizedImage = image.Clone(ctx =>
{
ctx.Resize(new ResizeOptions
{
Size = new Size(YoloConfig.ImageSize, YoloConfig.ImageSize),
Mode = ResizeMode.Pad, // 填充模式,保持比例
PadColor = Color.Black // 用黑色填充多余区域
});
});
// 2. 将像素值从 [0, 255] 归一化到 [0, 1]
var tensor = new float[3 * YoloConfig.ImageSize * YoloConfig.ImageSize];
int index = 0;
resizedImage.ProcessPixelRows(accessor =>
{
for (int y = 0; y < YoloConfig.ImageSize; y++)
{
var pixelRow = accessor.GetRowSpan(y);
for (int x = 0; x < YoloConfig.ImageSize; x++)
{
// 注意:YOLO 有时要求输入为 RGB 顺序,且归一化方式可能不同。
// 这里是最简单的归一化。对于某些模型,可能需要减去均值除以标准差。
tensor[index] = pixelRow[x].R / 255.0f; // R 通道
tensor[index + 1 * YoloConfig.ImageSize * YoloConfig.ImageSize] = pixelRow[x].G / 255.0f; // G 通道
tensor[index + 2 * YoloConfig.ImageSize * YoloConfig.ImageSize] = pixelRow[x].B / 255.0f; // B 通道
index++;
}
}
});
return tensor;
}
}
3.3 实现模型推理与后处理
这是最核心的部分,包括加载模型、运行推理和解析输出。
csharp
public class YoloDetector : IDisposable
{
private readonly InferenceSession _session;
public YoloDetector(string modelPath)
{
// 创建 ONNX Runtime 推理会话
var options = new SessionOptions();
// 可以根据需要设置执行提供程序,例如 CUDA, TensorRT 等以加速。
// options.AppendExecutionProvider_CUDA();
_session = new InferenceSession(modelPath, options);
}
```csharp
public class YoloDetector : IDisposable
{
private readonly InferenceSession _session;
public YoloDetector(string modelPath)
{
// 创建 ONNX Runtime 推理会话
var options = new SessionOptions();
// 可以根据需要设置执行提供程序,例如 CUDA, TensorRT 等以加速。
// options.AppendExecutionProvider_CUDA();
_session = new InferenceSession(modelPath, options);
}
public List<DetectionResult> Detect(Image<Rgb24> image)
{
// 1. 预处理
var inputTensor = ImageProcessor.PreprocessImage(image);
var inputDimensions = new[] { 1, 3, YoloConfig.ImageSize, YoloConfig.ImageSize };
using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(inputTensor, inputDimensions);
// 2. 准备输入
var inputs = new Dictionary<string, OrtValue>
{
{ "images", inputOrtValue } // 输入名称需与模型导出时一致,YOLOv8 通常为 "images"
};
// 3. 运行推理
using var outputs = _session.Run(inputs);
// 4. 获取输出 (YOLOv8 输出名通常为 "output0")
var output = outputs.First();
var outputData = output.GetTensorDataAsSpan<float>();
var outputShape = output.GetTensorTypeAndInfo().Shape;
// 5. 后处理:解析输出,应用非极大值抑制 (NMS)
var rawDetections = ParseRawOutput(outputData, outputShape);
var filteredDetections = ApplyNms(rawDetections, 0.45f, 0.5f); // 置信度阈值 0.45,IoU 阈值 0.5
// 6. 将检测框坐标映射回原始图像尺寸
return MapToOriginalImage(filteredDetections, image.Width, image.Height);
}
private List<DetectionResult> ParseRawOutput(Span<float> outputData, long[] shape)
{
var detections = new List<DetectionResult>();
// shape 应为 [1, 84, 8400]
int numBoxes = (int)shape[2]; // 8400
for (int i = 0; i < numBoxes; i++)
{
int baseIndex = i * (YoloConfig.ClassCount + 4); // 每个框有 4个坐标 + 80个类别分数
// 解析坐标 (cx, cy, w, h),这些坐标是相对于 640x640 输入网格的
float cx = outputData[baseIndex];
float cy = outputData[baseIndex + 1];
float w = outputData[baseIndex + 2];
float h = outputData[baseIndex + 3];
// 找到置信度最高的类别
float maxConfidence = 0;
int bestClassId = -1;
for (int c = 0; c < YoloConfig.ClassCount; c++)
{
float confidence = outputData[baseIndex + 4 + c];
if (confidence > maxConfidence)
{
maxConfidence = confidence;
bestClassId = c;
}
}
if (bestClassId >= 0 && maxConfidence > 0.25f) // 初步过滤低置信度检测
{
// 将中心点坐标转换为左上角坐标
float x1 = cx - w / 2;
float y1 = cy - h / 2;
detections.Add(new DetectionResult
{
Label = YoloConfig.ClassNames[bestClassId],
Confidence = maxConfidence,
BoundingBox = new RectangleF(x1, y1, w, h)
});
}
}
return detections;
}
// 简化的非极大值抑制实现
private List<DetectionResult> ApplyNms(List<DetectionResult> detections, float confidenceThreshold, float iouThreshold)
{
// 按置信度降序排序
detections = detections.OrderByDescending(d => d.Confidence).ToList();
var results = new List<DetectionResult>();
while (detections.Count > 0)
{
// 取置信度最高的检测结果
var current = detections[0];
results.Add(current);
detections.RemoveAt(0);
// 计算与剩余检测框的 IoU,移除重叠度高的
for (int i = detections.Count - 1; i >= 0; i--)
{
if (CalculateIoU(current.BoundingBox, detections[i].BoundingBox) > iouThreshold)
{
detections.RemoveAt(i);
}
}
}
return results;
}
private float CalculateIoU(RectangleF boxA, RectangleF boxB)
{
float x1 = Math.Max(boxA.Left, boxB.Left);
float y1 = Math.Max(boxA.Top, boxB.Top);
float x2 = Math.Min(boxA.Right, boxB.Right);
float y2 = Math.Min(boxA.Bottom, boxB.Bottom);
float interArea = Math.Max(0, x2 - x1) * Math.Max(0, y2 - y1);
float boxAArea = boxA.Width * boxA.Height;
float boxBArea = boxB.Width * boxB.Height;
return interArea / (boxAArea + boxBArea - interArea);
}
private List<DetectionResult> MapToOriginalImage(List<DetectionResult> detections, int originalWidth, int originalHeight)
{
// 注意:由于预处理时使用了 Pad 模式,坐标映射需要考虑填充和缩放。
// 这里是一个简化版本,假设图像被直接缩放到 640x640。
// 在实际应用中,需要根据 Letterbox 的缩放比例和填充偏移量进行精确映射。
float scaleX = originalWidth / (float)YoloConfig.ImageSize;
float scaleY = originalHeight / (float)YoloConfig.ImageSize;
foreach (var det in detections)
{
var box = det.BoundingBox;
box.X *= scaleX;
box.Y *= scaleY;
box.Width *= scaleX;
box.Height *= scaleY;
det.BoundingBox = box;
}
return detections;
}
public void Dispose()
{
_session?.Dispose();
}
}
3.4 主程序入口
最后,在 Program.cs 的 Main 方法中串联所有步骤。
csharp
// Program.cs
class Program
{
static void Main(string[] args)
{
// 1. 指定模型和测试图片路径
string modelPath = @"yolov8n.onnx";
string imagePath = @"test.jpg"; // 请准备一张包含常见物体(如人、车)的图片
if (!File.Exists(modelPath))
{
Console.WriteLine($"错误:未找到模型文件 '{modelPath}',请确保已下载并放置在项目输出目录。");
return;
}
if (!File.Exists(imagePath))
{
Console.WriteLine($"错误:未找到测试图片 '{imagePath}'。");
return;
}
// 2. 加载图片
using var image = Image.Load<Rgb24>(imagePath);
Console.WriteLine($"已加载图片: {imagePath} ({image.Width}x{image.Height})");
// 3. 创建检测器并进行推理
using var detector = new YoloDetector(modelPath);
Console.WriteLine("开始目标检测...");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var results = detector.Detect(image);
stopwatch.Stop();
Console.WriteLine($"检测完成,耗时: {stopwatch.ElapsedMilliseconds} ms");
// 4. 输出结果
Console.WriteLine($"\n共检测到 {results.Count} 个物体:");
foreach (var result in results)
{
Console.WriteLine($" - {result.Label} (置信度: {result.Confidence:F2}) 位置: [{result.BoundingBox.X:F0}, {result.BoundingBox.Y:F0}, {result.BoundingBox.Width:F0}, {result.BoundingBox.Height:F0}]");
}
// 5. (可选) 保存带标注的图片
DrawDetections(image, results);
string outputPath = @"output.jpg";
image.Save(outputPath);
Console.WriteLine($"\n带检测框的图片已保存至: {outputPath}");
}
static void DrawDetections(Image<Rgb24> image, List<DetectionResult> results)
{
// 首先需要安装 SixLabors.ImageSharp.Drawing 包
// dotnet add package SixLabors.ImageSharp.Drawing
if (results.Count == 0)
{
Console.WriteLine("未检测到任何物体,跳过绘制。");
return;
}
// 定义颜色和字体配置
var colors = new[]
{
Color.Red,
Color.Green,
Color.Blue,
Color.Yellow,
Color.Magenta,
Color.Cyan,
Color.Orange,
Color.Purple,
Color.Pink,
Color.Brown
};
// 创建绘图选项
var drawingOptions = new DrawingOptions
{
GraphicsOptions = new GraphicsOptions
{
Antialias = true,
BlendPercentage = 1f
}
};
// 创建画笔和画刷
var penWidth = Math.Max(2, image.Width / 400); // 根据图片大小自适应线宽
var fontScale = Math.Max(12, image.Width / 80); // 根据图片大小自适应字体大小
// 加载字体(需要安装 SixLabors.Fonts 包或使用系统字体)
// 这里使用默认字体,实际项目中可以加载自定义字体
var font = SystemFonts.CreateFont("Arial", fontScale, FontStyle.Bold);
var textOptions = new TextOptions(font)
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Origin = new PointF(0, 0)
};
// 开始绘制
image.Mutate(ctx =>
{
for (int i = 0; i < results.Count; i++)
{
var result = results[i];
var color = colors[i % colors.Length];
// 获取边界框
var box = result.BoundingBox;
// 确保边界框在图片范围内
box.X = Math.Max(0, Math.Min(box.X, image.Width - 1));
box.Y = Math.Max(0, Math.Min(box.Y, image.Height - 1));
box.Width = Math.Min(box.Width, image.Width - box.X);
box.Height = Math.Min(box.Height, image.Height - box.Y);
if (box.Width <= 0 || box.Height <= 0)
continue;
// 1. 绘制检测框
var pen = Pens.Solid(color, penWidth);
var rect = new RectangularPolygon(box.X, box.Y, box.Width, box.Height);
ctx.Draw(drawingOptions, pen, rect);
// 2. 绘制填充背景的标签
var label = $"{result.Label} {result.Confidence:F2}";
var textSize = TextMeasurer.MeasureSize(label, textOptions);
// 标签背景矩形
var labelRect = new RectangularPolygon(
box.X,
Math.Max(0, box.Y - textSize.Height - 2),
textSize.Width + 10,
textSize.Height + 4
);
// 绘制半透明背景
var backgroundBrush = Brushes.Solid(Color.FromRgba(0, 0, 0, 150));
ctx.Fill(drawingOptions, backgroundBrush, labelRect);
// 绘制标签边框
ctx.Draw(drawingOptions, Pens.Solid(color, 1), labelRect);
// 3. 绘制文本
var textBrush = Brushes.Solid(color);
var textPosition = new PointF(box.X + 5, Math.Max(2, box.Y - textSize.Height));
ctx.DrawText(drawingOptions, label, font, textBrush, textPosition);
// 4. 绘制置信度条(可选)
var confidenceBarWidth = box.Width * result.Confidence;
if (confidenceBarWidth > 5)
{
var confidenceBar = new RectangularPolygon(
box.X,
box.Y + box.Height - 3,
confidenceBarWidth,
3
);
var confidenceBrush = Brushes.Solid(Color.Lerp(Color.Red, Color.Green, result.Confidence));
ctx.Fill(drawingOptions, confidenceBrush, confidenceBar);
}
// 5. 绘制角标(可选,增强视觉效果)
var cornerSize = Math.Min(15, Math.Min(box.Width, box.Height) / 4);
if (cornerSize > 3)
{
// 左上角
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X, box.Y),
new PointF(box.X + cornerSize, box.Y));
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X, box.Y),
new PointF(box.X, box.Y + cornerSize));
// 右上角
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X + box.Width, box.Y),
new PointF(box.X + box.Width - cornerSize, box.Y));
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X + box.Width, box.Y),
new PointF(box.X + box.Width, box.Y + cornerSize));
// 左下角
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X, box.Y + box.Height),
new PointF(box.X + cornerSize, box.Y + box.Height));
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X, box.Y + box.Height),
new PointF(box.X, box.Y + box.Height - cornerSize));
// 右下角
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X + box.Width, box.Y + box.Height),
new PointF(box.X + box.Width - cornerSize, box.Y + box.Height));
ctx.DrawLines(drawingOptions, color, penWidth,
new PointF(box.X + box.Width, box.Y + box.Height),
new PointF(box.X + box.Width, box.Y + box.Height - cornerSize));
}
}
// 6. 绘制统计信息(可选)
if (results.Count > 0)
{
var statsText = $"检测到 {results.Count} 个物体";
var statsFont = SystemFonts.CreateFont("Arial", fontScale * 0.8f, FontStyle.Regular);
var statsSize = TextMeasurer.MeasureSize(statsText, new TextOptions(statsFont));
var statsBackground = new RectangularPolygon(
10,
10,
statsSize.Width + 20,
statsSize.Height + 10
);
ctx.Fill(drawingOptions, Brushes.Solid(Color.FromRgba(0, 0, 0, 180)), statsBackground);
ctx.DrawText(drawingOptions, statsText, statsFont, Color.White, new PointF(20, 15));
}
});
Console.WriteLine($"已在图片上绘制了 {results.Count} 个检测框。");
}
}
4. 运行与测试
-
准备测试图片 : 在项目根目录放置一张名为
test.jpg的图片。 -
运行程序 :
bashdotnet run -
查看结果 : 控制台会输出检测到的物体类别、置信度和位置。如果实现了
DrawDetections方法,还会生成一张带标注框的output.jpg。
5. 进阶与优化建议
恭喜!你已经成功搭建了基础版本。接下来可以考虑以下方向进行优化和扩展:
-
性能优化:
- GPU 加速 : 在
SessionOptions中启用 CUDA 或 TensorRT 提供程序,大幅提升推理速度。 - 模型量化: 使用 ONNX Runtime 的量化工具将 FP32 模型转换为 INT8 模型,减少模型体积和内存占用。
- 批处理: 修改代码以支持一次推理多张图片,提高吞吐量。
- GPU 加速 : 在
-
功能扩展:
- 视频流检测 : 使用
OpenCvSharp等库捕获摄像头或视频文件流,实现实时视频分析。 - Web API 服务: 将检测逻辑封装成 ASP.NET Core We
- 视频流检测 : 使用
6. 实战:构建 Web API 服务
将 YOLO 检测功能封装为 Web API 是现代应用开发的常见需求。下面我们使用 ASP.NET Core Minimal API 快速构建一个简单的检测服务。
6.1 创建 Web API 项目
首先,创建一个新的 Web API 项目或在现有项目中添加 Web API 支持:
bash
# 创建新的 Web API 项目
dotnet new webapi -n YoloWebApi
cd YoloWebApi
# 或在本项目基础上添加 Web API 支持
# 修改项目文件,将 SDK 改为 Microsoft.NET.Sdk.Web
6.2 安装额外依赖包
除了之前安装的包,还需要添加 ASP.NET Core 相关包:
bash
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Swashbuckle.AspNetCore
6.3 实现 Web API 端点
下面是Web API服务的请求处理流程图:
#mermaid-svg-RAxMmxrJx3EwswT3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RAxMmxrJx3EwswT3 .error-icon{fill:#552222;}#mermaid-svg-RAxMmxrJx3EwswT3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RAxMmxrJx3EwswT3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RAxMmxrJx3EwswT3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RAxMmxrJx3EwswT3 .marker.cross{stroke:#333333;}#mermaid-svg-RAxMmxrJx3EwswT3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RAxMmxrJx3EwswT3 p{margin:0;}#mermaid-svg-RAxMmxrJx3EwswT3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RAxMmxrJx3EwswT3 .cluster-label text{fill:#333;}#mermaid-svg-RAxMmxrJx3EwswT3 .cluster-label span{color:#333;}#mermaid-svg-RAxMmxrJx3EwswT3 .cluster-label span p{background-color:transparent;}#mermaid-svg-RAxMmxrJx3EwswT3 .label text,#mermaid-svg-RAxMmxrJx3EwswT3 span{fill:#333;color:#333;}#mermaid-svg-RAxMmxrJx3EwswT3 .node rect,#mermaid-svg-RAxMmxrJx3EwswT3 .node circle,#mermaid-svg-RAxMmxrJx3EwswT3 .node ellipse,#mermaid-svg-RAxMmxrJx3EwswT3 .node polygon,#mermaid-svg-RAxMmxrJx3EwswT3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RAxMmxrJx3EwswT3 .rough-node .label text,#mermaid-svg-RAxMmxrJx3EwswT3 .node .label text,#mermaid-svg-RAxMmxrJx3EwswT3 .image-shape .label,#mermaid-svg-RAxMmxrJx3EwswT3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-RAxMmxrJx3EwswT3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RAxMmxrJx3EwswT3 .rough-node .label,#mermaid-svg-RAxMmxrJx3EwswT3 .node .label,#mermaid-svg-RAxMmxrJx3EwswT3 .image-shape .label,#mermaid-svg-RAxMmxrJx3EwswT3 .icon-shape .label{text-align:center;}#mermaid-svg-RAxMmxrJx3EwswT3 .node.clickable{cursor:pointer;}#mermaid-svg-RAxMmxrJx3EwswT3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RAxMmxrJx3EwswT3 .arrowheadPath{fill:#333333;}#mermaid-svg-RAxMmxrJx3EwswT3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RAxMmxrJx3EwswT3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RAxMmxrJx3EwswT3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RAxMmxrJx3EwswT3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RAxMmxrJx3EwswT3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RAxMmxrJx3EwswT3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RAxMmxrJx3EwswT3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RAxMmxrJx3EwswT3 .cluster text{fill:#333;}#mermaid-svg-RAxMmxrJx3EwswT3 .cluster span{color:#333;}#mermaid-svg-RAxMmxrJx3EwswT3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RAxMmxrJx3EwswT3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RAxMmxrJx3EwswT3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-RAxMmxrJx3EwswT3 .icon-shape,#mermaid-svg-RAxMmxrJx3EwswT3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RAxMmxrJx3EwswT3 .icon-shape p,#mermaid-svg-RAxMmxrJx3EwswT3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RAxMmxrJx3EwswT3 .icon-shape .label rect,#mermaid-svg-RAxMmxrJx3EwswT3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RAxMmxrJx3EwswT3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RAxMmxrJx3EwswT3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RAxMmxrJx3EwswT3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 错误处理
服务器端
客户端
文件为空
文件类型不支持
文件过大
验证通过
用户上传图片
发送HTTP POST请求
接收请求
输入验证
返回400错误
返回400错误
返回400错误
读取图片数据
ImageSharp加载图片
YOLO检测器推理
解析检测结果
格式化为JSON
返回200成功响应
记录日志
返回错误信息
客户端接收结果
图:Web API服务请求处理流程图
修改 Program.cs,添加 Minimal API 端点:
csharp
using Microsoft.AspNetCore.Mvc;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
var builder = WebApplication.CreateBuilder(args);
// 添加服务
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 注册 YoloDetector 为单例服务
builder.Services.AddSingleton<YoloDetector>(provider =>
{
var modelPath = "yolov8n.onnx";
if (!File.Exists(modelPath))
{
throw new FileNotFoundException($"模型文件 '{modelPath}' 未找到,请确保已下载并放置在项目根目录。");
}
return new YoloDetector(modelPath);
});
var app = builder.Build();
// 配置中间件
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// 定义请求和响应模型
public record DetectionResponse
{
public string Label { get; init; } = string.Empty;
public float Confidence { get; init; }
public float X { get; init; }
public float Y { get; init; }
public float Width { get; init; }
public float Height { get; init; }
}
public record DetectionRequest
{
public IFormFile? Image { get; init; }
}
// 健康检查端点
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
// 主要检测端点
app.MapPost("/detect", async (
[FromForm] DetectionRequest request,
[FromServices] YoloDetector detector,
ILogger<Program> logger) =>
{
try
{
// 1. 验证输入
if (request.Image == null || request.Image.Length == 0)
{
return Results.BadRequest(new { error = "请上传有效的图片文件" });
}
// 2. 验证文件类型
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".bmp", ".gif" };
var fileExtension = Path.GetExtension(request.Image.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(fileExtension))
{
return Results.BadRequest(new {
error = "不支持的文件类型",
supportedTypes = allowedExtensions
});
}
// 3. 验证文件大小(限制为 10MB)
const long maxFileSize = 10 * 1024 * 1024; // 10MB
if (request.Image.Length > maxFileSize)
{
return Results.BadRequest(new {
error = "文件大小超过限制",
maxSize = $"{maxFileSize / (1024 * 1024)}MB"
});
}
logger.LogInformation("开始处理图片: {FileName} ({Size} bytes)",
request.Image.FileName, request.Image.Length);
// 4. 读取图片
using var memoryStream = new MemoryStream();
await request.Image.CopyToAsync(memoryStream);
memoryStream.Position = 0;
using var image = await Image.LoadAsync<Rgb24>(memoryStream);
logger.LogInformation("图片加载成功: {Width}x{Height}", image.Width, image.Height);
// 5. 执行检测
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var results = detector.Detect(image);
stopwatch.Stop();
logger.LogInformation("检测完成,耗时: {ElapsedMs}ms,检测到 {Count} 个物体",
stopwatch.ElapsedMilliseconds, results.Count);
// 6. 转换响应格式
var response = results.Select(r => new DetectionResponse
{
Label = r.Label,
Confidence = r.Confidence,
X = r.BoundingBox.X,
Y = r.BoundingBox.Y,
Width = r.BoundingBox.Width,
Height = r.BoundingBox.Height
}).ToList();
return Results.Ok(new
{
success = true,
processingTimeMs = stopwatch.ElapsedMilliseconds,
imageInfo = new
{
width = image.Width,
height = image.Height,
fileName = request.Image.FileName
},
detections = response,
detectionCount = response.Count
});
}
catch (Exception ex)
{
logger.LogError(ex, "图片处理失败");
return Results.Problem(
detail = "服务器内部错误,请稍后重试",
statusCode = StatusCodes.Status500InternalServerError);
}
})
.Accepts<DetectionRequest>("multipart/form-data")
.Produces<IResult>(StatusCodes.Status200OK)
.Produces<IResult>(StatusCodes.Status400BadRequest)
.Produces<IResult>(StatusCodes.Status500InternalServerError)
.WithName("DetectObjects")
.WithOpenApi(operation =>
{
operation.Summary = "使用 YOLOv8 进行目标检测";
operation.Description = "上传图片文件,返回检测到的物体列表";
return operation;
});
// 批量检测端点(可选)
app.MapPost("/detect/batch", async (
[FromForm] IFormFileCollection files,
[FromServices] YoloDetector detector,
ILogger<Program> logger) =>
{
if (files == null || files.Count == 0)
{
return Results.BadRequest(new { error = "请上传至少一张图片" });
}
const int maxBatchSize = 10;
if (files.Count > maxBatchSize)
{
return Results.BadRequest(new {
error = $"一次最多处理 {maxBatchSize} 张图片",
currentCount = files.Count
});
}
var results = new List<object>();
var totalTime = 0L;
foreach (var file in files)
{
try
{
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
memoryStream.Position = 0;
using var image = await Image.LoadAsync<Rgb24>(memoryStream);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var detections = detector.Detect(image);
stopwatch.Stop();
totalTime += stopwatch.ElapsedMilliseconds;
results.Add(new
{
fileName = file.FileName,
width = image.Width,
height = image.Height,
processingTimeMs = stopwatch.ElapsedMilliseconds,
detections = detections.Select(d => new DetectionResponse
{
Label = d.Label,
Confidence = d.Confidence,
X = d.BoundingBox.X,
Y = d.BoundingBox.Y,
Width = d.BoundingBox.Width,
Height = d.BoundingBox.Height
}).ToList(),
detectionCount = detections.Count
});
}
catch (Exception ex)
{
logger.LogError(ex, "处理文件 {FileName} 失败", file.FileName);
results.Add(new
{
fileName = file.FileName,
error = "处理失败",
message = ex.Message
});
}
}
return Results.Ok(new
{
success = true,
totalProcessingTimeMs = totalTime,
fileCount = files.Count,
results = results
});
})
.Accepts<IFormFileCollection>("multipart/form-data")
.WithName("BatchDetect")
.WithOpenApi();
app.Run();
6.4 配置文件设置
在 appsettings.json 中添加配置:
json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Yolo": {
"ModelPath": "yolov8n.onnx",
"MaxFileSizeMB": 10,
"AllowedExtensions": [".jpg", ".jpeg", ".png", ".bmp", ".gif"]
}
}
6.5 运行与测试
- 启动服务:
bash
dotnet run
- 使用 curl 测试单张图片检测:
bash
curl -X POST "https://localhost:5001/detect" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
-F "image=@test.jpg"
- 使用 curl 测试批量检测:
bash
curl -X POST "https://localhost:5001/detect/batch" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
-F "files=@test1.jpg" \
-F "files=@test2.jpg"
- 使用 Swagger UI 测试 :
访问https://localhost:5001/swagger查看 API 文档并在线测试。
6.6 使用 Postman 测试
- 创建新的 POST 请求
- URL:
https://localhost:5001/detect - Body 选择
form-data - Key 输入
image,类型选择File - Value 选择本地图片文件
- 点击 Send 查看响应
6.7 响应示例
成功响应:
json
{
"success": true,
"processingTimeMs": 156,
"imageInfo": {
"width": 1920,
"height": 1080,
"fileName": "test.jpg"
},
"detections": [
{
"label": "person",
"confidence": 0.89,
"x": 320.5,
"y": 180.2,
"width": 120.8,
"height": 350.6
},
{
"label": "car",
"confidence": 0.76,
"x": 800.3,
"y": 450.1,
"width": 300.5,
"height": 150.2
}
],
"detectionCount": 2
}
错误响应:
json
{
"error": "请上传有效的图片文件"
}
6.8 生产环境部署建议
#mermaid-svg-Iat2RkDqEzDo0txm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Iat2RkDqEzDo0txm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Iat2RkDqEzDo0txm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Iat2RkDqEzDo0txm .error-icon{fill:#552222;}#mermaid-svg-Iat2RkDqEzDo0txm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Iat2RkDqEzDo0txm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Iat2RkDqEzDo0txm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Iat2RkDqEzDo0txm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Iat2RkDqEzDo0txm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Iat2RkDqEzDo0txm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Iat2RkDqEzDo0txm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Iat2RkDqEzDo0txm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Iat2RkDqEzDo0txm .marker.cross{stroke:#333333;}#mermaid-svg-Iat2RkDqEzDo0txm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Iat2RkDqEzDo0txm p{margin:0;}#mermaid-svg-Iat2RkDqEzDo0txm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Iat2RkDqEzDo0txm .cluster-label text{fill:#333;}#mermaid-svg-Iat2RkDqEzDo0txm .cluster-label span{color:#333;}#mermaid-svg-Iat2RkDqEzDo0txm .cluster-label span p{background-color:transparent;}#mermaid-svg-Iat2RkDqEzDo0txm .label text,#mermaid-svg-Iat2RkDqEzDo0txm span{fill:#333;color:#333;}#mermaid-svg-Iat2RkDqEzDo0txm .node rect,#mermaid-svg-Iat2RkDqEzDo0txm .node circle,#mermaid-svg-Iat2RkDqEzDo0txm .node ellipse,#mermaid-svg-Iat2RkDqEzDo0txm .node polygon,#mermaid-svg-Iat2RkDqEzDo0txm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Iat2RkDqEzDo0txm .rough-node .label text,#mermaid-svg-Iat2RkDqEzDo0txm .node .label text,#mermaid-svg-Iat2RkDqEzDo0txm .image-shape .label,#mermaid-svg-Iat2RkDqEzDo0txm .icon-shape .label{text-anchor:middle;}#mermaid-svg-Iat2RkDqEzDo0txm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Iat2RkDqEzDo0txm .rough-node .label,#mermaid-svg-Iat2RkDqEzDo0txm .node .label,#mermaid-svg-Iat2RkDqEzDo0txm .image-shape .label,#mermaid-svg-Iat2RkDqEzDo0txm .icon-shape .label{text-align:center;}#mermaid-svg-Iat2RkDqEzDo0txm .node.clickable{cursor:pointer;}#mermaid-svg-Iat2RkDqEzDo0txm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Iat2RkDqEzDo0txm .arrowheadPath{fill:#333333;}#mermaid-svg-Iat2RkDqEzDo0txm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Iat2RkDqEzDo0txm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Iat2RkDqEzDo0txm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Iat2RkDqEzDo0txm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Iat2RkDqEzDo0txm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Iat2RkDqEzDo0txm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Iat2RkDqEzDo0txm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Iat2RkDqEzDo0txm .cluster text{fill:#333;}#mermaid-svg-Iat2RkDqEzDo0txm .cluster span{color:#333;}#mermaid-svg-Iat2RkDqEzDo0txm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Iat2RkDqEzDo0txm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Iat2RkDqEzDo0txm rect.text{fill:none;stroke-width:0;}#mermaid-svg-Iat2RkDqEzDo0txm .icon-shape,#mermaid-svg-Iat2RkDqEzDo0txm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Iat2RkDqEzDo0txm .icon-shape p,#mermaid-svg-Iat2RkDqEzDo0txm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Iat2RkDqEzDo0txm .icon-shape .label rect,#mermaid-svg-Iat2RkDqEzDo0txm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Iat2RkDqEzDo0txm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Iat2RkDqEzDo0txm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Iat2RkDqEzDo0txm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 安全防护
监控运维
部署环境
CI/CD流水线
开发环境
本地开发
单元测试
集成测试
代码提交
自动化构建
运行测试
生成Docker镜像
推送到镜像仓库
容器编排平台
Kubernetes集群
Pod部署
服务暴露
应用性能监控
日志收集
指标告警
自动扩缩容
API密钥认证
速率限制
CORS策略
请求验证
图:生产环境部署与运维架构图
-
性能优化:
- 使用
AddSingleton确保YoloDetector只初始化一次 - 启用响应压缩:
app.UseResponseCompression() - 配置 Kestrel 服务器限制
- 使用
-
安全性:
- 添加 API 密钥认证
- 配置 CORS 策略
- 实施速率限制
-
监控与日志:
- 集成 Application Insights
- 添加健康检查端点
- 记录详细的请求日志
-
容器化部署:
dockerfileFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 8080 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["YoloWebApi.csproj", "./"] RUN dotnet restore "YoloWebApi.csproj" COPY . . RUN dotnet build "YoloWebApi.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "YoloWebApi.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . COPY yolov8n.onnx . ENTRYPOINT ["dotnet", "YoloWebApi.dll"]
通过这个 Web API 服务,你可以轻松地将 YOLO 目标检测能力集成到各种前端应用、移动应用或其他微服务中,构建完整的 AI 应用解决方案。
6.9 系统数据流图
以下是整个YOLO检测系统的完整数据流图:
#mermaid-svg-tynRoIgmyY7uLdvg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tynRoIgmyY7uLdvg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tynRoIgmyY7uLdvg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tynRoIgmyY7uLdvg .error-icon{fill:#552222;}#mermaid-svg-tynRoIgmyY7uLdvg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tynRoIgmyY7uLdvg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tynRoIgmyY7uLdvg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tynRoIgmyY7uLdvg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tynRoIgmyY7uLdvg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tynRoIgmyY7uLdvg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tynRoIgmyY7uLdvg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tynRoIgmyY7uLdvg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tynRoIgmyY7uLdvg .marker.cross{stroke:#333333;}#mermaid-svg-tynRoIgmyY7uLdvg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tynRoIgmyY7uLdvg p{margin:0;}#mermaid-svg-tynRoIgmyY7uLdvg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tynRoIgmyY7uLdvg .cluster-label text{fill:#333;}#mermaid-svg-tynRoIgmyY7uLdvg .cluster-label span{color:#333;}#mermaid-svg-tynRoIgmyY7uLdvg .cluster-label span p{background-color:transparent;}#mermaid-svg-tynRoIgmyY7uLdvg .label text,#mermaid-svg-tynRoIgmyY7uLdvg span{fill:#333;color:#333;}#mermaid-svg-tynRoIgmyY7uLdvg .node rect,#mermaid-svg-tynRoIgmyY7uLdvg .node circle,#mermaid-svg-tynRoIgmyY7uLdvg .node ellipse,#mermaid-svg-tynRoIgmyY7uLdvg .node polygon,#mermaid-svg-tynRoIgmyY7uLdvg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tynRoIgmyY7uLdvg .rough-node .label text,#mermaid-svg-tynRoIgmyY7uLdvg .node .label text,#mermaid-svg-tynRoIgmyY7uLdvg .image-shape .label,#mermaid-svg-tynRoIgmyY7uLdvg .icon-shape .label{text-anchor:middle;}#mermaid-svg-tynRoIgmyY7uLdvg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tynRoIgmyY7uLdvg .rough-node .label,#mermaid-svg-tynRoIgmyY7uLdvg .node .label,#mermaid-svg-tynRoIgmyY7uLdvg .image-shape .label,#mermaid-svg-tynRoIgmyY7uLdvg .icon-shape .label{text-align:center;}#mermaid-svg-tynRoIgmyY7uLdvg .node.clickable{cursor:pointer;}#mermaid-svg-tynRoIgmyY7uLdvg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tynRoIgmyY7uLdvg .arrowheadPath{fill:#333333;}#mermaid-svg-tynRoIgmyY7uLdvg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tynRoIgmyY7uLdvg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tynRoIgmyY7uLdvg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tynRoIgmyY7uLdvg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tynRoIgmyY7uLdvg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tynRoIgmyY7uLdvg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tynRoIgmyY7uLdvg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tynRoIgmyY7uLdvg .cluster text{fill:#333;}#mermaid-svg-tynRoIgmyY7uLdvg .cluster span{color:#333;}#mermaid-svg-tynRoIgmyY7uLdvg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tynRoIgmyY7uLdvg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tynRoIgmyY7uLdvg rect.text{fill:none;stroke-width:0;}#mermaid-svg-tynRoIgmyY7uLdvg .icon-shape,#mermaid-svg-tynRoIgmyY7uLdvg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tynRoIgmyY7uLdvg .icon-shape p,#mermaid-svg-tynRoIgmyY7uLdvg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tynRoIgmyY7uLdvg .icon-shape .label rect,#mermaid-svg-tynRoIgmyY7uLdvg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tynRoIgmyY7uLdvg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tynRoIgmyY7uLdvg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tynRoIgmyY7uLdvg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输出模块
核心处理模块
控制台应用
Web API服务
控制台
Web API
可视化
输入源
选择处理方式
本地图片文件
HTTP请求上传
图像加载与预处理
请求验证与解析
YOLOv8模型推理
后处理NMS
坐标映射
输出格式
终端显示结果
JSON响应
保存标注图片
用户查看
客户端应用
结果文件
图:YOLO检测系统完整数据流图