はじめに
前回、家計簿アプリの設計,画面の作成を行いました。(AIがやりました)
今回からはOCRの実装を行います。
プロンプト
OCR機能を実装したい
コードを生成してください
回答
OCR機能の最低限実装例です。
以下構成で「画像選択 → OCR実行 → テキスト抽出 → 仮解析 → 画面反映」まで動作します。
前提として以下を使用します。
- OCR : Tesseract OCR
- 画像処理 : OpenCvSharp
NuGet
インストールしてください。
Install-Package Tesseract
Install-Package OpenCvSharp4
Install-Package OpenCvSharp4.runtime.win
日本語OCRデータ
以下から jpn.traineddata を取得してください。
配置先:
実行フォルダ
└─ 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不足 |
次段階でおすすめ
次は以下実装がおすすめです。
- SQLite保存
- OCR結果DB登録
- 支出一覧連携
- OCR精度改善
- 商品カテゴリマスタ
- AI分類
- Excel出力
- 月別集計グラフ
起動、確認
最後に
次回もOCRの改善を実施します。
