YOLO + .NET 10 快速入门:从零搭建实时目标检测应用

引言✍️

在人工智能和计算机视觉领域,实时目标检测(Object Detection)是一项核心且应用广泛的技术。YOLO(You Only Look Once)系列算法以其"单次前向传播"的极速推理能力,成为工业界和学术界的热门选择。对于 .NET 开发者而言,如何将强大的 YOLO 模型与熟悉的 .NET 生态结合,快速构建出可部署的应用程序,是一个极具价值的话题。

本文将带你从零开始,使用最新的 .NET 10YOLOv8 模型,一步步搭建一个完整的实时目标检测应用。无论你是刚接触计算机视觉的 .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.MLMicrosoft.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 模型

  1. 访问 Ultralytics YOLOv8 Releases 页面。
  2. 找到 Assets 部分,下载名为 yolov8n.onnx 的模型文件(这是 Nano 版本,体积小,速度快,适合入门)。
  3. 将下载的 yolov8n.onnx 文件复制到你的项目根目录下,并在 Visual Studio 或 Rider 中,将其"复制到输出目录"属性设置为"如果较新则复制"。

2.2 理解模型输入输出

  • 输入 : 一个形状为 [1, 3, 640, 640] 的张量,代表一张经过归一化处理的 640x640 RGB 图像。
  • 输出 : 一个形状为 [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.csMain 方法中串联所有步骤。

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. 运行与测试

  1. 准备测试图片 : 在项目根目录放置一张名为 test.jpg 的图片。

  2. 运行程序 :

    bash 复制代码
    dotnet run
  3. 查看结果 : 控制台会输出检测到的物体类别、置信度和位置。如果实现了 DrawDetections 方法,还会生成一张带标注框的 output.jpg

5. 进阶与优化建议

恭喜!你已经成功搭建了基础版本。接下来可以考虑以下方向进行优化和扩展:

  • 性能优化:

    • GPU 加速 : 在 SessionOptions 中启用 CUDA 或 TensorRT 提供程序,大幅提升推理速度。
    • 模型量化: 使用 ONNX Runtime 的量化工具将 FP32 模型转换为 INT8 模型,减少模型体积和内存占用。
    • 批处理: 修改代码以支持一次推理多张图片,提高吞吐量。
  • 功能扩展:

    • 视频流检测 : 使用 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 运行与测试

  1. 启动服务
bash 复制代码
dotnet run
  1. 使用 curl 测试单张图片检测
bash 复制代码
curl -X POST "https://localhost:5001/detect" \
  -H "accept: application/json" \
  -H "Content-Type: multipart/form-data" \
  -F "image=@test.jpg"
  1. 使用 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"
  1. 使用 Swagger UI 测试
    访问 https://localhost:5001/swagger 查看 API 文档并在线测试。

6.6 使用 Postman 测试

  1. 创建新的 POST 请求
  2. URL: https://localhost:5001/detect
  3. Body 选择 form-data
  4. Key 输入 image,类型选择 File
  5. Value 选择本地图片文件
  6. 点击 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策略
请求验证

图:生产环境部署与运维架构图

  1. 性能优化

    • 使用 AddSingleton 确保 YoloDetector 只初始化一次
    • 启用响应压缩:app.UseResponseCompression()
    • 配置 Kestrel 服务器限制
  2. 安全性

    • 添加 API 密钥认证
    • 配置 CORS 策略
    • 实施速率限制
  3. 监控与日志

    • 集成 Application Insights
    • 添加健康检查端点
    • 记录详细的请求日志
  4. 容器化部署

    dockerfile 复制代码
    FROM 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检测系统完整数据流图

相关推荐
雲明1 小时前
YOLO12目标检测:WebUI界面3步操作指南
目标检测·计算机视觉·webui·yolo12
深度知识积累AI1 小时前
RK3588 部署 YOLO26 目标检测:从训练到 NPU 推理
yolo
Ai缝合怪 博士2 小时前
【CVPR 2025即插即用】卷积模块篇 | EBlock有效编码器模块,适合低光图像增强、图像分类、实例分割、语义分割、图像去噪、边缘检测、医学图像分割、遥感目标检测等CV任务通用,涨点起飞
目标检测·低光增强·2026顶会顶刊即插即用模块·eblock有效编码器模块·图像分类、实例分割、语义分割·图像去噪、图像去模糊·darkir低光增强模型
阿_旭2 小时前
一文吃透 Grounding DINO:从原理到实战,文本驱动目标检测入门教程【附源码】
人工智能·目标检测·计算机视觉·groundingdino
星云_byto2 小时前
精读双模态目标检测系列八|TGRS 顶刊力作!CMFADet 狂涨 4.02% mAP,空域频域双增强 + 通道交互融合,轻量 108FPS 缝合即涨点!
人工智能·目标检测·计算机视觉·红外图像·rgb-ir融合
C_c..2 小时前
#YOLOv11 目标检测训练结果怎么看?一文看懂 Precision、Recall、mAP 指标
人工智能·yolo·目标检测·机器学习·计算机视觉·目标跟踪
笑脸惹桃花2 小时前
目标检测:YOLOv12环境配置,超详细,适合0基础纯小白
深度学习·yolo·目标检测·目标跟踪·yolov12
毕竟是shy哥12 小时前
TSDD-UB:UB:一种基于纹理简化的去噪扩散模型, 用于超声 B 扫信号下的无监督缺陷检测
目标检测·缺陷检测·扩散模型·工业缺陷检测·无损检测·超声检测·无监督缺陷检测
0x000721 小时前
译 Anders Hejlsberg 谈 C# 与 .NET
开发语言·c#·.net