「変更前と変更後のPDF、どこが変わったのか分からない…」

図面やレイアウト資料だと、目視で差分を探すのは本当に大変です。しかも、見落としが起きると確認作業をやり直す羽目になりがち。

この記事では、PythonとOpenCVを使ってPDFを画像化し、線・図形・枠線などの“構造的な差分”を検出して緑色でハイライト表示する方法を解説します。

クリック操作で使えるGUI付きなので、コードに慣れていない人でも扱いやすいのがポイントです。

ただし、この方法は文字の微妙なズレ・フォント変更などの差分には弱い面もあります。
そのため「何に強くて、何に弱いのか」も含めて、実用目線でまとめました。

こんな人におすすめ

  • 図面・設計資料・レイアウトPDFの変更点を素早く見つけたい
  • 目視チェックの工数を減らしたい
  • 差分を画像として保存して、共有や証跡に使いたい

逆に、向かないケース

  • 契約書や仕様書など、文章の差分を「単語レベル」で正確に比較したい
  • フォントや字間の微調整など、文字ベースの細かい差分を検出したい

事前準備:必要なもの

  • Python(3.10〜3.12推奨)
  • Poppler(WindowsでPDFを画像化するために必要)
  • Pythonライブラリ:pdf2image / OpenCV / Pillow / numpy

🛠 Popplerのインストールと設定方法(Windows)

pdf2imageでPDFを画像に変換するには、WindowsではPopplerが必要です。
ここで詰まる人が多いので、手順どおりに進めればOKです。

  1. Popplerをダウンロード

    Poppler for Windows(GitHub)

    から最新のZIPをダウンロードします。
  2. 解凍して任意のフォルダに配置
    例:C:\poppler-xx.xx.x
  3. 環境変数 Path に bin を追加
    C:\poppler-xx.xx.x\Library\bin をPathへ追加します。
  4. 動作確認
    コマンドプロンプトで pdfinfo -v を実行し、バージョン情報が出ればOKです。

よくあるつまずき

  • Pathに追加したのに認識されない:ターミナルを開き直す(反映されていないだけのことが多い)
  • 会社PCでPathが触れない:コード側で poppler_path を指定すれば回避可能(後述)

ライブラリをインストール

pip install pdf2image pillow opencv-python numpy

※tkinterは通常Pythonに同梱されています。もしエラーが出る場合は、Pythonのインストール構成(Add Tcl/Tk)を確認してください。

PDF差分検出の仕組み(ざっくり理解)

この方法は「PDFを画像として比べる」方式です。流れはシンプル。
文字の内容を読むのではなく、線や輪郭(エッジ)を取り出して差分を比較します。

  1. PDFを画像に変換(今回はまず1ページ目)
  2. グレースケール化
  3. Canny法でエッジ検出(線・図形を抽出)
  4. エッジ同士の差分を取り、差分部分をマスク化
  5. 差分マスクを緑色でハイライト表示

図面や枠線の変更に強い一方、少しのズレでも差分が増えやすいので、後述の「調整パラメータ」が重要になります。

🖥 GUI付き:PDF差分ハイライトツール(保存機能付き)

こちらが完成版コードです。
元のコードに加えて、実用上つらい「緑まみれ」になりやすい問題を減らすために、
ノイズ除去(小さい差分を無視)感度調整用パラメータを入れています。

import os
import cv2
import numpy as np
from tkinter import Tk, filedialog, Button, Label, Frame
from PIL import ImageTk, Image
from pdf2image import convert_from_path

# =========================
# 設定(ここを環境に合わせて変更)
# =========================
POPPLER_PATH = r"C:\poppler-xx.xx.x\Library\bin" # ←実際のパスに変更

# 差分検出の感度(まず触るのはここ)
CANNY_LOW = 50
CANNY_HIGH = 150
DIFF_THRESHOLD = 30 # 大きいほど鈍感(差分が減る)
MIN_CONTOUR_AREA = 80 # 小さい差分(ゴミ)を無視する面積しきい値

# プレビュー表示サイズ(GUI上)
PREVIEW_W, PREVIEW_H = 760, 520

# =========================
# GUI
# =========================
root = Tk()
root.title("PDF差分ハイライトツール(線・図形向け)")
root.geometry("900x720")

status = Label(root, text="変更前PDFと変更後PDFを選択してください", anchor="w", justify="left")
status.pack(fill="x", padx=10, pady=10)

pdf_old_path = ""
pdf_new_path = ""
result_img_full = None # 保存用(フル解像度)

image_label = Label(root)
image_label.pack(padx=10, pady=10)

btn_frame = Frame(root)
btn_frame.pack(pady=10)

def set_status(msg: str):
status.config(text=msg)

def select_old_pdf():
global pdf_old_path
path = filedialog.askopenfilename(filetypes=[("PDF files", "*.pdf")])
if path:
pdf_old_path = path
set_status(f"変更前PDF: {os.path.basename(pdf_old_path)}\n変更後PDF: {os.path.basename(pdf_new_path) if pdf_new_path else '未選択'}")

def select_new_pdf():
global pdf_new_path
path = filedialog.askopenfilename(filetypes=[("PDF files", "*.pdf")])
if path:
pdf_new_path = path
set_status(f"変更前PDF: {os.path.basename(pdf_old_path) if pdf_old_path else '未選択'}\n変更後PDF: {os.path.basename(pdf_new_path)}")

def render_first_page(pdf_path: str):
images = convert_from_path(
pdf_path,
poppler_path=POPPLER_PATH,
first_page=1,
last_page=1
)
return np.array(images[0]) # RGB

def highlight_diff(img_old_rgb: np.ndarray, img_new_rgb: np.ndarray):
# グレースケール
old_gray = cv2.cvtColor(img_old_rgb, cv2.COLOR_RGB2GRAY)
new_gray = cv2.cvtColor(img_new_rgb, cv2.COLOR_RGB2GRAY)

# エッジ抽出
old_edges = cv2.Canny(old_gray, CANNY_LOW, CANNY_HIGH)
new_edges = cv2.Canny(new_gray, CANNY_LOW, CANNY_HIGH)

# 差分
diff_edges = cv2.absdiff(old_edges, new_edges)

# 2値化(差分マスク)
mask = cv2.threshold(diff_edges, DIFF_THRESHOLD, 255, cv2.THRESH_BINARY)[1]

# 見やすくするため少し膨張
kernel = np.ones((3, 3), np.uint8)
mask = cv2.dilate(mask, kernel, iterations=1)

# 小さい差分を除外(ノイズ対策)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cleaned = np.zeros_like(mask)
for c in contours:
if cv2.contourArea(c) >= MIN_CONTOUR_AREA:
cv2.drawContours(cleaned, [c], -1, 255, thickness=cv2.FILLED)

# ハイライト(緑)
base = cv2.cvtColor(old_gray, cv2.COLOR_GRAY2BGR)
base[cleaned > 0] = [0, 255, 0]
return base

def compare_pdfs():
global result_img_full

if not pdf_old_path or not pdf_new_path:
set_status("⚠ 変更前・変更後のPDFを両方選択してください")
return

try:
old_rgb = render_first_page(pdf_old_path)
new_rgb = render_first_page(pdf_new_path)
except Exception as e:
set_status(
"❌ PDFの画像化に失敗しました。\n"
"Popplerのパスが正しいか、pdfinfoが動くか確認してください。\n\n"
f"エラー: {e}"
)
return

# サイズ違い対策
if old_rgb.shape != new_rgb.shape:
set_status(
"⚠ 2つのPDFで画像サイズが異なります。\n"
"簡易的に『変更後』を『変更前』に合わせてリサイズして比較します(精度は落ちます)。"
)
new_rgb = cv2.resize(new_rgb, (old_rgb.shape[1], old_rgb.shape[0]))

result_img_full = highlight_diff(old_rgb, new_rgb)

# プレビュー用に縮小
preview = cv2.resize(result_img_full, (PREVIEW_W, PREVIEW_H))
preview_rgb = cv2.cvtColor(preview, cv2.COLOR_BGR2RGB)
tk_img = ImageTk.PhotoImage(Image.fromarray(preview_rgb))

image_label.config(image=tk_img)
image_label.image = tk_img

set_status(
"✅ 変更された線・図形を緑でハイライトしました(1ページ目)。\n"
"差分が多すぎる場合は DIFF_THRESHOLD / MIN_CONTOUR_AREA を調整してください。"
)

def save_image():
global result_img_full
if result_img_full is None:
set_status("⚠ 画像がまだ生成されていません(先に『差分を比較』を実行してください)")
return

save_path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG files", "*.png")]
)
if save_path:
cv2.imwrite(save_path, result_img_full)
set_status(f"💾 画像を保存しました: {os.path.basename(save_path)}")

Button(btn_frame, text="変更前PDFを選択", command=select_old_pdf, width=20).grid(row=0, column=0, padx=6, pady=6)
Button(btn_frame, text="変更後PDFを選択", command=select_new_pdf, width=20).grid(row=0, column=1, padx=6, pady=6)
Button(btn_frame, text="差分を比較(1ページ目)", command=compare_pdfs, width=22).grid(row=0, column=2, padx=6, pady=6)
Button(btn_frame, text="画像を保存(PNG)", command=save_image, width=18).grid(row=0, column=3, padx=6, pady=6)

root.mainloop()

GUIイメージ

GUI内の余白はハイライト確認用です。

サンプルイメージを実行してみる

上記サンプル画像を比較ツールにかけてどんな結果が得られるか実験してみました。

結果が下図。

違いは△の有無でしたが、ちゃんと黄緑ハイライトされているのが分かります。抽出できているようです。

次に、実務で使えるレベルなのか、私が普段使っている”図面”で試してみます。

結果が下図。

完全一致している”はず”の部分もハイライトされてしまって分かりづらいですね。PDF同士の比較をする際に微小なズレが生じているせいですかね。

ただ、ズレている部分もちゃんと分かるので、ざっくり知りたい初期比較ツールとしては使えそうです。

差分が「緑まみれ」になるときの調整方法

この手法で一番多い悩みが「ページ全体が緑になる」問題です。
だいたいは差分感度が高すぎるか、微小なノイズが拾われすぎています。

  • 差分が多すぎる:
    DIFF_THRESHOLD を上げる(例:30 → 45 / 60)
  • ゴミっぽい点が多い:
    MIN_CONTOUR_AREA を上げる(例:80 → 150 / 300)
  • 逆に差分が出ない:
    DIFF_THRESHOLD を下げる(例:30 → 15)、
    もしくはCannyの閾値(CANNY_LOW/HIGH)を調整

まずは DIFF_THRESHOLDMIN_CONTOUR_AREA の2つを触るのが最短です。

よくあるエラーと解決策

1) PDFの画像化に失敗する(Poppler関連)

  • pdfinfo -v が通るか確認
  • 通らない場合、Path設定か POPPLER_PATH が間違っている可能性大
  • 会社PCでPath変更できないなら、コード側の POPPLER_PATH 指定で回避

2) 変更前後でページサイズが違う

サイズ差があると差分が増えます。可能なら同じサイズ出力のPDF同士で比較するのが理想です。
本コードでは簡易的にリサイズして比較しますが、精度は落ちる点だけ注意してください。

📌 まとめ

PythonとOpenCVを使えば、PDFの変更箇所を画像として比較し、線・図形の差分を緑色でハイライトできます。
図面やレイアウト資料の確認作業で、目視チェックの工数を減らしたいときに便利です。

一方で、文字の微妙なズレやフォント差分などは「差分が出すぎる」傾向があります。
そういうPDFは、テキスト抽出ベースの比較と併用すると安全です。

まずは本記事のコードをコピペして動かし、差分が多い場合は
DIFF_THRESHOLDMIN_CONTOUR_AREA を調整して、目的に合う設定を探してみてください。