10-2 AI 基礎テンプレートプロジェクトの構築:QDeepSample

第 10 章の実践セッションへようこそ!前のセクションでは、QDEEP エンジンの謎を解き明かし、AI 映像分析の標準 S.O.P(ストリームの受信 ➔ デコード ➔ AI エンジンの推論 ➔ OpenCV の描画)について学びました。
今後、新しい AI 機能を実装するたびに「UIの作成、ストリームの接続、環境設定」を繰り返し記述する煩雑なループに陥るのを防ぐため、今回のレッスンでは「汎用的な AI ビジュアル分析の基礎テンプレート (QDeepSample) を自作する」という1つのコアタスクのみを行います!
このテンプレートでは、前の章で学んだ「RTSP 受信側技術」と「OpenCV 画像処理ライブラリ」を完璧に組み合わせます。今日の最終目標は、「受信したストリーム映像をスムーズに確認する」ことであり、次の章で追加される AI 認識モデルのために最もクリーンな動作フレームワークを準備することです。この基盤さえ確立してしまえば、将来、人物検出、顔認識、ナンバープレート分析のいずれを実装する場合でも、「このテンプレートをコピーする ➔ モデルを置き換える ➔ 数行のコア API を追加し、描画方法を変更する」だけで、瞬時に開発を完了することができます!

➤ RTSP 受信側の概念はこちらで復習できます: 9-9 RTSP ストリーム受信側(クライアント)機能の実践チュートリアル
さあ、Qt Creator を開いて、私たちと一緒に一歩ずつ、この強固な AI 開発の基盤を築き上げましょう!

プロジェクトの作成とコア依存ファイルの準備

まず、第 9 章で学んだ手順を参考に、Qt Creator で新しい Qt Widgets Application を作成し、プロジェクト名を QDeepSample と命名してください。
➤ 基礎テンプレートの作成はこちらで復習できます: 9-3 基本テンプレートプロジェクトの構築:Hello NexVDO SDK!
このプロジェクトに「ネットワークストリームの受信」と「AI 分析」の両方の機能を持たせるため、SDK のコアファイルをプロジェクトディレクトリに配置する必要があります。

1. SDKライブラリのインポート: 解凍した NexVDO SDK フォルダを開き、それぞれ Qcap と Qdeep の2つのフォルダを見つけます。その中にある include と lib フォルダを、QDeepSample プロジェクトディレクトリに完全にコピーしてください。



2. AIモデルファイル (Models) のインポート: QDEEP エンジンを正式に呼び出す前に、事前学習済みのモデルファイルを準備する必要があります。YUANから提供されたモデルの圧縮ファイル( 例:Tiny Person モデル )を解凍し、末尾が .CFG ( 設定ファイル )および  .WEIGHTS ( 重みファイル )のファイルを、プロジェクトのビルド出力ディレクトリ( 通常は build-Release... フォルダ内 )にコピーしてください。

CMakeLists.txt の設定 ( OpenCV と SDK の連携 )

AI 認識に関わる場合、画面のレンダリングロジックは単なるプレイヤーとは異なります。AI エンジンが返す「 座標データ」を、よく見かける緑や赤の「 認識トラッキングボックス( バウンディングボックス )」として描画するには、強力なオープンソースの画像処理ライブラリである OpenCV を借りる必要があります。
プロジェクト内の CMakeLists.txt を開いてください。QCAP、QDEEP、および OpenCV を一度にすべて紐付ける必要があります:

1. SDKパスの設定: QCAP_DIR と QDEEP_DIR のパス定義を追加し、 include_directories を使用してそれらのヘッダーファイルをインクルードします。

2. OpenCVの検索とインクルード: find_package(Qt6...) の下に、 find_package(OpenCV REQUIRED) を追加します。

3. すべてのライブラリのリンク: 一番下の target_link_libraries に、 libqcap.so libQDEEP.SO 、および ${OpenCV_LIBS} をすべて追加します!

標準 AI テスト UI の構築

「映像の受信」と「AI 分析」のオン/オフを独立して制御するための、明確で直感的なコントロールパネルが必要です。 mainwindow.ui を開き、以下のコンポーネントを配置してください:

1. URL 入力ボックス ( QLineEdit ): StrURL  と命名し、RTSP ストリームの URL を入力するために使用します。
➤ URL 入力ボックスについてはこちらで復習できます: 9-9 RTSP ストリーム受信側(クライアント)機能の実践チュートリアル
2. ストリーム制御ボタン ( QPushButton ):ボタンを2つ配置し、それぞれ「 START RECEIVER 」と「 STOP RECEIVER 」と命名します。これらは映像の取得を制御するために使用します。

3. AI 制御ボタン ( QPushButton ):さらにボタンを2つ配置し、それぞれ「 START DETECTION 」と「 STOP DETECTION 」と命名します。これらは将来 AI モデルを起動するためのスイッチになります!
4. 表示キャンバス ( QFrame ):QFrame を配置し、 ClientWindow ( または PreviewWindow )と命名します。このキャンバスは後ほど OpenCV に引き継がれ、画像や認識枠の描画を担当します。
➤ QFrame の配置方法についてはこちらで復習できます : 9-3 基本テンプレートプロジェクトの構築:Hello NexVDO SDK!

ヘッダーファイルとコア変数の宣言

プロジェクトに NexVDO SDK の強力な機能を認識させ、今後のカスタム描画の準備をするために、 mainwindow.h で一連の宣言を行う必要があります。

QCAP、QDEEP、および Qt 描画ヘッダーファイルのインクルード

図に示すように、 mainwindow.h を開き、先頭に以下の不可欠な3種類のヘッダーファイルをインクルードしてください:

• QCAP シリーズ ( qcap.h, qcap.common.h, qcap.linux.h ) : これらは、NexVDO SDK の基礎となるキャプチャおよび受信機能を呼び覚ますための鍵です。
• QDEEP シリーズ ( QDEEP.H ) : この章ではまだ正式に AI 分析を開始していませんが、まずは AI エンジンの定義ファイルを準備しておきます。これは、次の章で「 シームレスに移行 」するための重要な伏線となります!
• Qt 描画シリーズ (, ,  ) :過去に使用していたレンダリング方法を放棄し、カスタムキャンバスを使用して画面を表示するように変更するため、Qt 関連の描画コンポーネントをインクルードする必要があります。

コア変数とスレッド間通信メカニズムの宣言

私たちの主役を宣言します:m_pReceiver ( RTSP 受信を担当 )、m_pDetector ( AI 分析を担当するハンドル )、および関連パラメータ:



次に、 MainWindow クラス内で、ストリームの受信を担当するハンドル PVOID m_pReceiver = NULL; と、デコード後の純粋な画像を保存するためのバッファポインタ BYTE *m_pNV12FrameBuffer = NULL; を宣言します。
さらに、低レイヤーのキャプチャコールバック ( Callback ) 関数と UI インターフェースは異なるスレッドに属しているため、スレッド間で画面の転送を行うために必ず  signal_UpdateImage と  slot_UpdateImage を宣言し、UI 画面をインターセプトして再描画するために eventFilterm_displayImage を追加してください。

変数の初期化と UI フールプルーフ安全メカニズム

C++ の世界において、「変数を使用する前に必ず初期化する」ことは非常に重要な安全規則です!同時に、この段階で UI ボタンのフールプルーフの骨組みや、ウィンドウを閉じた際のリソース解放メカニズムもしっかりと構築しておく必要があります。

コア変数の初期化

mainwindow.cpp のコンストラクタ MainWindow::MainWindow(...) を開き、ui->setupUi(this); の下に、先ほどヘッダーファイルで宣言したすべての変数にクリーンな初期値( 0NULL など )を与えてください。これにより、後でプログラムがポインタの状態を判定する際にクラッシュするのを防ぐことができます:


 

プログラムの起動直後は、まだストリームの受信を開始していないため、当然 AI 分析を起動することもできません。同時に、ユーザーが「 STOP 」を押さずにウィンドウの「 X 」を直接クリックしてソフトウェアを終了し、ネットワーク接続がフリーズしたりメモリリークが発生したりするのを防ぐ必要があります。したがって、完璧な保護メカニズムを構築する必要があります。

コンストラクタでのボタンのデフォルト状態の設定

mainwindow.cpp を開き、プログラムの起動時(つまり MainWindow::MainWindow コンストラクタ内)の変数初期化の直下に、プログラム起動時のボタンのフールプルーフ状態を設定します( 接続前は停止を押せない、画面がない場合は AI を起動できない ):

4つのボタンの状態切り替えロジック

4つのボタンのスロット関数内で、まずはボタンクリック後の「 状態の連動 ( setEnabled ) 」をシンプルに記述し、安全な制御フローを構築します。

デストラクタでのリソース解放

この手動で割り当てられたメモリがウィンドウを閉じる際に安全に解放されることを確認し、メモリリーク ( Memory Leak ) を防ぎます:

RTSP 受信と接続ロジックの実装

堅固なフールプルーフメカニズムが整ったので、ついに RTSP ネットワークストリーム受信のコア API を組み込むことができます!この手順は、以前の章で学んだ「 受信側の受信テクニック 」を完全に再現するものです。

➤RTSP ストリーム受信側についてはこちらで復習できます: 9-9 RTSP ストリーム受信側(クライアント)機能の実践チュートリアル

空のコールバック ( Callback ) 関数の作成

API をバインドして接続を開始する前に、まず mainwindow.cpp  の上部に、低レイヤーの情報を受信するための2つの Callback ハブを作成する必要があります。1つは接続状態の受信を担当し ( on_broadcast_client_connected_callback )、もう1つはデコード後の純粋な画像のインターセプトを担当します  ( on_video_decoder_broadcast_client_callback ) :

START RECEIVER の実装 ( 受信の開始 )

スロット関数 on_Button_RECEIVERStart_clicked() に戻り、元々記述していたボタン状態の切り替え (setEnabled) コードの上に、接続を開始する API を正式に追加します:
1. StrURL 入力ボックスから URL を読み取ります。
2. QCAP_CREATE_BROADCAST_CLIENT を呼び出して受信側を作成します。
3. QCAP_REGISTER_VIDEO_DECODER_BROADCAST_CLIENT_CALLBACK と QCAP_REGISTER_BROADCAST_CLIENT_CONNECTED_CALLBACK の2つのコールバックを登録します。
4. QCAP_START_BROADCAST_CLIENT を呼び出して、正式にストリームの受信を開始します!
    受信側が正式に起動したことをマークするために、忘れずに m_bStartReceiver = TRUE; を追加してください。

STOP RECEIVER の実装 ( 受信の停止 )

同様に、 on_Button_RECEIVERStop_clicked() スロット関数内で、安全に終了するためのリソース解放 API を追加し、関連する状態変数をリセットする必要があります:

NV12 バッファの動的割り当て

低レイヤーの映像をコピーして取り出す準備ができているため、それに十分な大きさのメモリ空間 (Buffer) を用意する必要があります。カメラの解像度は動的であるため、「接続に成功して幅と高さを取得した」瞬間に動的に割り当てを行い、プロジェクトを閉じる際に確実に解放する必要があります。

接続コールバックでのメモリ割り当て

on_broadcast_client_connected_callback というハブ関数に戻ります。 nVideoWidth と nVideoHeight を正常に取得した後、 malloc を使用して NV12 に必要なメモリサイズ ( 幅 × 高さ × 1.5 ) を割り当てます:



 

コア API と構造体の紹介

メモリの準備が整った後、ついに低レイヤーのデコードされた画面を取り出します!しかしその前に、非常に重要なコア概念を明確にしておく必要があります。

➤ コア概念の解析:なぜ「 構造体を手動で展開する 」必要があるのか?

第10章のチュートリアルを一緒に完了した方は、疑問に思うかもしれません。「 以前、純粋な再生や録画を行ったときは、受信したパラメータをそのまま次の API に渡すだけでよかったのでは?なぜ今回はこんなに手間をかけてメモリをコピーするのですか?」
この背景には、実は2つの重要な考慮事項があります:

1. AI と OpenCV が純粋な RAW Data ( 生データ ) を確実に受信するため:AI エンジン ( QDEEP ) と OpenCV は、「純粋で、隙間なく配置された、不純物のない RAW Data」しか認識しません!低レイヤーのアーキテクチャでは、デコーダが画面を出力するとき、これらのデータは qcap_av_frame_t という専用の構造体にラップされています。さらに、GPU やハードウェアが映像を処理する際には「メモリアライメント ( Memory Alignment ) 」というメカニズムがあるため、メモリ内の実際の画像のストライド ( nPitch または Stride ) が、画像の実際の幅 ( nWidth ) より大きくなることがよくあります。構造体を展開して余分なエッジの余白を取り除かないと、画像に深刻な歪みが生じます。
2. 低レイヤーメモリの安全保護メカニズム:データを構造体にラップし、API ( ロック ➔ コピー ➔ アンロック ) を通じてアクセスすることを開発者に要求するのは、生データがユーザーに直接触れられるのを防ぐためです。C/C++ 開発において、低レイヤーのポインタを直接開放すると、開発者が画像処理時に誤って「 範囲外の変更 」を引き起こしやすくなります。この保護メカニズムにより、低レイヤープログラムの実行に必要なメモリ空間が外部から誤って変更されにくくなり、システムの動作が安全になるだけでなく、プログラムのクラッシュのリスクも大幅に軽減されます。

純粋な画像を安全に取り出すためには、以下の3つの API を順番に呼び出し、 qcap_av_frame_t の内部構造を理解する必要があります:

QCAP_BUFFER_GET_RCBUFFER ( バッファの取得 )

コールバックの元のデータから、低レイヤーの画像構造体の Handle を取得するために使用します。

QCAP_RCBUFFER_LOCK_DATA ( ロックして構造体を展開する )

Handle を取得した後、コピーの途中でデータが低レイヤーによって上書きされるのを防ぐために、それを「ロック」する必要があります。ロックに成功すると、 qcap_av_frame_t 構造体が返されます!

qcap_av_frame_t 構造体の理解:この構造体を取得すると、その中には画像の4つの非常に重要な情報が含まれています:

QCAP_RCBUFFER_UNLOCK_DATA ( ロック解除と解放 )

memcpy を使用して qcap_av_frame_t 内のデータを自分のメモリに安全にコピーした後は、絶対にこの API を呼び出してロックを解除し、リソースを低レイヤーに返却することを忘れないでください。そうしないと、システムはすぐにフリーズしてしまいます!

構造体を展開せずに、メモリパッケージ全体をそのまま AI や OpenCV に渡してしまうと、余分な「 無効なパディングの余白 ( Padding ) 」により、画像が深刻に歪んでしまいます (例えば、緑や紫の斜めのストライプが現れるなど)。
したがって、このステップにおける私たちの神聖な使命は、構造体を開き、すべての余分な Padding を取り除き、純粋な Y Plane (輝度) と UV Plane (色差) を抽出して、先ほど割り当てた NV12 バッファに隙間なく配置することです!


 

NV12 の抽出と OpenCV 画像変換・レンダリング

mainwindow.cpp での OpenCV のインクルード

画像変換ロジックを書き始める前に、必ず mainwindow.cpp の一番上に戻り、OpenCV のコアヘッダーファイルをインクルードしてください:

バッファのロックと NV12 生画像の抽出

さあ、先ほど学んだ API を組み合わせてみましょう! on_video_decoder_broadcast_client_callback ハブに移動してください。画面を安全に取り出すために、「 バッファの取得 ➔ ロック ➔ コピー ➔ ロック解除 」という厳密なプロセスを実装します:

1. QCAP_BUFFER_GET_RCBUFFER を呼び出して画像構造体を取得します。
2. QCAP_RCBUFFER_LOCK_DATA を呼び出してデータをロックします( コピーの途中でデータが低レイヤーによって上書きされるのを防ぎます )。
3. NV12 フォーマットは Y Plane ( 輝度 ) と UV Plane ( 色差 ) の2つのプレーンに分かれています。ループと memcpy を使用し、それぞれのストライド ( nPitch ) に合わせて、pData ( Y ) と pData ( UV ) から、準備した m_pNV12FrameBuffer にデータをコピーします。
4. 完了したら、 QCAP_RCBUFFER_UNLOCK_DATA を呼び出してロックを解除します。

cv::Mat への変換とスレッド間での画像信号の送信

次に、 on_video_decoder_broadcast_client_callback ハブで、「 バッファの取得 ➔ ロック ➔ コピー ➔ ロック解除 」のプロセスに従って NV12 をコピーして取り出します。その後、先ほどインクルードした OpenCV を使用して即座に RGB フォーマットの QImage に変換し、最後に emit を通じて画像をスレッド間で送信します:

コンストラクタでのスレッド間レンダリングメカニズムの開始

先ほど信号を emit  で送信しましたが、UI インターフェースはどうやってそれを受信すればよいのでしょうか? mainwindow.cpp の コンストラクタ MainWindow::MainWindow(...) 内に戻ってください。ここで「信号のバインド」と「イベントフィルターのインストール」を行う必要があります。この2行は、画面を表示するための絶対的な鍵となります:

画像のキャッシュと再描画トリガーの実装

次に、 mainwindow.cpp の下部で、先ほどバインドした slot_UpdateImage を実装します。このスロット関数の役割は非常にシンプルで、渡された最新の画像を保存し、「 キャンバスを更新してください!」と呼びかけるだけです:

QPainter を使用した画面のレンダリング

最後のステップです! eventFilter ( イベントフィルター ) を実装します。先ほどの update()  が  ClientWindow  の Paint ( 描画 ) イベントをトリガーしたとき、即座にそれをインターセプトし、 m_displayImage を使用して QPainter を手動でウィンドウに描画します:

最終確認

素晴らしい!ここまでで、私たちの QDeepSample 基礎テンプレートが完璧に構築されました。それでは、「 Build and RUN 」 を押してこのプロジェクトを実行してください。RTSP URL を貼り付け、START RECEIVER をクリックすると、ご自身で記述した Qt と OpenCV の描画メカニズムを通じて、カメラのスムーズな映像がソフトウェアウィンドウに完璧に表示されるのが確認できるはずです!

鋭い方はお気づきかもしれませんが、インターフェース上の START DETECTION と STOP DETECTION ボタンは現在クリックしてもグレーアウトしたままで、何も反応しません。ご心配なく。今お持ちのこの QDeepSample プロジェクトには、すでに「純粋な NV12 画像抽出機能」と「 カスタムキャンバスレンダリング機能 」が備わっています。次章の実装では、ついに QDEEP エンジンを本格的に呼び覚まします。準備したこの NV12 画像パッケージをエンジンに与え、AI が返す座標を使って OpenCV で枠を描くだけで、あなたのソフトウェアは瞬時に映像を理解するスーパーパワーを手に入れます!それでは、次の章でお会いしましょう!

Copyright © 2026 YUAN High-Tech Development Co., Ltd.
All rights reserved.