投稿日:2025年10月12日

以下のブログで紹介した技術を用い、ラズパイカメラをQRコードリーダーとして活用しました。
※QRコードは株式会社デンソーウェーブの登録商標です。

今回は、この仕組みを応用してイベントや来場者受付を自動化するシステムを構築してみました。
QRコードを読み取るだけで出席チェックが完了し、手作業での確認を大幅に省力化できます。

ハードシステム構築

▲Raspberry Pi Zero 2 W(以下、Zero 2 W)とカメラを固定するため「Raspberry Pi Zero用 三脚&V3カメラマウントキット「RPZ-CamMountKit-V」」を購入しました。

▲付属のケーブルでZero 2 Wとカメラを接続します。
図の様な位置関係で取り付けます。

▲基盤が剥き出しでは不安なので、以前購入したケースを装着した状態で取り付けてみました。

▲普段、デジカメの外部モニターとして使用している7inchモニターを繋げます。
ケーブルを短いものにしたり、三脚をスタイリッシュなものにしたり、改善の余地は多々あります。

ソフトウェア

メインプログラムは Python で書きます。
Zero 2 Wでのブラウザ操作は動作が遅く、使えたものではありません。
フィールド値を更新するPHPをサーバーに設置し、Zero 2 WからそのPHPにアクセスして更新するようにします。

qr_camera_app.py

import tkinter as tk # GUIライブラリ、tkinterモジュールを読み込む
import requests # HTTP通信を行うライブラリ
from picamera2 import Picamera2 # カメラ制御用ライブラリ(picamera2)からPicamera2クラスを読み込む
import cv2 # 画像処理ライブラリ cv2を読み込む
from pyzbar import pyzbar #QRコード解析ライブラリ(pyzbar)からpyzbarモジュールを読み込む
import time # 時間に関する処理、timeを読み込む

SERVER_URL = "https://xxxxxxxxxxx/update.php"

def update_flag(id_num):
    try:
        r = requests.get(f"{SERVER_URL}?id={id_num}&flg=2")
        data = r.json()
        if data.get("status") == "success":
            record = data.get("data", {})
            update_ui(record)
        else:
            update_ui({"error": data.get("message")})
    except Exception as e:
        update_ui({"error": str(e)})

def update_ui(record):
    # 一旦クリア
    for w in frame_result.winfo_children():
        w.destroy()

    if "error" in record:
        tk.Label(frame_result, text=f"エラー: {record['error']}", fg="red").pack(anchor="w")
        return

    tk.Label(frame_result, text=f"No: {record.get('id')}", font=("Arial", 14)).pack(anchor="w")
    tk.Label(frame_result, text=f"申込日時: {record.get('created_at')}", font=("Arial", 14)).pack(anchor="w")
    tk.Label(frame_result, text=f"分科会: {record.get('special_lecture')}", font=("Arial", 14)).pack(anchor="w")
    tk.Label(frame_result, text=f"お名前: {record.get('name')}({record.get('name_kana')})", font=("Arial", 14)).pack(anchor="w")
    tk.Label(frame_result, text=f"email: {record.get('email')}", font=("Arial", 14)).pack(anchor="w")

# Tkinter ウィンドウ
root = tk.Tk()
root.title("QRコード読み取り結果")
root.geometry("500x300")

frame_result = tk.Frame(root)
frame_result.pack(fill="both", expand=True, padx=10, pady=10)

# カメラ初期化
picam2 = Picamera2()
picam2.preview_configuration.main.size = (640, 480)
picam2.preview_configuration.main.format = "RGB888"
picam2.configure("preview")
picam2.start()

last_id_num = None

def capture_loop():
    global last_id_num

    frame = picam2.capture_array()
    decoded_objs = pyzbar.decode(frame)

    for obj in decoded_objs:
        id_num = obj.data.decode("utf-8")
        if id_num != last_id_num:
            last_id_num = id_num
            update_flag(id_num)
            time.sleep(1)

    # 50ms ごとにループ
    root.after(50, capture_loop)

# ループ開始
capture_loop()

# Tkinterのウィンドウを開いたまま、QRコード読み取りを受け付け続ける
root.mainloop()

# 終了処理
picam2.stop()
cv2.destroyAllWindows()

1〜6行目、読み込むライブラリ。
8行目、サーバーに設置したデータベースを操作するPHPを指定。
10〜19行目、QRコード受付システムの心臓部。
QRコードで読み取った id番号(id_num) をサーバーに送信し、そのデータを更新(flg=2に変更)したあと、結果を UI(画面)に反映 するための関数です。
22〜29行目、PythonのUI(tkinter)に結果を表示する部分。31〜35行目のUIに出力します。
38〜43行目、QRコードの読み取り結果を表示するウィンドウを作成。
52~65行目、カメラで撮影した画像をPicamera2で取得し、pyzbarでQRコードを解析します。前回と異なるIDが読み取られた場合のみ、update_flag() 関数を実行してサーバーへ送信し、データベースのflgを更新。その結果をUIに反映する処理を行います。
同じQRコードを連続で処理しないように1秒間の待機(time.sleep(1))も入っています。

update.php

サーバー側でQRコードのIDを受け取り、対応するレコードのflg値を更新し、更新後の情報をJSON形式で返すPHPになります。

<?php
// =========================================
// CORSとレスポンス設定(外部からのアクセスを許可)
// =========================================
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json; charset=UTF-8");

// =========================================
// データベース接続情報(本番用)
// =========================================
$dsn = 'mysql:host=xxxxxxxxxxxxxx;dbname=xxxxxxxxxxxxxx;charset=utf8mb4';
$user = 'xxxxxxxxxxxxxx';
$pass = 'xxxxxxxxxxxxxx';

try {
    // =========================================
    // PDOでデータベースに接続
    // =========================================
    $pdo = new PDO($dsn, $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 例外モード
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);         // エミュレーション無効(セキュリティ向上)

    // =========================================
    // GETパラメータ(id, flg)が指定されているか確認
    // =========================================
    if (isset($_GET['id']) && isset($_GET['flg'])) {
        $id = (int)$_GET['id'];   // idを整数に変換
        $flg = (int)$_GET['flg']; // flgを整数に変換

        // =========================================
        // customersテーブルのflg値を更新
        // =========================================
        $stmt = $pdo->prepare("UPDATE customers SET flg = ? WHERE id = ?");
        $stmt->execute([$flg, $id]);

        // =========================================
        // 更新後のレコード情報を取得
        // =========================================
        $stmt = $pdo->prepare("
            SELECT id, created_at, special_lecture, name, name_kana, email 
            FROM customers 
            WHERE id = ?
        ");
        $stmt->execute([$id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        // =========================================
        // レコードが存在する場合:成功レスポンス
        // =========================================
        if ($row) {
            echo json_encode([
                "status" => "success",
                "message" => "更新しました",
                "data" => $row
            ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        } else {
            // =========================================
            // レコードが見つからない場合:エラー
            // =========================================
            echo json_encode([
                "status" => "error",
                "message" => "データが見つかりません"
            ]);
        }
    } else {
        // =========================================
        // パラメータ不足の場合のエラー
        // =========================================
        echo json_encode([
            "status" => "error",
            "message" => "パラメータが不足しています"
        ]);
    }
} catch (PDOException $e) {
    // =========================================
    // データベース接続または実行時エラー処理
    // =========================================
    echo json_encode([
        "status" => "error",
        "message" => "接続エラー: " . $e->getMessage()
    ]);
}

システムの稼働

▲Zero 2 Wでqr_camera_app.pyをThonnyで起動すると、上図のような画面が表示されます。

▲QRコードを読み取ります。

▲ちゃんと認識し反応しました。

▲受け付けシステムも反応しました。

まとめ

カメラを使ったQRコード読み取りシステムを作りたい。
それが、私がRaspberry Piを購入した最初の目的でした。
ここに至るまでには、さまざまな検証や試行錯誤を重ねてきました。
その過程で得た知見や実装の一部は、これまでのブログ記事でも紹介してきました。

人感センサーや温度センサーを使ったプロジェクトはすでに多くの人が挑戦しており、ネット上にも数多くの事例が見られます。
だからこそ、「誰もまだ作っていないものを形にしたい」という思いが、今回の開発の原動力になりました。

このQRコード受付システムは、そうした探究心の延長線上にある成果のひとつです。

課題として、読み込んだ際に、画面に表示される以外の反応がないので、音を出したり、LEDを点灯させたりの工夫が必要だと思いました。

より実用的で、かつ新しいアイデアに挑戦していきたいと思います。

最後までお読みいただき、ありがとうございました。