実家にある見守りカメラを見ていて、ふと思った。
これ、メーカーアプリを使わずに直接プログラムできるのでは?
そこで、同じWi-Fiネットワークに接続したMacから調べてみた。
最終的には、
- RTSPで映像取得
- HTTPでスナップショット取得
- DVRIP/SofiaでPTZ制御
- OpenCVで人物検出
- 人物の画面内位置を判定
- AIの判断でカメラをLEFT / CENTER / RIGHTへ移動
- 移動完了を映像側から確認
という、最小の閉ループ人物追尾まで実機で動かすことができた。
この記事は、その過程の記録である。
注: 本記事では、自分が所有・管理するカメラを、自宅LAN内で正規の認証情報を使用して検証しています。
対象カメラ
対象は家庭用の見守りカメラ。1
スマートフォンでは Tris Home アプリから利用していた。
最初はメーカーも通信仕様もよく分からない状態だった。
同じWi-Fiに接続したMacから、まずLAN内の機器を確認した。
arp -a
カメラの電源を切った状態と入れた状態を比較し、
192.168.10.73
がカメラだと特定した。
nmapで調べる
次にポートを確認した。
nmap -sV 192.168.10.73
結果は、
80/tcp open http
554/tcp open rtsp H264DVR rtspd 1.0
だった。
80番にはWeb Viewerがあり、554番ではRTSPサーバーが動いていた。
さらに調査すると、
34567/tcp
も開いていた。
これはXiongmai系のDVRIP/Sofiaプロトコルでよく使われるポートである。
Web UIやRTSP応答も含めて調べた結果、このカメラは
Xiongmai / XMEye系のOEMファームウェア
と判断できた。
Web Viewerを解析する
80番へアクセスすると、ブラウザには Web Viewer が表示された。
HTMLを見ると、
<script src="js/jquery-3.5.0.min.js"></script>
や、
$.getScript("pluginVersion.js")
$.getScript("js/main.js")
といった構造になっていた。
さらに、
VideoPlayTool://1
という独自プロトコルも見つかった。
つまり、ブラウザ側だけで映像再生しているのではなく、専用プラグインをローカルで起動する古いタイプの構成だった。
今回はこの専用プラグインは使わず、カメラ本体の通信仕様を直接調べることにした。
RTSP映像を取得する
RTSPはDigest認証が必要だった。
正規のデバイス資格情報を使って確認すると、主ストリーム・副ストリームの両方を取得できた。
主ストリーム:
H.265
1920x1080
25fps
PCM alaw audio
副ストリーム:
H.265
800x448
audioあり
AI解析には副ストリームで十分なので、こちらを使うことにした。
OpenCV側では、
cap = cv2.VideoCapture(rtsp_url)
でフレームを取得する。
実際には接続切れを考慮して、別スレッドで最新フレームを保持し、自動再接続する構造にした。
HTTPスナップショットも取得できた
HTTP側にはスナップショット用のエンドポイントもあった。
/webcapture.jpg?command=snap&channel=0
認証を通せばJPEGを取得できた。
RTSPが取れなくても、静止画AIならこれだけでも使える。
DVRIPでPTZを調べる
次にカメラを動かしてみる。
34567番のDVRIPを調べると、デバイス情報や設定を読み取ることができた。
機種情報は、
IPC_GK7201V300_G2-WR-V
PTZ設定には、
Pelco-D
115200bps
が見えていた。
さらに既存プリセットとして、
Preset 1
Preset 100
が登録されていた。
PTZ制御は、
OPPTZControl
で行う。
GotoPresetは簡単に動いた
まず既存プリセットを呼び出した。
{
"Name": "OPPTZControl",
"OPPTZControl": {
"Command": "GotoPreset",
"Parameter": {
"Channel": 0,
"Preset": 1,
"Step": 8,
"Pattern": "Start"
}
}
}
結果は、
Ret:100
実際にカメラも動き、プリセット位置で自動停止した。
ここまでは順調だった。
一番苦労したのは「止める」こと
AI追尾を考えるなら、
右へ少し動く
↓
止める
という相対制御ができると扱いやすい。
そこで、
DirectionRight
を送ると、実際にカメラは右へパンした。
問題は停止だった。
XM/DVRIP系の既存実装を調べながら、複数の方式を試した。
Pattern="Stop"
まず、
Command=DirectionRight
Pattern=Stop
を試した。
しかし止まらなかった。
Preset=-1
OpenIPC/python-dvrでは、
START:
Pattern=SetBegin
Preset=65535
STOP:
Pattern=SetBegin
Preset=-1
という方式が使われている。
canonical実装に近い形で試した。
START/STOP両方で、
Ret:100
は返る。
しかし実機ではSTOP後もカメラが動き続けた。
Command="Stop"
ICSee系の実装では、
Command=Stop
を使う例があった。
これも試した。
やはり、
Ret:100
は返る。
しかし、STOP応答後しばらくしてからカメラが動き始めた。
このカメラでは、STARTを送ってから物理的にモーターが動き始めるまで約1〜1.5秒の潜時があった。
STOPが内部キューをキャンセルしているようには見えなかった。
Ret:100を信用しすぎてはいけない
この実験で重要だったのは、
Ret:100だから成功とは限らない
という点だった。
PTZコマンドは成功応答を返している。
しかし物理世界では期待どおりに止まっていない。
そこで、カメラ自身の映像から、
- 動き始めたか
- 動いているか
- 止まったか
を判定するようにした。
カメラの移動と人物の移動を分離する
単純な画像差分では、人物が動いただけでも「カメラが動いた」と判定してしまう。
そこで、
- Shi-Tomasi特徴点
- Lucas-Kanade Optical Flow
- affine transform
- RANSAC
を使い、背景全体のグローバルモーションを推定した。
人物だけが動く場合は局所変化になる。
カメラがパンすると、背景全体がほぼ同じ方向へ移動する。
これで、
人物の動き
と、
カメラ自身の移動
をかなり分離できた。
絶対PTZは使えなかった
次に、
pan=1234
tilt=567
のような絶対位置指定ができないか調べた。
しかし、
PtzStatus
PtzPosition
PositionQuery
Camera.Position
などはすべて非対応だった。
Pelco-Dの開ループ制御で、位置エンコーダを持っていない構成らしい。
つまり、
現在の角度を読むことも、絶対座標へ移動することもできない。
そこで連続PTZ制御を捨てることにした。
プリセット方式へ設計変更
GotoPresetは確実に自動停止する。
ならば、
LEFT
CENTER
RIGHT
の3つの視点をあらかじめ登録しておき、
AIが見る場所を選べばよい。
実験用に、
51 = LEFT
52 = CENTER
53 = RIGHT
を作った。
既存の家庭用プリセット1/100には触れない。
初期キャリブレーションは人間が行う
SetPresetは、
現在のカメラ位置を保存する
コマンドだった。
しかし連続PTZの停止が不安定なので、Pythonから正確にLEFT/CENTER/RIGHTへ動かすのは難しい。
そこで初期登録だけはスマホアプリを使った。
人間がアプリでLEFTへ向ける
↓
SetPreset 51
CENTERへ向ける
↓
SetPreset 52
RIGHTへ向ける
↓
SetPreset 53
初期キャリブレーションは人間。
運用はAI。
この構成にした。
GotoPresetの完了判定も必要だった
もう一つハマった。
GotoPresetを送って、
6秒待つ
という実装では不十分だった。
同じPreset 1を呼んだのに、一度だけ別の画角に見えたことがあった。
調べると、プリセット自体の再現性は高かった。
Preset 1 → 100 → 1 → 100 → 1
と交互に移動させても、ORB/homographyで同じ背景へ戻ることを確認できた。
問題は、
固定時間待機では移動完了を正しく判定できないこと
だった。
そこで、
GotoPreset
↓
global motion開始
↓
motion収束
↓
背景静止
↓
鮮鋭フレーム選択
↓
基準画像とORB/homography照合
という完了検出を入れた。
人物検出
人物位置の判定には、まず軽量なYuNet顔検出を使った。
OpenCV ZooのONNXモデルを利用した。
検出結果は、
(x, y, w, h, score)
として扱う。
画面サイズが800x448なら、
center_x = 400
center_y = 224
人物bboxの中心を求め、
norm_x = (person_center_x - image_center_x) / image_center_x
として正規化した。
判定は、
norm_x < -0.12
→ LEFT
-0.12 <= norm_x <= 0.12
→ CENTER
norm_x > 0.12
→ RIGHT
とした。
状態機械を入れる
認識結果をそのままPTZへ送ると、カメラが左右に振動する。
そこで状態機械を入れた。
OBSERVE
↓
同方向を5フレーム連続検出
↓
GOTO_PENDING
↓
GotoPreset
↓
MOVING
↓
移動完了検出
↓
COOLDOWN
↓
OBSERVE
安全条件は、
STABLE_N = 5
COOLDOWN = 4秒
MAX_MOVES = 3
とした。
さらに、
- カメラ移動中は人物判定しない
- 同じPresetへの連続Gotoは禁止
- 1〜2フレームの人物ロストは無視
- 3秒以上ロストしたら追尾停止
- 勝手にCENTERへ戻さない
- 複数人物がいても対象を勝手に切り替えない
- Direction系PTZは禁止
- GotoPresetのみ使用
- 自動リトライなし
とした。
dry-runで先に確認する
実機を動かす前に、
--commit
なしではPTZ送信ゼロになるdry-runを作った。
dry-runでは、
bbox
norm_x
LEFT/CENTER/RIGHT
stable counter
current preset
wanted preset
state
を時系列で表示した。
これで、
人物が左に5フレーム
↓
GotoPreset 51 を送る予定
という判断だけを先に確認できた。
実デモ
最後に実機で閉ループを動かした。
開始時はCENTERへ移動。
その後、人物が左右へ動いた。
結果は、
人物 LEFT
→ GotoPreset 51
人物 CENTER
→ GotoPreset 52
人物 LEFT
→ GotoPreset 51
となった。
3回の移動すべてで、
- GotoPreset送信
- カメラ移動
- motion収束
- 静止確認
- 次の人物判定
まで閉ループが正常に回った。
誤追尾や振動も発生しなかった。
最終構成
最終的にはこうなった。
市販見守りカメラ
│
RTSP / H.265
│
▼
OpenCV
│
人物・顔検出
│
bbox中心計算
│
LEFT / CENTER / RIGHT
│
状態機械・安全判定
│
▼
DVRIP GotoPreset
│
▼
PTZモーター
│
▼
物理世界
│
RTSPで再観察
└───────────
完全な閉ループである。
やってみて分かったこと
一番大きかったのは、
AI画像認識そのものより、物理世界との接続部分の方が難しい
ということだった。
人物検出は比較的簡単にできる。
しかし、
- カメラが本当に動いたのか
- いつ動き始めたのか
- いつ止まったのか
- コマンド成功応答を信用してよいのか
- 人物の移動とカメラの移動をどう分離するのか
といった部分が、実際のシステムでは重要になる。
これはロボティクスそのものだった。
連続制御を諦めるのも設計
当初は、
人物が右
↓
右へ少しパン
↓
Stop
という制御を考えていた。
しかしStopが安定しない。
そこで、
LEFT
CENTER
RIGHT
という離散的なプリセット制御へ切り替えた。
結果的にはこちらの方が安全で、状態管理もしやすかった。
「できないAPIを無理に使う」のではなく、
確実に動く機能を組み合わせて目的を達成する
方が実用的だった。
AIに身体を与えるとは
今回使ったのは、普通に家庭で売られている見守りカメラだった。
特別なロボットではない。
それでも、
- カメラはAIの目
- PTZはAIの首
- RTSPは感覚入力
- DVRIPは運動出力
として使える。
そして、
見る
↓
判断する
↓
動く
↓
もう一度見る
というループを作れば、十分にフィジカルAIになる。
AIに身体を与えるとは、必ずしもヒューマノイドを一から作ることではない。
すでに身の回りにあるセンサーやモーターを、AIが直接使えるようにすることでも始められる。
今回の実験で一番面白かったのは、そこだった。
- 実験用コード: GitHub: camera-ai-tracker
自分が所有・管理する機器を対象に、自宅LAN内で検証したコードです。
認証情報や環境依存情報は含めていません。
-
今回使用した見守りカメラ:Amazon商品ページ ↩