Real-time Object Detection: Lessons from Building a Low-Latency Pipeline
ブロッキング処理の初期段階から安定したストリーミングアーキテクチャに至るまで、低遅延のリアルタイム物体検出パイプラインを構築した際の実践的な経験。
最近、私はコンピュータビジョン業界における「Hello World」のように思える問題、つまり一見簡単そうに見えて、実環境に導入すると非常に難しい問題、Real-time Object Detectionに取り組み始めました。
私のハードウェアセットアップは非常に基本的でした:
- Sensor: 民生用Tapoカメラ (RTSP Stream, 最大 15 FPS, 解像度 720p)。
- Compute: 開発用のハイスペックPC (NVIDIA GPU) と、デプロイ用の NVIDIA Jetson Nano。
- Goal: 物体を検出し、可能な限り低い遅延(レイテンシ)で結果をストリーミングすること。
私は単純に考えていました。「cv2.VideoCapture() して、フレームを YOLO に投げて、cv2.imshow() すれば終わりだろう」と。しかし、現実はすぐに私の夢を打ち砕きました。遅延(レイテンシ)は 3秒 にも達しました。自動化の世界において、3秒という時間は、システムを「使えるもの」から、高速な応答を必要とするアプリケーションでは「全く使い物にならないもの」に変えるのに十分な長さです。
この記事は、可能な限り低い遅延を実現するために、私がどのようにこのパイプラインを破壊し、再構築したかを記録した技術日誌です。
1. ナイーブなアプローチとリアルタイムに関するよくある誤解#
最初のステップとして、私は最も単純な方法(ナイーブなアプローチ)で問題に取り組みました:
import cv2
cap = cv2.VideoCapture("rtsp://admin:pass@ip:554/stream", cv2.CAP_FFMPEG)
while True:
ret, frame = cap.read() # Blocking I/O
# ... AI処理 ...
cv2.imshow("Camera", frame) # Render UI
if cv2.waitKey(1) == ord('q'): breakその結果、表示される画像は実際より約 3秒 遅れていました。原因は ブロッキングI/O(Blocking I/O) と 内部バッファ(Internal Buffer) のメカニズムにありました:
-
速度差(Producer-Consumer Gap): カメラは15 FPSでデータを送ってきます(Producer)。しかし、
imshowのUI描画やAIの負荷により、whileループが10 FPS(Consumer)しか処理できない場合、毎秒余分に発生する5フレームはどこへ行くのでしょうか? -
Buffer: それらは OpenCV/FFmpeg バックエンドの内部バッファに詰め込まれます。このバッファは徐々に一杯になっていきます。
-
帰結:
cap.read()を呼び出した時、私は到着したばかりのフレームを取得しているのではなく、バッファの先頭に並んでいるフレーム(最も古いフレーム)を取得しているのです。
スループット(Throughput) vs. レイテンシ(Latency)
システムを最適化するには、これら2つの概念を明確に区別する必要があります:
-
Throughput (FPS): システムが1秒間に何枚の画像を処理できるか。(サーバー/バッチ処理で重要)。
-
Latency (遅延): 実際のイベントが発生してから、処理画面に表示されるまでにどれくらいの時間がかかるか。(ロボット/安全性/インタラクティブシステムで重要)。
60 FPS(滑らかな映像)であっても、レイテンシが3秒(過去の映像)であるという状況はあり得ます。これが 高スループット・高レイテンシ(High Throughput, High Latency) の状態です。
OpenCV ブロッキングI/O の問題点#
OpenCVのデフォルトのパイプラインは ブロッキング メカニズムで動作します:
-
cap.read(): プログラムは停止し、キュー内の次のフレームがデコードされるのを待ちます。 -
model.predict(): プログラムは停止し、AIの実行完了を待ちます。 -
cv2.imshow(): プログラムは停止し、UIのレンダリングを待ちます。
カメラが30 FPSを送信していても、ループの合計処理時間が10 FPSにしか達しない場合、残りの20フレームはOpenCVの 内部バッファ に詰め込まれます。このバッファはどんどん膨れ上がり、画面で見ているフレームは実は... 3秒前に送信されたものになります。
動画処理システムにおけるFPS基準:
-
< 10 FPS: スマートパーキングや小売店での行動分析など、対象物の動きが遅く、即時の反応を必要としない受動的な監視アプリケーションに適しています。
-
15–24 FPS: 人間の目が動きを連続的であると感じるための最低ラインです。このレベルを下回ると、画像がカクカクしたり遅れて見えたりします。安価な CCTV システムでよく見られます。
-
30–60 FPS: このFPS範囲は、ユーザーとの直接的な対話(UI/UX)が必要なアプリケーションや、基本的なレベルの 運転支援システム における黄金基準とされています。
-
> 60 FPS: このレベルでは、システムは人間の視覚だけでなく、機械の反応要件も満たします。わずか数ミリ秒の遅れが事故や製品の欠陥につながる可能性があります。自動運転車 や 産業用検査 で典型的です。
現在、商業環境で「良い」と評価されるリアルタイム物体検出システムは、通常、中〜高性能のエッジデバイスおよびエンドユーザーのUI(モバイル/Web)上で 25–30 FPS で安定して動作する必要があります。10–15 FPSというレベルは、超低価格帯や、極めて時間に敏感でないアプリケーションでのみ許容されます。
2. 解決策と教訓#
マルチスレッディング (Multithreading)#
最初のアイデアは、画像の読み込み と 画像の処理 を2つの別々のスレッド(Thread)に分けることでした。
-
Thread A (Reader): 常にカメラから最新のフレームを読み込み、固定サイズ(size=1)のQueueに入れます。Queueが一杯の場合、古いフレームを捨てて新しいフレームで上書きします。
-
Thread B (Processor): 処理のためにQueueからフレームを取り出します。
結果: 積極的に古いフレームを捨てている(Frame Dropping)ため、レイテンシは大幅に減少しました。しかし、コードは複雑になりました(競合状態、スレッドセーフ)。さらに、バッファはPythonスレッドに到達する前のドライバ/FFmpeg層でまだ一杯になる可能性がありました。
FFmpeg CLI vs. GStreamer#
私は、プロトコルにより深く介入するために、デフォルトのOpenCVラッパーを回避することにしました。
FFmpegのテスト (Subprocess):
-
subprocessを使用して ffmpeg を呼び出し、UDPの使用を強制し、フラグnobufferを使用しました。 -
結果: レイテンシは極めて低くなりました(ほぼリアルタイム)。
-
問題: FFmpegの哲学はスループットの最適化(できるだけ多く処理する バッファが大きい)にあります。Low Latencyで動作させるには多くのパラメータ設定が必要で、rawデータをPythonにパイプするのはかなり手動的な作業です。
cmd = [
"ffmpeg",
"-rtsp_transport", "udp", # UDPを使用して遅延を減らす
"-fflags", "nobuffer", # 内部バッファを無効化
"-flags", "low_delay", # 低遅延モードを有効化
"-reorder_queue_size", "0", # パケットを並べ替えない
"-use_wallclock_as_timestamps", "1",
"-i", RTSP_URL,
"-f", "rawvideo", # raw出力
"-pix_fmt", "bgr24", # OpenCV用ピクセルフォーマット
"-"
]GStreamerへの移行 (業界標準):
-
GStreamerはモジュール式のパイプラインとして動作し、
sourcedemuxparsedecodesinkといった各リンク(Element)への深い介入を可能にします。特に、FFmpegよりも ゼロコピー(Zero-copy)(CPU/GPU間のデータコピー制限)をサポートすることに優れています。 -
大手カメラメーカー(Hikvisionなど)やNVIDIA(DeepStream)は皆、ハードウェアデコーディングを活用するためにGStreamerに似たパイプラインプラットフォーム上でSDKを構築しています。
-
GStreamerはLow Latencyにとって最適な選択肢ですが、学習曲線は非常に急です。
3. GStreamerとOpenCVを組み合わせる際の依存関係地獄#
これは私が最も時間を浪費した部分です。GStreamer + Python + OpenCV の組み合わせは、初心者にとって悪夢です。
-
Conda: Conda内にGStreamerをインストールしようとしました。結果は悲惨でした。ライブラリパッケージが絶えず衝突しました。
-
OpenCV-Pythonの問題: PyPIにある
pip install opencv-pythonパッケージは、デフォルトでは GStreamer バックエンドを サポートしていません。このパッケージを使用すると、コードは実行されますが、パイプラインを開くことができません。 -
Ultralytics (YOLO):
ultralyticsをインストールすると、自動的にopencv-python(ビルド済みバージョン)で上書きインストールされます。これにより、私がセットアップしたGStreamer環境が壊れてしまいました。1
解決策:
CondaやDocker(Dockerイメージのビルドデバッグは非常に時間がかかります)を使用する代わりに、Linux上で最も速く安定した解決策は、システムパッケージと組み合わせて venv を使用することです:
# 1. GStreamerとOpenCVをシステム全体にインストール (Ubuntu)
sudo apt-get install python3-opencv libgstreamer1.0-dev ...
# 2. --system-site-packages フラグを使用して venv を作成し、システムライブラリを継承する
python3 -m venv myenv --system-site-packages
source myenv/bin/activate
# 3. 注意: ultralyticsをインストールする場合、opencvを上書きしていないかよく確認してください4. 最適なパイプラインアーキテクチャ (Stream-to-Stream)#
最高のパフォーマンスを実現するために、アーキテクチャを変更しました:
-
Input: GStreamer Pipeline (入力 RTSP Appsink)。
-
Process: OpenCV + YOLO。
-
Output:
imshow(メインスレッドをブロックする)の代わりに、ローカルのRTSPサーバーへ結果をストリーミングします。
RTSPサーバーとして MediaMTX を使用しました(Docker経由で実行)。1
各コンポーネントの速度制限分析 (FPS Hierarchy)#
| コンポーネント | 実際の速度 / 制限 | 備考 |
|---|---|---|
| Camera (Sensor) | 15 FPS | Tapoカメラの物理的制限 (720p, RTSP stream) |
| Inference (YOLO11n) | ≈550–700 FPS (RTX A4000上, 640×640) | 1フレームあたりのレイテンシ ≈ 1.4–1.8 ms (Ultralyticsベンチマークによる測定) |
| Encoding + Network | > 200 FPS | x264enc preset ultrafast + zerolatency で60 FPSを余裕で処理可能 |
| Display / 画面 | 60 FPS (60 Hz) | 画面および人間の目の一般的なリフレッシュレート制限 |
上記の表からの結論:
- 最も遅いコンポーネント(ボトルネック)は 15 FPSのカメラ です 残りの部分がどれだけ速くても、システム全体は15 FPSより速く動作することはできません。
- 推論はフレームあたり≈1.5 msしかかかりませんが、新しいフレームは≈66 msごとにしか到着しません AIは98%以上の時間アイドル状態です。
- したがって、このケースでは 複雑なマルチスレッディング/マルチプロセッシングは不要 でありながら、リアルタイムかつ極めて低いレイテンシ(エンドツーエンドで < 100 ms)を実現できます。
- 60 FPSのカメラにアップグレードしたり、より重いモデル(YOLO11m/x, RVTなど)を使用する場合に初めて、フレーム落ちを防ぐためにプロセスを分離したり、キュー + ワーカーを使用する必要があります。
重要な注意: 最終的なシステムのFPSは、常にパイプライン内の最小FPSと等しくなります。ここではセンサーの15 FPSです。
The Final Code#
以下は、GStreamerパイプライン用のOpenCVラッパーを組み合わせ、遅延を排除したコードです:
import cv2
from ultralytics import YOLO
def main():
# ローカルRTSPサーバー
rtsp_url = "rtsp://admin:pass@192.168.1.50:554/stream"
output_url = "rtsp://localhost:8554/stream"
model = YOLO("checkpoints/yolo11n.pt")
# --- INPUT PIPELINE ---
# latency=0: ネットワークバッファの遅延を減らす試み
# appsink sync=false drop=true max-buffers=1:
# -> これが鍵です! 最新の1フレームだけを保持し、古いフレームはすべて捨てます。
input_pipeline = (
f"rtspsrc location={rtsp_url} latency=0 ! queue ! "
f"rtph264depay ! h264parse ! avdec_h264 ! "
f"videoconvert ! appsink sync=false drop=true max-buffers=1"
)
cap = cv2.VideoCapture(input_pipeline, cv2.CAP_GSTREAMER)
if not cap.isOpened():
print("Error: Cannot open input pipeline")
return
# ストリームパラメータの取得
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) or 25
# --- OUTPUT PIPELINE ---
# appsrc -> x264enc -> rtspclientsink
# tune=zerolatency: エンコーダをリアルタイム用に最適化
# speed-preset=ultrafast: 速度のために圧縮率を犠牲にする
output_pipeline = (
"appsrc is-live=true ! "
"videoconvert ! video/x-raw,format=I420 ! "
"x264enc tune=zerolatency bitrate=2000 speed-preset=ultrafast key-int-max=30 ! "
f"rtspclientsink location={output_url}"
)
writer = cv2.VideoWriter(
output_pipeline,
cv2.CAP_GSTREAMER,
0, fps, (width, height), True
)
if not writer.isOpened():
print("Error: Cannot open output pipeline")
return
try:
while True:
ret, frame = cap.read()
if not ret: break
# Inference YOLO
results = model(frame, verbose=False)
annotated_frame = results[0].plot()
# 出力をストリーミング (GUIレンダリングはしない)
writer.write(annotated_frame)
except KeyboardInterrupt:
print("Stopping...")
finally:
cap.release()
writer.release()
if __name__ == "__main__":
main()結果は非常に有望です。レイテンシは100 ms未満に低下し、大きなオーバーヘッドはほとんど残っていません。
注意すべきいくつかのリスク
上記のパイプラインは、ネットワークがクリーンでジッターが低く、パケットロスがほとんどないLAN環境では非常にうまく機能します。しかし、将来的に弱いWiFi、マルチホップネットワーク、またはインターネットに展開する場合、考慮すべきいくつかの問題が発生します:
-
latency=0: すべてのカメラがこれに耐えられるわけではありません。GStreamerはジッターバッファを完全に無効化します レイテンシは極めて低くなりますが、ネットワークが不安定な場合、画像の乱れやフレームロスが発生しやすくなります。 -
max-buffers=1 drop=true: 極低遅延には役立ちますが、推論が時折遅くなった場合に重要なイベントを見逃す可能性があります。 -
UDPは高速ですが、パケットロスが発生しやすいです。LAN内では問題ありませんが、干渉の多いWAN/WiFiに出ると 出力がカクついたり、フレームが飛んだりします。
-
x264の
ultrafast + zerolatencyは非常に高いビットレートを生成するため、強力なネットワークが必要です。弱いインターネット環境では、輻輳を引き起こし、遅延が増加する可能性があります。
5. 結論#
低遅延のリアルタイム物体検出パイプラインを構築することは、当初想像していたほど単純ではありませんでした。このプロセスを通じて、私は以下の点について多くの貴重な教訓を得ました:
-
ビデオストリーミングにおけるバッファリングとレイテンシのメカニズムを理解すること。
-
GStreamerを使用してパイプラインを詳細に制御すること。
-
ライブラリの競合を避けるために適切な開発環境を設定すること。
-
ハードウェアの制限と実際の要件に適したシステムアーキテクチャを設計すること。
これらの共有が、リアルタイム動画処理システムの構築に苦労している皆さんのお役に立てば幸いです。幸運を祈ります!
閲覧数
— 閲覧数
Nguyen Xuan Hoa
nguyenxuanhoakhtn@gmail.com