技術 約3分で読めます

NDLOCRの段組認識問題をヒストグラム解析で力技解決

🎯 目的

縦書き日本語書籍(4段組)のOCR処理

💻 動作環境

項目内容
OSWindows 11
Python3.14.0

試行1: NDLOCR単体

結果: △ 動くがレイアウト認識が弱い

  • 段組が正しく取れず、読み順が崩れる
  • NDLOCRのレイアウト解析は古い縦書き書籍向けで、現代的な段組に弱い

試行2: Layout Parser + NDLOCR

結果: ✗ インストール失敗

  • Layout Parser自体は pip install layoutparser で入る
  • Detectron2がWindowsでビルドエラー
    • pip install "detectron2@git+..." → ビルド失敗
    • Windows環境でのC++コンパイラ問題

試行3: PyMuPDF + ヒストグラム解析(代替案)

結果: ○ 動作OK

  • Detectron2不要
  • 縦方向ヒストグラムで段の境界(谷)を検出
  • 4段固定で切り出し → NDLOCRに渡す

✅ 最終構成

PDF

extract_blocks.py(PyMuPDF + ヒストグラム解析)

4分割画像(page01_1.jpg ~ page01_4.jpg)

NDLOCR(OCRのみ)

テキスト出力

必要なもの

  • Python + PyMuPDF, PIL, numpy
  • Docker + NDLOCR
  • extract_blocks.py(ヒストグラム解析スクリプト)

📄 extract_blocks.py

import fitz  # PyMuPDF
from PIL import Image
import io
import os
import numpy as np

# --- 設定項目 ---
pdf_path = r"C:\ndlocr_work\input\test.pdf"
output_dir = r"C:\ndlocr_work\input\img"
# ----------------

os.makedirs(output_dir, exist_ok=True)

try:
    pdf_document = fitz.open(pdf_path)
except fitz.errors.FitzError as e:
    print(f"エラー: PDFファイルを開けません。パスが正しいか確認してください。: {e}")
    exit()

print(f"{len(pdf_document)}ページの処理を開始します。")

def find_split_point(histogram, center_y, search_range):
    """ヒストグラムの谷の中央を見つける関数"""
    start = max(0, center_y - search_range)
    end = min(len(histogram), center_y + search_range)
    
    # 探索範囲内のヒストグラムを切り出す
    segment = histogram[start:end]
    
    # 谷の深さ(最小値)を見つける
    min_val = np.min(segment)
    
    # 最小値を持つすべてのインデックス(谷底)を取得
    valley_indices = np.where(segment == min_val)[0]
    
    # 谷底の中央のインデックスを取得
    middle_of_valley = valley_indices[len(valley_indices) // 2]
    
    # 元のヒストグラム全体での座標を返す
    return start + middle_of_valley

for page_num in range(len(pdf_document)):
    page = pdf_document[page_num]
    print(f"\n--- ページ {page_num + 1} ---")

    # 1. 高解像度で画像化
    mat = fitz.Matrix(3, 3)
    pix = page.get_pixmap(matrix=mat, alpha=False)
    img = Image.open(io.BytesIO(pix.tobytes("jpeg")))

    # 2. 2値化
    gray_img = img.convert('L')
    binary_img = gray_img.point(lambda x: 0 if x < 200 else 255, '1')

    # 3. 縦方向のヒストグラムを計算
    np_img = np.array(binary_img)
    histogram = np.sum(255 - np_img, axis=1)

    # 4. 3つの分割点を谷の中央として検出
    height, width = np_img.shape
    search_range = int(height * 0.05) # 中心の上下5%を探索
    
    split_points = [
        find_split_point(histogram, int(height * 0.25), search_range),
        find_split_point(histogram, int(height * 0.50), search_range),
        find_split_point(histogram, int(height * 0.75), search_range)
    ]
    
    split_points.sort()
    print(f"検出された分割点のY座標: {split_points}")

    # 5. 画像を切り出して保存
    boundaries = [0] + split_points + [height]

    for i in range(4):
        y_start = boundaries[i]
        y_end = boundaries[i+1]
        
        segment = img.crop((0, y_start, width, y_end))
        output_path = os.path.join(output_dir, f"page{page_num+1:02d}_{i+1}.jpg")
        segment.save(output_path, "JPEG", quality=95)
        print(f"  段{i+1}を保存しました: {output_path}")

pdf_document.close()
print("\n全ての処理が完了しました!")