Microsoft OCR をデスクトップのWFPアプリで動かす方法

list このエントリーをはてなブックマークに追加

巷ではGoogleのリアルタイム翻訳アプリが流行っているのだが、あいにく手元にある Android が古いので、それほど面白い画像が取れない。ので、Google Cloud Vision で OCR 機能を試してみたのだが、実は Microsoft OCR もあるよ、ってことで。

こんなものがある。

Windows.Media.Ocr namespace – Windows app development
https://msdn.microsoft.com/library/windows/apps/windows.media.ocr.aspx?f=255&MSPPError=-2147217396&cs-save-lang=1&cs-lang=csharp#code-snippet-4

試してみると、Microsoft OCR も Google のそれに対してさほど悪い結果でないことが分かった。発表の頃はもうちょっとダメな感じがあったのだが、単純に文字列を読み取る OCR という点では Google のとどっこいどっこいという感じである。Google Cloud Vision の場合は無料枠だと 1,000回/月 という制限があるので、無料の範囲であって Windows 上であれば Microsoft OCR でも十分かもしれない。まあ、とはいえ Google のほうも 100万回/月にしたって $2.5 で使えるので実験レベルであれば十分だろう。

Microsoft OCR のサンプルがストアアプリになっている

Windows-universal-samples/Samples/OCR at master ・ Microsoft/Windows-universal-samples
https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/OCR

サンプルコードをダウンロードして実行してみると、UWPアプリ(ストアアプリ)で動く。

確かに、UWP アプリにするとストアにアップできたり、Windows IoT Core で動いたりと便利ではあるのだが、画像とファイルの扱いが面倒になっているので、できることならば従来通りのデスクトップアプリで試しておきたい…と思いつつ調べたのだが、サンプルが見当たらない。

実は OCR を提供している Windows.Media.Ocr が「Windows Runtime API」に入っていて UWP アプリで使うことを想定している状態なのであった。じゃあ仕方がない、以前の WinRT をライブラリを WPF アプリから使えるようにしてみよう、と思って作ったサンプルがこれです。

https://github.com/moonmile/google-cloud-vision-sample/tree/master/src/OCR/MsOcrWPF

WinRT のライブラリを使う準備

Windows 8.1 の頃にやった技は Windows 10 でも有効なので、*.csproj に TargetPlatformVersion タグを追加する。このバージョンは、適当に UWP アプリから取ってくる。

<TargetPlatformVersion>10.0.14393.0</TargetPlatformVersion>

すると参照設定で「Windows」→「コア」というのが使えるようになって、WinRT のアセンブリを指定できるようになる。

OCR は「Windows.Media」のほうに入っている。

OCR を使う場合は、結果的に

– Windows.Foundation
– Windows.Graphics
– Windows.Media
– Windows.Storage

の4つのWinRTアセンブリを追加することになる。

実は、サジェストを使ってアセンブリを自動的にロードすることもできるのだが、Windows.Foundation.UniversalApiContract が他のものと競合してしまってうまくいかないので、自前でやる。もし Windows.Foundation.UniversalApiContract が参照設定に入ってしまったときは、参照から外してしまう。

ビルドをするときに、WinRT のランタイムが足りないと怒られるので、System.Runtime.WindowsRuntime.dll も追加しておく。

C:/Program Files (x86)/Reference Assemblies/Microsoft/Framework/.NETCore/v4.5.1/System.Runtime.WindowsRuntime.dll

OCR を使うコード

実は Microsoft OCR を使うときのコードは非常に単純で、次のように SoftwareBitmap を渡して、OCR結果である OcrResult オブジェクトを受け取ることができる。

public async Task<OcrResult> detect( SoftwareBitmap bitmap)
{
    var ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages();
    var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
    return ocrResult;
}

画像を渡して結果を受け取るだけなので、使い方は単純なのだが、

  • SoftwareBitmap クラスを扱わないといけない
  • WinRT の Async はデスクトップの async/await とは違った形になっている

の2点が難関になっている。

GetAwaiter メソッドを自作で拡張する

http://blog.xin9le.net/entry/2012/11/12/123231

を参考にして…というかそのままコードを持ってきて、以下のように GetAwaiter メソッドを拡張する。

public static class TaskEx
{
    public static Task<T> AsTask<T>(this IAsyncOperation<T> operation)
    {
        var tcs = new TaskCompletionSource<T>();
        operation.Completed = delegate  //--- コールバックを設定
        {
            switch (operation.Status)   //--- 状態に合わせて完了通知
            {
                case AsyncStatus.Completed: tcs.SetResult(operation.GetResults()); break;
                case AsyncStatus.Error: tcs.SetException(operation.ErrorCode); break;
                case AsyncStatus.Canceled: tcs.SetCanceled(); break;
            }
        };
        return tcs.Task;  //--- 完了が通知されるTaskを返す
    }
    public static TaskAwaiter<T> GetAwaiter<T>(this IAsyncOperation<T> operation)
    {
        return operation.AsTask().GetAwaiter();
    }
}

実はアセンブリ System.Runtime.WindowsRuntime.dll を追加すれば拡張クラスがあるはずなのだが、うまくいかないので自前で作る。クラスを作らない場合は、こんなビルドエラーがでてくるはず。

エラー	CS0012	型 'IAsyncAction' は、参照されていないアセンブリに定義されています。アセンブリ 'Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime' に参照を追加する必要があります。

SoftwareBitmap クラスを扱えるようにする

SoftwareBitmap ってのも、WinRT の世界のものなので、デスクトップアプリからは直接扱えない。これでは面倒なので、デスクトップアプリで使えるようにファイルパスを渡して SoftwareBitmap オブジェクトを生成する関数を作っておく。

/// <summary>
/// ファイルパスを指定して SoftwareBitmap を取得
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
private async Task<SoftwareBitmap> LoadImage(string path)
{
    var fs = System.IO.File.OpenRead(path);
    var buf = new byte[fs.Length];
    fs.Read(buf, 0, (int)fs.Length);
    var mem = new MemoryStream(buf);
    mem.Position = 0;

    var stream = await ConvertToRandomAccessStream(mem);
    var bitmap = await LoadImage(stream);
    return bitmap;
}
/// <summary>
/// IRandomAccessStream から SoftwareBitmap を取得
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
private async Task<SoftwareBitmap> LoadImage(IRandomAccessStream stream)
{
    var decoder = await Windows.Graphics.Imaging.BitmapDecoder.CreateAsync(stream);
    var bitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    return bitmap;
}
/// <summary>
/// MemoryStream から IRandomAccessStream へ変換
/// </summary>
/// <param name="memoryStream"></param>
/// <returns></returns>
public async Task<IRandomAccessStream> ConvertToRandomAccessStream(MemoryStream memoryStream)
{
    var randomAccessStream = new InMemoryRandomAccessStream();
    var outputStream = randomAccessStream.GetOutputStreamAt(0);
    var dw = new DataWriter(outputStream);
    var task = new Task(() => dw.WriteBytes(memoryStream.ToArray()));
    task.Start();
    await task;
    await dw.StoreAsync();
    await outputStream.FlushAsync();
    return randomAccessStream;
}
  1. System.IO.File でファイルオープンして MemoryStream へ読み込み
  2. MemoryStream から IRandomAccessStream に変換
  3. IRandomAccessStream から SoftwareBitmap に変換
  4. SoftwareBitmap オブジェクトを OCRエンジンに渡す

というややこしい手順になっている。

画面にマークを付ける

UWP アプリのサンプルでは、マークを XAML の Rectangle を使っているが、デスクトップアプリの場合は Bitmap に直接書き込んだほうが手軽だろう。そのために WPF アプリでやりたいわけだし。

private async void clickDetect(object sender, RoutedEventArgs e)
{
    var bitmap = await LoadImage(screenFile);
    var result = await detect(bitmap);
    text1.Text = result.Text;

    // 認識した個所をマークする
    var bmp = Bitmap.FromFile(screenFile) as Bitmap;
    var g = Graphics.FromImage(bmp);
    var br = new SolidBrush(System.Drawing.Color.FromArgb(0x80, System.Drawing.Color.Blue));
    var text = "";
    foreach (var line in result.Lines)
    {
        text += line.Text + " ";
        foreach (var it in line.Words )
        {
            var rc = new System.Drawing.Rectangle(
                (int)it.BoundingRect.X, (int)it.BoundingRect.Y,
                (int)it.BoundingRect.Width, (int)it.BoundingRect.Height); 
            g.FillRectangle(br, rc);
            g.DrawRectangle(Pens.Red, rc);
            text += it.Text + " ";
        }
    }
    image.Source = bmp.ToImageSource();
}

public static class BitmapEx
{
    /// <summary>
    /// Bitmap から BitmapSource へ変換
    /// </summary>
    /// <param name="bmp"></param>
    /// <returns></returns>
    public static System.Windows.Media.ImageSource ToImageSource(this Bitmap bmp)
    {
        BitmapSource bitmapSource = Imaging.CreateBitmapSourceFromHBitmap
            (
                bmp.GetHbitmap(),
                IntPtr.Zero,
                Int32Rect.Empty,
                BitmapSizeOptions.FromEmptyOptions()
            );
        return bitmapSource;
    }
}

そんな訳で

C# – C#で画像ファイルからOCRしたい(14229)|teratail
https://teratail.com/questions/14229

な回答が分かった訳だが(この質問は、OCR のサンプルを作った後に見つけた)、これ、ちょっと大変過ぎるよ。どうしたものか。

サンプルコード

https://github.com/moonmile/google-cloud-vision-sample

参考先

非同期メソッド入門 (10) – WinRTとの相互運用 – xin9le.net
http://blog.xin9le.net/entry/2012/11/12/123231
WinRTのAPIをデスクトップアプリから使う 8.1版 – かずきのBlog@hatena
http://blog.okazuki.jp/entry/2014/06/26/201327
c# – Is there a way to convert a System.IO.Stream to a Windows.Storage.Streams.IRandomAccessStream? – Stack Overflow
http://stackoverflow.com/questions/7669311/is-there-a-way-to-convert-a-system-io-stream-to-a-windows-storage-streams-irando
C# – C#で画像ファイルからOCRしたい(14229)|teratail
https://teratail.com/questions/14229

カテゴリー: 開発, C#, WinRT パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*