タカハシ業務システム / 機能解説

手書き納品書を、
コンピューターが読み取るしくみ

仕入先からいただく手書きの納品書をスキャンするだけで、日付・金額・品目が ひとりでに表データになります。その裏側で何が起きているのかを、実際の伝票を見ながら順番にご説明します。

手書きの納品書の束が、パソコン画面のきれいな表に変わっていくイメージ

OCR(オーシーアール)とは? 「光学文字認識」のことで、紙に書かれた文字を コンピューターが見て読み取る技術です。このしくみは、仕入先ごとに様式の違う手書き納品書を そのままスキャンして取り込むことを目指しています。担当者が数字を手で打ち直す手間をなくし、 打ち間違いも防ぎます。

取り込みの流れ

1枚のスキャン(PDF)が、次の順番で「表データ」に変わっていきます。 すべて自動で進み、最後に人が目で確認します。下の画像はすべて実際の伝票を処理したものです。

1

スキャンする

納品書を、緑色の台のスキャナーに乗せて読み取ります。1回のスキャンに納品書が 2枚並んでいてもかまいません。向きが横でも逆さでも、後の工程で自動的に直します。

緑色の台の上に手書き納品書が2枚、横向きに置かれたスキャン画像
スキャンしたそのままの状態。緑の台の上に納品書が2枚(阿部製作所)。
2

紙を1枚ずつ切り出して、まっすぐにする

背景の緑色を手がかりに「紙の部分」だけを見つけ出し、納品書を1枚ずつ取り出します。 置いたときの傾きもこの段階でまっすぐに直します。

2枚の納品書それぞれをオレンジ色の枠で囲って検出した画像
① 紙を1枚ずつ見つける(オレンジの枠)
切り出して傾きを直した納品書1枚
② 切り出して、傾きを直す
仕組み(技術)
緑のスキャナ台を HSV 色域でマスクして紙領域を反転抽出し、連結成分で1〜2枚へ分割。 各紙片は minAreaRect(最小外接矩形)の4隅を基準枠へアフィン変換(回転+切り抜きのみ)。 平台スキャンに遠近ゆがみは無いという前提で、台形補正(透視変換)は一切しません
3

どの仕入先かを見分ける

納品書の上のほうにある会社名のロゴ(社名バナー)を手がかりに、どの仕入先のものかを 自動で判定します。あらかじめ登録した各社のロゴ見本と照らし合わせ、いちばんよく一致した会社に決めます。 このとき紙の向き(上下・左右)も同時に分かるので、逆さでも正しく立て直せます。

阿部製作所の社名バナー見本
阿部製作所
板倉製作所の社名バナー見本
板倉製作所

判定にどれだけ自信があるかも数値で分かります。見本とよく一致した点が 多いほど確実。今回の板倉の伝票では 175 か所が一致しました(他社の見本ではせいぜい10〜18か所)。 自信が足りないものは「不明」として人の確認に回します。

仕組み(技術)
SIFT 特徴点でロゴ見本と照合し、estimateAffinePartial2DRANSAC で 相似変換(回転+拡大縮小+平行移動)を当てます。SIFTは拡大・回転に強いので、向き不明・解像度違いでも 1回の照合で「仕入先・回転角・確信度(一致点数)」をまとめて得られます。一致点が 25 未満は仕入先不明=人手送り。
4

基準のフォームにぴったり重ねる

同じ仕入先でも、印刷のずれやスキャンのたびの微妙な位置ちがいがあります。そこで、その仕入先の 「お手本」の用紙に、印刷された罫線や項目名を頼りにスキャンをぴたっと重ね合わせます。 こうすると、いつでも「同じ場所に同じ欄がある」状態になり、後の読み取りが正確になります。

基準フォームに重ね合わせて正立した板倉の納品書
重ね合わせ後(まっすぐ・定位置)
板倉の基準フォーム(お手本の用紙)
仕入先ごとの「お手本」
仕組み(技術)
基準フォーム画像へ SIFT 対応+estimateAffinePartial2D/RANSAC の 相似変換でワープ。手書きは1枚ごとに違うので RANSAC が外れ値として自然に除外し、印刷罫線だけで合わせます。 村山のように社名が黒ゴム印の仕入先は、整合の特徴を「印刷フォーム色のみ」に絞って精度を上げています (実測の残差ずれ:黒印込み26.5px → フォーム色のみ0.4px)。
5

印刷の罫線を消して、手書きだけを残す

用紙の印刷された罫線(マス目)と手書きは同じ青色のことが多く、色だけでは分けられません。 そこで「形」で見分けます。罫線は紙を端から端まで貫く長い直線、手書きは短く曲がった線—— この違いを使って長い直線(=罫線)だけを取り除き、手書きの文字を残します。

印刷の罫線が取り除かれ、手書きの数字と社名だけが残った画像
マス目の罫線が消え、手書きの数字がくっきり残った状態。
仕組み(技術)
彩度のある色/暗い画素を「インク」として抽出し、長い1次元カーネルの モルフォロジー OPEN で「長い直線=グリッド」だけを取り出して差し引きます。 色相が同じでも、罫線(長い直線)と手書き(短い曲線)は形が違うので分離できます。
6

AIが読みやすい形に組み直す

手書きだけになった紙を、AIにそのまま見せるのではなく、一行ずつ切り離して並べ直し、 「ここは日付」「ここは1行目の品名」とラベルを付けた1枚の絵に作り変えます。隣の行と文字がくっつくのを防ぎ、 AIが迷わず読めるようにする工夫です。

日付・伝票番号・各行をラベル付きで分解して並べ直した、AIが読むための合成画像
AIに渡す「組み直した画像」。YEAR/MONTH/DAY や ROW 1〜7 に分解されている。
仕組み(技術)
外部ライブラリ ocr-form-kit が、手書きのみを白地に描き直し(red-kill clean)、 明細表を行ごとの帯(strip)に「展開(explode)」、チェック印や取り消し線はインク被覆から 決定的に判定して合成画像を1枚生成します。様式(列・項目・ラベル)は仕入先プロファイルから組み立てるので、 コードを書き換えずに新しい様式へ対応できます。
7

AIが文字を読み取り、人が確認する

組み直した画像をAI(Google Gemini)が読み取り、日付・伝票番号・合計・明細の数字へ変換します。 手書きの「26年」のような年号は西暦(2026年)に直すなどの整理も自動で行います。 最後に、読み取り結果を人が画面で確認・修正してから取り込みます。 自信のない箇所や計算が合わない行には印が付くので、そこだけ見れば大丈夫です。

AIに丸投げはしません。 数量×単価=金額の検算など、 コンピューターで確かめられることは自動で検証し、確認すべき箇所を人に示します。最終確認は必ず人が行います。

読み取り結果の例

上の流れで、板倉製作所の実際の納品書を処理した結果です(AIが本当に読み取った値)。

納品書 — 株式会社板倉製作所
仕入先板倉(株式会社板倉製作所)
伝票番号5-1
日付2026年5月28日 (手書き「26年5月28日」→ 西暦へ自動換算)
税込合計¥71,100
品名数量単価金額検算
250801-1155 790 ¥90 ¥71,100 ✓ 一致

790 × ¥90 = ¥71,100。明細金額と合計が一致したため、この伝票は「確認OK」として取り込めます。 読み取りにかかった時間は約9秒、費用は1枚あたり約0.17円です。

うまく読むための3つの工夫

手書き・様式バラバラの納品書を安定して読むために、特に効いている考え方が3つあります。

📐

ゆがみは直すが、伸ばさない

平らな台のスキャンに遠近のゆがみは無い、という前提に立ち、回転とまっすぐ化だけを行います。 余計な変形をしないぶん、文字がくずれません。

✏️

色ではなく「形」で分ける

罫線と手書きが同じ色でも、長い直線か短い曲線かで見分けます。色だけに頼らないので、 どんなインク色でも手書きだけを残せます。

🤝

AIと人で役割分担

読み取りはAI、確かめられる計算はコンピューター、最終判断は人。 それぞれの得意を組み合わせて、速さと正確さを両立します。

対応している仕入先

現在、次の3社の様式に対応しています。新しい仕入先は、見本の用紙を1枚登録するだけで 追加でき、プログラムの書き換えは不要です。

阿部製作所
有限会社阿部製作所
村山電機製作所
黒ゴム印の社名にも対応
板倉製作所
株式会社板倉製作所

※ 日進瓦斯(ガス容器系・別様式)は近日対応予定です。

技術詳細(ITご担当者向け)

本機能は「幾何補正・仕入先判定(本リポジトリ takahashi/services/ocr_ingest)」と 「手書きOCR+検証(外部ライブラリ ocr-form-kit)」の2層構成です。 全工程に共通する設計判断は 剛体変換のみ(透視補正をしない) こと。 平台スキャンに透視歪みは無いという前提に立ち、回転+等方スケール+平行移動だけで扱います。

パイプライン全体

PDF ──300dpi レンダリング (PyMuPDF)
    → split_slips      緑台を背景に紙領域を連結成分で 1〜2 枚へ分割
    → deskew + crop     minAreaRect の4隅を (rw,rh) へアフィン写像(回転+切抜きのみ)
    → BannerMatcher     社名バナーを SIFT 照合 → {仕入先, 回転, 確信度} を一括決定
    → 正立へ粗回転 (0/90/180/270)
    → FormRegistrar     仕入先の基準フォームへ相似整合 (estimateAffinePartial2D/RANSAC)
    → Slip(image, vendor, confidence, aligned, …)
    ── ここまで本リポジトリ (cv2/numpy/pymupdf, "ocr" extra) ──
    → ocr_form_kit.read(image, profile_dir)
        precropped CV 戦略  色×形で手書きを分離(長い直線=罫線を差し引く)
        → composite        手書きを白地へ再描画 + 明細を行ごとに展開 + ラベル付与
        → Gemini           合成画像を1コールで読取り
        → reconcile        qty×price==amount 等の検算 + needs_human 集約
    → records.purchase_record   日付(令和/西暦)・金額の正規化、レビュー理由の集約

1. 分割・正立(frontend.py)

段階手法要点
紙領域抽出HSV 緑域 inRange → bitwise_not → open/close緑台を除いた紙マスク。CLOSE で膨らんだ境界は切抜き時に ERODE で打ち消す
分割connectedComponentsWithStatsページ面積の 4% 以上の成分を左→右順で 1〜2 枚
デスキューminAreaRect 4隅 → getAffineTransform画像全体を回さず矩形を正準枠へ直接写像。出力寸法が安定(角度依存のばらつき・はみ出し切れを排除)

2. 仕入先判定(BannerMatcher)

SIFT(nfeatures=1500) でロゴ見本(fiducials/<vendor>.png, git追跡・644)を照合。 estimateAffinePartial2D + RANSAC(reproj=5) の相似行列からインライア数=確信度、 atan2(m10,m00) から回転角を読み、0/90/180/270 へスナップ。 しきい値 MIN_INLIERS=25(実測:正解 90〜186 / 他社 10〜18)。残差は skew として情報保持。

3. 基準整合(FormRegistrar)

SIFT(nfeatures=4000) で基準フォーム画像(デプロイ資産 var/ocr_profiles/<vendor>/reference.jpg)へ estimateAffinePartial2D/RANSAC(reproj=3) の相似変換でワープ。MIN_ALIGN_INLIERS=40 未満は 無変換で返し aligned=False(人手送りの判断材料)。 村山は黒ゴム印が整合を引っ張るため、整合特徴を彩度のある「フォーム色のみ」へ絞る(per-vendor)。 実測残差:阿部 ~0.1px / 村山 ~0.3px。出力画像にはゴム印・手書きをそのまま残す。

4. 手書き分離(precropped 戦略 / ocr-form-kit)

色相が罫線と手書きで共通のため色分離は不可。形で分離する: ink = (S>45) | (V<140) で色/暗インクを取り、長い 1D カーネル (幅/12 × 11 × 高/12)の MORPH_OPEN で長い罫線だけを取り出して減算。 短く曲がった手書きストロークだけが残る。

5. 合成画像(composite.py)

手書きのみを白地へ再描画(red-kill clean)→ 明細表を行ごとの帯へ explode(隣接行の文字混入を防止)→ 列ラベル・ROW n ラベル付与 → チェック印(○)/取り消し線(VOID)はインク被覆から決定的に判定。 様式依存(列・項目・ラベル・マーク列)は CompositeSpec としてプロファイルから導出するので、 レンダラ自体は様式非依存。

6. 読取りエンジン(Gemini)

合成画像を Google Gemini に1コールで渡し、profile-keyed なエンベロープ (fields / lines / marks / reconcile / preview_png / canonical_png / areas)を取得。 実測 ~9秒・~¥0.17/枚。FakeEngine でネットワーク無し回帰テスト可。

7. 正規化(records.py — 純ロジック・cv2非依存)

  • 年号:4桁はそのまま。元号付き(令和7/R7)は明示換算。裸の1〜2桁は令和(2018+n)か西暦下2桁(2000+n)の曖昧があるため ref_year 近傍を採用(板倉=西暦下2桁、阿部=令和)。
  • 金額:¥/円/カンマ/符号を除いた純数字のみ採用。読めなければ None=未確定。
  • 数量:生値を保持("1set" を壊さない)し、先頭数値部のみ検算用に取り出す。
  • レビュー理由:整合失敗/日付・合計未確定/reconcile の needs_human/占有行ありなのに明細ゼロ、等を集約。review_reasons が空でなければ人手送り。

構成・リポジトリ

役割場所追跡
分割/正立/整合/分類・正規化・オーケストレータ・CLItakahashi/services/ocr_ingest/git
OCRライブラリ+precropped 戦略ocr-form-kit(別repo, git rev 固定)git(別)
仕入先プロファイル(基準画像/pptx/toml)var/ocr_profiles/<vendor>/非追跡(デプロイ資産)
社名バナー見本fiducials/<vendor>.pnggit(イメージ同梱)

パッケージ層:frontend(cv2:分割/正立/整合/分類)・records(純ロジック:正規化、cv2非依存で単体テスト可)・ orchestrator(cv2+OCR:PDF群→読取り→集約)。__init__ が重い名前を遅延importするため、 コアWebイメージは cv2 無しで起動でき、実OCRはアップロード処理内で遅延importする。

新様式オンボーディング(コード変更なし)

observe → calibrate(canonical基準像)→ pptx で role[.type]:name ラベル付き矩形を描く
        → overlay で枠の一致を目視 → <vendor>.profile.toml(CV戦略/しきい値/プロンプト/labels)を調整
        → var/ocr_profiles/<vendor>/ に reference + pptx + toml を配置
        → fiducials/<vendor>.png(社名バナー, 644)を追加 → 再ビルド

実行例

uv sync --extra ocr   # numpy/opencv/pymupdf + ocr-form-kit を1 venv に

# 分割/正立/分類のみ(OCRなし, 正立画像を書き出し)
python -m takahashi.services.ocr_ingest orient bin/docs/納品書*.pdf -o /tmp/out

# 取込: 分割→正立→OCR→仕入台帳向けレコード(JSON)
python -m takahashi.services.ocr_ingest ingest "bin/docs/納品書(板倉).pdf"   # 実 Gemini
python -m takahashi.services.ocr_ingest ingest bin/docs/*.pdf --fake          # ネット無し

※ 読み取り後の「仕入台帳への記帳」部分は本ページの対象外(別途設計中)。 本ページはスキャン→構造化データ→人手確認までの読み取りしくみを扱います。