0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

市販の見守りカメラを解析して、AI人物追尾まで実装してみた

0
Posted at

実家にある見守りカメラを見ていて、ふと思った。

これ、メーカーアプリを使わずに直接プログラムできるのでは?

そこで、同じ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が直接使えるようにすることでも始められる。

今回の実験で一番面白かったのは、そこだった。


自分が所有・管理する機器を対象に、自宅LAN内で検証したコードです。
認証情報や環境依存情報は含めていません。

  1. 今回使用した見守りカメラ:Amazon商品ページ

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?