65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WPF 和 Microsoft.Ink 进行手写识别

2020年10月17日

CPOL

26分钟阅读

viewsIcon

12058

downloadIcon

552

Microsoft 笔计算有哪些可用, 哪些不可用? 如何处理和识别任何语言的手写输入?

Hello, Microsoft Ink!

引言

怀疑论者是一个看到墙上笔迹时,声称那是伪造的人。

莫里斯·本德

目录

动机
Microsoft 手写识别问题
Windows 10 屏幕键盘的可用性
必备组件
收集墨迹数据
最简单的识别
收集墨迹数据:深入探讨
调整笔画
识别选定的笔画
识别不同语言
识别候选项
候选项选择界面
测试支持的属性
后台识别和同步
候选项的来源?
整合所有内容
Microsoft.Ink.Analysis 如何?
还有什么?
构建和兼容性
结论

动机

引言

没有什么比一切都与你作对的情况更令人兴奋的了。

夏洛克·福尔摩斯,阿瑟·柯南·道尔爵士的《巴斯克维尔的猎犬

大约在2005年,微软平板电脑提供的手写识别质量令我惊喜。我没有平板电脑,只有一个绘图板,画了很多画,只是想更好地利用它。事实证明,一个人并不真正需要平板电脑,因为它足以安装一个单独的产品,该产品的SDK,并且可以在非专业化的Windows版本上使用。很难理解为什么微软不能为其他用户(非软件开发人员)提供类似的东西。SDK附带的演示软件已经足够好,可以作为用户级别的软件。我不能说它完美,但即使对我来说,用笔输入文本也几乎是实用的。如果我必须在没有物理键盘的情况下工作,我甚至会在我的实践中使用它。

现在我几乎到处都有触摸屏,还有一支不错的细线手写笔,而且,所有的电脑都强大得多,但是……那个漂亮的演示应用程序去哪里了?我不确定还能不能找到平板电脑SDK。Windows 10捆绑的远不够好。原则上,识别质量与2005年相同,但应用程序本身无法给我2005年手写输入的一小部分便利。

所以,我很好奇我们能否找回2005年的成就,并以更实用的方式使用手写识别。我发现:哦,是的,我们可以,甚至更多!它只是需要一些额外的努力。

Microsoft 手写识别问题

引言

你可能读不懂医生的笔迹和处方,但你会发现他的账单打印得整整齐齐。

厄尔·威尔逊

Microsoft Windows 笔式计算机组件确实提供了非常好的性能和识别质量。

但是……只有当你懂得如何使用时。可能需要花费大量时间进行枯燥的研究和测试不同的识别方法,面对只在某些特殊情况下发生的意外故障。让我概述一下我遇到的一些问题以及不一致的API或行为案例。

  • 选择SDK是一个问题,因为……
  • 文档质量差或具有误导性,这本身就是一个问题。
  • 并非所有功能都真正实现。
  • 看起来SDK完全面向System.Windows.Forms,这并不完全是这样,但需要一些变通方法。
  • 用户级别的手写支持是隐藏的,几乎无法使用,但让我们仔细看看。

Windows 10 屏幕键盘的可用性

首先,点击主菜单,Ctrl+Esc,然后输入“键盘”。它将带你到“屏幕键盘”。启动它并尝试找到手写识别模式。不,没有这样的东西!好的,尝试检查是否安装了手写识别引擎。为此,启动主“设置”菜单 => “时间和语言” => “区域和语言”。你可以看到一个或多个带有手写图标的语言。然而,它仍然没有显示在你的屏幕键盘上。

然而,这个功能确实存在。如果你使用另一种方式,你就会看到它。右键单击工具栏以查看工具栏的上下文菜单,然后选择“显示触摸键盘按钮”。当按钮添加到工具栏后,点击它。Bingo!它显示了手写识别模式、语音识别模式(可能需要单独的文章)和其他功能。但是……为什么会这样?!怎么可能让最终用户如此困惑?再说一次,这表明微软很少关注将原本高质量的手写识别集成到操作系统的UI中。

另一个使应用程序几乎无法使用的问题是手势识别。除了手写识别,应用程序还使用手势来修正一些已写文本。我们可能需要划掉识别错误或书写不正确的文本,分离或连接某些片段。但是应用程序如何区分手势笔画和构成字符的笔画呢?答案是:很难。事实上,应用程序无法可靠地区分两者,并且会犯很多错误,因此修正错误的文本非常痛苦,通常重新开始会容易得多。这只是一个很大的误解。

还有许多其他主要和次要问题使得使用此应用程序变得困难,但我特别要指责异步识别的理念。笔画与用户输入同时识别,然后被移除并转换为文本。用户无法控制这些事件,这看起来像是不可预测的行为,并导致许多错误。

好的,我们有了一个手写识别键盘,但这值得我们付出努力吗?你可以尝试一下并自行判断,但我个人对此表示怀疑。不,我认为它根本无法使用。

但这是否意味着 Windows 中的手写识别如此糟糕?完全不是!我认为它非常好,只是没有以任何合理的方式呈现给最终用户。

下面,我将尝试展示如何在引擎中真正利用所有这些卓越的识别功能。

必备组件

基本上,需要 .NET 3.5 或更高版本,以及一个或多个支持手写功能的语言包。

演示软件可以在 Windows 7 或更高版本(包括 Windows 10)上“原样”构建。安装将至少包含一个语言包。

但是,最好找到文件“Microsoft.Ink.dll”并更改引用:删除我提供的“Assemblies”目录中的“Microsoft.Ink.dll”,并将其替换为现有安装中找到的文件。该文件位于“c:\Program Files\Common Files\microsoft shared\ink”或“c:\Program Files\Reference Assemblies\Microsoft\Tablet PC\v1.7”。如果找不到此文件,安装最新的 Platform SDK 会有所帮助。目前,Windows 7 Platform SDK 应该足够了。

有可能对于某些文化,默认语言包不支持手写识别。演示应用程序将显示这种情况,并显示相应的状态。在这种情况下,可以安装一个额外的语言包。

它也可以用于旧版本的操作系统,从以下版本开始:

Windows 2000 Service Pack 4 Windows Server 2003 Windows XP Professional Edition Windows XP Tablet PC Edition 2005

在这些情况下,需要安装旧的 Microsoft Tablet PC SDK。我不知道在哪里可以找到 Tablet PC SDK 的安装程序。从 Windows Vista 开始,Tablet PC SDK 随 Platform SDK 一起提供。

无需 Visual Studio;该软件可以使用提供的“build.batch”文件进行构建。在这种情况下,它将使用 .NET 捆绑的编译器。

SDK 帮助可以在这里找到
https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-3.5/ms571346%28v%3dvs.90%29,
https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-3.5/ms581553%28v=vs.90%29

Microsoft 帮助页面查找帮助的路径:旧版本文档(页面底部)=> .NET => .NET Framework 3.5 => .NET Framework 综合参考 => 附加托管参考主题 => Microsoft.Ink 命名空间 => ...

收集墨迹数据

<ResourceDictionary>
    <!-- ... -->
    <Style x:Key="grip" TargetType="ui:LocationGrip">
        <Setter Property="Cursor" Value="SizeAll"/>
        <Setter Property="Width" Value="{StaticResource locationGripSelectionSize}"/>
        <Setter Property="Height" Value="{StaticResource locationGripSelectionSize}"/>
        <Style.Triggers>
    </Style>
</ResourceDictionary>

最简单的识别

假设墨迹笔画数据作为集合System.Windows.Ink.StrokeCollection收集。这是最简单地将其识别为文本字符串的方法。

using StrokeCollection = System.Windows.Ink.StrokeCollection;
using MemoryStream = System.IO.MemoryStream;
using Microsoft.Ink;

//...

static class TextRecognizer {

    internal static string Recognize(StrokeCollection strokes) {
        using (var ink = new Ink()) {
            PopulateInk(ink, strokes);
            return ink.Strokes.ToString(); // here!
        }
    }

    private static void PopulateInk(Ink ink, StrokeCollection strokes) {
        using (var ms = new MemoryStream()) {
            strokes.Save(ms);
            ink.Load(ms.ToArray());
        }
    }

}

请注意,System.Windows.Ink.StrokeCollectionMicrosoft.Ink.Strokes 是不相关的类型,对应的 Stroke 类型也是如此。它们不能相互赋值,但具有相同的流表示。这样,Microsoft.Ink.Ink 对象可以通过内存流填充来自 WPF System.Windows.Ink.StrokeCollection 集合的笔画,就像 PopulateInk 所做的那样。

识别本身是在重写的 Microsoft.Ink.Ink.Strokes.ToString 方法中执行的。这种最简单的识别方法只适用于为系统和当前用户配置的默认输入语言。

在深入研究更高级的手写识别之前,让我们回到墨迹收集并讨论一些更高级的细节。

收集墨迹数据:深入探讨

调整笔画

引言

他们决定让它完全没有错误。他们制作了二十份校对稿,然而在扉页上却印着:“Encyclopidae Britannica”

伊利亚·伊尔夫

控件类System.Windows.Controls.InkCanvas已经具备了操作已输入笔画所需的一切功能。

将墨迹输入分离成单独的词素并非易事。什么是单独的词?这并不是一个单一的笔画,因为用户在书写一个词时可能会用触控笔多次触碰屏幕;即使是一个单一的字母(如“i”、“j”等)也可能需要两次或更多次触碰。分离的判断依据是空白区域的相对大小、笔画大小和其他特征。笔画之间的空格可以被认为是空白区域,也可以被认为是单个单词中字符之间的间隙,这取决于几个因素,包括同一笔画集合中其他空格的大小。为了理解这一点,让我们看一个有趣的例子。我们尝试将“CodeProject”作为一个单词输入。我们首先用宽字母间距输入“Code”,尝试识别它,然后在其右侧添加“Project”。请注意,第一个笔画集合被识别为“Code”,而不是“C o d e”。

Recognized as Code

Recognized as Co! Project

我们可以看到,添加“Project”的笔画会改变“Code”的识别结果。不仅“d”和“e”之间的间隙变成了空白,而且我们预期的“d e”被识别为“! ”。这发生的原因是“eProject”中紧凑的字母间距与“Code”中宽大的间距相比,混淆了识别器。例如,尝试将“Project”向右移动一点,你就会得到“Code Project”。

话虽如此,拥有一个调整某些笔画子集位置的工具非常重要。我们还需要一种方法来删除一些笔画并重新输入它们,而无需删除整个图像。

在我的演示应用程序中,可以看到三个样式为三个图像切换按钮的单选按钮(选定的一个对比度更高,颜色更深):“墨迹模式 (Ctrl 或 Shift 键抬起)”、“橡皮擦模式 (Ctrl 键按下)”和“选择/移动/缩放/擦除模式 (Shift 键按下)”。

Three editing modes

这三串字符显示在控件的提示信息中,也显示在底部的状态行中。这些模式可以通过点击切换,就像普通的单选按钮一样,并且如括号中的提示所示,这些模式可以使用键盘状态键 Ctrl 和 Shift 临时选择。为此,可以在按住 Ctrl 或 Shift 的同时操作笔画;当键释放时,状态会回到默认的“墨迹模式”。

橡皮擦模式下,当笔画被划掉手势划过时,它就会被删除。(还有另一种 System.Windows.Controls.InkCanvas 输入模式,当笔画通过点击一行被删除时,但这非常不方便,因为它需要太高的手势精度。)

选择/移动/缩放/擦除模式下,可以通过套索选择一组笔画,然后进行移动、缩放或删除。无需闭合套索;恰恰相反:不完整的套索曲线实际上有助于隔离具有高度重叠区域的笔画子集。

此功能非常重要,因为它有助于将两组笔画合并识别为一个单词,或将它们分开,使其识别为两个单独的单词。

最后,我添加了使用 Ctrl-Z 删除最后输入的笔画的功能。请不要指望我的演示提供完整的撤销/重做功能:实现起来很简单,但我没有看到有什么充分的理由去费心。重新输入笔画会容易得多。

让我们从 Ctrl+Z 功能开始,它并不像看起来那么简单。这是因为 API 中没有这样的函数。(有一个擦除矩形区域墨迹的方法,但它完全不够用,因为不同笔画的区域经常重叠)。相反,应该使用 Strokes.Replace

var emptyStrokeCollection = new StrokeCollection();
// ...
// in the keyboard handler:
inkCanvas.Strokes.Replace(inkCanvas.Strokes[inkCanvas.Strokes.Count - 1], emptyStrokeCollection);

设置三个System.Windows.Ink.StrokeCollection编辑模式中的任何一个都是微不足道的。

// Ink mode:
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
// Eraser mode: 
inkCanvas.EditingMode = InkCanvasEditingMode.EraseByStroke;
// Select/Move/Scale/Erase mode:
inkCanvas.EditingMode = InkCanvasEditingMode.Select;

此外,我还想警告不要使用 InkCanvasEditingMode.InkAndGesture。是的,这种模式允许识别手势并将其与表示文本输入的墨迹笔画分离。然而,这种分离的置信度并不高,即使只使用 System.Windows.Controls.InkCanvas.SetEnabledGestures 启用了少数手势。事实上,许多枚举为 System.Windows.Ink.ApplicationGesture 的手势很容易与字符混淆。识别器偶尔会未能将笔画识别为文本,将其接受为手势,反之亦然。Windows 10 屏幕键盘的行为就是如此,这使得手写变得相当困难,特别是当需要修正笔画时。

从各方面来看,上面描述的利用不同编辑模式的 InkCanvas 行为比 Windows 10 屏幕键盘应用程序的行为要方便得多。

识别选定的笔画

一旦我们能够选择输入笔画的某个子集,当 System.Windows.Controls.InkCanvas 处于 Select 模式时,我们就可以将这些选定的笔画传递给识别器。在这种情况下,只有选定的笔画会被识别。当什么都没有选中时,我们需要传递所有笔画。让我们将其添加到“识别”按钮的事件处理程序中。

var strokes = inkCanvas.Strokes; // all strokes on the canvas
if (inkCanvas.EditingMode == InkCanvasEditingMode.Select) {
    var selectedStrokes = inkCanvas.GetSelectedStrokes();
    if (selectedStrokes.Count > 0)
        strokes = selectedStrokes;
}
// pass strokes to the recognizer
// as a System.Windows.Ink.StrokeCollection argument

识别不同语言

引言

一群老鼠被一只大猫吓了一跳。鼠爸爸跳起来说:“汪汪!”
猫跑开了。
“那是什么,爸爸?”小老鼠问。
“嗯,孩子,这就是为什么学习第二语言很重要。”

好的,让我们为系统添加第二种语言,看看我们是否能实现每种语言的手写识别。如果系统只有一个手写识别语言包,可以下载并安装一个额外的语言包。特别是,在 Windows 10 中,可以通过主“设置”菜单 => “时间和语言” => “区域和语言” => “添加语言”来完成。它会弹出一个对话框,显示支持的语言,并在按钮处有一个小说明,帮助查看是否支持所需语言的手写识别。

当语言包设置好后,软件可以使用Microsoft.Ink.Recognizers类轮询该集合。该类的构造函数会创建一个对象,作为Microsoft.Ink.Recognizer对象集合的容器。每个Recognizer实例实际上代表一个手写识别语言包。通常,这并非单一语言,而是一组以Microsoft.Ink.Recognizer.Languages表示的子语言,以及一个short整型值数组,每个值代表一个语言ID。此字中的低位字节代表主要语言,高位字节代表子语言。更多详细信息请参阅https://docs.microsoft.com/en-us/windows/win32/intl/language-identifier-constants-and-strings

为了提供所需语言包的选择,我们可以将所有 Recognizer 实例放入组合框中。

using Microsoft.Ink;

//...

public partial class MainWindow {

    private int SetupRecognizerSet() {
        var defaultRecognizer = recognizerSet.GetDefaultRecognizer();
        int defaultRecognizerIndex = 0;
        for (int index = 0; index < recognizerSet.Count; ++index) {
            var recognizer = recognizerSet[index];
            // listLanguages is a combo box defined in the window's XAML:
            if (recognizer.Languages.Length > 0)
                this.listLanguages.Items.Add(recognizer);
            // Recognizer class has identity problem, so only this way:
            if (recognizer.Name == defaultRecognizer.Name) 
                defaultRecognizerIndex = index;
        } //loop
        var count = listLanguages.Items.Count;
        if (count < 1) return count;
        listLanguages.SelectedIndex = defaultRecognizerIndex;
        listLanguages.Focus();
        return count;
    }
        
    private Recognizers recognizerSet = new Microsoft.Ink.Recognizers();
    
}

请注意,我将主窗体类的这一部分放在单独的文件和单独的类部分中使用 partial。除其他外,它有助于我隔离具有相似或相同名称但不同声明的命名空间,即 Microsoft.InkSystem.Windows.Ink,这使得代码更具可读性,无需冗余的完全限定名称。

另请注意代码示例中提到的上述身份问题。这是Microsoft.Ink.Recognizer类的一个小缺陷,可以用以下方式解释。

var recognizerSet = new Microsoft.Ink.Recognizers();
var recognizer1 = recognizerSet[0]; 
var recognizer2 = recognizerSet[0];
// at this point, recognizer1 == recognizer2 returns false 
// which looks illogical

这就是我按名称比较识别器的原因。请务必小心。

现在,当所有 Recognizer 实例都收集完毕后,我们可以在“识别”按钮的处理程序中从组合框的 Items 中提取一个实例。

Main.IRecognitionResultSelector selectorToUse = this.checkBoxAdvanced.IsChecked == true ? selector : null;
string text = Main.TextRecognizer.Recognize(
    strokes,
    (Microsoft.Ink.Recognizer)(this.listLanguages.SelectedItem),
    selectorToUse);

我稍后将解释最后一个函数参数 selectorToUse 。它在选中复选框 checkBoxAdvanced “高级识别”时才有效。目前,我们假设它未选中;那么此参数为 null

现在让我们看看函数 Main.TextRecognizer.Recognize 是如何工作的。对于多种语言,仅使用 Microsoft.Ink.Ink 是不够的,但可以在未确定 Microsoft.Ink.Recognizer 实例(在我们的例子中为 null)时将其用作默认值。为了进行识别,我们需要另一个对象,即 Microsoft.Ink.CreateRecognizerContext 的实例,它需要在 using 构造的作用域退出时自动释放。

using StrokeCollection = System.Windows.Ink.StrokeCollection;
using MemoryStream = System.IO.MemoryStream;
using ManualResetEvent = System.Threading.ManualResetEvent;
using Microsoft.Ink;

static class TextRecognizer {

    internal static string Recognize(
        StrokeCollection strokes,
        Recognizer recognizer,
        Main.IRecognitionResultSelector selector // will it explain later
        ) {
        using (var ink = new Ink()) {
            PopulateInk(ink, strokes);
            if (recognizer == null)
                // default recognizer and language:
                return ink.Strokes.ToString(); 
            using (var context = recognizer.CreateRecognizerContext()) {
                if (ink.Strokes.Count < 1) return null;
                context.Strokes = ink.Strokes;
                if (selector == null) {
                    RecognitionStatus status;
                    var result = context.Recognize(out status);
                    if (status == RecognitionStatus.NoError)
                        return result.TopString;
                    else
                        return null;
                } else // will explain it later:
                    return RecognizeWithAlternates(context, selector);
            } //using context
        } //using ink
    } //Recognize

    //...

}

我认为识别本身是不言自明的。

现在我必须解释那些带有“稍后解释”注释的部分。它与更高级的识别方法(我在演示应用程序中通过“高级识别”复选框可选启用)有关。这是带有候选项的识别。为了展示它的工作原理,只需展示参数selector的类型及其在RecognizeWithAlternates方法中的用法就足够了。

识别候选项

如果你过度专注于一扇关闭的门,你可能会错过替代的便捷入口!——穆罕默德·穆拉特·伊尔丹

人应该总是在寻找可能的替代方案,并加以防范。——夏洛克·福尔摩斯,阿瑟·柯南·道尔爵士

当你有两个选择时,你首先要做的是寻找你没有想到的第三个选择,那个不存在的选择。——西蒙·佩雷斯

你更愿意和谁在一起:选择多的人还是选择少的人?——安迪·邓恩

候选项选择界面

首先,我们定义一个接口类型,用于表示UI中的候选项以及选择所需候选项组合的对象。

using Microsoft.Ink;
    
interface IRecognitionResultSelector {
    string Select(Recognizer recognizer, RecognitionResult result);
}

此接口的实现应接收 Microsoft.Ink.RecognitionResult 实例,向用户呈现所有候选项,并让用户选择。根据用户的选择,实现返回结果字符串,它是识别单词的众多可能组合之一。

Selector of alternates

请注意,只有部分单词显示出来,没有选择。这是我发现实用的一种实现行为选项:如果某个单词的识别置信度Microsoft.Ink.RecognitionConfidence.Strong(这是最高置信度),则它显示为文本块,而不是选择组合框。此外,如果传递给识别器的整个墨迹输入都以最高置信度识别,则根本不显示此窗口;IRecognitionResultSelector.Selector实现只返回识别到的最高置信度字符串。

这种行为的实现是另一个问题。我们来解决它。

测试支持的属性

问题是这样的:当我们尝试检查 Microsoft.Ink.RecognitionResult 实例的识别置信度时,它可能适用于某个语言包 (Recognizer),但对于另一个语言包则会抛出异常“指定的属性标识符无效”。

using Microsoft.Ink;

string Main.IRecognitionResultSelector.Select(Recognizer recognizer, RecognitionResult result) {
    //...
    // can throw "The specified property identifier was invalid" exception:
    var confidence = result.TopConfidence;
    //...
    var alts = result.GetAlternatesFromSelection(positions[index], words[index].Length);
    // can throw "The specified property identifier was invalid" exception:
    var topAlternateConfidence = alts[0].Confidence;    
    //...
}

也就是说,在我们读取那些 TopConfidenceConfidence 属性之前,我们必须检查它们的实现在给定的 Microsoft.Ink.Recognizer 实例中是否真的受支持。这就是接口方法 IRecognitionResultSelector.Select 使用 recognizer 参数的原因。下面是查找方法:

private static bool IsConfidenceLevelPropertySupported(Recognizer recognizer) {
    bool result = false;
    foreach (var guid in recognizer.SupportedProperties)
        if (guid == RecognitionProperty.ConfidenceLevel) {
            result = true;
            break;
        } 
    return result;
}

然后我们可以小心地使用这些属性来确定置信度级别。如果支持对应于 RecognitionProperty.ConfidenceLevel 的属性实现,我们可以调用 TopConfidenceConfidence。如果不支持,我们 simply 假设置信度总是很差,也就是说,选择器 UI 窗口总是显示,并且每个单词的候选项都会呈现给用户。

后台识别和同步

现在是时候展示RecognizeWithAlternates的工作原理了。有一个小问题需要解决。在Microsoft.Ink中,只有一种带有候选项的识别方法,而且这种方法是异步的:Microsoft.Ink.RecognizerContext.BackgroundRecognizeWithAlternates。这是一个通过事件Microsoft.Ink.RecognizerContext.RecognitionWithAlternates工作的无返回值方法。但是我的调用代码是为同步行为设计的。首先,System.Ink.RecognizerContext的实例在每次识别事件中同步处置。如果BackgroundRecognizeWithAlternatesusing语句块中调用,RecognitionWithAlternates事件的事件处理程序将永远不会被调用。

因此,在我的演示应用程序中,我坚持同步识别,我是认真的。我讨厌 Windows 10 屏幕键盘中实现的那种想法,即在我用手写笔书写时进行识别,在一些不可预测的时刻将我的笔画转换为文本对象,无论我认为我的输入是否完成。我确实希望识别在我发出命令时执行。实现异步行为不是问题,但我想使其同步。这个问题通过使用闭包功能和 System.Threading.ManualResetEvent 得到了简单的解决。

using ManualResetEvent = System.Threading.ManualResetEvent;
using Microsoft.Ink;

//...

private static string RecognizeWithAlternates(
        RecognizerContext context,
        Main.IRecognitionResultSelector selector)
{
    RecognitionResult result = null;
    ManualResetEvent completionEvent = new ManualResetEvent(false);
    context.RecognitionWithAlternates += (
        object sender,
        RecognizerContextRecognitionWithAlternatesEventArgs e) => {
            result = e.Result;
            completionEvent.Set();
    }; //context.RecognitionWithAlternates
    context.BackgroundRecognizeWithAlternates();
    completionEvent.WaitOne();
    return selector.Select(context.Recognizer, result);
}

然而,没有必要使用这种简单的同步。即使我们希望识别由用户命令触发,我们也可以保持其异步,因为笔画会同步地馈送到 RecognizerContext。笔画的异步处理将识别所有可用笔画并进入等待状态

我实施基于同步的机制是出于以下原因:1) 它完全足够,2) 它更适合我的教学目的,这是因为我想在一个方法中演示三种不同的识别方法static string Recognize,3) 这可能是最简单的方法。

让我们看看异步识别可能是什么样子,以及它可能有多复杂。实际上,这并不是一个大问题。首先,每次用户选择语言包后,在RecognizerContext实例实例化后,我们需要立即启动BackgroundRecognizeWithAlternates。如果之前实例化过RecognizerContext对象,则需要将其释放。在应用程序终止之前,该类的实例化实例应该停止识别,然后被释放。

使用带有附加参数 customData 的方法是一个好主意:System.Ink.RecognizerContext.BackgroundRecognizeWithAlternates(object customData)。我会使用这个参数来传递一些 UI 对象,最好是某个窗口类实现的某个接口的实现。这个对象将作为 System.Ink.RecognizerContext.RecognizerContextRecognitionWithAlternatesEventHandler 事件被调用时事件参数对象的一部分传递。这个接口的实现应该获取 RecognitionResult 实例并使用它来填充 UI。一个丑陋的方面是需要将 object customData 类型转换为所需的运行时类型实例。

很明显,事件处理程序将在单独的线程中调用。也就是说,UI 应该只通过 System.Windows.Threading.Dispatcher.Invoke 进行操作。

我看不出这种小小的复杂性有什么意义,所以我会把它留给感兴趣的读者的家庭作业。

候选项的来源?

到目前为止,我还没有解释候选项是如何工作的,这不是一个非常明显的事情。这将是与带候选项的识别相关的最后一个缺失部分。让我们看看。

假设我们已经获得了 Microsoft.Ink.RecognitionResult 的实例。这是获取一组候选项的最简单方法。

string Main.IRecognitionResultSelector.Select(Recognizer recognizer, RecognitionResult result) {
    int maximumNumberOfAlternates = 0x100; // why not?
    string text = result.TopString;
    var alternates = result.GetAlternatesFromSelection(0, text.Length, maximumNumberOfAlternates);
    foreach (var alt in alts)
        // someComboBox defined in XAML
        someComboBox.Items.Add(alt.ToString());
    // ShowDialog, for example
    return someComboBox.SelectedItem.ToString();
}

它会奏效,并给我们提供我们想要的尽可能多的变体(限制数量可以指定为 maximumNumberOfAlternates)。但这完全不切实际。此外,它同时是冗余和不足的。例如,如果识别将识别的文本划分为 9 个词素,并且每个词素有 10 个候选项(此 API 的默认值),它将产生 10⁹ = 1,000,000,000 个变体,而描述识别结果所需的只有每个词素 10 个候选项,总共 90 个。这些变体中的大多数都没有意义,而且预期的变体仍然可能缺失。对 result.GetAlternatesFromSelection()(无参数)的调用将只返回 10 个组合候选项,因此在我们的示例中,每个词素不能显示超过两个候选项。

这种相当奇怪的 API 的真正意图是不同的。它可以为每个词素表示单独的候选项,如上图所示。问题是,所有候选项的识别文本划分是相同的。假设我们必须通过空格字符将result.TopString解析成词素,并且对于每个词素,将其在此字符串中的位置单独传递给result.GetAlternatesFromSelection

public partial class RecognitionResultSelectorWindow : 
    Window, Main.IRecognitionResultSelector {

    string Main.IRecognitionResultSelector.Select(
        Recognizer recognizer,
        RecognitionResult result) {

        string topResult = result.TopString;
        var confidenceLevelPropertySupported =
            IsConfidenceLevelPropertySupported(recognizer);
        //...
        var words = topResult.Split(Main.DefinitionSet.WordSeparator);
        int currentPosition = 0;
        int[] positions = new int[words.Length];
        //...
        for (var index = 0; index < words.Length; ++index) {
            positions[index] = currentPosition;
            currentPosition += words[index].Length + 1;
            var alts = result.GetAlternatesFromSelection(
                positions[index],
                words[index].Length);
            // create a UI element to represent a lexeme
            // populate UI the element with alternates:
            foreach (var alt in alts) { /* ... */ }
            if (ShowDialog() != true) return null;
            return // a string composed from the user choice of alternates
        }
    }

}

如需完整实现,请参见“Ui/RecognitionResultSelectorWindow.xaml.cs”。

整合所有内容

如需完整实现识别功能,请参阅原始源代码:“Main/IRecognitionResultSelector.cs”、“Main/TextRecognizer.cs”、“Ui/RecognitionResultSelectorWindow.xaml”和“Ui/RecognitionResultSelectorWindow.xaml.cs”。

让我们总结一下。

UI 的“识别”按钮处理程序调用方法 Main.TextRecognizer.Recognize。它创建一个 Microsoft.Ink.Ink 实例,并用作为第一个参数 System.Windows.Ink.StrokeCollection 传递的笔画填充它。第二个参数是 Microsoft.Ink.Recognizer 类型。如果它为 null,则执行最简单的识别,即 ink.ToString(),用于默认语言包。如果不是(在我的演示应用程序中总是如此),则使用方法 Microsoft.Ink.RecognizerContext.Recognize 对选定的语言包执行识别,其中 Microsoft.Ink.RecognizerContext 的实例是从用户选择的 Recognizer 实例中获取的。最后,如果第三个参数 Main.IRecognitionResultSelector selector 不为 null,则执行带候选项的识别。

方法 Main.TextRecognizer.RecognizeWithAlternates 接受 Microsoft.Ink.RecognizerContextMain.IRecognitionResultSelector selector 的实例,以调用方法 Microsoft.Ink.RecognizerContext.BackgroundRecognizeWithAlternates 来获取通过 Microsoft.Ink.RecognizerContextRecognitionWithAlternatesEventArg 类型的事件参数传递给事件 Microsoft.Ink.RecognizerContext.RecognitionWithAlternates 的处理程序的识别结果。

获取到的 Microsoft.Ink.RecognitionResult 实例被传递给 Main.IRecognitionResultSelector 接口的实现,该实现解析识别结果的 TopString,以获取该字符串中每个词素的候选项集。在用户确认候选项选择后,结果字符串将返回给 UI。

Microsoft.Ink.Analysis 如何?

“Microsoft.Ink.Analysis.dll”是 Microsoft Tablet PC SDK 中发现的另一个主要程序集模块。与“Microsoft.Ink”不同,我在我的 Windows 10 安装中找不到它。当 Microsoft Tablet PC SDK 作为平台 SDK 的一部分额外安装时,它会与“Microsoft.Ink.dll”一起提供。在我的情况下,它出现在“c:\Program Files\Reference Assemblies\Microsoft\Tablet PC\v1.7”中。

根据 Tablet PC SDK 文档,Microsoft.Ink.Analysis 提供了墨迹数据识别的另一种方式,包括手写识别和一些高级功能。特别是,可以使用 Microsoft.Ink.InkInkAnalyzer.Analyze 识别手写,而类 Microsoft.Ink.InkRecognizerMicrosoft.Ink.InkInkAnalyzer 中扮演的角色与类 Microsoft.Ink.RecognizerMicrosoft.Ink 中扮演的角色大致相同。

然而,尝试使用此 API 使此程序集的使用有些可疑。首先,尝试在正确设置的分析器实例上调用 Microsoft.Ink.InkInkAnalyzer.Analyze 会抛出 FileNotFoundException 异常:{"无法加载文件或程序集 'IALoader, Version=1.7.6223.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' 或其依赖项之一。系统找不到指定的文件。":"IALoader, Version=1.7.6223.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"}。显然,未找到该文件。问题中的文件“IALoader.dll”可以在“Program Files”下找到并添加。

很好,现在调用会为“AnyCPU”架构抛出 BadImageFormatException,这几乎可以肯定地表明该模块是专门为 x86 平台构建的。是的,将整个应用程序重新定位到 x86 可以使应用程序正常工作。我对微软的问题是:如果这个模块是为 x86 构建的,为什么它会被部署到 x86-64 Windows 10 安装的“Program Files”中,而不是“c:\Program Files (x86)”?我称之为一团糟。

一些非官方文档也表明“IALoader.dll”仅存在于x86平台。如今要找到原始的Tablet PC Platform SDK相当困难。当添加此文件后,手写识别和几何形状识别才能工作。

无论如何,在纯文本手写识别方面,与“Microsoft.Ink.dll”相比,这个程序集没有增加任何新功能。识别质量?并没有更好,甚至可能更差。

我没有展示我的实验成果,因为我认为它没有什么显著价值,但如果有人在评论中就此主题向我提问,我将回答。我只是得出结论,这个程序集的开发从未真正完成。

还有什么?

引言

没有人会拥抱不可拥抱的事物。

科兹马·普鲁特科夫

WPF 在墨迹支持方面拥有更多功能,即使没有 Microsoft.Ink。首先,它包括手势识别,可用于调用各种输入事件,通常解释为命令。此功能与 Microsoft.Ink 部分重叠,后者也拥有自己的手势识别功能。

除了手势和手写识别,Microsoft.Ink 还涵盖了一些识别主题。值得注意的是,它包含基本几何形状的识别以及将笔画集分类为图形和文本区域的功能,这可以通过Microsoft.Ink.Divider类实现。该技术为自定义对象(可能包括乐谱、几何形状、数学表达式、图表等)的识别提供了一个基本框架:https://docs.microsoft.com/en-us/windows/win32/tablet/object-recognizers

Microsoft.Ink 识别主题概述可以在这里找到:https://docs.microsoft.com/en-us/windows/win32/tablet/about-handwriting-recognition

WPF 和 Microsoft.Ink 的组合功能足以开发功能齐全的像素或矢量编辑器,并利用平板数字转换器。这之所以可能,是因为墨迹输入可以完全从 UI 中抽象出来,图形渲染也可以完全定制。墨迹输入可以使用 Microsoft.Ink.InkCollector 类附加到任何窗口。

我甚至没有尝试通用 Windows 平台 (UWP)。与 WPF 不同,它是自包含的,不需要 Microsoft.Ink 程序集。它有自己对基本形状和手写识别的支持:https://docs.microsoft.com/en-us/windows/uwp/design/input/ink-walkthrough。我认为它不值得关注,因为 UWP 无论如何都不是 .NET 或 Windows 开发的未来。

那么最新的 .NET Core 呢?我还不清楚。

构建和兼容性

由于代码基于 WPF,我使用了第一个与 WPF 兼容的平台版本——Microsoft.NET v.3.5。因此,我为 Visual Studio 2008 提供了解决方案和项目。我这样做是有意为之,以覆盖所有可能使用 WPF 的读者。更高版本的 .NET 将得到支持;更高版本的 Visual Studio 可以自动升级解决方案和项目文件。

事实上,构建并不需要 Visual Studio。代码可以通过使用提供的批处理文件“build.bat”进行批处理构建。如果您的 Windows 安装目录与默认目录不同,构建仍然可以工作。如果 .NET 安装目录与默认目录不同,请参阅此批处理文件的内容及其第一行的注释——可以修改下一行以进行构建。

结论

引言

如果你有一个喷泉,把它关掉。让喷泉也休息一下。

科兹马·普鲁特科夫

通过本文,我为使用 Windows 和 Microsoft 笔式计算组件实现全功能手写识别铺平了道路,但并未涵盖所有相关主题。技术描述可用于开发基于 WPF 和 Microsoft.Ink 程序集的各种应用程序功能,但仅限于将手写识别为纯文本的功能。

© . All rights reserved.