【ルネサス技術コラム】Kakipの物体検知サンプルを改造して混雑度表示アプリを作ってみた

前回の記事では、ルネサスのRZ/V2Hが搭載されたKakip向けに公開されているAIアプリケーションサンプル「R01_object_detection」 を使いつつ、「エレクトロニクスを簡単に」をテーマとしてUSBカメラ映像へのYOLOv3物体検知を試しました。

今回はその続編として、検出結果をもとに「混雑度」を画面に表示するC++アプリケーションを作ってみました。
前回の記事で展望として挙げていた「人数カウントによる混雑度可視化」の第一歩です。

ポイントは、サンプルのソースコードに数行を追加するだけで実現できることです。AIモデルの再学習も、推論環境の再構築も不要です。
※Kakip本体のセットアップと、前回記事の物体検知サンプルのビルドが完了している前提で進めます。

目次

今回作るもの

今回はKakip AI-SBC上で動作するR01_object_detectionをベースに開発します。
KakipはRenesas RZ/V2Hを搭載したAI開発向けボードであり、DRP-AIによる高速推論環境を利用できます。
USBカメラの映像内に映る 「人 (person)」を検出し、その人数をもとに混雑度を画面に表示します。

検出人数

表示レベル

バッジ色

0~1人

Free

緑

2~4人

Normal

黄

5人以上

Busy

赤

画面にはカメラ映像の左上に次のような状態パネルが表示されます。レイアウトは、以前Raspberry Piで作ったWeb表示版PoC (上部に「現在人数/混雑度/最終更新」の3カードが出る画面) と同じ情報構成を、映像に直接重ねる形で意識しています。

People: 3           (現在人数: 大きな白文字)
[Level: Normal ]    (混雑度: レベル色のバッジ 緑/黄/赤)
Time: 20:47:17      (最終更新時刻)

仕様はこれだけです。まずは最小構成で「動くところ」を目指します。

処理フロー

USB Camera
    ↓
YOLOv3推論(DRP-AI)
    ↓
person検出数集計
    ↓
混雑度判定
    ↓
HDMI表示

実装方針

前回使用したR01_object_detection は、次の流れで動作します。

  • USBカメラから映像を取得

  • YOLOv3で物体検知 (DRP-AIで推論)

  • 検出結果(バウンディングボックス+ラベル) をHDMI画面へ表示

そして検出結果は、main.cpp内では次の変数に格納されています。

  • det…全検出結果のリスト (クラス番号・確度・座標)

  • print_det…画面描画用にフィルタされた検出結果

そこで今回は、

  1. R01_object_detection を丸ごとコピーして R02_congestion_display

  2. main.cpp に 「personの検出数を数えて、混雑度を描画する関数」を追加

  3. print_result() の先頭でその関数を呼び出す

という方針にします。変更するファイルは main.cpp のみです。

アプリケーションの準備

前回ビルドしたR01_object_detection をコピーして、新しいアプリケーション用のディレクトリを作成します。

export WORK=/home/ubuntu/workspace/kakip_ai_apps/
cd $WORK
cp -r R01_object_detection R02_congestion_display

AIモデル (deploy.so) も引き継ぎます。

cd $WORK/R02_congestion_display/exe
cp ../../R01_object_detection/exe/yolov3_onnx/deploy.so ./yolov3_onnx/

※前回のdeploy.soが残っていない場合は、取得し直してもOKです。

wget https://github.com/Kakip-ai/kakip_ai_apps/releases/download/v1.0.0/R01_object_detection_deploy.so

main.cppの変更点

$WORK/R02_congestion_display/src/main.cpp を編集します。変更は2か所だけです。

(1) 混雑度表示用の関数を追加

print_result 関数の直前に、次のブロックを丸ごと追加します。
※サンプルコードは生成AIを活用して作成し、動作確認および内容確認を行っています。

/*Colors for congestion level display*/
#define YELLOW_DATA (0xFFFF00u) /* in RGB */
#define RED_DATA    (0xFF0000u) /* in RGB */

/*Congestion level thresholds (number of persons)*/
#define LEVEL_NORMAL (2) /* 2 people or more: Normal */
#define LEVEL_BUSY   (5) /* 5 people or more: Busy */

/*Congestion panel layout (top-left of the 1920x1080 screen)*/
#define PANEL_X (40)
#define PANEL_Y (40)
#define PANEL_W (420)
#define PANEL_H (230)

#include <ctime>

/*************************************************************************
* Function Name : print_congestion
* Description   : Count detected persons and
*                 draw the congestion panel on the image.
* Arguments     : img pointer to Image object to draw on
* Return value  : -
**************************************************************************/
void print_congestion(Image* img)
{
    /* Count up detections whose label is "person" */
    uint32_t person_count = 0;
    for (uint32_t i = 0; i < print_det.size(); i++)
    {
        if (label_file_map[print_det[i].c] == "person")
        {
            person_count++;
        }
    }

    /* Map the person count to a congestion level */
    const char* level = "Free";
    uint32_t level_rgb = GREEN_DATA;

    if (person_count >= LEVEL_BUSY)
    {
        level = "Busy";
        level_rgb = RED_DATA;
    }
    else if (person_count >= LEVEL_NORMAL)
    {
        level = "Normal";
        level_rgb = YELLOW_DATA;
    }

    /* RGB -> BGR order for OpenCV */
    uint32_t r = (level_rgb >> 16) & RGB_FILTER;
    uint32_t g = (level_rgb >> 8) & RGB_FILTER;
    uint32_t b = level_rgb & RGB_FILTER;

    /* Draw a dark panel like the status cards of the Web PoC */
    /* (print_result() runs after convert_size(), so img_mat is 1920x1080 BGRA) */
    cv::rectangle(img->img_mat, cv::Point(PANEL_X, PANEL_Y),
        cv::Point(PANEL_X + PANEL_W, PANEL_Y + PANEL_H),
        cv::Scalar(0x1E, 0x1E, 0x1E, 0xFF), cv::FILLED);

    std::stringstream stream;

    /* Line 1: number of people (large white text) */
    stream << "People: " << person_count;
    img->write_string_rgb(stream.str(), ALIGHN_LEFT, PANEL_X + 20, PANEL_Y + 80, 1.4, WHITE_DATA);

    /* Line 2: congestion level (colored badge + black text) */
    cv::rectangle(img->img_mat, cv::Point(PANEL_X + 20, PANEL_Y + 110),
        cv::Point(PANEL_X + PANEL_W - 20, PANEL_Y + 170),
        cv::Scalar(b, g, r, 0xFF), cv::FILLED);

    stream.str("");
    stream << "Level: " << level;
    img->write_string_rgb(stream.str(), ALIGHN_LEFT, PANEL_X + 35, PANEL_Y + 155, 0.9, BLACK_DATA);

    /* Line 3: last update time */
    char time_str[16];
    std::time_t t = std::time(nullptr);
    std::strftime(time_str, sizeof(time_str), "%H:%M:%S", std::localtime(&t));

    stream.str("");
    stream << "Time: " << time_str;
    img->write_string_rgb(stream.str(), ALIGHN_LEFT, PANEL_X + 20, PANEL_Y + 210, 0.7, WHITE_DATA);
}

やっていることはシンプルです。

  • print_det からラベルが "person" の検出結果を数える

  • 人数をしきい値 (LEVEL_NORMAL / LEVEL_BUSY) で3段階に分類

  • cv::rectangle() で映像の左上に暗色パネルを描画 (Image クラスの img_mat は public なので直接触れる。print_result() は convert_size() 後に呼ばれるため、img_mat は1920×1080の出力解像度になっている)

  • 1行目に人数(大きな白文字)、2行目に cv::rectangle() の塗りつぶし + write_string_rgb() でレベル色のバッジ、3行目に更新時刻を描画

(2) 呼び出しを1行追加

同じく main.cpp 内の print_result 関数の先頭に、1行だけ追加します。

int8_t print_result(Image* img)
{
    print_congestion(img); /* この1行を追加 */

    std::stringstream stream;
    ...(以下、既存のコードのまま)

(3) バウンディングボックスを 「person」だけにする

検出ボックスを人だけにする場合、draw_bounding_box() の for ループ内にある if (det_buff[i].prob == 0) continue; の次に、次の1行を追加してください。

if (label_file_map[det_buff[i].c] != "person") continue; /* Skip non-person boxes */

以上で main.cpp の変更は完了です。

なお、実行ファイル名を変えたい場合は、src/CMakeLists.txt の set(EXE_NAME object_detection) を変更してください (今回はデフォルトのまま進めます)。

ビルドと実行

前回と同じ手順でビルドします。

cd $WORK/R02_congestion_display
rm -r build
mkdir build && cd build
cmake ../src
make -j$(nproc)

cd ../exe
cp ../build/object_detection .

USBカメラとHDMIディスプレイを接続して実行します。

cd $WORK/R02_congestion_display/exe
./object_detection

実行結果

正常に起動すると、前回の物体検知画面に加えて、映像の左上に状態パネルが表示されます。

  • 一番上…… People: 3 と現在の人数 (大きな白文字)

  • 中央…… Level: Busy のように混雑度に応じて色が変わるバッジ (Free=緑 / Normal=黄 / Busy=赤)

  • 下…… Time: 20:47:17 と表示を更新した時刻

  • カメラに誰も映っていないとき……People: 0 Level: Free (緑のバッジ)

  • 2人が映ったとき……People: 2 Level: Normal (黄のバッジ)

  • 5人以上映ったとき……People: 5 Level: Busy (赤のバッジ)

以前Raspberry Piで作ったWeb表示版PoCと同じ「人数・混雑度・更新時刻」が、HDMI画面の映像に直接重なって見えるので、部屋の隅からでも混雑状況がひと目で分かるようになりました。

簡易実装の限界と次のステップ

今回の実装は「1フレームごとの単純カウント」です。実際の現場で使うには、以下の点に注意が必要です。

  • 追跡処理がないため、同じ人をフレームごとに再カウントする(人数がブレる) → 移動平均などで平滑化すると安定する

  • 重なって映った人は1人として検出されることがある

  • しきい値 (2人/5人) はカメラの画角・設置場所に合わせて調整する

  • 入退室を数える 「人数カウント」らしさを出すには、ライン通過判定やトラッキングの追加が必要

逆に言えば、この小さな土台に対してこれらの機能を足し込んでいくだけで、実用的なアプリに育てていける、ということでもあります。

KakipでRZ/V2Hを使って物体検知を試すメリット

Raspberry PiはPoCには便利ですが、KakipでRenesasのRZ/V2Hを使うと、AIを試すだけでなく、そのまま実機向け開発につなげやすいのが大きなメリットです。

RZ/V2Hは、AI推論用のDRP-AI3に加え、Linux向けのCortex-A55、リアルタイム制御向けのCortex-R8、システム制御向けのCortex-M33を搭載しており、推論・画像処理・機器制御を並列に進めやすい構成になっています。さらに、OpenCV系の画像処理もアクセラレータで分担できるため、AIカメラやロボティクス用途を意識した開発と相性が良い点もRenesas製品の強みです。

また、RenesasはRZ/V2H AI Software Development Kitやサンプル環境を用意しているため、サンプル取得、ビルド、モデル配置、実行という流れで物体検知をすぐ試せます。USBカメラ入力から推論、描画、表示まで一通りそろっており、実装の手間を抑えながら、製品化を見据えた検証に入りやすいのも魅力です。

今回の混雑度表示アプリは、会議室監視、食堂混雑監視、工場内の作業エリア監視などに応用できます。
将来的にはRZ/Vシリーズを利用した製品組込みや、エッジAIソリューションへの展開も期待できます。

まとめ

今回は、Kakipの物体検知サンプルを改造して、検出人数と混雑度を画面に表示するC++アプリケーションを作成しました。

  • サンプルディレクトリをコピーして、main.cppに約60行を追加するだけで実現

  • AIモデルの再学習・再コンパイルは不要

  • 人数・混雑度・更新時刻を映像に重ねて表示でき、人数カウントや混雑度可視化への発展の土台になる

公開サンプルを「動かして終わり」にせず、少し読んで改造してみると、エッジAIアプリケーションの構成がぐっと理解しやすくなります。次回はトラッキングの追加や、Webダッシュボードとの連携にも挑戦してみたいと思います。

参考リンク