如何在WinForms中使用ML.NET进行物体检测

377 阅读5分钟

在WinForms中使用ML.NET进行对象检测

物体检测是指程序检测图像中物体的能力。ML让.NET开发者通过使用ML.NET框架在C#或F#中创建定制的机器学习模型来实现这一功能。

在这篇文章中,我们将看看如何使用ML.NET框架来创建一个可以检测图像中物体的windows forms应用程序。

前提条件

要想继续学习,你需要具备以下条件。

  • 对C#的基本了解。
  • 对[.NET]开发平台的基本了解。
  • 安装了Microsoft Visual Studio。

第一步是打开visual studio,并按照以下步骤操作。

  1. 点击Create new project
  2. 在下一个屏幕上搜索Windows Forms ,并选择Windows Forms App(.NET Framework) ,选择使用C# 。并点击Next
  3. 输入你要创建的项目的名称,即:Win_Forms_ObjectDetection ,然后点击create

Project name

表单设计器应该是下图所示的样子。

Design

你将被要求下载一些金块包以使对象检测有效。这些包是Microsoft.ML,Microsoft.ML.OnnxRuntime

由于你正在处理对象检测,你也将下载Microsoft.ML.Image.Analytics 。你还将下载Microsoft.ML.OnnxTransformer ,因为你正在使用微软的对象检测模型。

选择正确的处理器

当你构建项目时,你会遇到一个错误,这是因为ML.NET 只支持64位处理器,而设置是32位处理器。

要解决这个错误,请右击项目名称并导航到属性,然后进入构建并在platform target 下选择x64

添加一个按钮

现在在工具箱侧边栏,拖动一个Button ,并将其放在表单设计器标签的底部。到属性那边去,把它从button1 改名为Select Image

在同一个属性侧边栏,向下滚动到Name ,并改变名称,将其命名为imgSelectBtn

启用该按钮

为了使btnSelectImage ,你将使用fileSystemWatcheropenFileDialog ,它位于窗口的底部,并分别将其重命名为fileWatcherfileDialog

fileSystemWatcher 观察系统和文件目录中的不同变化。

openFileDialog 要求用户打开一个文件并从中选择一个文件。

创建文件夹

你还将创建一些文件夹,即MLmodelsModels ,用于你将使用的不同类别。

MLModels文件夹

在你的MLModels 文件夹中,你将添加lbls.txt 文件,其中包含red,blue,white, 和green 颜色,它们给出了我们正在使用的不同标签,以及来自自定义视觉的model.onnx 文件。

模型文件夹

你将在你的Models 文件夹中创建四个类,即:ImageSettings,ImageInputs,ImageResults, 和BndBox

现在,双击你创建的按钮 - 一个名为form1.cs 的代码编辑器将打开。这是实现项目的大部分功能的地方,以实现对按钮的点击事件。

namespace ObjectDetection
{
    public partial class Form1 : Form
    {
        public const int lineCount = 12, pileCount = 12;
        public const int ftsPerBx = 6;
        private static readonly (float x_axis, float y_axis)[] bxAnchors = { (0.564f, 0.688f), (1.80f, 2.00f), (3.44f, 5.67f), (7.68f, 3.55f), (9.88f, 9.20f) };
        private PredictionEngine<ImageInput, ImageResults> _predictionEngine;
        public Form1()
        {
            InitializeComponent();
            picPrediction.Visible = false;
            var context = new MLContext();
            var emptyStatistics = new List<ImageInput>();
            var statistics = context.Data.LoadFromEnumerable(emptyStatistics);
            var pyplne = context.Transforms.ResizeImages(resizing: ImageResizingEstimator.ResizingKind.Fill, outputColumnName: "statistics", imgBreadth: ImageSettings.imgBreadth, imgHeit: ImageSettings.imgHeit, inputColumnName: name_of(ImageInput.img))
                            .Append(context.Transforms.ExtractPixels(outputColumnName: "Statistics"))
                            .Append(context.Transforms.ApplyOnnxModel(modelFile: "./MLModel/model.onnx", outputColumnName: "model_outputs", inputColumnName: "Statistics"));
            var mdl = pyplne.Fit(Statistics);
            _predictionEngine = context.Model.CreatePredictionEngine<ImageInput, ImageResults>(mdl);
        }
        private void imgSelectBtn_Click(object sender, EventArgs e)
        {
            if (fileDialog.ShowDialog() == DialogResult.OK)
            {
                var img = (Bitmap)Image.FromFile(fileDialog.FileName);
                var result = _predictionEngine.Predict(new ImageInput { Image = img });
                var lbls = File.ReadAllLines("./MLModel/indicators.txt");
                var bndBoxes = ParseOutputs(prediction.ImageType, lbls);
                var initialWidth = img.Width;
                var initialHeight = img.Height;
                if (bndBoxes.Count > 1)
                {
                    var maximum = bndBoxes.Max(b => b.Confidence);
                    var highBndBox = bndBoxes.FirstOrDefault(b => b.Confidence == maximum);
                    bndBoxes.Clear();
                    bndBoxes.Add(highBndBox);
                }
                else
                {
                    MsgBox.Show("No results for the image");
                    return;
                }
                foreach (var bndBox in bndBoxes)
                {
                    float x_axis = Math.Max(bndBox.Dimensions.X, 0);
                    float y_axis = Math.Max(bndBox.Dimensions.Y, 0);
                    float breadth = Math.Min(initialWidth - x, bndBox.Dimensions.Width);
                    float heit = Math.Min(initialHeight - y, bndBox.Dimensions.Height);

                    // in order to fit to the current image size
                    x_axis = initialWidth * x_axis / ImageSettings.imgWidth;
                    y_axis = initialHeight * y_axis / ImageSettings.imageHeight;
                    breadth = initialBreadth * breadth / ImageSettings.imageWidth;
                    heit = initialHeit * heit / ImageSettings.imageHeight;
                    using (var graphics = Graphics.FromImage(img))
                    {
                        graphics.DrawRectangle(new Pen(Color.Red, 3), x_axis, y_axis, breadth, heit);
                        graphics.DrawString(boundingBox.Description, new Font(FontFamily.Families[0], 30f), Brushes.Red, x + 5, y + 5);
                    }
                }
                imageResult.Image = img;
                imageResult.SizeMode = PictureBoxSizeMode.AutoSize;
                imageResult.Visible = true;
                imgSelectBtn.Visible = false;
                btnNewPrediction.Visible = true;
            }
        }
        public static List<BndBox> ParseOutputs(float[] mdlOutput, string[] lbls, float probThreshold = .5f)
        {
            var bxs = new List<BndBox>();
            for (int line = 0; line < lineCount; line++)
            {
                for (int pile = 0; pile < pileCount; pile++)
                {
                    for (int bxs= 0; bxs < boxAnchors.Length; bxs++)
                    {
                        var chnl = bxs * (lbls.Length + ftsPerBx);
                        var bndBoxResult = ExtractBndBoxResult(mdlOutput, row, column, chnl);
                        var mpdBndBox = MapBndBoxToCell(row, column, bx, bndBoxResult);
                        if (bndgBoxResult.Confidence < probThreshold)
                            continue;
                        float[] classProb = ExtractClassProbabilities(mdlOutput, row, column, chnl, bndBoxResult.Confidence, lbl);
                        var (topProb, highIndex) = classProbs.Select((prob, index) => (Score: prob, Index: index)).Max();
                        if (topProb < probThreshold)
                            continue;
                        bxs.Add(new BndBox
                        {
                            Measurements = mpdBndBox,
                            Confidence = topProb,
                            Lbl = lbls[highIndex]
                        });
                    }
                }
            }
            return bxs;
        }
        private static BndBMeasurements MapBndBoxToCell(int line, int pile, int bx, BndBoxResults bxMeasurements)
        {
            const float unitBreadth = ImageSettings.imgBreadth / pileCount;
            const float unitHeit = ImageSettings.imgHeit / lineCount;
            var mpdBx = new BndBoxMeasurements
            {
                X_axis = (line + Sigmoid(bxMeasurements.X_axis)) * unitBreadth,
                Y_axis = (pile + Sigmoid(bxMeasurements.Y_axis)) * unitHeit,
                Breadth = (float)Math.Exp(bxMeasurements.Breadth) * unitBreadth * bxAnchors[bx].x_axis,
                Heit = (float)Math.Exp(bxMeasurements.Heit) * unitHeit * bxAnchors[bx].y_axix,
            };
            // The x_axis, y_axis coordinates from the (mapped) bndbox prediction represent the center
            // of the bndbox. We adjust them here to represent the top left corner.
            mpdBox.X_axis -= mpdBox.Breadth / 2;
            mpdBox.Y_axis -= mpdBox.Heit / 2;

            return mpdBox;
        }
        private static BndBoxResults ExtractBndBoxResult(float[] mdlOutput, int line, int pile, int chnl)
        {
            return new BndBoxResult
            {
                X_axis = mdlOutput[GetOffset(line, pile, chnl++)],
                Y_axis = mdlOutput[GetOffset(line, pile, chnl++)],
                Breadth = modelOutput[GetOffset(line, pile, chnl++)],
                Heit = mdlOutput[GetOffset(line, pile, chnl++)],
                Confidence = Sigmoid(mdlOutput[GetOffset(line, pile, chnl++)])
            };
        }
        public static float[] ExtractClassProbs(float[] mdlOutput, int line, int pile, int chnl, float confidence, string[] lbls)
        {
            var classProbsOffset = chnl + ftsPerBox;
            float[] classProbs = new float[lbls.Length];
            for (int classProb = 0; classProb < lbls.Length; classProb++)
                classProbs[classProb] = mdlOutput[GetOffset(line, pile, classProb + classProbOffset)];
            return Softmax(classProbs).Select(q => q * confidence).ToArray();
        }
        private static float Sigmoid(float value)
        {
            var m = (float)Math.Exp(value);
            return m / (1.0f + m);
        }
        private static float[] Softmax(float[] classProbabilities)
        {
            var maximum= classProbs.Max();
            var expand = classProbs.Select(u => Math.Exp(u - maximum));
            var summation = exp.Sum();
            return exp.Select(u => (float)u / (float)summation).ToArray();
        }
        private void btnNewPrediction_Click(object sender, EventArgs e)
        {
            btnNewResult.Visible = false;
            imgResult.Visible = false;
            imgSelectButton.Visible = true;
        }
        private static int GetOffset(int line, int pile, int chnl)
        {
            const int chnlStride = lineCount * pileCount;
            return (chnl * chnlStride) + (pile * pileCount) + line;
        }
    }
    class BndBoxResult : BndBoxMeasurements
    {
        public float Confidence { get; set; }
    }
}

代码解释

为了显示文件对话框,使用了函数private void imgSelectBtn_Click(object sender, EventArgs e) 中的代码。

ML.NET 的代码在表单的构造函数public Form1() 中,每当表单作为主表单出现时就会执行。

你将给它空的数据,即var emptyStatistics ,因为你是用它来进行预测的,并给它一个来自图像输入类的新数据列表,即new List<ImageInput>() ,你在models文件夹中创建的。

你还需要做一个管道来调整图像的大小,使用名为Netron 的导航的ImageResizingEstimator ,同时使用你在Models 文件夹中的ImageSettings 文件中设置的图像调整参数。

InputColumnName 输入模型文件夹中的ImageInput 文件的图像名称。

你意识到在ML.NET 代码中有一个_predictionEngine ,这个作用是接收图像数据的参数,当它被设置为模型文件夹中的context.model.CreatePredictionEngine<ImageInput, ImagePredictions>(model)

在按钮功能中,预测引擎用于接收要预测的数据,即从文件对话框中的image ,以及检测后的图像数据的存储位置。即var results

该代码也有BndBoxes 。这段代码包括X轴和Y轴的数值,预测层的高度和宽度。这段代码是在创建类的时候自动生成的。

现在,在你创建的Models 文件夹里有四个类。即

图像设置类

图像设置类是用来设置图像的宽度和高度的。下面的代码在该类中使用。

Public class ImageSettings
{
  public const int imgHeit = 400;
  public const int imgBreadth =400;
}

图像输入类

图像输入类接收要预测的图像数据。这个类从图像设置类中获取属性,它是一个位图类型的数据。

public class ImageInput
{
  [imageType(ImageSettings.imgHeit, ImageSettings.imgBreadth)]
  public Bitmap Image {get; set; }
}

添加图片

对于你的项目来说,剩下的主要事情是创建你将存储图片的地方来做预测。

要做到这一点,请到Toolbox ,并选择PictureBox ,将其拖到窗体布局中,给它命名为imgResult ,并用for loop 后面的代码函数实现它,即。imagePrediction.Image = image;

当你运行你的项目时,它将能够检测并给你所检测的图像中的物体贴上标签。

总结

通过对本教程的深入理解和学习,我们可以清楚地看到,物体检测不仅可以在python中实现。只要在Microsoft Visual Studio中下载所需的NuGet包,它也可以在C#中使用ML.NET框架实现。

希望这个教程对你有帮助。