スキャンしたPDFから(指などの)色のついた部分をとる

## はじめに
pdfから色のついた部分を識別し、その部分を全て白くするコードをPythonで作りました。
本をスキャンするとき、スキャンアプリが自動で指などを消してくれるのですが、うまく消えないことがあります。今まではこれを無視するか、自分で白塗りしていました。しかし、このくらいのことなら自動化できるよな、と思いやってみることにしました。

## コード全文
以下がコードの全文です。もしも使いたい方がいたら自由にコピペして使ってください。実行するとファイルの選択画面が開くので、白塗りしたいファイルを選択するとよいです。

<details><summary>コード全文</summary>

```Python:pdfWhieoutTool.py
import os
from pdf2image import convert_from_path
import cv2
import numpy as np
from PIL import Image
from tkinter import filedialog, Tk
from tqdm import tqdm
from PyPDF2 import PdfReader

def process_pdf(pdf_path):
    # まずPyPDF2でPDFを読み込み、各ページのサイズ（ポイント単位）を取得
    reader = PdfReader(pdf_path)
    pages_dimensions = []
    for i in range(len(reader.pages)):
        page = reader.pages[i]
        media_box = page.mediabox
        # 幅・高さはポイント単位（1pt = 1/72インチ）
        width_pt = float(media_box.width)
        height_pt = float(media_box.height)
        # 300dpiでのピクセル数に変換： ピクセル数 = (ポイント / 72) * 300
        width_px = int(width_pt / 72 * 300)
        height_px = int(height_pt / 72 * 300)
        pages_dimensions.append((width_px, height_px))
    
    # PDFを画像に変換
    images = convert_from_path(pdf_path, dpi=300)
    processed_images = []
    
    # tqdmで進行状況を表示
    for i, img in tqdm(enumerate(images), total=len(images), desc="ページ処理中"):
        # PIL画像をNumPy配列に変換
        img_np = np.array(img)
        
        # 各ピクセルのRGBの最大値と最小値の差を計算し、閾値より大きければ色味があると判断
        diff = np.max(img_np, axis=-1) - np.min(img_np, axis=-1)
        threshold = 30  # この閾値で色味の有無を判定
        mask = diff > threshold
        
        # 色味があると判定されたピクセルを白に置換
        img_np[mask] = [255, 255, 255]
        
        # 変換前の各ページのサイズ（ピクセル単位）を取得（ページ毎に異なる場合にも対応）
        original_size = pages_dimensions[i] if i < len(pages_dimensions) else img.size
        
        # NumPy配列からPIL画像に戻し、元のサイズにリサイズ
        processed_img = Image.fromarray(img_np)
        processed_img = processed_img.resize(original_size, Image.LANCZOS)
        
        processed_images.append(processed_img)
    
    # Tkinterダイアログで出力先PDFの保存先を指定
    output_path = filedialog.asksaveasfilename(defaultextension=".pdf",
                                               filetypes=[("PDF files", "*.pdf")])
    
    if output_path:
        # PILのsave()でPDFに保存する際、dpiパラメータを指定することでDPI情報を付与
        processed_images[0].save(output_path, save_all=True,
                                  append_images=processed_images[1:], dpi=(300, 300))
        print(f"PDFが保存されました: {output_path}")
    else:
        print("保存先が指定されませんでした。")

if __name__ == "__main__":
    # Tkinterのルートウィンドウを非表示にしてファイル選択ダイアログを表示
    root = Tk()
    root.withdraw()
    pdf_path = filedialog.askopenfilename(title="PDFファイルを選択", 
                                          filetypes=[("PDF files", "*.pdf")])
    if pdf_path:
        process_pdf(pdf_path)
    else:
        print("PDFファイルが選択されませんでした。")


```

</details>


## コードの説明とかこだわりポイントとか

### 色の判定
色の判定は以下の部分で行なっています。
```python
        # 各ピクセルのRGBの最大値と最小値の差を計算し、閾値より大きければ色味があると判断
        diff = np.max(img_np, axis=-1) - np.min(img_np, axis=-1)
        threshold = 30  # この閾値で色味の有無を判定
        mask = diff > threshold
```
白黒灰色の場合、RGBのそれぞれの値が等しくなるので、RGBの数値間の差が大きければ「色」があると判定するようにしました。thresholdの値を変更することで色の判定の強さを変更することができます。本当は指を認識することができたらよかったかもしれないですが、白黒の本に割り切り、このようにしました。

### PDFの大きさ
PDFには解像度以外にも大きさが設定されています。元のPDFと同じ大きさになるようにしました。
（もともと大きさは気にせず作ったんですが、GoodNoteに入れたときに「これでは不便だ」と思い追加しました）


## おわりに
「ChatGPTに聞けばすぐだろ」と思い作り始めたんですが、なんだかんだで2,3時間かかってしまいました。微妙なところをなおしたり、起動修正をしたり。当初は指を認識して消そうかと思ったんですが、あまりうまくいかず色を排除する方向に起動修正しました。色の判定方法は私が提案しています。
今回に限っていえば、自分で白塗りした方が早かったんですが、これで今度から楽ちんです。めでたしめでたし。