IPカメラのRTSP映像から人物とQRコードを検知

IPカメラのRTSP映像から人物とQRコードを検知してMQTTでおくるコードを作成しています。

GPU付のWindowsマシンで、MQTT Brokerをローカルで動かしPythonで作成したコードがこちら。

import cv2
import time
import json
import paho.mqtt.client as mqtt
import torch
from pyzbar.pyzbar import decode
from ultralytics import YOLO

# MQTT設定
BROKER = "localhost"
PORT = 1883
TOPIC = "detected"

mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.connect(BROKER, PORT, 60)

# RTSP カメラURL(低遅延化パラメータ付き)
RTSP_URL = "rtsp://Smartlight:smartlight@192.168.1.240:554/stream1?fflags=nobuffer&rtsp_transport=tcp"
cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 15)

# 解像度を明示的に上げる(必要に応じて)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

if not cap.isOpened():
    print("カメラに接続できませんでした。")
    exit()

# YOLOv8 モデルをGPUにロード
model = YOLO("yolov8n.pt")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
print(f"YOLOv8 running on device: {device}")

# 状態管理
last_sent_time = 0
qr_display_expire = 0
last_qr_data = None

while True:
    # 古いフレームを読み捨てて最新フレームだけ使う
    for _ in range(5):
        cap.read()
    ret, frame = cap.read()
    if not ret:
        print("フレーム取得失敗。再接続中...")
        cap.release()
        time.sleep(3)
        cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
        continue

    current_time = time.time()
    send_payload = {}

    # ---------- QRコード検出(スケーリング付き) ----------
    scale_factor = 2.0
    scaled_frame = cv2.resize(frame, None, fx=scale_factor, fy=scale_factor)
    qr_codes = decode(scaled_frame)

    if qr_codes:
        for qr in qr_codes:
            data = qr.data.decode("utf-8")
            x, y, w, h = qr.rect
            # スケーリングされた座標を元に戻す
            x = int(x / scale_factor)
            y = int(y / scale_factor)
            w = int(w / scale_factor)
            h = int(h / scale_factor)
            x_center = x + w // 2
            y_center = y + h // 2

            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
            cv2.putText(frame, data, (x, y - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)

            send_payload["qr_code"] = {
                "data": data,
                "x": x_center,
                "y": y_center
            }

            last_qr_data = data
            qr_display_expire = current_time + 1.5
    elif current_time > qr_display_expire:
        last_qr_data = None

    # ---------- 人物検出(YOLOv8 + GPU) ----------
    results = model.predict(source=frame, classes=[0], conf=0.4, verbose=False, device=0)

    person_data = {}
    person_count = 0
    for box in results[0].boxes:
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        conf = float(box.conf[0])
        x_center = int((x1 + x2) / 2)
        y_center = int((y1 + y2) / 2)

        person_count += 1
        person_data[f"person{person_count}"] = {
            "x": x_center,
            "y": y_center,
            "confidence": round(conf, 2)
        }

        cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 0, 0), 2)
        label = f"person ({x_center},{y_center}) {conf:.2f}"
        cv2.putText(frame, label, (x1, y1 - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 0), 2)

    if person_data:
        send_payload["persons"] = person_data

    # ---------- MQTT送信(1秒に1回) ----------
    if send_payload and (current_time - last_sent_time >= 1.0):
        mqtt_client.publish(TOPIC, json.dumps(send_payload))
        print(f"[MQTT] Sent: {json.dumps(send_payload, indent=2)}")
        last_sent_time = current_time

    # 表示
    cv2.imshow("QR + Person Detection (Low Latency)", frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
cv2.destroyAllWindows()
mqtt_client.disconnect()

実行結果がこちら

人物についてはカメラの角度や大きさも含めて認識しますが、QRコードについては大きさやカメラに対して正面に表示&大きさで認識率が悪くなります。

ちなみに上記写真で認識しているQRコードの大きさが7.5cmx7.5㎝で、たまに6cmx6cmのQRコードも認識するような状況です。

現状はQRコードに角度をつけていますが、これが普通に机の上に置いていても認識されるように修正します。

QRコードンイン式をpyzbarからopencv-QRCodeDetectorに変更

QRコードが「カメラの正面でないと認識されない」という問題の原因は、現在使用している pyzbar ライブラリが歪んだ(傾き・角度あり)QRコードの検出に弱いためです。斜めや遠距離、光の反射などにより正常に読み取れないケースが多くあります。

cv2.QRCodeDetector()であれば角度対応もしているということでコードを修正。

すると、先ほどまで認識していたQRコードも認識しなくなりました。また、少しスピードも遅くなっています。

pyzbar を使ったまま、斜め方向や歪んだQRコードでも認識できるようにするための前処理として射影変換(透視補正)を実装

pyzbar を使ったまま、斜め方向や歪んだQRコードでも認識できるようにするための前処理として、射影変換(透視補正)を加えたコードに修正。

import cv2
import time
import json
import paho.mqtt.client as mqtt
import torch
from pyzbar.pyzbar import decode
from ultralytics import YOLO
import numpy as np

# MQTT設定
BROKER = "localhost"
PORT = 1883
TOPIC = "detected"

mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.connect(BROKER, PORT, 60)

# RTSP カメラURL(低遅延化パラメータ付き)
RTSP_URL = "rtsp://Smartlight:smartlight@192.168.1.240:554/stream1?fflags=nobuffer&rtsp_transport=tcp"
cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 15)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

if not cap.isOpened():
    print("カメラに接続できませんでした。")
    exit()

# YOLOv8 モデルをGPUにロード
model = YOLO("yolov8n.pt")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
print(f"YOLOv8 running on device: {device}")

last_sent_time = 0
qr_display_expire = 0
last_qr_data = None

def four_point_transform(image, rect):
    (x, y, w, h) = rect
    src_pts = np.array([
        [x, y],
        [x + w, y],
        [x + w, y + h],
        [x, y + h]
    ], dtype="float32")
    dst_pts = np.array([
        [0, 0],
        [w - 1, 0],
        [w - 1, h - 1],
        [0, h - 1]
    ], dtype="float32")
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)
    warped = cv2.warpPerspective(image, M, (w, h))
    return warped

while True:
    for _ in range(5):
        cap.read()
    ret, frame = cap.read()
    if not ret:
        print("フレーム取得失敗。再接続中...")
        cap.release()
        time.sleep(3)
        cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
        continue

    current_time = time.time()
    send_payload = {}

    # ---------- QRコード検出(スケーリングと補正あり) ----------
    scale_factor = 2.0
    scaled_frame = cv2.resize(frame, None, fx=scale_factor, fy=scale_factor)
    qr_codes = decode(scaled_frame)

    if qr_codes:
        for qr in qr_codes:
            data = qr.data.decode("utf-8")
            x, y, w, h = qr.rect
            x = int(x / scale_factor)
            y = int(y / scale_factor)
            w = int(w / scale_factor)
            h = int(h / scale_factor)

            roi = frame[y:y+h, x:x+w]
            if roi.shape[0] > 0 and roi.shape[1] > 0:
                # 補正した領域に再デコード
                warped = four_point_transform(frame, (x, y, w, h))
                decoded = decode(warped)
                if decoded:
                    qr = decoded[0]
                    data = qr.data.decode("utf-8")

            x_center = x + w // 2
            y_center = y + h // 2

            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
            cv2.putText(frame, data, (x, y - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)

            send_payload["qr_code"] = {
                "data": data,
                "x": x_center,
                "y": y_center
            }

            last_qr_data = data
            qr_display_expire = current_time + 1.5
    elif current_time > qr_display_expire:
        last_qr_data = None

    # ---------- 人物検出(YOLOv8 + GPU) ----------
    results = model.predict(source=frame, classes=[0], conf=0.4, verbose=False, device=0)

    person_data = {}
    person_count = 0
    for box in results[0].boxes:
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        conf = float(box.conf[0])
        x_center = int((x1 + x2) / 2)
        y_center = int((y1 + y2) / 2)

        person_count += 1
        person_data[f"person{person_count}"] = {
            "x": x_center,
            "y": y_center,
            "confidence": round(conf, 2)
        }

        cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 0, 0), 2)
        label = f"person ({x_center},{y_center}) {conf:.2f}"
        cv2.putText(frame, label, (x1, y1 - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 0), 2)

    if person_data:
        send_payload["persons"] = person_data

    # ---------- MQTT送信(1秒に1回) ----------
    if send_payload and (current_time - last_sent_time >= 1.0):
        mqtt_client.publish(TOPIC, json.dumps(send_payload))
        print(f"[MQTT] Sent: {json.dumps(send_payload, indent=2)}")
        last_sent_time = current_time

    # 表示
    cv2.imshow("QR + Person Detection (Low Latency)", frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
cv2.destroyAllWindows()
mqtt_client.disconnect()

このようにカメラに正面でないQRコードも認識するようになりましたが、カメラの遅延が大きくなりました。

遅延を抑えるため検出頻度を1FPSに変更

フレームレートを制御し、人物検出・QR検出を1秒間に1回(=1 FPS)に制限するようコードを修正。

修正したコードを以下に提示します。

import cv2
import time
import json
import paho.mqtt.client as mqtt
import torch
from pyzbar.pyzbar import decode
from ultralytics import YOLO
import numpy as np

# MQTT設定
BROKER = "localhost"
PORT = 1883
TOPIC = "detected"

mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.connect(BROKER, PORT, 60)

# RTSP カメラURL
RTSP_URL = "rtsp://Smartlight:smartlight@192.168.1.240:554/stream1?fflags=nobuffer&rtsp_transport=tcp"
cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 15)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

if not cap.isOpened():
    print("カメラに接続できませんでした。")
    exit()

# YOLOv8 モデル
model = YOLO("yolov8n.pt")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
print(f"YOLOv8 running on device: {device}")

last_sent_time = 0
qr_display_expire = 0
last_qr_data = None

def four_point_transform(image, rect):
    (x, y, w, h) = rect
    src_pts = np.array([
        [x, y],
        [x + w, y],
        [x + w, y + h],
        [x, y + h]
    ], dtype="float32")
    dst_pts = np.array([
        [0, 0],
        [w - 1, 0],
        [w - 1, h - 1],
        [0, h - 1]
    ], dtype="float32")
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)
    warped = cv2.warpPerspective(image, M, (w, h))
    return warped

while True:
    ret, frame = cap.read()
    if not ret:
        print("フレーム取得失敗。再接続中...")
        cap.release()
        time.sleep(3)
        cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
        continue

    current_time = time.time()
    send_payload = {}

    # ---------- 1秒ごとに処理 ----------
    if current_time - last_sent_time >= 1.0:

        # QRコード検出(pyzbar + 補正)
        scale_factor = 2.0
        scaled_frame = cv2.resize(frame, None, fx=scale_factor, fy=scale_factor)
        qr_codes = decode(scaled_frame)

        if qr_codes:
            for qr in qr_codes:
                data = qr.data.decode("utf-8")
                x, y, w, h = qr.rect
                x = int(x / scale_factor)
                y = int(y / scale_factor)
                w = int(w / scale_factor)
                h = int(h / scale_factor)

                roi = frame[y:y+h, x:x+w]
                if roi.shape[0] > 0 and roi.shape[1] > 0:
                    warped = four_point_transform(frame, (x, y, w, h))
                    decoded = decode(warped)
                    if decoded:
                        qr = decoded[0]
                        data = qr.data.decode("utf-8")

                x_center = x + w // 2
                y_center = y + h // 2

                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                cv2.putText(frame, data, (x, y - 10),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)

                send_payload["qr_code"] = {
                    "data": data,
                    "x": x_center,
                    "y": y_center
                }

                last_qr_data = data
                qr_display_expire = current_time + 1.5
        elif current_time > qr_display_expire:
            last_qr_data = None

        # 人物検出(YOLO)
        results = model.predict(source=frame, classes=[0], conf=0.4, verbose=False, device=0)

        person_data = {}
        person_count = 0
        for box in results[0].boxes:
            x1, y1, x2, y2 = map(int, box.xyxy[0])
            conf = float(box.conf[0])
            x_center = int((x1 + x2) / 2)
            y_center = int((y1 + y2) / 2)

            person_count += 1
            person_data[f"person{person_count}"] = {
                "x": x_center,
                "y": y_center,
                "confidence": round(conf, 2)
            }

            cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 0, 0), 2)
            label = f"person ({x_center},{y_center}) {conf:.2f}"
            cv2.putText(frame, label, (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 0), 2)

        if person_data:
            send_payload["persons"] = person_data

        # MQTT送信
        if send_payload:
            mqtt_client.publish(TOPIC, json.dumps(send_payload))
            print(f"[MQTT] Sent: {json.dumps(send_payload, indent=2)}")

        last_sent_time = current_time

    # 表示(毎フレーム更新)
    if last_qr_data:
        cv2.putText(frame, f"Last QR: {last_qr_data}", (10, 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)

    cv2.imshow("QR + Person Detection (1FPS)", frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
cv2.destroyAllWindows()
mqtt_client.disconnect()

上記の変更で遅延は改善されました。次は6cmx6cmのQRコードも検出できるように修正します。

対策内容
① 解像度を向上cap.set()で1920×1080などに設定
② QR検出時のスケーリング強化scale_factorを2.0→3.0や4.0に変更
③ 画像の鮮明化(前処理)グレースケール → ヒストグラム均等化 or シャープ化
④ 読み取り範囲の補正強化射影変換(既対応済)で傾き補正を継続
import cv2
import time
import json
import paho.mqtt.client as mqtt
import torch
from pyzbar.pyzbar import decode
from ultralytics import YOLO
import numpy as np

# MQTT設定
BROKER = "localhost"
PORT = 1883
TOPIC = "detected"

mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.connect(BROKER, PORT, 60)

# RTSP カメラURL
RTSP_URL = "rtsp://Smartlight:smartlight@192.168.1.240:554/stream1?fflags=nobuffer&rtsp_transport=tcp"
cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 15)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)  # 高解像度に変更
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

if not cap.isOpened():
    print("カメラに接続できませんでした。")
    exit()

# YOLOv8 モデル
model = YOLO("yolov8n.pt")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
print(f"YOLOv8 running on device: {device}")

last_sent_time = 0
qr_display_expire = 0
last_qr_data = None

def four_point_transform(image, rect):
    (x, y, w, h) = rect
    src_pts = np.array([
        [x, y],
        [x + w, y],
        [x + w, y + h],
        [x, y + h]
    ], dtype="float32")
    dst_pts = np.array([
        [0, 0],
        [w - 1, 0],
        [w - 1, h - 1],
        [0, h - 1]
    ], dtype="float32")
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)
    warped = cv2.warpPerspective(image, M, (w, h))
    return warped

while True:
    ret, frame = cap.read()
    if not ret:
        print("フレーム取得失敗。再接続中...")
        cap.release()
        time.sleep(3)
        cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
        continue

    current_time = time.time()
    send_payload = {}

    # ---------- 1FPS制御 ----------
    if current_time - last_sent_time >= 1.0:

        # 小さなQRコードにも対応するためスケーリング強化
        scale_factor = 3.0
        scaled_frame = cv2.resize(frame, None, fx=scale_factor, fy=scale_factor)
        gray_scaled = cv2.cvtColor(scaled_frame, cv2.COLOR_BGR2GRAY)
        equalized = cv2.equalizeHist(gray_scaled)

        qr_codes = decode(equalized)

        if qr_codes:
            for qr in qr_codes:
                data = qr.data.decode("utf-8")
                x, y, w, h = qr.rect
                x = int(x / scale_factor)
                y = int(y / scale_factor)
                w = int(w / scale_factor)
                h = int(h / scale_factor)

                roi = frame[y:y+h, x:x+w]
                if roi.shape[0] > 0 and roi.shape[1] > 0:
                    warped = four_point_transform(frame, (x, y, w, h))
                    warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
                    sharpened = cv2.equalizeHist(warped_gray)
                    decoded = decode(sharpened)
                    if decoded:
                        qr = decoded[0]
                        data = qr.data.decode("utf-8")

                x_center = x + w // 2
                y_center = y + h // 2

                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                cv2.putText(frame, data, (x, y - 10),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)

                send_payload["qr_code"] = {
                    "data": data,
                    "x": x_center,
                    "y": y_center
                }

                last_qr_data = data
                qr_display_expire = current_time + 1.5
        elif current_time > qr_display_expire:
            last_qr_data = None

        # 人物検出
        results = model.predict(source=frame, classes=[0], conf=0.4, verbose=False, device=0)
        person_data = {}
        person_count = 0
        for box in results[0].boxes:
            x1, y1, x2, y2 = map(int, box.xyxy[0])
            conf = float(box.conf[0])
            x_center = int((x1 + x2) / 2)
            y_center = int((y1 + y2) / 2)

            person_count += 1
            person_data[f"person{person_count}"] = {
                "x": x_center,
                "y": y_center,
                "confidence": round(conf, 2)
            }

            cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 0, 0), 2)
            label = f"person ({x_center},{y_center}) {conf:.2f}"
            cv2.putText(frame, label, (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 0), 2)

        if person_data:
            send_payload["persons"] = person_data

        # MQTT送信
        if send_payload:
            mqtt_client.publish(TOPIC, json.dumps(send_payload))
            print(f"[MQTT] Sent: {json.dumps(send_payload, indent=2)}")

        last_sent_time = current_time

    # 表示
    if last_qr_data:
        cv2.putText(frame, f"Last QR: {last_qr_data}", (10, 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)

    cv2.imshow("QR + Person Detection (Enhanced)", frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
cv2.destroyAllWindows()
mqtt_client.disconnect()

今日の成果

小さいQRコードについては認識が悪く。それがカメラの性能なのかもしれませんが、本日の成果は最終的にこちら。

import cv2
import time
import json
import paho.mqtt.client as mqtt
import torch
from pyzbar.pyzbar import decode
from ultralytics import YOLO
import numpy as np

# MQTT設定
BROKER = "localhost"
PORT = 1883
TOPIC = "detected"

mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.connect(BROKER, PORT, 60)

# RTSP カメラURL
RTSP_URL = "rtsp://Smartlight:smartlight@192.168.1.240:554/stream1?fflags=nobuffer&rtsp_transport=tcp"
cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
cap.set(cv2.CAP_PROP_FPS, 15)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

if not cap.isOpened():
    print("カメラに接続できませんでした。")
    exit()

# YOLOv8 モデル
model = YOLO("yolov8n.pt")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
print(f"YOLOv8 running on device: {device}")

last_sent_time = 0
qr_display_expire = 0
last_qr_data = None

def four_point_transform(image, rect):
    (x, y, w, h) = rect
    src_pts = np.array([
        [x, y],
        [x + w, y],
        [x + w, y + h],
        [x, y + h]
    ], dtype="float32")
    dst_pts = np.array([
        [0, 0],
        [w - 1, 0],
        [w - 1, h - 1],
        [0, h - 1]
    ], dtype="float32")
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)
    warped = cv2.warpPerspective(image, M, (w, h))
    return warped

while True:
    for _ in range(4): cap.read()
    ret, frame = cap.read()

    if not ret:
        print("フレーム取得失敗。再接続中...")
        cap.release()
        time.sleep(3)
        cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
        continue

    current_time = time.time()
    send_payload = {}

    if current_time - last_sent_time >= 2.0:
        scale_factor = 4.0
        scaled = cv2.resize(frame, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_CUBIC)
        gray = cv2.cvtColor(scaled, cv2.COLOR_BGR2GRAY)
        eq = cv2.equalizeHist(gray)
        blur = cv2.GaussianBlur(eq, (5, 5), 0)
        sharp = cv2.addWeighted(eq, 2.0, blur, -1.0, 0)  # 強シャープ化

        qr_codes = decode(sharp)

        if qr_codes:
            for qr in qr_codes:
                data = qr.data.decode("utf-8")
                x, y, w, h = qr.rect

                # スケールを戻す
                x = int(x / scale_factor)
                y = int(y / scale_factor)
                w = int(w / scale_factor)
                h = int(h / scale_factor)

                # 再検出用に余白を広げて拡大
                margin = 30
                x_cut = max(0, x - margin)
                y_cut = max(0, y - margin)
                w_cut = w + margin * 2
                h_cut = h + margin * 2

                roi = frame[y_cut:y_cut + h_cut, x_cut:x_cut + w_cut]
                if roi.shape[0] > 0 and roi.shape[1] > 0:
                    warped = four_point_transform(frame, (x_cut, y_cut, w_cut, h_cut))
                    warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
                    sharp_roi = cv2.addWeighted(warped_gray, 2.0, cv2.GaussianBlur(warped_gray, (5, 5), 0), -1.0, 0)
                    decoded = decode(sharp_roi)
                    if decoded:
                        data = decoded[0].data.decode("utf-8")

                x_center = x + w // 2
                y_center = y + h // 2

                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                cv2.putText(frame, data, (x, y - 10),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)

                send_payload["qr_code"] = {
                    "data": data,
                    "x": x_center,
                    "y": y_center
                }

                last_qr_data = data
                qr_display_expire = current_time + 4.0
        elif current_time > qr_display_expire:
            last_qr_data = None

        # 人物検出(YOLOv8)
        results = model.predict(source=frame, classes=[0], conf=0.5, verbose=False, device=0)
        person_data = {}
        person_count = 0
        for box in results[0].boxes:
            x1, y1, x2, y2 = map(int, box.xyxy[0])
            conf = float(box.conf[0])
            x_center = int((x1 + x2) / 2)
            y_center = int((y1 + y2) / 2)

            person_count += 1
            person_data[f"person{person_count}"] = {
                "x": x_center,
                "y": y_center,
                "confidence": round(conf, 2)
            }

            cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 0, 0), 2)
            label = f"person ({x_center},{y_center}) {conf:.2f}"
            cv2.putText(frame, label, (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 0), 2)

        if person_data:
            send_payload["persons"] = person_data

        if send_payload:
            mqtt_client.publish(TOPIC, json.dumps(send_payload), qos=0, retain=False)
            print(f"[MQTT] Sent: {json.dumps(send_payload, indent=2)}")

        last_sent_time = current_time

    if last_qr_data:
        cv2.putText(frame, f"Last QR: {last_qr_data}", (10, 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)

    display_frame = cv2.resize(frame, (960, 540))
    cv2.imshow("QR + Person Detection (Small QR Enhanced)", display_frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
mqtt_client.disconnect()

あとは、YOLOR8nでQRコードを抽出するモデルをつくって、検出する方法を調べます。