PDFからQRコードを読み取る
— PDF→画像変換編
1. 全体の流れ
PDF 内の QR コードを Python で取得するには、以下の 2 ステップが基本です。PDF は直接 QR コードを読み取れないため、まずページを画像に変換してから QR デコードを行います。
必要なライブラリの全体像
| 役割 | ライブラリ | 特徴 |
|---|---|---|
| PDF → 画像変換 | pdf2image |
Poppler ベース。DPI 指定が柔軟。Windows では別途 Poppler が必要 |
| PDF → 画像変換 | PyMuPDF(fitz) |
pip のみで完結。高速。Windows 対応良好 |
| QR コード読取 | pyzbar |
ZBar ベース。QR・バーコード両対応。軽量で高速 |
| QR コード読取 | OpenCV(cv2) |
追加依存なし。OpenCV 4.x 以降で QR 検出器内蔵 |
2. QRコード読取ライブラリの選択
QR コード読取には主に pyzbar と OpenCV の QRCodeDetector の 2 択です。まずそれぞれのインストール方法を確認します。
pyzbar のインストール
pyzbar は ZBar ライブラリの Python バインディングです。QR コードだけでなく Code128・EAN などバーコード全般も読み取れます。
# pyzbar 本体
pip install pyzbar
# Pillow も必要(画像の受け渡しに使用)
pip install Pillow
pip install pyzbar[scripts] を実行するか、公式 README に従って libiconv.dll と libzbar-64.dll(または 32bit 版)を PATH の通った場所に配置してください。
OpenCV のインストール
OpenCV 4.x 以降には QR コード検出器が標準搭載されています。すでに PART 05 でインストール済みの場合は不要です。
# OpenCV(GUI 不要のサーバー向け)
pip install opencv-python-headless
# または GUI 機能込み(デスクトップ環境向け)
pip install opencv-python
3. PDF→画像変換:pdf2image の使い方
pdf2image は Poppler の pdftoppm コマンドをラップした Python ライブラリです。DPI(解像度)を自由に指定でき、高解像度な画像変換が得意です。
インストール
# pdf2image のインストール
pip install pdf2image
# ── Poppler のインストール(別途必要)──────────────────────────────
# ▼ Windows
# https://github.com/oschwartz10612/poppler-windows/releases
# 上記から最新版をダウンロードして解凍し、
# bin フォルダのパスを環境変数 PATH に追加するか、
# convert_from_path(..., poppler_path=r"C:\poppler\bin") で直接指定する
# ▼ macOS(Homebrew)
# brew install poppler
# ▼ Ubuntu / Debian
# apt-get install poppler-utils
基本的な使い方 — 全ページを変換
from pdf2image import convert_from_path
# PDF の全ページを Pillow Image のリストとして取得
# dpi=200 以上を推奨(QR コードは細かいパターンのため)
pages = convert_from_path("sample.pdf", dpi=200)
print(f"総ページ数: {len(pages)}")
for i, page in enumerate(pages):
print(f" ページ {i+1}: サイズ={page.size}, モード={page.mode}")
# 画像を保存したい場合(確認用)
# page.save(f"page_{i+1:03d}.png", "PNG")
特定ページのみ変換
from pdf2image import convert_from_path
# first_page / last_page で変換するページ範囲を指定(1始まり)
pages = convert_from_path(
"sample.pdf",
dpi=200,
first_page=1, # 変換開始ページ
last_page=1 # 変換終了ページ(1ページ目のみ)
)
first_page_img = pages[0]
print(f"ページ1のサイズ: {first_page_img.size}")
Windows で Poppler パスを直接指定する場合
from pdf2image import convert_from_path
# Windows で Poppler の bin フォルダを直接指定する場合
pages = convert_from_path(
"sample.pdf",
dpi=200,
poppler_path=r"C:\poppler\poppler-24.08.0\Library\bin" # ← 実際のパスに変更
)
dpi=150 は最低ライン、dpi=200〜300 が実用的な推奨値です。PDF ページが大きく QR コードが小さい場合は dpi=300 以上を使いましょう。
4. PDF→画像変換:PyMuPDF(fitz)の使い方
PyMuPDF は fitz という名前でインポートします。pip のみで完結するため、Poppler 不要で Windows でも手軽にセットアップできます。処理速度も pdf2image より速い傾向があります。
インストール
# PyMuPDF のインストール(外部依存なし・pip のみで完結)
pip install PyMuPDF
基本的な使い方 — 全ページを変換
import fitz # PyMuPDF
from PIL import Image
import io
def pdf_to_images_pymupdf(pdf_path: str, dpi: int = 200) -> list:
"""
PDF の全ページを Pillow Image のリストに変換する(PyMuPDF 版)
Args:
pdf_path: PDF ファイルパス
dpi: 出力解像度(QR コード用に 200 以上推奨)
Returns:
list[PIL.Image.Image]
"""
doc = fitz.open(pdf_path)
images = []
# zoom = dpi / 72 (PDF の標準解像度は 72 DPI)
zoom = dpi / 72
mat = fitz.Matrix(zoom, zoom) # 拡大行列
for page_num in range(len(doc)):
page = doc[page_num]
# ページをピクセルマップとしてレンダリング
pix = page.get_pixmap(matrix=mat)
# PNG バイト列に変換 → Pillow Image に変換
img_bytes = pix.tobytes("png")
pil_img = Image.open(io.BytesIO(img_bytes))
images.append(pil_img)
print(f"ページ {page_num + 1}: サイズ={pil_img.size}")
doc.close()
return images
# 使用例
pages = pdf_to_images_pymupdf("sample.pdf", dpi=200)
print(f"総ページ数: {len(pages)}")
特定ページのみ変換
import fitz
from PIL import Image
import io
def get_page_image(pdf_path: str, page_number: int = 0, dpi: int = 200) -> Image.Image:
"""
指定ページだけを画像に変換する
Args:
pdf_path: PDF ファイルパス
page_number: ページ番号(0 始まり)
dpi: 出力解像度
Returns:
PIL.Image.Image
"""
doc = fitz.open(pdf_path)
page = doc[page_number] # 0 始まりでアクセス
zoom = dpi / 72
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat)
img = Image.open(io.BytesIO(pix.tobytes("png")))
doc.close()
return img
# 1 ページ目(index=0)だけ取得
img = get_page_image("sample.pdf", page_number=0, dpi=200)
print(f"1ページ目のサイズ: {img.size}")
pdf2image と PyMuPDF の比較
| 項目 | pdf2image | PyMuPDF(fitz) |
|---|---|---|
| 外部依存 | 要 Poppler | pip のみ |
| Windows 対応 | Poppler の配置が必要 | そのまま動く |
| 処理速度 | 普通 | 速い |
| DPI 指定 | dpi= で直接指定 | zoom = dpi/72 で計算 |
| ページ指定 | first_page / last_page | インデックスで直接指定 |
5. pyzbar でQRコードを読み取る
pyzbar は Pillow Image を直接受け取れるため、PDF→画像変換と非常に相性が良いです。
基本的な使い方
from pyzbar.pyzbar import decode
from PIL import Image
# 画像ファイルから QR コードを読み取る
img = Image.open("sample_with_qr.png")
decoded_objects = decode(img)
print(f"検出した QR コード数: {len(decoded_objects)}")
for obj in decoded_objects:
# obj.type : シンボルの種類("QRCODE", "EAN13" など)
# obj.data : デコードされたバイト列
# obj.rect : 位置情報(left, top, width, height)
# obj.polygon: 4 頂点の座標リスト
value = obj.data.decode("utf-8") # bytes → 文字列に変換
print(f"種類 : {obj.type}")
print(f"値 : {value}")
print(f"位置 : left={obj.rect.left}, top={obj.rect.top}, "
f"width={obj.rect.width}, height={obj.rect.height}")
print(f"頂点 : {obj.polygon}")
print("---")
QR コードの値だけをリストで取得
from pyzbar.pyzbar import decode
from PIL import Image
def extract_qr_values(pil_image: Image.Image) -> list[str]:
"""画像から QR コードの値をリストで返す(QR コードのみフィルタ)"""
objects = decode(pil_image)
return [
obj.data.decode("utf-8")
for obj in objects
if obj.type == "QRCODE"
]
img = Image.open("sample_with_qr.png")
values = extract_qr_values(img)
print(values)
# ['https://example.com/item/12345', '管理番号:ABC-9876']
obj.data はバイト列(bytes)です。通常は UTF-8 でデコードできますが、旧システムで作成された QR コードは Shift-JIS の場合があります。その場合は obj.data.decode("shift_jis") を試してください。
6. OpenCV でQRコードを読み取る
OpenCV 4.x には QRCodeDetector クラスが内蔵されています。pyzbar のような追加依存なしで使えるため、OpenCV をすでに使っている環境に適しています。
基本的な使い方
import cv2
import numpy as np
from PIL import Image
def decode_qr_opencv(pil_image: Image.Image) -> list[dict]:
"""
OpenCV の QRCodeDetector で QR コードを検出・デコードする
Args:
pil_image: Pillow Image オブジェクト
Returns:
list of dict: {'value': str, 'polygon': list}
"""
# Pillow Image → NumPy 配列(BGR 形式)に変換
img_np = np.array(pil_image.convert("RGB"))
img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
# QRCodeDetector を初期化
detector = cv2.QRCodeDetector()
# detectAndDecodeMulti: 画像内の全 QR コードを一括検出・デコード
# retval: 検出成功フラグ(bool)
# decoded_info: デコードされた文字列のタプル
# points: 各 QR コードの 4 頂点座標
# straight_qrcode: 正規化された QR コード画像
retval, decoded_info, points, _ = detector.detectAndDecodeMulti(img_cv)
results = []
if retval and decoded_info:
for value, pts in zip(decoded_info, points):
if value: # 空文字(デコード失敗)はスキップ
polygon = pts.astype(int).tolist()
results.append({
'value': value,
'polygon': polygon
})
return results
# 使用例
img = Image.open("sample_with_qr.png")
qr_results = decode_qr_opencv(img)
print(f"検出した QR コード数: {len(qr_results)}")
for r in qr_results:
print(f" 値 : {r['value']}")
print(f" 頂点 : {r['polygon']}")
OpenCV の QR 読取:1つだけ検出する場合
import cv2
import numpy as np
from PIL import Image
def decode_qr_single_opencv(pil_image: Image.Image) -> str | None:
"""
画像内の最初の QR コードをデコードして値を返す
見つからない場合は None を返す
"""
img_np = np.array(pil_image.convert("RGB"))
img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
detector = cv2.QRCodeDetector()
# detectAndDecode は最初の1つのみ検出
value, points, _ = detector.detectAndDecode(img_cv)
if value:
return value
return None
img = Image.open("sample_with_qr.png")
result = decode_qr_single_opencv(img)
print(f"QR コードの値: {result}")
7. PDF→画像変換 + QR読取の統合スクリプト
PyMuPDF で PDF をページ画像に変換し、pyzbar で QR コードを読み取る統合スクリプトです。
"""
qr_from_pdf.py
PDF のページを画像に変換し、各ページの QR コードを読み取るスクリプト
"""
import io
import fitz # PyMuPDF
from PIL import Image
from pyzbar.pyzbar import decode
def pdf_page_to_image(pdf_path: str, page_number: int, dpi: int = 200) -> Image.Image:
"""PDF の指定ページを Pillow Image に変換する"""
doc = fitz.open(pdf_path)
page = doc[page_number]
zoom = dpi / 72
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat)
img = Image.open(io.BytesIO(pix.tobytes("png")))
doc.close()
return img
def read_qr_from_image(pil_image: Image.Image) -> list[dict]:
"""Pillow Image から QR コードを読み取り、値と位置情報を返す"""
objects = decode(pil_image)
results = []
for obj in objects:
if obj.type == "QRCODE":
results.append({
'value': obj.data.decode("utf-8", errors="replace"),
'rect': obj.rect, # Rect(left, top, width, height)
'polygon': obj.polygon # [Point(x,y), ...]
})
return results
def scan_pdf_for_qr(pdf_path: str, dpi: int = 200) -> dict:
"""
PDF の全ページを走査して QR コードを抽出する
Returns:
{
'total_pages': int,
'pages': {
page_num: [{'value': str, 'rect': ..., 'polygon': ...}, ...]
},
'all_values': [str, ...] # 全ページの QR 値フラット
}
"""
doc = fitz.open(pdf_path)
total_pages = len(doc)
doc.close()
result = {'total_pages': total_pages, 'pages': {}, 'all_values': []}
for page_num in range(total_pages):
img = pdf_page_to_image(pdf_path, page_num, dpi=dpi)
qr_list = read_qr_from_image(img)
if qr_list:
result['pages'][page_num + 1] = qr_list
for qr in qr_list:
result['all_values'].append(qr['value'])
print(f"ページ {page_num + 1}: {len(qr_list)} 件検出")
else:
print(f"ページ {page_num + 1}: QR コードなし")
return result
# ── メイン ────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
pdf_path = sys.argv[1] if len(sys.argv) > 1 else "sample.pdf"
result = scan_pdf_for_qr(pdf_path, dpi=200)
print("\n" + "=" * 50)
print(f"総ページ数: {result['total_pages']}")
print(f"QR コード検出総数: {len(result['all_values'])}")
print("\n【検出した QR コードの値】")
for page_num, qr_list in result['pages'].items():
for qr in qr_list:
print(f" p.{page_num:>3} {qr['value']}")
8. 複数ページ PDF を一括処理する
ページ数が多い PDF を効率よく処理するためのテクニックを紹介します。
全ページ一括変換してから QR スキャン
import io
import fitz
from PIL import Image
from pyzbar.pyzbar import decode
def scan_all_pages(pdf_path: str, dpi: int = 200) -> list[dict]:
"""
PDF の全ページを処理し、QR コードが見つかったページの情報を返す
Returns:
list of dict: [{'page': int, 'value': str, 'rect': Rect}, ...]
"""
doc = fitz.open(pdf_path)
zoom = dpi / 72
mat = fitz.Matrix(zoom, zoom)
all_results = []
for page_num in range(len(doc)):
page = doc[page_num]
pix = page.get_pixmap(matrix=mat)
pil_img = Image.open(io.BytesIO(pix.tobytes("png")))
for obj in decode(pil_img):
if obj.type != "QRCODE":
continue
try:
value = obj.data.decode("utf-8")
except UnicodeDecodeError:
value = obj.data.decode("shift_jis", errors="replace")
all_results.append({
'page': page_num + 1, # 1 始まり
'value': value,
'rect': obj.rect
})
doc.close()
return all_results
# 使用例
results = scan_all_pages("sample.pdf", dpi=200)
print(f"合計 {len(results)} 件の QR コードを検出")
for r in results:
print(f" p.{r['page']:>3} {r['value']}")
メモリ節約:ページごとに逐次処理
ページ数が多い(数十ページ以上)PDF の場合、全ページを一度にメモリに乗せると重くなります。ページごとに変換→読取→破棄するのが効率的です。上記 scan_all_pages 関数はすでにこの方式を採用しています(全画像をリストに溜めずページごとに処理)。
9. ライブラリ比較まとめ
| 組み合わせ | セットアップ難度 | 検出精度 | 処理速度 | Windows |
|---|---|---|---|---|
| PyMuPDF + pyzbar(推奨) | 低 | 高 | 速い | DLL 配置が必要 |
| PyMuPDF + OpenCV | 最低 | 中 | 速い | 問題なし |
| pdf2image + pyzbar | 中 | 高 | 普通 | Poppler + DLL が必要 |
| pdf2image + OpenCV | 中 | 中 | 普通 | Poppler が必要 |
Windows 環境で最も手軽なのは PyMuPDF + OpenCV(pip のみで完結)。
検出精度を最大化したいなら PyMuPDF + pyzbar(DLL のセットアップが追加で必要)。
次の PART 11 では、特定エリア(右上・左上など)に絞って QR コードを読み取る方法を解説します。