盆暗の学習記録

データサイエンス ,エンジニアリング,ビジネスについて日々学んだことの備忘録としていく予定です。初心者であり独学なので内容には誤りが含まれる可能性が大いにあります。

[OpenCV + tkinter]電子書籍の自作のための画像編集を行うアプリをPythonで作る

作業効率化のため、自分用に↑のようなアプリを作ってみました。

背景

私はとても狭い部屋に住んでいるため(&電子書籍が好きなので)、紙媒体でしか売っていない本を買った際は一度読んだら電子書籍を自炊しています。

電子書籍を自炊する際は

  1. 裁断する
  2. スキャンする
  3. 画像を編集する
  4. pdfにする

という手順で作業しているのですが、この「画像を編集する」の部分をpythonでやってみたのでどうやったのかメモしておきます。

マニアックな話題ですがもし刺さる人がいたらご覧ください。

画像の処理

ロジック

やりたいこととしては

1. 表紙(カバー)以外の画像に対して、色合いに補正をかけて文字がはっきり映るようにする
2. 全画像に対して、画像の横幅を同じサイズに揃える(縦横比は維持)

です。

表紙の処理について

表紙だけ処理を分ける理由

表紙(カバー)は基本的にツルツルの加工紙でできており、色合いが明瞭なままスキャンできます。 他方で本の中身のページは表面加工がない紙であることが多く、スキャンすると若干白っぽくなるため文字を色濃くしたいという意図があります。

表紙の判別ロジック

「モノクロ画像なら表紙とする」という判別方法をとってみることにします。 (フルカラーの本という例外もありますが数は少ないので一旦無視しています)

文字の色を濃くする処理について

具体的にどう処理するか考えていきます。

「文字の色を濃くしたい」と考えたとき、画像処理の素人の私は「コントラストを強くすれば…」と考えたのですが、画像処理でコントラストというと、もとの画像のピクセル  x \in [0,255] に対する、コントラスト(contrast)  \alpha明るさ(brightness) \beta による線形変換

 y = \alpha x + \beta

のことを指すようです(OpenCV Tutorials)。

また、ガンマ補正(gamma correction)

 y = \left( \frac{x}{255} \right)^{\gamma} \times 255

という非線形な変換もあるようです。

今回はガンマ補正を使うことにしました。理由は、今まで電子書籍を自炊するために画像を編集する際にはIrfanViewというアプリでgammaという項目を調整していて少し馴染みがあったのと、線形変換を少し試したもののαとβのちょうどいい値の組み合わせが簡単には見つからず、パラメータがγ一つですむガンマ補正のほうがパラメータの調整が簡単そうだったためです。

コード

画像の編集に関するコードは以下のようになりました。

from pathlib import Path
import numpy as np
import cv2
from PIL import Image


def gamma_correction(img: np.ndarray, gamma: float) -> np.ndarray:
    """ガンマ補正 (gamma correction)

    次のような変換を行う
        y = (x / 255)^gamma * 255
    """
    # 非線形関数(look up table)を作る
    look_up_table = np.empty((1, 256), np.uint8)
    for i in range(256):
        look_up_table[0, i] = np.clip(pow(i / 255.0, gamma) * 255.0, 0, 255)
    # 補正をかける
    return cv2.LUT(img, look_up_table)


def edit_image(input_path: Path, save_dir: Path, gamma: float = 1.6, new_width: int = 1080):
    # read
    # cv2.imread()/cv2.imwrite()はパスが日本語を含むとき文字化けしてエラーになるためPILを使う
    img = np.array([])
    with Image.open(input_path) as pil_img:
        img = np.array(pil_img)

    is_color = img.ndim == 3
    if is_color:  # カラー画像のときは、RGBからBGRへ変換する
        img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

    # gamma correction
    if not is_color:
        img = gamma_correction(img, gamma=gamma)

    # resize
    height, width = img.shape[0:2]
    new_height = round((height / width) * new_width)
    img = cv2.resize(src=img, dsize=(new_width, new_height))

    # save
    save_path = str(save_dir / input_path.name)

    if is_color:  # カラー画像のときは、BGRからRGBへ変換する
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    Image.fromarray(img).save(save_path)
    print(f"success: {input_path} -> {save_path}")

if __name__ == '__main__':
    source_dir = Path('./sample_data')
    save_dir = Path('./output')
    save_dir.mkdir(exist_ok=True)

    paths = list(source_dir.glob('*.jpg'))
    for path in paths:
        edit_image(input_path=path, save_dir=save_dir)

アプリ化

pythonスクリプト単体のままでは使いにくいので、GUIつきのアプリにしました。

GUIにするライブラリはpython標準ライブラリのtkinterを使うことにしました。flutterのラッパーのfletDearPyGuiなどのモダンなGUIライブラリも使ってみたかったのですが、「ファイルをドラッグ&ドロップできる」という機能が見当たらなかった一方でtkinterならドラッグ&ドロップが簡単に実装できそうだったので、楽な道を選ぶことにしました。

アプリ部分のコードはけっこう長くなってしまったのでGithubにあげておきます。

github.com

ビルドしたexeファイルもreleaseのところに置いておきますので、もしご興味がおありでしたらお試しください。