PythonでQRコードを塗りつぶす
はじめに
とある日のイベントにて、会場のパネルに写っているQRコードをその場で読み取って記録していくデジタルスタンプラリーを体験してきました。
推しのパネルはSNSに上げたい!けど、会場に来ないと見れないパネルなのでそのままアップするのには抵抗がある…
なので手作業でQRコードを塗りつぶしていました。
推しだけならまだしも全メンバー手作業で塗りつぶすの大変だな…と思い、QRコードを塗りつぶすプログラムを作成しました。
QRコードを塗りつぶした画像
ソースコード
さくっとソースを見たい方はこちら↓
やってることはシンプルで、
- 画像からQRコードを検出する
- 塗りつぶす
以上です。
(画像によっては検出できないこともあります 今後の課題です)
プログラムについて
すんなりできると思ってたのですが、ちょっとつまづいた部分もあったのでそれに関してもメモしていきます。
環境
Pythonを使います。
venv
で環境を作り、そこに入っておきます。
❯ python -V
Python 3.9.13
❯ python -m venv .venv
❯ source .venv/bin/activate
(ここでは3.9.13を使っていますが他のバージョンでも動くと思います。)
QRコード検出~塗りつぶし
PythonでQRコードを検出するライブラリにはpyzbarもありましたが、OpenCVも使えそうだったので、親しみのあるOpenCVを選びました。
pip install opencv-python
動作確認もかねて、先駆者様のドキュメントから「QRコード検出→枠線を描画」を試してみます。
今回使うQRコード
こちらのサイトで作りました。
import cv2
# 画像を読み込み
img = cv2.imread("qrcode.jpg")
# QRコード検出インスタンス生成
qr = cv2.QRCodeDetector()
# 検出実行
data, points, rectified_image = qr.detectAndDecode(img)
# バウンディングボックスを描画
points = points.astype(int)
# cv2.polylines(画像, 各点の座標, 開始と終了を結ぶかどうか, 線の色, 線の太さ, 線のタイプ)
cv2.polylines(img, [points], True, (0, 255, 0), 5, cv2.LINE_AA)
# 保存
cv2.imwrite("detected_qrcode.jpg", img)
検出したQRコードの枠に沿って線が描画されました。
枠の中を塗りつぶすには、cv2.polyLines
をcv2.fillPoly
に置き換えます。
- cv2.polylines(img, [points], True, (0, 255, 0), 5, cv2.LINE_AA)
+ cv2.fillPoly(img, [points], (0, 255, 0)
枠の中が塗りつぶされました。
これでOKと思いきや、画像によってはQRコードが検出できない問題がありました。
画像の中の小さいQRコードを検出する(WeChatQRCode)
というのも、検出に失敗しているのは「画像内に写っているQRコードが小さいもの」でした。
OK | NG |
---|---|
※別のQRコードに置き換えています |
これはOpenCVのサードパーティとして開発されているWeChatQRCode
を使うことで解決しました。
CNNベースのライブラリ…なんかすごく検出できそう!
デフォルトには組み込まれていないので、サードパーティのライブラリをpipで入れます。
pip install opencv-contrib-python
また、WeChatQRCode
インスタンスの初期化時にCNNモデルのアーキテクチャファイル(.prototxt)と重みファイル(.caffemodel)が必要なので、リポジトリからダウンロードしておきます。
では早速検出です。
import cv2
fname = "small_qr_code.jpg"
img = cv2.imread(fname)
# インスタンス初期化
qr = cv2.wechat_qrcode.WeChatQRCode(
"/path/to/detect.prototxt",
"/path/to/detect.caffemodel",
"/path/to/sr.prototxt",
"/path/to/sr.caffemodel")
# 検出
data, points = qr.detectAndDecode(img)
if len(data) > 0 and len(points):
# 戻り値がタプルなので取り出す
data = data[0]
points = points[0]
points = points.astype(int)
# 検出できていたら塗りつぶす
cv2.fillPoly(img, [points], (0, 255, 0))
cv2.imwrite("small_qr_code_fill.jpg", img)
塗りつぶせました!
WeChatQRCodeのロジックについては深追いしませんが、検出器モデルと超解像モデル(画像の解像度を向上する)のおかげで検出精度が上がってるみたいです。
最後に
これで全てのメンバーのQRが塗りつぶせたらよかったのですが、一部検出できないものもありました。
ここは今後の課題です。
また、QRコードを検出できれば位置がわかるので、別のQRコードに置き換えてしまうなんてこともできますね。
試しに作ってみましたのでよかったら見ていってください。
参考リンク
Discussion