物理本を scan & OCR で NotebookLMに読み込ませる方法

物理本をデジタル化してNotebookLMに読み込ませることで、キーワード検索や質疑応答が可能になります。この記事では、物理本しか手元にない状況で、テキストデータを取得してPDFファイルにまとめる方法について解説します。透明テキスト付きPDFの作成ですね。

## 作業1：本を裁断してスキャンし、OCRにかける

まず、本を裁断してスキャンします。スキャナ付属のOCRソフトが使えたら楽ですが、試してみて精度がいまいちの場合、純粋にページのスキャン画像だけを得ることをお勧めします。スキャン設定はページごとのjpeg形式にし、後工程でOCRをかけるために高画質でスキャンします。

### 技術的注意点:
- **スキャン解像度**: 300dpi以上の高解像度でスキャンすることで、OCRの精度が向上します。
- **スキャン環境**: スキャナのガラス面や本の表面を清潔に保つことで、ノイズを減少させ、よりクリアな画像を得ることができます。
- **前処理**: OCRの前に画像の前処理（例えば、ノイズ除去やコントラスト調整）を行うことで、認識精度を向上させることができます。
- **スキャン画像のファイル名**: ページ数をそのままファイル名にするのが扱いやすいです（例：0001.jpg） 

## 作業2：jpeg画像をOCRにかける

次に、スキャンしたjpeg画像をOCRにかけます。ローカルで動くTesseractを試しましたが、日本語の認識精度が低く、特に専門用語はグチャグチャになることが多かったです。そこで、Google Cloud Vision APIを試したところ、実用に耐えられる精度が確認できたので、これを使用することにしました。

```Python:jpg_ocr.py
import os
from google.cloud import vision
import io

# 環境変数の設定
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'json形式の認証ファイルのパス指定'

def detect_text(image_path):
    """Detects text in the file."""
    client = vision.ImageAnnotatorClient()

    with io.open(image_path, 'rb') as image_file:
        content = image_file.read()

    image = vision.Image(content=content)

    response = client.text_detection(image=image)
    texts = response.text_annotations

    if response.error.message:
        raise Exception(f'{response.error.message}')

    return texts[0].description if texts else ''

def process_images_in_folder(folder_path, output_file):
    with open(output_file, 'w', encoding='utf-8') as out_file:
        filenames = sorted([f for f in os.listdir(folder_path) if f.lower().endswith('.jpg')])
        for filename in filenames:
            print(f'Processing {filename}...')
            image_path = os.path.join(folder_path, filename)
            try:
                text = detect_text(image_path)
                out_file.write(f'File: {filename}\n')
                out_file.write(f'OCR Text:\n{text}\n\n')
            except Exception as e:
                print(f'Error processing {filename}: {e}')

# フォルダパスと出力ファイルの指定
folder_path = '/document_ocr/input/' # ocr 対象の jpeg ファイル群を入れたフォルダ
output_file = '/document_ocr/output_text_file.txt'

process_images_in_folder(folder_path, output_file)
print('Text extraction complete.')
```

### 技術的注意点:
- **Cloud Vision APIの認証**: 利用にあたっては認証が必要です。詳しくは別途調べて下さい。基本有料ですが、月1000ページ程度が無料で使える模様。
- **DOCUMENT_TEXT_DETECTION と TEXT_DETECTION の違い**: ORCのAPIは二種類あって、PDF文章だとDOCUMENT_TEXT_DETECTIONを使うのが通常のようです。文脈分析してより正しい文字を当てはめるDeepLっぽい仕組みがあるようですが、OCR結果より文脈の正しさを優先して文字を勝手に変えることがあるとの情報もあり、今回は愚直なOCR認識を優先させたいのでTEXT_DETECTIONを使いました。（https://cloud.google.com/vision/pricing?hl=ja）
- **表形式データの認識**: 複雑な図表は、完全なOCRは困難なので、そこは割り切って元画像を参照する方式です。


## 作業3：ページごとのjpeg画像ファイルとOCRテキストをまとめて一つのPDFにする

pypdfを使って、PDFファイルにします。
OCRで得られた文章は行単位（文字エリア単位）で改行されていますが、キーワード検索を行う上では、改行コードを取り除いて一行にするのが望ましいです。改行コードで区切られた単語が引っかからなくなるので。ページごとのjpeg画像ファイルとOCRテキストを用意して、プログラムで一つのPDFにまとめます。見た目は各ページにjpegをそのまま貼り付け、テキストは透明テキストとして埋め込みます。

```Python:jpeg_to_text_embedded_pdf.py
import os
from PIL import Image
from pypdf import PdfWriter, PdfReader
from reportlab.pdfgen import canvas
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
import io
import re

def compress_and_resize_image(input_path, output_path, quality=60, target_size=(2480, 3508)):
    """
    画像を圧縮してファイルサイズを削減し、指定されたサイズにリサイズする。
    :param input_path: 入力画像のパス
    :param output_path: 圧縮後の出力画像のパス
    :param quality: 圧縮品質 (1-95)。高品質はファイルサイズが大きくなる。
    :param target_size: リサイズ後の画像のサイズ (幅, 高さ)。
    """
    with Image.open(input_path) as img:
        img = img.resize(target_size, Image.LANCZOS)
        img.save(output_path, "JPEG", quality=quality)

def create_pdf_from_images(image_folder, ocr_text_file, output_pdf_file, font_path):
    # OCRテキストファイルを読み込み、ページごとに分割
    with open(ocr_text_file, 'r', encoding='utf-8') as f:
        ocr_text = f.read()
    ocr_pages = re.split(r'File:\d{4}.jpg', ocr_text) # jpeg画像のファイル名は、4桁のページ番号で命名していた
    # 最初の空の要素を削除
    ocr_pages = ocr_pages[1:]
    
    # 画像ファイルをソートして取得
    images = sorted([img for img in os.listdir(image_folder) if img.endswith('.jpg')])
    
    # PDFライターを作成
    pdf_writer = PdfWriter()
    
    # カスタムフォントを登録
    pdfmetrics.registerFont(TTFont('NotoSansJP', font_path))
    
    for i, image_file in enumerate(images):

        image_path = os.path.join(image_folder, image_file)
        compressed_image_path = os.path.join(image_folder, f"compressed_{image_file}")
        print(f"Processing {image_path}")

        # 画像を圧縮して保存
        compress_and_resize_image(image_path, compressed_image_path)
        
        # 圧縮した画像を読み込み
        image = Image.open(compressed_image_path)
        image_width, image_height = image.size
        
        # ReportLabのcanvasを使用してPDFを作成
        packet = io.BytesIO()
        can = canvas.Canvas(packet, pagesize=(image_width, image_height))
        can.drawImage(compressed_image_path, 0, 0, image_width, image_height)

        # OCRテキストを透明テキストとして追加
        if i < len(ocr_pages):
            ocr_text_content = ocr_pages[i].strip()

            # ocr_textから改行コードを除去
            ocr_text_1row = ocr_text_content.replace('\n', '')
            
            if ocr_text_content:
                can.setFillColorRGB(0, 0, 0, 0)  # 透明テキスト
                can.setFont("NotoSansJP", 6)
                text_object = can.beginText(10, image_height -10)  # ページの左上から10ポイント下　(※座標 (0, 0) はページの左下隅)
                text_object.textLines(ocr_text_1row) # 行の折り返しで単語が分割されて検索に引っかからない状況を無くすために、改行コードを除去した検索用テキストを追加
                text_object.textLines(ocr_text_content) # 複数行のテキストを追加（NotebookLMでは、１行が長くなりすぎるとテキストとして処理されないため）
                can.drawText(text_object)

        can.save()

        # PDFページを読み込み
        packet.seek(0)
        new_pdf = PdfReader(packet)
        pdf_page = new_pdf.pages[0]

        # PDFライターにページを追加
        pdf_writer.add_page(pdf_page)
        
        # 圧縮画像の削除
        os.remove(compressed_image_path)
    
    # PDFファイルを書き出す
    with open(output_pdf_file, 'wb') as f:
        pdf_writer.write(f)
    
    print(f"PDF created successfully: {output_pdf_file}")


# 特定のフォルダとファイルのパスを指定
image_folder = "/document_ocr/input/"  # 画像ファイルが入っているフォルダのパスを指定
ocr_text_file = "/document_ocr/output_text_file.txt"       # OCRテキストファイルのパスを指定
output_pdf_file = "/document_ocr/output.pdf"             # 出力するPDFファイルのパスを指定
font_path = '/document_ocr/NotoSansJP-VariableFont_wght.ttf'  # 日本語フォントファイルのパス

create_pdf_from_images(image_folder, ocr_text_file, output_pdf_file, font_path)
```



### 技術的注意点:
- **日本語の取り扱い**: 日本語フォントを明示的に指定してあげないと、日本語文字が全部潰れます。
- **PDFの最適化**: NotebookLMで読み込み可能なファイルサイズの上限は、200MBです（2024/06時点）。PDFを200MB以内に収めるために、画像圧縮を行う際の品質設定を調整します。A4サイズの場合、quality=60, target_size=(2480, 3508)ぐらいで良いかと思いますが、内容に応じて適宜調整します。


## 作業4：NotebookLM での利用

出来上がったPDFをNotebookLMに読み込ませたところ、テキストを認識してくれませんでした。透明テキストを認識しない仕様かと考え、色々試したところ、一行が長すぎるテキストを無視する仕様であることが判明しました。改行コードを入れたテキストも別途埋め込むと、NotebookLMでテキスト認識ができました。

### 技術的注意点:
- **テキストの分割**: 一行が長すぎるテキストはNotebookLMで認識されないため、適度な長さで改行を入れながら埋め込むことが重要です。（2024/06時点）
- **テストと検証**: PDFを作成した後、NotebookLMに読み込ませてテキスト認識や検索機能が適切に動作するかをテストし、必要に応じてPDFを再調整します。

これらの手順を踏むことで、物理本をデジタル化し、NotebookLMで効率的に利用することが可能になります。

### 法律的注意点:
- **PDFの利用範囲**: スキャンしてOCRかけたPDFファイルは、私的利用の範囲内でのみ利用が可能です。他人との共有は止めておきましょうね。