C#でWindows標準のOCRを使って領収書の文字を読む|Windows.Media.Ocr

C#・.NET

領収書の金額や日付を、画像から読み取りたい

経費精算では、領収書を見ながら金額・日付・店名を入力するのが手間です。

スマートフォンで撮った領収書の画像から、金額と日付を読み取ってほしい

画像から文字を読み取るOCRは、クラウドのサービスを使う方法がよく紹介されています。ただ、Windows 10・11には、追加のインストールも費用もかからない文字認識の機能(Windows.Media.Ocr)が入っていて、C#から使えます。

本記事では、C#からWindows標準のOCRを使って領収書の文字を読み取り、金額・日付・登録番号を拾う方法を紹介します。自作の立替精算システムの、領収書の読み取りで使っている部品です。

立替精算システムで使ったところ

私が自作した立替精算システムでは、明細に領収書の画像を添付すると、Windows OCRで読み取って、金額・日付・登録番号の候補を出しています。

立替精算システムの領収書の確認の画面。左に領収書の画像、右にWindows OCRで読み取った金額・日付の候補が並んでいる

領収書を添付したときの確認画面です。右側に、Windows OCRで読み取った金額や日付の候補が、選んだ理由と一緒に並びます。押した候補だけが入力欄に入り、自動では入力しません。

クラウドのOCRは読み取りの精度が高い一方で、社内のネットワークやVPNの中だけで使うシステムからは呼べないことがあり、領収書という取引の情報を社外に出すことにもなります。Windows標準のOCRなら、サーバのPCの中だけで読み取れます。

使う準備

Windows.Media.OcrはWindowsの機能なので、プロジェクトの対象をWindows 10(バージョン2004)以降にします。Visual Studioのコンソールアプリなら、.csprojの TargetFramework を次のようにしてください。

<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>

.NET 8 の場合は net8.0-windows10.0.19041.0 です。サンプルコードの1行目の #:property は、.NET 10 で dotnet run ファイル名.cs として実行するときに、同じ指定をするための書き方です。Visual Studioで実行する場合は、この行を消して.csprojで指定してください。

日本語の読み取りには、Windowsの日本語の言語機能が使われます。日本語版のWindowsなら、ふつうは最初から入っています。

C#でWindows標準のOCRを使って領収書を読むサンプルコード

動作確認は Windows 11・.NET 10 です。読み取る画像のパスを引数で渡します(無ければ receipt.png を読みます)。

#:property TargetFramework=net10.0-windows10.0.19041.0
using System.Text.RegularExpressions;
using Windows.Globalization;
using Windows.Graphics.Imaging;
using Windows.Media.Ocr;
using Windows.Storage;

// 読み取る画像(引数が無ければ receipt.png)
var path = Path.GetFullPath(args.Length > 0 ? args[0] : "receipt.png");

// 日本語の文字認識エンジンを作る。日本語の言語機能が入っていないと null になる
var engine = OcrEngine.TryCreateFromLanguage(new Language("ja"))
             ?? OcrEngine.TryCreateFromUserProfileLanguages();
if (engine is null)
{
    Console.WriteLine("文字認識に使える言語がありません。Windows の設定で日本語の言語機能を追加してください。");
    return;
}

Console.WriteLine($"認識エンジンの言語:{engine.RecognizerLanguage.DisplayName}");

// 画像を読み込んで、認識エンジンに渡せる形(SoftwareBitmap)にする
var file = await StorageFile.GetFileFromPathAsync(path);
using var stream = await file.OpenAsync(FileAccessMode.Read);
var decoder = await BitmapDecoder.CreateAsync(stream);
using var bitmap = await decoder.GetSoftwareBitmapAsync(
    BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);

// 読み取る
var result = await engine.RecognizeAsync(bitmap);

// result.Text は行を空白でつなげてしまうので、行ごとに改行でつなぐ
var zenbun = string.Join('\n', result.Lines.Select(x => x.Text));
Console.WriteLine("---- 読み取った文字 ----");
Console.WriteLine(zenbun);
Console.WriteLine("------------------------");

// 日本語の文字の間に入った空白を詰める(「領 収 書」→「領収書」)
var tsumeta = Regex.Replace(zenbun,
    @"(?<=[\p{IsHiragana}\p{IsKatakana}\p{IsCJKUnifiedIdeographs}]) +(?=[\p{IsHiragana}\p{IsKatakana}\p{IsCJKUnifiedIdeographs}])", "");

// 金額:「¥」の後ろの数字(いちばん大きいものを合計とみなす)
var kingaku = Regex.Matches(tsumeta, @"[¥\\¥]\s*([0-9]{1,3}(?:\s*,\s*[0-9]{3})+|[0-9]+)")
    .Select(m => long.Parse(Regex.Replace(m.Groups[1].Value, @"[\s,]", "")))
    .DefaultIfEmpty()
    .Max();

// 日付:「2026年10月6日」「2026/10/06」などの西暦
var hiduke = Regex.Match(tsumeta, @"(20[0-9]{2})\s*[/年\.\-]\s*([0-9]{1,2})\s*[/月\.\-]\s*([0-9]{1,2})");

// 登録番号:T + 13 桁
var touroku = Regex.Match(tsumeta, @"T\s*((?:[0-9]\s*){13})");

Console.WriteLine($"金額の候補    :{(kingaku > 0 ? $"{kingaku:N0} 円" : "読めませんでした")}");
Console.WriteLine($"日付の候補    :{(hiduke.Success ? $"{hiduke.Groups[1].Value}/{hiduke.Groups[2].Value}/{hiduke.Groups[3].Value}" : "読めませんでした")}");
Console.WriteLine($"登録番号の候補:{(touroku.Success ? "T" + Regex.Replace(touroku.Groups[1].Value, @"\s", "") : "読めませんでした")}");

読み取らせた領収書の画像(説明のために作った見本)です。

読み取りに使った見本の領収書。サンプルホテル新大阪、金額12,800円、2026年10月6日、登録番号T1234567890123

実行結果

認識エンジンの言語:日本語
---- 読み取った文字 ----
領 収 書
株 式 会 社 サ ン プ ル 商 事 様
\ 12 , 800-
但 し 宿 泊 代 と し て
2026 年 10 月 6 日 上 記 正 に 領 収 い た し ま し た
税 抜 金 額 ( 10 % 対 象 )
消 費 税 等 (10%)
サ ン プ ル ホ テ ル 新 大 阪
大 阪 府 大 阪 市 サ ン プ ル 区 1-2-3
TEL 06-0000-0000
登 録 番 号 T1234567890123
¥ 11 , 637
¥ 1 , 163
------------------------
金額の候補    :12,800 円
日付の候補    :2026/10/6
登録番号の候補:T1234567890123

読み取った文字は、日本語の1文字ずつの間に空白が入り、「¥」が「\」になるなど、そのままでは使いにくい形で返ってきます。空白を詰めてから正規表現で探すと、金額・日付・登録番号を拾えました。

コードの説明

  • OcrEngine.TryCreateFromLanguage(new Language("ja")) で、日本語の文字認識エンジンを作ります。日本語の言語機能が入っていないと null になるので、そのときはWindowsの言語設定に任せる TryCreateFromUserProfileLanguages() を試しています。
  • StorageFile と BitmapDecoder で画像を読み込み、GetSoftwareBitmapAsync で認識エンジンに渡せる形(SoftwareBitmap)にしています。
  • RecognizeAsync の結果には、行ごとの Lines と、全体をつなげた Text があります。Text は行と行を空白でつなげてしまうので、行の区切りを使いたいときは Lines を改行でつなげます。
  • 日本語は1文字ずつ区切られて返ってくることがあるため、ひらがな・カタカナ・漢字の間の空白を正規表現で詰めています。
  • 金額は「¥」の後ろの数字を探し、いちばん大きいものを合計とみなしています。「¥」は「\」や「¥」に読まれることもあるので、3つとも探しています。

立替精算システムでの工夫

実際の領収書の写真は、見本の画像よりずっと読み取りにくいものです。立替精算システムでは、次のような工夫をしています。

  • スマートフォンの写真は大きすぎて、認識エンジンの上限(OcrEngine.MaxImageDimension)を超えると1文字も読めません。縦横比を保ったまま、上限の大きさまで縮小してから渡しています。
  • 横倒しや上下逆に撮られた写真は、金額と日付が読めなかったときだけ90度ずつ回して読み直し、いちばん読めた向きを採っています。
  • 縮小や回転は、読み取りのための一時的な画像だけに行い、保存する領収書の画像(原本)は変えません。
  • 「お預り」「お釣り」「ポイント」の行の金額は候補にせず、「合計」の語がある行の金額を優先しています。
  • 候補は1つに絞らず、選んだ理由を添えて最大3件出し、使う人に選んでもらいます。
// 認識エンジンが扱える大きさまで縮小し、指定の向きに回す
var jougen = OcrEngine.MaxImageDimension;
var chouhen = Math.Max(decoder.PixelWidth, decoder.PixelHeight);
var bairitsu = chouhen <= jougen ? 1.0 : (double)jougen / chouhen;

var transform = new BitmapTransform
{
    ScaledWidth = Math.Max(1, (uint)(decoder.PixelWidth * bairitsu)),
    ScaledHeight = Math.Max(1, (uint)(decoder.PixelHeight * bairitsu)),
    Rotation = kaiten,                                // 0・90・180・270 度
    InterpolationMode = BitmapInterpolationMode.Fant, // 縮小しても文字の輪郭が残りやすい
};

using var bitmap = await decoder.GetSoftwareBitmapAsync(
    BitmapPixelFormat.Bgra8,
    BitmapAlphaMode.Premultiplied,
    transform,
    ExifOrientationMode.RespectExifOrientation,
    ColorManagementMode.ColorManageToSRgb);

注意点

  • 読み取りの精度は、クラウドのAI-OCRには及びません。かすれた印字や手書きは読めないことがあります。そのため立替精算システムでは、読み取った値を自動では入力せず、候補として出して人が選ぶ形にしています。
  • Windowsの機能なので、Linuxのサーバでは使えません。立替精算システムは、Windowsサービスとして動かしているサーバのPCで読み取っています。
  • 英語版のWindowsなどで日本語の言語機能が入っていない場合は、Windowsの「設定」→「時刻と言語」→「言語と地域」で日本語を追加すると使えるようになります。

最後に

以上が、C#でWindows標準のOCRを使って領収書の文字を読む方法です。

追加のインストールも費用もかからず、社外に画像を出さずに読み取れるのが、Windows標準のOCRのよいところです。精度には限りがあるので、「候補を出して人が選ぶ」使い方にすると、入力の手間を減らしつつ、間違いも防げます。

この部品を使っている立替精算システムは、こちらで紹介しています。

PythonでPDFをOCR処理する方法は、こちらで紹介しています。

コメント

タイトルとURLをコピーしました