Scanned PDF cleaner
スマートフォンで開いた本を撮影すると、ノド付近で湾曲し、傾き、グレーがかって、端に親指や隣のページの切れ端が写り込みます。このスキルは、それらの写真を印刷物のようにきれいなPDFに変換します。
スクリプトがピクセル処理を行います。あなたは判断を行います。各ページの四隅をトレースし、除去された結果を確認し、テキストブロック外のマークのうち印刷されたもの(保持)とそうでないもの(除去)を決定します。各ステップでコンタクトシートが画像として出力されるので、必ず確認してください。ステップが正しく機能したと想定しないでください。
セットアップ(一度だけ実行)
SKILL=<このスキルのフォルダ> # SKILL.mdが格納されているフォルダ
python3 -m venv ~/.cache/scanned-pdf-cleaner-venv
~/.cache/scanned-pdf-cleaner-venv/bin/pip install -q -r "$SKILL/requirements.txt"
PY=~/.cache/scanned-pdf-cleaner-venv/bin/python
以降のコマンドはすべて cd "$SKILL/scripts" && $PY <スクリプト> ... として実行します。ドキュメントごとに新しい空の作業フォルダを、ユーザーのプロジェクトの外(スクラッチまたは一時フォルダ)に作成してください。ユーザーのファイルは読み取りのみ行われます。
ワークフロー
1. 準備
$PY prepare.py INPUT.pdf WORK # または画像フォルダ、画像ファイル群(HEICは事前に変換)
WORK/src/p-NNN.jpg(正立化された写真)、WORK/grid/g-NNN.jpg(各写真に100pxごとの赤いグリッド、四辺に100単位のラベル付き)、および各写真の仮の輪郭を含むWORK/rough.jsonを出力します。
写真が横向きまたは上下逆の場合、次のコマンドで回転させます: $PY prepare.py --turn WORK p-003.jpg 90|180|270(時計回り)。
2. ページの四隅をトレース(あなた)
すべてのグリッド画像を読みます。各写真についてドキュメントに属するページを見つけ、その四隅をソースピクセル単位でWORK/rough.jsonに書き込みます。順序はページの読み方向に従って左上、右上、右下、左下です。グリッドラベルが横7、縦12の場合、[700, 1200]と記述します。
{"001": {"src": "p-000.jpg", "quad": [[96,72],[930,92],[945,1468],[70,1480]]}}
- キーは出力ページの順序(
001、002、...)です。この順序が読み方向と一致している必要があります。 - ページの紙の端を取得します。テキストではありません。ノド付近では、ページが折り返して溝に消えていくラインを取得します。写真の端からはみ出ているページは、写真の端を四隅とします。
- 見開きの写真の場合、同じ
srcに対して2つのエントリ(各ページ1つ)を作成します。例:"005"と"006"。 - 30ピクセル以内の精度で十分です。次のステップで実際のエッジにスナップされます。
3. 紙のエッジにスナップ
$PY refine.py WORK
WORK/quads.jsonとWORK/sheets/quads-NN.jpgを出力します(オレンジ=あなたのトレース、赤=スナップ後、緑のドット=左上)。シートを確認します。赤い輪郭が影や机、隣のページに飛び移っていた場合、そのトレースを修正してステップを再実行します。
4. フラット化、トーン調整、エッジクリーニング
$PY clean.py WORK # または clean.py WORK 004 007 で特定ページのみ再実行
各ページに対して以下を実行します:
- テキスト行と両端揃えされたカラムエッジを検出;
- テキストブロックを正方形化し、各行に1つの滑らかなベンド(曲線)面をフィットさせ、湾曲した行を真っ直ぐに;
- 紙を白、インクを黒にトーニング;
- ページの端に接する暗いマーク(指、ノドのカール、折り目、隣ページの切れ端)を除去。
テキスト行が5行未満のページ(タイトルページや全ページ図など)は、単純な四隅ワープが適用されます。出力: WORK/pages/、WORK/blocks.json、WORK/sheets/review-NN.jpg(赤=除去、青=判断保留、緑=テキストブロック)。
レビューシートを確認します。注意すべき点:
- 行がまだ湾曲している、またはページが傾いている: トレースがずれています。
rough.jsonを修正し、そのページに対してrefineとcleanを再実行します。 - 緑のボックス内のテキストが赤で表示されている: ユーザーに伝えます。エッジクリーニングは、テキストがページ端のバンド上にある場合を除き、ブロック内のテキストを除去しません。
5. テキストブロック外のマークをリストアップ
$PY flags.py WORK
WORK/flags/flags-N.jpgを出力します。テキストブロック外の暗いマーク1つ1つに番号が付けられたタイル画像です。
6. 保持または除去の判定(あなた)
フラグシートを確認し、WORK/decisions.jsonに書き込みます:
{"keep": [0, 3, 4, 7], "strokes_only": [23], "erase": [{"page": "014", "box": [1500, 300, 80, 900], "mode": "strokes"}]}
- keep: 印刷されたものすべて。これにはページ番号、柱(ランニングヘッド)、章番号、キャプション、サイドラベル、表の罫線、図、イラストが含まれます。印刷物は鮮明で規則的です。本の中のイラスト(たとえ子供の落書きのようなものでも)は印刷物として扱います。
- remove: 番号をリストから外します。これには手書き文字、ペンや鉛筆のメモ、汚れ、斑点、指紋、グレーのバー、破れた端の影が含まれます。ペンの線は細く、不均一でしばしば斜めで、フォントと一致しません。
- strokes_only: 印刷された文字の上または横にペンで引かれた線。1.5行より長いストロークのみを除去します。
- erase: ページピクセル単位のボックスを
pages/page-KEY.jpg上に指定し、テキストブロック内にあって除去を依頼されたマークを対象とします。"mode": "all"はボックス内を白く塗りつぶし、"strokes"は長いストロークのみを除去します。
ユーザーが除去したがる可能性があるものについては、事前に確認してください。これには、テキスト内のペンによる下線や行間のメモ、蛍光ペンが含まれます。デフォルト: 余白のメモは除去、テキスト内の下線は保持。印刷物とペン書きの区別がつかない場合も確認してください。タイル画像を見せて、説明だけでは伝えないでください。
7. 仕上げ
$PY finish.py WORK OUT.pdf # --no-frame で各ページの独自フレームを維持; --dpi デフォルト300
処理内容:
- テキストブロック外の領域を、保持対象を除いて白く塗りつぶします。
- ブロックの側面に密着した細くて長いペンのブラケット(括弧)を除去します。
- すべてのページを1つのページサイズにフレーミング:
- テキストのサイズを統一: 近くで撮影された写真も遠くで撮影された写真も同じテキストサイズになります。スケーリングにはテキストブロックの幅、または図によってカラムが狭くなっている場合は行ピッチを使用します。
- サイドマージンを均等にするため、一方の端に寄っていたり、本の厚みで中央からずれていたコンテンツが中央揃えになります。
- すべてのページの上部マージンを同じにします。
引き渡す前に WORK/sheets/final-NN.jpg を確認してください。各ページに残存する汚れ、欠けたテキスト、誤って保持したものがないかチェックします。その後、ユーザーにPDFのパスを伝えます。特に指定がない限り、PDFは入力ファイルの隣に保存します(例:<元のファイル名>_clean.pdf)。
理想的な仕上がり
- テキスト行が端から端まで真っ直ぐで水平。ノドに最も近い行も含みます。
- 紙面は純白、文字は均一な黒で、細いストロークも保持。大きな暗い図は塗りつぶしを維持。
- 指、影、隣ページの切れ端、斑点がない。サイドマージンは均等で、すべてのページで同じテキストサイズ。
実際の使用から得られた注意点
- 補正機能はテキストを読み取るため、両端揃えの行が多いページで最も効果的です。右横書き(ヘブライ語、アラビア語)を含むあらゆる文字体系で動作します。行の形状のみを参照するためです。
- 図の中の短い行(ラベルなど)はベンドフィッティング時に無視されるため、図形がページを折り曲げることはありません。
- ページの大部分を占める暗い図は、単純なトーニングでは灰色ににじんでしまいます。
tone()は周囲の紙から推定される紙の値を低く抑えます。 - ページのカールによりノド付近の数文字が写真で復元できない場合、その旨を伝え、テキストを創作しないでください。
- 2段組みの論文: テキストブロックは両カラムをまたぎます。レビューシートで、ノドをまたいでフィットされた線を確認してください。