AI-OCR × 現場帳票で始める「脱・紙&手入力」 #2
AI-OCR × 現場帳票で始める「脱・紙&手入力」 #2
Python + AI-OCR API で最小サンプルをつくる
はじめに
第1回では、
- 現場帳票のパターン整理
- AI-OCR 導入前に決めておくべき事項
- 全体アーキテクチャと PoC → 本番のステップ
を、現場サイドの視点で整理しました。
第2回となる本記事では、
Python + 汎用 AI-OCR API を使って、
「現場帳票 1 種類」を対象とした最小 PoC パイプラインを動かす
ことをゴールにします。
特定のベンダー名には依存せず、
- 画像ファイルを Python から送信
- JSON で返ってきた結果をパース
- 帳票項目にマッピング
- CSV として出力
という一連の流れを、あくまで「最小構成」で整理します。
前提とゴール
対象読者
- Python 3.x の基本的なコードが読める
- コマンドラインから
python/pipを実行できる - レイアウト固定の帳票を 1 種類決めて PoC をしたい
今回のゴール
-
input/フォルダにある帳票画像(例:sample_001.jpg)を Python で読み込む - 汎用 AI-OCR API に投げて、結果 JSON を取得
- JSON から「日付・担当者名・温度・チェック結果」などの項目を抽出
-
output/result.csvとして書き出す
ここまでできれば、過去帳票をまとめて処理して精度検証に進めます。
想定するサンプル帳票
説明を具体化するため、サンプル帳票を次のように仮定します。
- 帳票名:日次点検チェックシート(1 枚 1 日分)
- 主な項目:
-
date:点検日(例:2025/12/15) -
operator:点検者名 -
temp_fridge:冷蔵庫温度(℃) -
temp_freezer:冷凍庫温度(℃) -
check_ok:総合結果(OK / NG)
-
A4 縦、レイアウト固定、毎日同じフォーマットで利用されている前提です。
Step 1:環境準備
1-1. ディレクトリ構成
PoC 用に、最低限次のような構成を用意します。
aiocr_poc/
input/
sample_001.jpg
sample_002.jpg
...
output/
src/
config_example.yaml
main.py
ocr_client.py
mapper.py
writer.py
・input/:帳票画像(JPEG / PNG など)
・output/:CSV などの出力先
・src/:Python スクリプト一式
1-2. 仮想環境と依存ライブラリ
仮想環境は任意ですが、分けておくと管理しやすくなります。
cd aiocr_poc
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS / Linux:
source .venv/bin/activate
pip install requests pyyaml
今回は最小構成として、
・requests:HTTP クライアント
・PyYAML:設定ファイル読み込み用
のみ利用します。
Step 2:AI-OCR API クライアント
特定ベンダーには依存せず、「画像を送り、JSON が返る」抽象クライアントを実装します。
2-1. 設定ファイル
src/config_example.yaml(コピーして config.yaml として利用):
ocr:
endpoint: "https://api.example-ocr.com/v1/ocr" # 実際のエンドポイントに置き換え
api_key: "YOUR_API_KEY_HERE" # 実際の API キーに置き換え
timeout_sec: 30
実運用では config.yaml は .gitignore し、
config_example.yaml のみリポジトリに含める運用が一般的です。
2-2. 汎用 AI-OCR クライアント実装
src/ocr_client.py:
import base64
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict
import requests
import yaml
@dataclass
class OcrConfig:
endpoint: str
api_key: str
timeout_sec: int = 30
def load_config(path: Path) -> OcrConfig:
with path.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
ocr_cfg = data["ocr"]
return OcrConfig(
endpoint=ocr_cfg["endpoint"],
api_key=ocr_cfg["api_key"],
timeout_sec=int(ocr_cfg.get("timeout_sec", 30)),
)
class AiOcrClient:
def __init__(self, config: OcrConfig) -> None:
self.config = config
def _encode_image_base64(self, image_path: Path) -> str:
with image_path.open("rb") as f:
b = f.read()
return base64.b64encode(b).decode("ascii")
def recognize(self, image_path: Path) -> Dict[str, Any]:
"""
汎用 AI-OCR API に画像を投げて JSON を返す。
レスポンス形式は利用する API に合わせて調整する。
"""
img_b64 = self._encode_image_base64(image_path)
payload = {
"image": img_b64,
"options": {
"language": "ja",
"format": "json",
},
}
headers = {
"Authorization": f"Bearer {self.config.api_key}",
"Content-Type": "application/json",
}
response = requests.post(
self.config.endpoint,
headers=headers,
data=json.dumps(payload),
timeout=self.config.timeout_sec,
)
response.raise_for_status()
return response.json()
利用する AI-OCR サービスの仕様に応じて payload は調整してください。
Step 3:帳票項目へのマッピング
AI-OCR のレスポンスはサービスごとに形式が違いますが、
ここでは例として次のような JSON を想定します。
{
"pages": [
{
"blocks": [
{ "text": "2025/12/15", "bbox": [100, 80, 200, 120] },
{ "text": "山田 太郎", "bbox": [120, 140, 260, 180] },
{ "text": "2.5", "bbox": [400, 220, 450, 250] }
]
}
]
}
・text:認識文字列
・bbox:画像上の座標(左上 x, y, 右下 x, y)
この座標をもとに、各項目ごとに読むエリアを定義します。
3-1. 座標によるエリア定義
src/mapper.py:
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
@dataclass
class FieldRegion:
name: str
# (x1, y1, x2, y2)
bbox: Tuple[int, int, int, int]
# 帳票テンプレートに合わせて座標を定義
FIELD_REGIONS: List[FieldRegion] = [
FieldRegion(name="date", bbox=(90, 70, 210, 130)),
FieldRegion(name="operator", bbox=(110, 130, 280, 190)),
FieldRegion(name="temp_fridge", bbox=(390, 210, 470, 260)),
FieldRegion(name="temp_freezer", bbox=(390, 260, 470, 310)),
FieldRegion(name="check_ok", bbox=(100, 320, 220, 380)),
]
def _is_inside(inner: List[int], outer: Tuple[int, int, int, int]) -> bool:
x1, y1, x2, y2 = inner
ox1, oy1, ox2, oy2 = outer
return x1 >= ox1 and y1 >= oy1 and x2 <= ox2 and y2 <= oy2
def map_ocr_result_to_fields(ocr_json: Dict[str, Any]) -> Dict[str, str]:
"""
OCR 結果の JSON から、帳票項目ごとの値を抜き出す。
"""
result: Dict[str, str] = {f.name: "" for f in FIELD_REGIONS}
pages = ocr_json.get("pages", [])
if not pages:
return result
blocks = pages[0].get("blocks", [])
for field in FIELD_REGIONS:
texts: List[str] = []
for block in blocks:
bbox = block.get("bbox")
text = block.get("text")
if not bbox or not text:
continue
if _is_inside(bbox, field.bbox):
texts.append(str(text))
# 同じエリアに複数ブロックがあれば結合
if texts:
result[field.name] = " ".join(texts)
return result
実際には、利用する OCR サービスのレスポンスに合わせて
pages / blocks の部分を読み替えます。
3-2. 簡易的な後処理(数字・日付など)
最低限の型変換もこの段階でまとめて行います。
def normalize_fields(raw: Dict[str, str]) -> Dict[str, Any]:
"""
生テキストから、アプリケーションで扱いやすい型に変換する例。
"""
from datetime import datetime
out: Dict[str, Any] = dict(raw)
# 日付
date_str = raw.get("date", "").replace("年", "/").replace("月", "/").replace("日", "")
for fmt in ("%Y/%m/%d", "%Y-%m-%d"):
try:
dt = datetime.strptime(date_str, fmt)
out["date"] = dt.strftime("%Y-%m-%d")
break
except ValueError:
continue
# 温度(数値変換)
for key in ("temp_fridge", "temp_freezer"):
v = raw.get(key, "").replace("℃", "").replace("°C", "").strip()
try:
out[key] = float(v)
except ValueError:
out[key] = None
# チェック結果(OK/NG 正規化)
check = raw.get("check_ok", "").upper()
if "OK" in check or "〇" in check:
out["check_ok"] = "OK"
elif "NG" in check or "×" in check:
out["check_ok"] = "NG"
return out
Step 4:CSV への書き出し
帳票 1 枚につき 1 行の CSV として書き出します。
src/writer.py:
import csv
from pathlib import Path
from typing import Any, Dict, List
COLUMNS = ["file_name", "date", "operator", "temp_fridge", "temp_freezer", "check_ok"]
def write_csv(output_path: Path, rows: List[Dict[str, Any]]) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=COLUMNS)
writer.writeheader()
for row in rows:
writer.writerow({col: row.get(col, "") for col in COLUMNS})
Step 5:全体をつなぐ main.py
最後に、ocr_client.py・mapper.py・writer.py をまとめて呼び出す main.py を用意します。
src/main.py:
from pathlib import Path
from typing import Any, Dict, List
from ocr_client import AiOcrClient, load_config
from mapper import map_ocr_result_to_fields, normalize_fields
from writer import write_csv
def process_single_image(client: AiOcrClient, image_path: Path) -> Dict[str, Any]:
print(f"[INFO] processing: {image_path.name}")
ocr_json = client.recognize(image_path)
raw_fields = map_ocr_result_to_fields(ocr_json)
norm_fields = normalize_fields(raw_fields)
norm_fields["file_name"] = image_path.name
return norm_fields
def main() -> None:
base_dir = Path(__file__).resolve().parent.parent
config_path = base_dir / "src" / "config.yaml"
input_dir = base_dir / "input"
output_csv = base_dir / "output" / "result.csv"
cfg = load_config(config_path)
client = AiOcrClient(cfg)
rows: List[Dict[str, Any]] = []
for image_path in sorted(input_dir.glob("*.jpg")):
try:
row = process_single_image(client, image_path)
rows.append(row)
except Exception as e:
print(f"[ERROR] failed: {image_path.name}: {e}")
write_csv(output_csv, rows)
print(f"[INFO] done. output: {output_csv}")
if __name__ == "__main__":
main()
実行方法:
cd aiocr_poc
# 仮想環境を有効化したうえで
python src/main.py
・input/*.jpg が順に AI-OCR にかけられ、
・各帳票の項目が抽出され、
・output/result.csv に 1 行ずつ出力されます。
ここまでで分かることと、次のステップ
この最小パイプラインまで構築できると、
・帳票ごとの読み取り精度(数値・日付・チェック欄など)
・項目ごとの誤読パターン(0/8、1/7、記入位置のズレ 等)
・前処理の有無による精度差(傾き補正・二値化 等)
を定量的に評価できるようになります。
次回以降は、今回のパイプラインをベースに、
・スキャン条件・撮影条件が悪い画像に対する前処理(傾き補正・トリミング)
・帳票テンプレートが複数存在する場合の「帳票種類判定」
・Web UI 上での確認・修正画面(人による最終チェックを組み込んだワークフロー)
といった実務寄りの内容を扱っていく予定です。
Discussion