13.3 Windows中模型的部署
13.3 Windows中模型的部署⚓︎
微软开源了ONNX运行时库(ONNX Runtime),可以进行高性能的推理,支持主流三大操作系统平台,支持使用GPU,同时提供Python、C#、C/C++、Ruby等开发语言,能满足各种场景的使用。
下面我们将使用前面生成的mnist.onnx
,创建一个Windows桌面应用,实现手写数字的识别。这里使用C#开发语言,需要安装Visual Studio开发环境。
创建WPF项目
打开Visual Studio 2017,新建项目,在Visual C#分类中选择WPF应用
,填写项目名称为OnnxDemo,点击确定,即可完成一个空白项目的创建,如图13-10所示。
图13-10 创建WPF项目
添加模型文件到项目中
打开解决方案资源管理器中,在项目上点右键->添加->现有项,如图13-11所示。
图13-11 添加现有项
在弹出的对话框中,将文件类型过滤器改为所有文件,然后导航到模型所在目录,选择模型文件并添加,如图13-12所示。本示例中使用的模型文件是mnist.onnx
。
图13-12 选择模型文件
模型是在应用运行期间加载的,所以在编译时需要将模型复制到运行目录下。在模型文件上点右键,属性,然后在属性面板上,将生成操作
属性改为内容
,将复制到输出目录
属性改为如果较新则复制
。如图13-13所示。
图13-13 修改文件属性
添加OnnxRuntime库
微软开源的ONNX运行时库(ONNX Runtime)库提供了NuGet包,可以很方便的集成到Visual Studio项目中。
打开解决方案资源管理器,在引用上点右键,管理NuGet程序包。如图13-14所示。
图13-14 管理NuGet程序包
在打开的NuGet包管理器中,切换到浏览选项卡,搜索onnxruntime
,找到Microsoft.ML.OnnxRuntime
包,当前版本是1.0.0,点击安装,按提示完成安装即可。该版本不支持AnyCPU平台,所以需要将项目的目标架构显式的改为x64或x86。在解决方案上点右键,选择配置管理器。如图13-15所示。
图13-15 打开配置管理器
在配置管理器对话框中,将活动解决方案平台切换为x64或x86。如果没有x64和x86,在下拉框中选择新建,按提示新建x64或x86平台。如图13-16所示。
图13-16 新建解决方案平台
设计界面
打开MainWindow.xaml
,将整个Grid片段替换为如下代码:
<Grid>
<StackPanel>
<Grid Width="336" Height="336">
<InkCanvas x:Name="inkCanvas" Width="336" Height="336" Background="Black"/>
</Grid>
<TextBlock x:Name="lbResult" FontSize="26" HorizontalAlignment="Center"/>
<Button x:Name="btnClean" Content="Clean" Click="BtnClean_Click" FontSize="26"/>
</StackPanel>
</Grid>
显示效果如图13-17所示。
图13-17 应用程序界面设计
其中:
inkCanvas
是写数字的画布,由于训练mnist用的数据集是黑色背景白色字,所以这里将画布也设置为黑色背景lbResult
文本控件用来显示识别的结果btnClean
按钮用来清除之前的画布
然后在MainWindow
构造函数中调用InitInk
方法初始化画布,设置画笔颜色为白色:
private void InitInk()
{
// 将画笔改为白色
var attr = new DrawingAttributes();
attr.Color = Colors.White;
attr.IgnorePressure = true;
attr.StylusTip = StylusTip.Ellipse;
attr.Height = 24;
attr.Width = 24;
inkCanvas.DefaultDrawingAttributes = attr;
// 每次画完一笔时,都触发此事件进行识别
inkCanvas.StrokeCollected += InkCanvas_StrokeCollected;
}
private void InkCanvas_StrokeCollected(object sender, InkCanvasStrokeCollectedEventArgs e)
{
// 从画布中进行识别
RecogNumberFromInk();
}
其中,RecogNumberFromInk
就是对画布中的内容进行识别,在后面的小节中再添加实现。
然后添加btnClean
按钮事件的实现:
private void BtnClean_Click(object sender, RoutedEventArgs e)
{
// 清除画布
inkCanvas.Strokes.Clear();
lbResult.Text = string.Empty;
}
画布数据预处理
前面图13-9中可以看到,输入fc1x
是一个大小为1x784的float数组,对应的是28x28大小图片的每个像素点的色值,输出activation3y
是1x10的float数组,分别代表识别为数字0-9的得分,值最大的即为识别结果。
因此这里需要添加几个函数对画布数据进行处理,转为模型可以接受的数据。以下几个函数分别是将画布渲染到28x28的图片,读取每个像素点的值,生成模型需要数组:
private BitmapSource RenderToBitmap(FrameworkElement canvas, int scaledWidth, int scaledHeight)
{
// 将画布渲染到bitmap上
RenderTargetBitmap rtb = new RenderTargetBitmap((int)canvas.Width, (int)canvas.Height, 96d, 96d, PixelFormats.Default);
rtb.Render(canvas);
// 调整bitmap的大小为28*28,与模型的输入保持一致
TransformedBitmap tfb = new TransformedBitmap(rtb, new ScaleTransform(scaledWidth / rtb.Width, scaledHeight / rtb.Height));
return tfb;
}
public byte[] GetPixels(BitmapSource source)
{
if (source.Format != PixelFormats.Bgra32)
source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
int width = source.PixelWidth;
int height = source.PixelHeight;
byte[] data = new byte[width * 4 * height];
source.CopyPixels(data, width * 4, 0);
return data;
}
public float[] GetInputDataFromInk()
{
var bitmap = RenderToBitmap(inkCanvas, 28, 28);
var imageBytes = GetPixels(bitmap);
float[] data = new float[784];
for (int i = 0; i < 784; i++)
{
// 画布为黑白色的,可以直接取RGB中的一个分量作为此像素的色值
int baseIndex = 4 * i;
data[i] = imageBytes[baseIndex];
}
return data;
}
调用模型进行推理
整理好输入数据后,就可以调用模型进行推理并输出结果了。前面界面设计部分,inkCanvas
添加了响应事件并调用了RecogNumberFromInk
方法,这里给出对应的实现:
private void RecogNumberFromInk()
{
// 从画布得到输入数组
var inputData = GetInputDataFromInk();
// 从文件中加载模型
string modelPath = AppDomain.CurrentDomain.BaseDirectory + "mnist.onnx";
using (var session = new InferenceSession(modelPath))
{
// 支持多个输入,对于mnist模型,只需要一个输入
var container = new List<NamedOnnxValue>();
// 输入是大小1*784的一维数组
var tensor = new DenseTensor<float>(inputData, new int[] { 1, 784 });
// 输入的名称是port
container.Add(NamedOnnxValue.CreateFromTensor<float>("fc1x", tensor));
// 推理
var results = session.Run(container);
// 输出结果是IReadOnlyList<NamedOnnxValue>,支持多个输出,对于mnist模型,只有一个输出
var result = results.FirstOrDefault()?.AsTensor<float>()?.ToList();
// 从输出中取出得分最高的
var max = result.IndexOf(result.Max());
// 显示在控件中
lbResult.Text = max.ToString();
}
}
至此,所有的代码就完成了,按F5
即可开始调试运行。
结果展示
运行程序并书写数字,即可看到模型推理结果,如图13-18所示。
图13-18 推理结果
代码位置
ch13, WPF