跳转至

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