背景
QRコードに色を付ける機能を、自分のツールにはずっと前から入れていました。
ただ、色を指定できるだけで、それが読めるかどうかは何も見ていなかった…。
「明るさの差が十分なら読めるでしょう」くらいの気持ちで放置していたのが正直なところです。
今回それを判定するツールを作ったのですが、作ってみたら自分の前提のほうが間違っていました。
間違い方が面白かったので書いておきます。
作ったもの
QRコードの前景色と背景色を入れると、次の3つを出すツールです。
- WCAG のコントラスト比
- その配色のままQRを生成して、実際にデコードできたかどうか
- 読めた場合の型番
WCAG は Web Content Accessibility Guidelines のことで、文字と背景の明るさの差をどれだけ確保すべきかを決めた指針です。
そこで使う「コントラスト比」は 1.00 から 21.00 の範囲を取ります。
白と黒の組み合わせが最大の 21.00 になります。
コントラスト比の計算
比の求め方は WCAG の定義そのままです。
下記が実際に動いているコードで、引数の hex は #1f6feb のような文字列、toRgb はそれを [31, 111, 235] に直す自前の関数です。
function luminance(hex){
var c = toRgb(hex).map(function(v){
v = v / 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
}
各チャネルを 0〜1 に直し、ガンマを戻してから、RGB それぞれに重みを掛けて足しています。
返るのは相対輝度です。
比は、この輝度の大きいほうを L1、小さいほうを L2 として (L1 + 0.05) / (L2 + 0.05) で出します。
予想が外れた2件
デコードには jsQR 1.4.0 を使いました。
ブラウザだけで画像からQRを読むライブラリです。
生成したQRを canvas に描き、getImageData で画素を取り出して渡しています。
本番に出してから実際に動かしたら、こうなりました。
-
#ff0000/#008000(赤と緑)… コントラスト比 1.28、デコード成功 -
#ffffff/#0d1117(白地に濃紺の反転)… コントラスト比 18.92、デコード失敗
赤と緑は、WCAG の基準では文字を置いてはいけないレベルの組み合わせです。
それが読めました。
逆に反転配色は比が 18.92 もあるのに読めませんでした。
最初に書いた説明は「明るさが近いと読み取りに失敗します」でした。
これは書き直しになりました。
失敗したほうの理由
反転配色が読めなかったのは、こちらの指定が原因です。
jsQR(imageData.data, w, h, { inversionAttempts: 'dontInvert' });
jsQR は既定だと、そのままで読めなかったときに白黒を反転してもう一度試します。
このツールでそれを許すと「反転すれば読めます」という結果が出てしまう。
知りたいのは目の前の配色で読めるかどうかなので、反転は試させない設定にしています。
つまりライブラリの限界ではなく、こちらの意図どおりの失敗です。
読めたほうの理由
こちらが本題です。
最初は「デコーダの明るさの求め方が WCAG と違うのだろう」と考えていました。
確かめたくて jsQR のバンドルを直接読んだのですが、輝度の係数は同じでした。
.2126*i + .7152*B + .0722*k
係数が同じなら、差は二値化のやり方にあります。
jsQR は画像を 8x8 ピクセルのブロックに分け、ブロックごとの平均輝度をしきい値にします。
コントラストが低いブロックでは、隣(左・上・左上)のブロックの平均も見てしきい値を動かします。
局所適応二値化と呼ばれる方式で、照明ムラのある写真からでも読めるようにするための工夫らしいです。
ここが分かれ目でした。
- WCAG の比は、画像全体でひとつの数字を出す大域の指標
- デコーダが要求するのは、8x8 の近所で明暗が分かれること
赤と緑は、比にすると 1.28 です。
でも輝度そのものは赤が約 0.213、緑が約 0.154 で、値としては離れています。
ブロック内でどちらが上か下かさえ決まれば、セルは読めるわけです。
比が 1.28 に見えるのは、分母が小さいと比が伸びないという数式の性質の話であって、局所的に分離できないという意味ではなかった…。
書き直した文言
FAQ をこう変えました。
- 変更前: 明るさが近いと読み取りに失敗します
- 変更後: 読み取りが不安定になります。画面上のきれいな画像なら読めても、印刷とカメラ撮影を挟むと失敗しやすいです
「失敗する」を「不安定になる」にしただけですが、意味はかなり違います。
実測で読めてしまうものを「失敗する」と書くのは、ただの嘘なので。
そのうえで、印刷やカメラを挟むとノイズと滲みが乗ってブロック内の差がさらに縮むので危ない、という話は残しました。
結論としては、コントラスト比と機械可読性は別の指標であり、片方の数字だけで可否を決めてはいけません。
配色は比を見て決めて、そのあとで必ず実際にデコードして確かめてください。
私は今回それを逆の順でやって、本番で試すまで間違った説明を書いていました。
本記事はAI補助で執筆した、個人開発の紹介記事です。