c# 基于 ML.Net中使用ONNX检测对象

教程:在 ML.NET 中使用 ONNX 检测对象

ONNX 对象检测示例概述

此示例创建一个 .NET 核心控制台应用程序,该应用程序使用预训练的深度学习 ONNX 模型检测图像中的对象。

了解模型

对象检测是图像处理任务。 因此,训练解决该问题的大多数深度学习模型都是 CNN。 本教程中使用的模型是 Tiny YOLOv2 模型,

什么是 ONNX 模型?

开放神经网络交换 (ONNX) 是 AI 模型的开放源代码格式。 ONNX 支持框架之间的互操作性。 这意味着,你可以在许多常见的机器学习框架(如 pytorch)中训练模型,将其转换为 ONNX 格式,并在其他框架(如 ML.NET)中使用 ONNX 模型。 有关详细信息,请参阅 ONNX 网站

创建桌面应用程序

  1. 创建名为“ObjectDetection”的 C# 控制台应用程序。 单击“下一步”按钮。

  2. 选择 .NET Framework 8.0 作为要使用的框架。 单击“创建” 按钮。

  3. 安装“Microsoft.ML”NuGet 包

创建类和定义路径

创建 DimensionsBase 类文件

    public class DimensionsBase
    {
        public float X { get; set; }
        public float Y { get; set; }
        public float Height { get; set; }
        public float Width { get; set; }
    }

创建YoloBoundingBox.cs 文件 


    public class BoundingBoxDimensions : DimensionsBase { }

    public class YoloBoundingBox
    {
        public BoundingBoxDimensions Dimensions { get; set; }

        public string Label { get; set; }

        public float Confidence { get; set; }

        public RectangleF Rect
        {
            get { return new RectangleF(Dimensions.X, Dimensions.Y, Dimensions.Width, Dimensions.Height); }
        }

        public Color BoxColor { get; set; }
    }

 创建 YoloOutPutParser.cs 文件

 class YoloOutputParser
 {
     class CellDimensions : DimensionsBase { }

     public const int ROW_COUNT = 13;
     public const int COL_COUNT = 13;
     public const int CHANNEL_COUNT = 125;
     public const int BOXES_PER_CELL = 5;
     public const int BOX_INFO_FEATURE_COUNT = 5;
     public const int CLASS_COUNT = 20;
     public const float CELL_WIDTH = 32;
     public const float CELL_HEIGHT = 32;

     private int channelStride = ROW_COUNT * COL_COUNT;

     private float[] anchors = new float[]
     {
         1.08F, 1.19F, 3.42F, 4.41F, 6.63F, 11.38F, 9.42F, 5.11F, 16.62F, 10.52F
     };

     private string[] labels = new string[]
     {
         "aeroplane", "bicycle", "bird", "boat", "bottle",
         "bus", "car", "cat", "chair", "cow",
         "diningtable", "dog", "horse", "motorbike", "person",
         "pottedplant", "sheep", "sofa", "train", "tvmonitor"
     };

     private static Color[] classColors = new Color[]
     {
         Color.Khaki,
         Color.Fuchsia,
         Color.Silver,
         Color.RoyalBlue,
         Color.Green,
         Color.DarkOrange,
         Color.Purple,
         Color.Gold,
         Color.Red,
         Color.Aquamarine,
         Color.Lime,
         Color.AliceBlue,
         Color.Sienna,
         Color.Orchid,
         Color.Tan,
         Color.LightPink,
         Color.Yellow,
         Color.HotPink,
         Color.OliveDrab,
         Color.SandyBrown,
         Color.DarkTurquoise
     };

     private float Sigmoid(float value)
     {
         var k = (float)Math.Exp(value);
         return k / (1.0f + k);
     }

     private float[] Softmax(float[] values)
     {
         var maxVal = values.Max();
         var exp = values.Select(v => Math.Exp(v - maxVal));
         var sumExp = exp.Sum();

         return exp.Select(v => (float)(v / sumExp)).ToArray();
     }

     private int GetOffset(int x, int y, int channel)
     {
         // YOLO outputs a tensor that has a shape of 125x13x13, which 
         // WinML flattens into a 1D array.  To access a specific channel 
         // for a given (x,y) cell position, we need to calculate an offset
         // into the array
         return (channel * this.channelStride) + (y * COL_COUNT) + x;
     }

     private BoundingBoxDimensions ExtractBoundingBoxDimensions(float[] modelOutput, int x, int y, int channel)
     {
         return new BoundingBoxDimensions
         {
             X = modelOutput[GetOffset(x, y, channel)],
             Y = modelOutput[GetOffset(x, y, channel + 1)],
             Width = modelOutput[GetOffset(x, y, channel + 2)],
             Height = modelOutput[GetOffset(x, y, channel + 3)]
         };
     }

     private float GetConfidence(float[] modelOutput, int x, int y, int channel)
     {
         return Sigmoid(modelOutput[GetOffset(x, y, channel + 4)]);
     }

     private CellDimensions MapBoundingBoxToCell(int x, int y, int box, BoundingBoxDimensions boxDimensions)
     {
         return new CellDimensions
         {
             X = ((float)x + Sigmoid(boxDimensions.X)) * CELL_WIDTH,
             Y = ((float)y + Sigmoid(boxDimensions.Y)) * CELL_HEIGHT,
             Width = (float)Math.Exp(boxDimensions.Width) * CELL_WIDTH * anchors[box * 2],
             Height = (float)Math.Exp(boxDimensions.Height) * CELL_HEIGHT * anchors[box * 2 + 1],
         };
     }

     public float[] ExtractClasses(float[] modelOutput, int x, int y, int channel)
     {
         float[] predictedClasses = new float[CLASS_COUNT];
         int predictedClassOffset = channel + BOX_INFO_FEATURE_COUNT;
         for (int predictedClass = 0; predictedClass < CLASS_COUNT; predictedClass++)
         {
             predictedClasses[predictedClass] = modelOutput[GetOffset(x, y, predictedClass + predictedClassOffset)];
         }
         return Softmax(predictedClasses);
     }

     private ValueTuple<int, float> GetTopResult(float[] predictedClasses)
     {
         return predictedClasses
             .Select((predictedClass, index) => (Index: index, Value: predictedClass))
             .OrderByDescending(result => result.Value)
             .First();
     }

     private float IntersectionOverUnion(RectangleF boundingBoxA, RectangleF boundingBoxB)
     {
         var areaA = boundingBoxA.Width * boundingBoxA.Height;

         if (areaA <= 0)
             return 0;

         var areaB = boundingBoxB.Width * boundingBoxB.Height;

         if (areaB <= 0)
             return 0;

         var minX = Math.Max(boundingBoxA.Left, boundingBoxB.Left);
         var minY = Math.Max(boundingBoxA.Top, boundingBoxB.Top);
         var maxX = Math.Min(boundingBoxA.Right, boundingBoxB.Right);
         var maxY = Math.Min(boundingBoxA.Bottom, boundingBoxB.Bottom);

         var intersectionArea = Math.Max(maxY - minY, 0) * Math.Max(maxX - minX, 0);

         return intersectionArea / (areaA + areaB - intersectionArea);
     }

     public IList<YoloBoundingBox> ParseOutputs(float[] yoloModelOutputs, float threshold = .3F)
     {
         var boxes = new List<YoloBoundingBox>();

         for (int row = 0; row < ROW_COUNT; row++)
         {
             for (int column = 0; column < COL_COUNT; column++)
             {
                 for (int box = 0; box < BOXES_PER_CELL; box++)
                 {
                     var channel = (box * (CLASS_COUNT + BOX_INFO_FEATURE_COUNT));

                     BoundingBoxDimensions boundingBoxDimensions = ExtractBoundingBoxDimensions(yoloModelOutputs, row, column, channel);

                     float confidence = GetConfidence(yoloModelOutputs, row, column, channel);

                     CellDimensions mappedBoundingBox = MapBoundingBoxToCell(row, column, box, boundingBoxDimensions);

                     if (confidence < threshold)
                         continue;

                     float[] predictedClasses = ExtractClasses(yoloModelOutputs, row, column, channel);

                     var (topResultIndex, topResultScore) = GetTopResult(predictedClasses);
                     var topScore = topResultScore * confidence;

                     if (topScore < threshold)
                         continue;

                     boxes.Add(new YoloBoundingBox()
                     {
                         Dimensions = new BoundingBoxDimensions
                         {
                             X = (mappedBoundingBox.X - mappedBoundingBox.Width / 2),
                             Y = (mappedBoundingBox.Y - mappedBoundingBox.Height / 2),
                             Width = mappedBoundingBox.Width,
                             Height = mappedBoundingBox.Height,
                         },
                         Confidence = topScore,
                         Label = labels[topResultIndex],
                         BoxColor = classColors[topResultIndex]
                     });
                 }
             }
         }
         return boxes;
     }

     public IList<YoloBoundingBox> FilterBoundingBoxes(IList<YoloBoundingBox> boxes, int limit, float threshold)
     {
         var activeCount = boxes.Count;
         var isActiveBoxes = new bool[boxes.Count];

         for (int i = 0; i < isActiveBoxes.Length; i++)
             isActiveBoxes[i] = true;

         var sortedBoxes = boxes.Select((b, i) => new { Box = b, Index = i })
                             .OrderByDescending(b => b.Box.Confidence)
                             .ToList();

         var results = new List<YoloBoundingBox>();

         for (int i = 0; i < boxes.Count; i++)
         {
             if (isActiveBoxes[i])
             {
                 var boxA = sortedBoxes[i].Box;
                 results.Add(boxA);

                 if (results.Count >= limit)
                     break;

                 for (var j = i + 1; j < boxes.Count; j++)
                 {
                     if (isActiveBoxes[j])
                     {
                         var boxB = sortedBoxes[j].Box;

                         if (IntersectionOverUnion(boxA.Rect, boxB.Rect) > threshold)
                         {
                             isActiveBoxes[j] = false;
                             activeCount--;

                             if (activeCount <= 0)
                                 break;
                         }
                     }
                 }

                 if (activeCount <= 0)
                     break;
             }
         }
         return results;
     }

检测对象

现在所有设置都已完成,可以检测一些对象。

对模型输出进行评分和分析

 

string assetsRelativePath = @"../../../assets";
string assetsPath = GetAbsolutePath(assetsRelativePath);
string modelFilePath = Path.Combine(assetsPath, "Model", "TinyYolo2_model.onnx");
string imagesFolder = Path.Combine(assetsPath, "images");
string outputFolder = Path.Combine(assetsPath, "images", "output");

// Initialize MLContext
MLContext mlContext = new MLContext();
try
{
    // Load Data
    IEnumerable<ImageNetData> images = ImageNetData.ReadFromFile(imagesFolder);
    IDataView imageDataView = mlContext.Data.LoadFromEnumerable(images);

    // Create instance of model scorer
    var modelScorer = new OnnxModelScorer(imagesFolder, modelFilePath, mlContext);

    // Use model to score data
    IEnumerable<float[]> probabilities = modelScorer.Score(imageDataView);

    // Post-process model output
    YoloOutputParser parser = new YoloOutputParser();

    var boundingBoxes =
        probabilities
        .Select(probability => parser.ParseOutputs(probability))
        .Select(boxes => parser.FilterBoundingBoxes(boxes, 5, .5F));

    // Draw bounding boxes for detected objects in each of the images
    for (var i = 0; i < images.Count(); i++)
    {
        string imageFileName = images.ElementAt(i).Label;
        IList<YoloBoundingBox> detectedObjects = boundingBoxes.ElementAt(i);

        DrawBoundingBox(imagesFolder, outputFolder, imageFileName, detectedObjects);

        LogDetectedObjects(imageFileName, detectedObjects);
    }
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
}

将预测结果可视化

在模型对图像进行评分并处理好输出后,必须在图像上绘制边界框。  DrawBoundingBox 的方法。

   void DrawBoundingBox(string inputImageLocation, string outputImageLocation, string imageName, IList<YoloBoundingBox> filteredBoundingBoxes)
   {
       Image image = Image.FromFile(Path.Combine(inputImageLocation, imageName));

       var originalImageHeight = image.Height;
       var originalImageWidth = image.Width;

       foreach (var box in filteredBoundingBoxes)
       {
           // Get Bounding Box Dimensions
           var x = (uint)Math.Max(box.Dimensions.X, 0);
           var y = (uint)Math.Max(box.Dimensions.Y, 0);
           var width = (uint)Math.Min(originalImageWidth - x, box.Dimensions.Width);
           var height = (uint)Math.Min(originalImageHeight - y, box.Dimensions.Height);

           // Resize To Image
           x = (uint)originalImageWidth * x / OnnxModelScorer.ImageNetSettings.imageWidth;
           y = (uint)originalImageHeight * y / OnnxModelScorer.ImageNetSettings.imageHeight;
           width = (uint)originalImageWidth * width / OnnxModelScorer.ImageNetSettings.imageWidth;
           height = (uint)originalImageHeight * height / OnnxModelScorer.ImageNetSettings.imageHeight;

           // Bounding Box Text
           string text = $"{box.Label} ({(box.Confidence * 100).ToString("0")}%)";

           using (Graphics thumbnailGraphic = Graphics.FromImage(image))
           {
               thumbnailGraphic.CompositingQuality = CompositingQuality.HighQuality;
               thumbnailGraphic.SmoothingMode = SmoothingMode.HighQuality;
               thumbnailGraphic.InterpolationMode = InterpolationMode.HighQualityBicubic;

               // Define Text Options
               Font drawFont = new Font("Arial", 12, FontStyle.Bold);
               SizeF size = thumbnailGraphic.MeasureString(text, drawFont);
               SolidBrush fontBrush = new SolidBrush(Color.Black);
               Point atPoint = new Point((int)x, (int)y - (int)size.Height - 1);

               // Define BoundingBox options
               Pen pen = new Pen(box.BoxColor, 3.2f);
               SolidBrush colorBrush = new SolidBrush(box.BoxColor);

               // Draw text on image 
               thumbnailGraphic.FillRectangle(colorBrush, (int)x, (int)(y - size.Height - 1), (int)size.Width, (int)size.Height);
               thumbnailGraphic.DrawString(text, drawFont, fontBrush, atPoint);

               // Draw bounding box on image
               thumbnailGraphic.DrawRectangle(pen, x, y, width, height);
           }
       }

       if (!Directory.Exists(outputImageLocation))
       {
           Directory.CreateDirectory(outputImageLocation);
       }

       image.Save(Path.Combine(outputImageLocation, imageName));
   }


 

 LogDetectedObjects 的方法,以将检测到的对象输出到控制台。

C#复制

void LogDetectedObjects(string imageName, IList<YoloBoundingBox> boundingBoxes)
{
    Console.WriteLine($".....The objects in the image {imageName} are detected as below....");

    foreach (var box in boundingBoxes)
    {
        Console.WriteLine($"{box.Label} and its Confidence score: {box.Confidence}");
    }

    Console.WriteLine("");
}

结果

按照上述步骤操作后,运行控制台应用 (Ctrl + F5)。 结果应如以下输出所示。 你可能会看到警告或处理消息,为清楚起见,这些消息已从以下结果中删除。

控制台复制

=====Identify the objects in the images=====

.....The objects in the image image1.jpg are detected as below....
car and its Confidence score: 0.9697262
car and its Confidence score: 0.6674225
person and its Confidence score: 0.5226039
car and its Confidence score: 0.5224892
car and its Confidence score: 0.4675332

.....The objects in the image image2.jpg are detected as below....
cat and its Confidence score: 0.6461141
cat and its Confidence score: 0.6400049

.....The objects in the image image3.jpg are detected as below....
chair and its Confidence score: 0.840578
chair and its Confidence score: 0.796363
diningtable and its Confidence score: 0.6056048
diningtable and its Confidence score: 0.3737402

.....The objects in the image image4.jpg are detected as below....
dog and its Confidence score: 0.7608147
person and its Confidence score: 0.6321323
dog and its Confidence score: 0.5967442
person and its Confidence score: 0.5730394
person and its Confidence score: 0.5551759

========= End of Process..Hit any Key ========

若要查看带有边界框的图像,请导航到 assets/images/output/ 目录。 以下是其中一个已处理的图像示例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值