0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに頼り切って家計簿アプリを追加開発する。:OCR実装①

0
Last updated at Posted at 2026-05-25

はじめに

前回、家計簿アプリの設計,画面の作成を行いました。(AIがやりました)
今回からはOCRの実装を行います。

プロンプト

OCR機能を実装したい
コードを生成してください

回答

OCR機能の最低限実装例です。
以下構成で「画像選択 → OCR実行 → テキスト抽出 → 仮解析 → 画面反映」まで動作します。

前提として以下を使用します。


NuGet

インストールしてください。

Install-Package Tesseract
Install-Package OpenCvSharp4
Install-Package OpenCvSharp4.runtime.win

日本語OCRデータ

以下から jpn.traineddata を取得してください。

tessdata repository

配置先:

実行フォルダ
└─ tessdata
   └─ jpn.traineddata

例:

bin
└─ Debug
   └─ tessdata
      └─ jpn.traineddata

OCR処理全体構成

画像選択
 ↓
OpenCV前処理
 ↓
Tesseract OCR
 ↓
テキスト解析
 ↓
画面へ反映

Services/OcrResult.cs

using System;
using System.Collections.Generic;

namespace KakeiboApp.Services
{
    public class OcrResult
    {
        public DateTime? Date { get; set; }

        public decimal TotalAmount { get; set; }

        public List<OcrItem> Items { get; set; }
            = new List<OcrItem>();
    }

    public class OcrItem
    {
        public string Name { get; set; }

        public decimal Amount { get; set; }

        public int Quantity { get; set; } = 1;
    }
}

Services/OcrService.cs

using OpenCvSharp;
using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using Tesseract;

namespace KakeiboApp.Services
{
    public class OcrService
    {
        public OcrResult Execute(string imagePath)
        {
            string tempPath = PreprocessImage(imagePath);

            string text = ExecuteTesseract(tempPath);

            File.Delete(tempPath);

            return ParseReceipt(text);
        }

        private string PreprocessImage(string imagePath)
        {
            Mat src = Cv2.ImRead(imagePath);

            Mat gray = new Mat();
            Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);

            Mat blur = new Mat();
            Cv2.GaussianBlur(gray, blur, new Size(3, 3), 0);

            Mat binary = new Mat();
            Cv2.Threshold(
                blur,
                binary,
                0,
                255,
                ThresholdTypes.Binary | ThresholdTypes.Otsu);

            string tempPath = Path.Combine(
                Path.GetTempPath(),
                $"{Guid.NewGuid()}.png");

            Cv2.ImWrite(tempPath, binary);

            return tempPath;
        }

        private string ExecuteTesseract(string imagePath)
        {
            using var engine = new TesseractEngine(
                "./tessdata",
                "jpn",
                EngineMode.Default);

            using var img = Pix.LoadFromFile(imagePath);

            using var page = engine.Process(img);

            return page.GetText();
        }

        private OcrResult ParseReceipt(string text)
        {
            OcrResult result = new OcrResult();

            string[] lines = text.Split(
                new[] { "\r\n", "\n" },
                StringSplitOptions.RemoveEmptyEntries);

            foreach (string line in lines)
            {
                ParseDate(line, result);

                ParseTotal(line, result);

                ParseItem(line, result);
            }

            return result;
        }

        private void ParseDate(
            string line,
            OcrResult result)
        {
            Match match = Regex.Match(
                line,
                @"(\d{4})[\/\-年](\d{1,2})[\/\-月](\d{1,2})");

            if (!match.Success)
            {
                return;
            }

            if (DateTime.TryParse(
                match.Value,
                out DateTime date))
            {
                result.Date = date;
            }
        }

        private void ParseTotal(
            string line,
            OcrResult result)
        {
            if (!line.Contains("合計") &&
                !line.Contains("税込"))
            {
                return;
            }

            Match match = Regex.Match(
                line,
                @"([0-9,]+)");

            if (!match.Success)
            {
                return;
            }

            string value = match.Value.Replace(",", "");

            if (decimal.TryParse(
                value,
                out decimal amount))
            {
                result.TotalAmount = amount;
            }
        }

        private void ParseItem(
            string line,
            OcrResult result)
        {
            Match match = Regex.Match(
                line,
                @"^(.*?)[\s ]+([0-9,]+)$");

            if (!match.Success)
            {
                return;
            }

            string name = match.Groups[1].Value.Trim();

            string amountText =
                match.Groups[2].Value.Replace(",", "");

            if (string.IsNullOrWhiteSpace(name))
            {
                return;
            }

            if (!decimal.TryParse(
                amountText,
                NumberStyles.Any,
                CultureInfo.InvariantCulture,
                out decimal amount))
            {
                return;
            }

            result.Items.Add(new OcrItem
            {
                Name = name,
                Amount = amount,
                Quantity = 1
            });
        }
    }
}

ExpenseEntryForm 修正

以下フィールド追加。

private string _selectedImagePath;

画像選択処理修正

private void BtnSelectImage_Click(object sender, EventArgs e)
{
    OpenFileDialog dialog = new OpenFileDialog();

    dialog.Filter =
        "画像ファイル|*.png;*.jpg;*.jpeg";

    if (dialog.ShowDialog() != DialogResult.OK)
    {
        return;
    }

    _selectedImagePath = dialog.FileName;

    MessageBox.Show(
        $"画像選択:\n{_selectedImagePath}");
}

OCRボタン処理修正

private void BtnOcr_Click(object sender, EventArgs e)
{
    if (string.IsNullOrWhiteSpace(_selectedImagePath))
    {
        MessageBox.Show("画像を選択してください");
        return;
    }

    try
    {
        OcrService service = new OcrService();

        OcrResult result =
            service.Execute(_selectedImagePath);

        ApplyOcrResult(result);

        MessageBox.Show("OCR完了");
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

OCR結果反映処理追加

private void ApplyOcrResult(OcrResult result)
{
    if (result.Date.HasValue)
    {
        txtDate.Text =
            result.Date.Value.ToString("yyyy/MM/dd");
    }

    txtAmount.Text =
        result.TotalAmount.ToString();

    dgvItems.Rows.Clear();

    foreach (var item in result.Items)
    {
        dgvItems.Rows.Add(
            item.Name,
            "",
            item.Amount,
            item.Quantity,
            "10",
            "");
    }
}

OCR精度を上げる場合の改善ポイント

最低限実装だと精度はそこまで高くありません。
実運用レベルでは以下が重要です。


1. 傾き補正

レシート斜め撮影対応。

HoughLines
minAreaRect

で補正。


2. ノイズ除去

MedianBlur
MorphologyEx

追加。


3. 明るさ補正

暗い画像対策。

AdaptiveThreshold

推奨。


4. 店舗別パターン解析

実際には:

  • セブン
  • ファミマ
  • イオン
  • 西友

などでレイアウトが違います。

OCR後に:

店舗名判定
 ↓
店舗別Regex

のほうが精度が高いです。


実運用でおすすめな追加機能

  • OCRプレビュー
  • 元画像表示
  • OCR信頼度表示
  • OCR再実行
  • 商品カテゴリ自動判定
  • 重複商品統合
  • 消費税自動判定
  • OCRログ保存
  • AI補正

OCRでよくある問題

問題 原因
8→B 二値化不足
金額認識ミス 傾き
日本語崩壊 traineddata不足
商品が分割 行解析不足
合計誤認識 Regex不足

次段階でおすすめ

次は以下実装がおすすめです。

  1. SQLite保存
  2. OCR結果DB登録
  3. 支出一覧連携
  4. OCR精度改善
  5. 商品カテゴリマスタ
  6. AI分類
  7. Excel出力
  8. 月別集計グラフ

起動、確認

起動、OCRで読み込んでみましたがうまくいきません。
image.png

最後に

次回もOCRの改善を実施します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?