同じURLを二つのQRコード生成器に渡しても、見た目が大きく違う画像ができることがあります。一方は大きなマスで比較的すっきりし、もう一方は細かく複雑に見えます。どちらも同じページを開くのに、なぜマスの数が違うのでしょうか。細かい方が多くの情報を保存していたり、必ず読み取りに強かったりするのでしょうか。

この問いには、入力内容、符号化の設定、表示サイズを分けて考える必要があります。保存する内容が同じでも、白黒の模様が一つに決まるとは限りません。誤り訂正のためのデータを増やせば、マスが増える場合もあります。実際に使う際は、訂正能力と各マスを識別できる大きさを合わせて考えます。

本文は一般的な正方形の QR Code Model 2 を対象にします。ブログの既存の実在するリンクから7枚のPNGを生成し、別のソフトウェアで読み取りました。以下では公式の説明、実際のファイル実験、幾何学的な計算を区別します。スマートフォンのカメラ、印刷、遮蔽や破損の試験は行っていません。

「サイズ」を三つの量に分ける

QRコードの黒または白の小さなマスをモジュールと呼びます。モジュール数は記号の格子を、一つを何ピクセルで描くかは描画の方法を表します。画像全体の幅には周囲の余白も含まれます。すべてを単に「サイズ」と呼ぶと、描画解像度の変更と符号化容量の変更を混同しやすくなります。

通常のQRコードにはVersion 1–40があります。Version 1は21×21モジュールで、バージョンが一つ上がるごとに一辺が4モジュール増え、Version 40では177×177になります。したがってバージョンをvとすると、記号の一辺は 17+4v です。ここでのVersionは格子の規格であり、生成ライブラリのソフトウェア版ではありません。DENSO WAVE:バージョンと容量

例えばVersion 5は37×37モジュールです。一つを6×6ピクセルで描き、四辺に4モジュールずつ余白を付けると、画像全体の幅は (37+4+4)×6=270 ピクセルです。一つを12×12ピクセルに変えれば画像は大きくなりますが、格子は37×37のままで、保存するバイトも増えません。

また、見た目の黒い部分の多さを、そのまま情報量とは考えられません。格子には位置検出などのための構造があり、データ領域にはマスクも適用されます。密度を比較するなら、黒い面積よりもバージョンと一辺のモジュール数を先に確認する方が有用です。符号化の各部分はNayukiの生成手順に説明されています。Nayuki:モジュール、符号化、マスク

同じ66バイトのリンクで、誤り訂正レベルだけ変える

入力には、公開済みの流星群の記事の完全なURLを使いました。

https://gumi.ink/posts/2026-10-08-how-to-read-meteor-shower-rates/

UTF-8での長さは 66バイトです。すべての例で、単一の8ビットバイトモードのデータセグメントを明示し、黒いモジュールを白い背景に描き、一つを6ピクセルにしました。通常の例は4モジュールの余白を保ち、内容を収められるバージョンとマスクを自動選択します。最初の組ではL、M、Q、Hの誤り訂正レベルだけを変えました。

データモードを固定したのは、比較で変わる条件を減らすためです。QRコードには数字や英数字などのモードもあり、文字の組み合わせとセグメントへの分け方で容量が変わります。本文ではこれらをすべて試していません。以下は指定したURLと単一のバイトセグメントの結果であり、「あらゆる66バイトの内容に共通するバージョン表」ではありません。Nayuki:セグメントと容量の関係

生成にはqrcodeを、読み取りには別実装のzxing-cppを使いました。まずPNGを保存し、改めてそのファイルを開いて読み取っています。生成器に渡した入力を「読み取り結果」としたものではありません。

誤り訂正レベル自動選択されたバージョン記号の一辺のモジュール数余白を含む元PNG読み取った文字列が入力と完全一致
L433246×246はい
M537270×270はい
Q641294×294はい
H849342×342はい

同じリンクのLレベルのQRコード、Version 4、33×33モジュール

Lレベルの元画像は246×246ピクセルです。モジュールの辺は6ピクセル、四辺の余白はそれぞれ24ピクセルです。

同じリンクのHレベルのQRコード、Version 8、49×49モジュール

Hレベルは342×342ピクセルです。モジュールの辺と余白の幅はLレベルと同じです。ページのレイアウトに合わせて縮小表示される場合があるため、元の寸法はダウンロードしたファイルで確認できます。

これで最初の問いの一部に答えられます。同じリンクでも、誤り訂正レベルによって大きな格子が必要になることは実際にあります。 4枚から読み取った内容は増えておらず、Hレベルに二つ目のURLが入っているわけでもありません。増えたのは符号化に必要な冗長性であって、ページの内容ではありません。

強い誤り訂正には空間が必要で、最終表示も関わる

誤り訂正は、一部が汚れたり傷付いたりしてもデータを復元できる可能性を持たせます。DENSO WAVEはReed–Solomon符号を用いることと、レベルを上げるとデータ量が増えることを説明しています。L、M、Q、Hの一般的な復元割合の目安は7%、15%、25%、30%です。この割合は総コードワード数に対する説明で、コードワードは8ビットです。DENSO WAVE:誤り訂正と割合の対象、qrcode:4レベルの設定説明

この数字から「画像面積の30%をどの位置で隠してもHなら読める」とは導けません。位置検出などの構造を隠す場合もありますし、画像面積と誤りのあるコードワードの割合は同じではありません。今回は破損試験をしていないため、完全なPNGを読めた結果から、安全に入れられるロゴの大きさも求められません。

訂正データを増やして元のバージョンに収まらなくなれば、生成器は大きなバージョンを使います。モジュール当たりのピクセル数を固定すると画像も大きくなります。一方、デザインではQRコードの表示幅を固定することがあります。その場合、増えたマスは小さな空間へ配分されます。

余白を含む全体の幅を200ピクセルに固定する幾何学的な例を考えます。Lレベルの一辺は余白込みで41モジュール分なので、一つは約 200÷41=4.88 ピクセルです。Hレベルは57モジュール分で、約 200÷57=3.51 ピクセルになります。これは寸法関係の計算で、特定のスマートフォンの読み取り限界や、縮小後の読み取り試験を表す数字ではありません。

したがってHを選びながら小さな枠に収める場合は、最終表示でも各モジュールを区別できるかを確認する必要があります。訂正用の冗長性を増やすと同時に、格子も細かくなるからです。レベルを比較するときは復元割合だけで判断せず、最終サイズ、余白、実際の読み取り条件を合わせて見ます。

マスの数が同じでも、模様は変わる

二つ目の組ではMレベルを固定し、マスクを0と1に指定しました。どちらも Version 5、37×37モジュール、270×270ピクセルです。プログラムで実際のモジュール行列を比較して異なることを確認し、保存後のPNGをzxing-cppで読むと、同じ完全なリンクが得られました。

Mレベル、マスク0のQRコード

Mレベル、マスク1のQRコード

マスクは規則に従ってデータ領域の白黒配置を変えます。通常のQRコードには8種類のマスクがあり、自動選択では模様のペナルティを評価し、低いものを選びます。使用したマスクは形式情報に記録され、読み取り時にデータを戻せます。したがって、入力バイトをそのまま黒く塗った一通りの模様になるわけではありません。Nayuki:マスク選択と形式情報

この実験では、カメラでどちらのマスクが読みやすいかを比較していません。どの生成器も同じ模様を出す、と証明したわけでもありません。確認できたのは、格子の規格も読み取った内容も同じなのに、白黒の行列が異なる場合があることです。二つが同じ場所を指すかを判断するには、読み取った完全な文字列を比較します。外見だけを見ると、正しい別の符号化を内容の変更と取り違えかねません。

余白をなくしても、今回は読めた理由

QRコードの周囲の空白はクワイエットゾーンと呼ばれ、読み取り器が記号と周囲の内容を区別するのを助けます。DENSO WAVEは四辺それぞれに4モジュール幅の空白を求めています。単位はモジュールで、常に4ピクセルという意味ではありません。今回の1モジュールは6ピクセルなので、通常の余白は各辺24ピクセルです。DENSO WAVE:余白と記号の寸法

三つ目の組では、Mレベルの border を意図的に0にしました。37×37モジュールの記号は変わらず、PNGは222×222ピクセルになり、必要な4モジュールの余白を提供しなくなります。余白なしの比較画像

実際には、今回のzxing-cppでも正しいリンクを読み取れました。 この読み取り処理が、きれいで白黒の明瞭なファイルを扱えたという結果です。起きなかった失敗を実測として書くことはできません。一方、成功したからといって余白の要件を満たしたことにも、文字や図柄に近接する実際の紙面で同じように読めることにもなりません。

ここに、ファイル実験と使用環境の違いがあります。今回扱ったのは撮影ノイズのないデジタルファイルです。正式な用途には、周辺内容、距離、照明、焦点のずれが関わることがあります。最終レイアウトでも余白は保つべきで、一度の寛容な読み取り結果ではその条件を置き換えられません。本文ではこれらの環境要因をさらに模擬していません。

再現では、画像と失敗の結果を残す

今回の環境は macOS 27.0.1、arm64、Python 3.12.14、依存パッケージは qrcode 8.2、Pillow 12.3.0、zxing-cpp 3.1.1 です。生成器の設定は公式リポジトリ、別実装の読み取りバインディングの版情報は zxing-cpp 3.1.1 で確認できます。

完全な実験パッケージを展開し、そのディレクトリで固定した依存関係をインストールして実行します。

python3.12 -m venv .venv
.venv/bin/python -m pip install -r qr-density-requirements.txt
.venv/bin/python qr-density-lab.py qr-density-output

生成の主要部分は次のとおりです。完全なスクリプトには、保存、別実装による読み取り、結果の記録も含まれています。

qr = qrcode.QRCode(
    version=None,
    error_correction=qrcode.constants.ERROR_CORRECT_M,
    box_size=6, border=4,
)
qr.add_data(QRData(PAYLOAD.encode("utf-8"), mode=MODE_8BIT_BYTE))
qr.make(fit=True)

version=None と fit=True で、内容を収められるバージョンを自動的に探します。box_size はピクセルによる描画、border は余白を制御します。いずれも入力のデータ量を減らしません。qrcode:QRCodeの設定と使い方

スクリプトでは同じURLをVersion 1、Mレベルへ強制し、バージョンの自動拡大を無効にする例も試しました。実際に DataOverflowError が発生し、詳細は次のとおりでした。この条件の画像は生成されていません。

Code length overflow. Data size (540) > size available (128)

今回インストールした生成ライブラリのソースを確認すると、この二つの長さはビット単位で、540にはセグメントのヘッダーの符号化分も含まれます。指定した構成は容量不足です。小さなバージョンを指定するだけでは、完全なURLを押し込めません。実際にはバージョンの拡大を許すか、何を符号化するかを再検討します。入力を変更したなら、読み取った完全な文字列も改めて確認します。スクリプトは成功させるためにURLを切り詰めず、容量エラーを空のQRコードとして出力してもいません。

実行後には7枚のPNGと results.json が残ります。結果には環境、入力長、各画像のバージョンと寸法、読み取った文字列、マスクの行列比較、容量エラーが含まれます。スクリプト、依存関係一覧、今回の生の結果、実行説明も個別に取得できます。スクリプトは符号化とファイル読み取りだけを行い、URLにはアクセスしません。文字列を読めることと、リンク先のページを開けることは、別々に確認する必要がある段階です。

実際には、何を保つかを先に決める

通常のページリンクなら、よく使われるMレベルを比較の出発点にして、最終的な媒体で確認できます。DENSO WAVEも環境とサイズに応じた選択を説明し、Mを一般的なレベルとしています。すべての用途でMが最適だという保証ではありません。DENSO WAVE:環境に合わせたレベルの選択

まず完全な内容を決め、生成後に読み取った文字列を照合します。その上で正式なページ、ラベル、印刷見本の最終サイズと4モジュールの余白を確認し、想定する読み取り機器で試します。二つの生成器を比較する場合は、データモード、バージョン、誤り訂正レベル、マスク、余白を記録します。二枚のスクリーンショットだけでは、何を変えたのか特定しにくいからです。

同じリンクのQRコードは、格子が粗くなったり細かくなったり、単に白黒の配置が変わったりします。本文では、誤り訂正レベルが必要な容量と格子の大きさを変えること、マスクが同じ規格で模様を変えることを確認しました。適したQRコードを選ぶには、正しい内容、十分な表示空間、必要な余白を保ち、実際の用途で検証します。マスが多いほど内容が多いとは言えず、訂正レベルの高さだけで全体の読みやすさを証明することもできません。