在WinForms中使用ML.NET进行对象检测
物体检测是指程序检测图像中物体的能力。ML让.NET开发者通过使用ML.NET框架在C#或F#中创建定制的机器学习模型来实现这一功能。
在这篇文章中,我们将看看如何使用ML.NET框架来创建一个可以检测图像中物体的windows forms应用程序。
前提条件
要想继续学习,你需要具备以下条件。
- 对C#的基本了解。
- 对[.NET]开发平台的基本了解。
- 安装了Microsoft Visual Studio。
第一步是打开visual studio,并按照以下步骤操作。
- 点击
Create new project。 - 在下一个屏幕上搜索
Windows Forms,并选择Windows Forms App(.NET Framework),选择使用C#。并点击Next - 输入你要创建的项目的名称,即:
Win_Forms_ObjectDetection,然后点击create。

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

你将被要求下载一些金块包以使对象检测有效。这些包是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 ,你将使用fileSystemWatcher 和openFileDialog ,它位于窗口的底部,并分别将其重命名为fileWatcher 和fileDialog 。
fileSystemWatcher 观察系统和文件目录中的不同变化。
openFileDialog 要求用户打开一个文件并从中选择一个文件。
创建文件夹
你还将创建一些文件夹,即MLmodels 和Models ,用于你将使用的不同类别。
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框架实现。
希望这个教程对你有帮助。