AQ Tech Blog

エッジAI×YOLOで混雑検知!LLMと連携して接客提案まで試してみた

作成者: daisuke.katayama|2026年08月05日

はじめに

店舗・オフィス・イベント会場・公共施設など、人が集まる場所での「混雑度の可視化」は、業種を問わず多くの現場で求められているテーマです。人員配置や案内の最適化、快適な空間づくりなど、活用の仕方は現場ごとに様々ですが、いずれも「今、その場に何人いるか」をリアルタイムに把握できることが第一歩になります。

映像解析というと、カメラの映像をクラウドにアップロードして解析する「クラウド集中型」の構成を思い浮かべる方も多いかもしれません。ですが、人数カウントのような軽量なタスクであれば、映像そのものをクラウドに送らず、カメラの近く(エッジ)で検出処理まで完結させてしまう方が、通信コストやプライバシーの観点で合理的な場合も少なくありません。

本記事では、フロントエンドを手軽に構築できるStreamlitと、軽量・高速な物体検出モデルであるYOLOを組み合わせ、小売店舗デモアプリに実装した「AI混雑検知・人数カウント」機能の実装プロセスと技術的な工夫についてご紹介します。エッジでの推論だけで手軽に「混雑検知」を試せる点について、中心にお伝えします。なお、本記事では検出結果を活用する具体例として小売店舗での接客アドバイスを取り上げますが、同じ仕組みはオフィスの会議室利用状況や、イベント会場の入場者管理など、様々な現場に応用できます。

今回実装した機能

今回実装したのは、カメラ映像やアップロードされた画像から、AIがリアルタイムに「現場の人数」を検出・カウントし、その結果に応じた「対応アドバイス」を自動生成する機能です。具体例として小売店舗の「来店客数カウント+接客アドバイス」を実装しています。

  • どんな技術なのか: PythonベースのUIフレームワーク「Streamlit」を基盤に、物体検出AI「YOLO」と大規模言語モデル(LLM)の「Gemini API」を組み合わせた機能です。YOLOで検出した人数をGeminiに渡し、状況に応じたアドバイスを生成します。
  • 主要な特徴や用途: カメラ映像から人物(person)のみを検出し、混雑レベルを可視化します。さらに、検出された人数をトリガーに、AIが現場の状況に応じた具体的なアクションを提案します(今回の例では小売店舗を想定し「レジ応援」や「誘導」を提案)。
  • 期待できる効果: 従来、現場担当者の「勘と経験」に頼っていた人員配置や対応判断について、リアルタイムな数値データとAIのサポートによって、より適切な判断を後押しできると考えています。

システム構成

このシステムは、エッジ(ローカル環境)での高速な画像処理と、クラウド(LLM)による柔軟な言語処理を組み合わせたハイブリッド構成です。構成要素はPC1台に収まり、シンプルにまとまっています。


1. 主要な機能と構成要素

  • 物体検出レイヤ(YOLO): 人数カウントではリアルタイム性が重要となるため、推論速度と精度のバランスに優れたYOLOを採用しました。GPUがないCPUのみの環境でも、軽量モデルなら実用的なフレームレートで人物を検出できます。
  • UI・カメラ取得レイヤ(Streamlit): PC上でそのまま動かす前提の構成です。cv2.VideoCaptureでPCに接続したカメラ映像を直接取得し、そのままYOLOで推論する、という一本道の処理になっています。
  • 推論処理の工夫: カメラ映像は1フレームずつwhileループで取得・推論・描画し、Streamlitの画面をその都度更新します。クラウドにデプロイしてブラウザ越しにカメラ映像を送る場合はstreamlit-webrtc(WebRTC)を使う構成に切り替えることも可能ですが、その分TURNサーバーなどのネットワーク構成が必要になり、複雑さが増します。今回はエッジで手軽に試せることを重視し、ローカル完結の構成にフォーカスします。

2. メリット・デメリット

  • メリット: データベースや大掛かりなバックエンドサーバーが不要で、PCとカメラさえあれば動くため、PoC(概念実証)やデモを短期間で立ち上げられます。
  • デメリット: CPUのみで推論するため、高解像度・高FPSを求めるとカクつきが発生します。長時間稼働させる場合は、メモリ消費(OOM)にも注意が必要です。

ハンズオン

ここでは、この機能のコアとなる「YOLOによる人物検出」と「AIアドバイス生成」について、Streamlit実装のベースとなるコードサンプルを紹介します。


1. セットアップ

必要なパッケージをインストールします。

pip install streamlit ultralytics google-genai opencv-python-headless

AIアドバイス生成にはGemini APIを利用するため、取得したAPIキーを環境変数に設定しておきます。genai.Client()はこの環境変数を自動で読み込みます。

export GEMINI_API_KEY="取得したAPIキー"

2. コードサンプル(検出部分)

YOLOでの人物検出部分は、モデルのロードと推論をシンプルなクラスに切り出しています。デフォルトでは高速な「Nano」モデル(yolo26n.pt)を使い、必要に応じて高精度な「Small」モデル(yolo26s.pt)にUI上で切り替えられるようにしています。

class YOLODetector:
    def __init__(self, model_name: str = "yolo26n.pt", device: str = "cpu", conf: float = 0.45):
        from ultralytics import YOLO
        self.model = YOLO(model_name)
        self.device = device
        self.conf = conf

    def detect(self, frame, classes: list | None = None):
        # classes=[0] を渡すとperson(人物)クラスのみ検出する
        return self.model(frame, device=self.device, verbose=False, conf=self.conf, classes=classes)[0]

    def draw_annotations(self, frame, results):
        return results.plot()

conf(検出信頼度のしきい値)はStreamlitのスライダーで10〜90%の範囲で調整できるようにしています。低くすると検出しやすくなる一方で誤検出も増え、高くすると逆に見逃しが増えるため、現場の照明や設置角度に応じて微調整できる設計です。


3. コードサンプル(リアルタイム検出とAIアドバイス)

ローカルでStreamlitを動かす場合、リアルタイムカメラ映像はcv2.VideoCaptureでPCのカメラを直接開き、whileループの中で1フレームずつ検出・描画・画面更新を繰り返すだけの構成です。

import cv2
import streamlit as st

st.checkbox("リアルタイムカメラ起動", key="run_camera")
frame_ph = st.empty()

if st.session_state.get("run_camera"):
    cap = cv2.VideoCapture(0)

    while st.session_state.get("run_camera"):
        ok, frame = cap.read()
        if not ok:
            break

        # YOLOで人物のみ検出・アノテーション描画
        results = detector.detect(frame, classes=[0])
        annotated = detector.draw_annotations(frame, results)
        rgb = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)

        person_boxes = [b for b in results.boxes if results.names[int(b.cls[0])] == "person"]
        person_count = len(person_boxes)
        # 検出人数と平均信頼度をダッシュボードに表示する
        avg_conf = sum(float(b.conf[0]) for b in person_boxes) / person_count if person_count else None

        st.session_state["last_person_cnt"] = person_count
        st.session_state["last_avg_conf"] = avg_conf
        frame_ph.image(rgb, channels="RGB", use_container_width=True)

    cap.release()

チェックボックスがオンの間だけループが回り、映像・検出人数・平均信頼度がリアルタイムに更新され続けます。特別なスレッド制御などは必要なく、素直なwhileループで完結しているのがポイントです。

AIアドバイスの生成は、検出した画像ではなく人数(整数)だけをプロンプトに埋め込んでGeminiに渡すシンプルな仕組みです。0人のときにAIを呼んでしまうと不自然なアドバイスが返ってくるため、呼び出し元でガードしています。

from google import genai

STAFFING_PROMPT = """あなたは小売店舗の運営アドバイザーです。
現在、店内カメラで {person_count} 人のお客様が検出されています。
混雑レベルの基準に基づき、推奨スタッフ人数や具体的な接客アクションを
簡潔に箇条書きで提案してください。"""

def get_staffing_advice(person_count: int) -> str:
    client = genai.Client()
    prompt = STAFFING_PROMPT.format(person_count=person_count)
    response = client.models.generate_content(
        model="gemini-3.1-flash-lite",
        contents=prompt,
    )
    return response.text

if st.button("AIに現場判断を求める"):
    person_count = st.session_state.get("last_person_cnt", 0)
    if person_count == 0:
        st.info("現在お客様が検出されていません。")
    else:
        st.info(get_staffing_advice(person_count))

ボタンを押すと、検出人数に応じたアドバイスがカード形式で表示されます。上記のプロンプトは要点のみですが、実際には「何人で混雑とみなすか」の基準や出力フォーマットも指定することで、下図のように整った回答を引き出しています。

なお、カメラの用意が難しい場面や、過去に撮影した画像で検出結果を確認したい場合を想定し、「画像アップロードモード」もフォールバックとして用意しています。処理の流れはリアルタイムモードと同じで、アップロードされた1枚の画像に対してYOLO推論を1回だけ実行する形です。リアルタイムモードが1フレームごとにdetector.detect()を呼ぶのに対し、こちらは1枚の画像に対して1回呼ぶだけの違いで、静止画か動画かによるコード上の差はほとんどありません。


4. トラブルシューティング

カメラ映像が映らない場合: PCに複数のカメラが接続されているとcv2.VideoCapture(0)が意図したデバイスを開けないことがあります。インデックス番号(0, 1, 2…)を変えて試してみてください。また、映像がカクつく場合は、YOLOモデルを高速な「Nano」に切り替える、入力画像の解像度を下げる、あるいはフレームを間引いて推論回数を減らすと改善することがあります。

まとめ

StreamlitとYOLOを組み合わせることで、店舗・オフィス・イベント会場など様々な現場で使える「混雑検知・人数カウント」のプロトタイプを短い工数で開発できました。人数カウントのような軽量なタスクであれば、映像をクラウドに送らずエッジだけで完結できることも、実際に動かして確認できました。

今回はリアルタイムのカウントに焦点を当てましたが、同じYOLOの検出結果を使えば、店内のどこに人が滞留しやすいかを可視化するヒートマップ分析への応用も考えられます。検出対象を店舗独自の商品などに広げたい場合は、YOLOモデルを店舗のデータで追加学習(ファインチューニング)する方法もあります。カウントデータを時間軸で蓄積すれば、機械学習モデルと組み合わせた来客予測への発展も見込めます。これらの応用は、機会があれば別の記事でご紹介します。

今回ご紹介したのはあくまでデモですが、弊社ではこうした画像認識AIの技術検証(PoC)から、お客様の既存システムなどと連携した本番環境の構築までを一気通貫でご支援しております。自社の課題にAIが適用できるか、まずはアイデアベースからでもお気軽にご相談ください。