🙄

【猫監視システムへの道】3.Tapo C210のPTZ(Pan/Tilt/Zoom)操作

2023/08/21に公開

Tapo C210の映像取得

Tapo C210の映像取得に関しては、監視カメラビューアをPySimpleGuiでつくるという記事を参考にしたら、サクッと実現できました。

Pan/Tilt操作

PTZ操作の種類

まず、ONVIF規格のPTZ(Pan/Tilt/Zoom)操作には3通りの方法が用意されています。

  • ContinuousMove(旋回時間指定旋回機能)
  • AbsoluteMove(絶対座標指定旋回機能)
  • RelativeMove(相対座標指定旋回機能)

それぞれ、以下のような使い分けになります。

  • 移動方向を指定する。最初に指定した時間の経過、または停止命令、移動範囲外に達するまで移動を続ける。
  • 移動先のPTZ座標を指定する。
  • 現在の座標を基準とした、移動量を指定する。

うちの猫は、日中はキャットタワーかハンモックの上、
深夜はソファの上にいる事が多いので、
定位置へ移動させたい場合は、AbsoluteMoveを使う。

また、Tapoアプリで「動作トラッキング」をONにしていると、
猫(や人)を追ってカメラが自動追尾してくれるので、
定位置への移動以外はRelativeMoveを使う。

ContinuousMoveは使用予定なし。

いざ実装

これも、監視カメラビューアをPySimpleGuiでつくるという記事を参考にしました。

ContinuousMove

まずは、参考サイトのコードを丸っとコピペして、動くかどうか確認。
ContinuousMoveの方は、他の方からのコメント通りC210ではエラーになりました。
ただ、使う予定はなかったので気を取り直してAbsoluteMoveへ。

AbsoluteMove

こちらも、参考サイトのコードを丸っとコピペして、動くかどうか確認。
ところが、下記の★マークのところで、「Bad Request」が返ります。

ptz_viewer.py
        if is_streaming:
            oreq = onvif_request.absolute_move(cur_x, cur_y)
            req = urllib.request.Request(url, data=oreq.encode(), method='POST', headers=headers)
            try:
                with urllib.request.urlopen(req) as response:   # ★
                    pass

詳しい例外を取得すると、「HTTP Error 400: Bad Request」になりました。

400は、クライアントの要求に何らかの誤りや問題があり
処理を完了できなかったことを示すステータスコードなので、
urllib.request.Request()に渡す引数が不適切、引数の中身が不適切、
という事を示しているのだと思います。

urlやheadersをprint()文で出力しても問題なさそうだったので、
dataに渡す値がおかしいのだろうと目星をつけました。
で、そうなると、onvifreq.py全体が怪しくなるわけです。

ONVIFのPTZ規格を調べたり、諦めて他のPythonライブラリに変えてみたり、
色々なフォーラムを覗いたりしても解決せず、丸1日費やしました。

結局『ONVIF Device Manager』というソフトを使っている時に、
AbsoluteMoveに渡す座標が、x, yは-1~1, zoomは0~1だと気付き、

ptz_viewer.py
# パンチルトパラメータ
X_MIN, X_MAX, X_STP = -170, 170, 10
Y_MIN, Y_MAX, Y_STP = -30, 30, 5

を、下記のように書き換えたら動きました。

ptz_viewer.py
# パンチルトパラメータ(最小座標、最大座標、移動量)
X_MIN, X_MAX, X_STP = -1, 1, 0.1
Y_MIN, Y_MAX, Y_STP = -1, 1, 0.1

RelativeMove

onvifreq.pyに、RelativeMove用の関数を追加すればよいだろうと考え、
ONVIF PTZ Service Specで情報を探しました。

space=に渡すURLをRelativeMove用にすればいいのでは?
と考えたら、ほぼ正解でした。

absolute_move()の下に、下記を追加。

onvifreq.py
    def relative_move(self, x, y):
        command = """
        <RelativeMove xmlns="http://www.onvif.org/ver20/ptz/wsdl">
            <ProfileToken>profile_1</ProfileToken>
            <Translation>
                <PanTilt x="{x}" y="{y}" xmlns="http://www.onvif.org/ver10/schema" \
                    space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace"/>
                <Zoom x="0" xmlns="http://www.onvif.org/ver10/schema" \
                    space="http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace"/>
            </Translation>
        </RelativeMove>
        """
        return self.request(self.username, self.password, command.format(x=x, y=y))

ContinuousMove用も以下のような関数を追加すると、それっぽく動きました。
こちらは、RelativeMove用の引数を渡して、動く事だけ確認済。
本来は移動を停止させる関数や、移動時間を指定するパラメータ等が必要だと思います。
今回はContinuousMoveは使わない想定なので、そこまでは確認していません。

onvifreq.py
    def continuous_move(self, x, y):
        command = """
        <ContinuousMove xmlns="http://www.onvif.org/ver20/ptz/wsdl">
            <ProfileToken>profile_1</ProfileToken>
            <Velocity>
                <PanTilt x="{x}" y="{y}" xmlns="http://www.onvif.org/ver10/schema" \
                    space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace"/>
                <Zoom x="0" xmlns="http://www.onvif.org/ver10/schema" \
                    space="http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace"/>
            </Velocity>
        </ContinuousMove>
        """
        return self.request(self.username, self.password, command.format(x=x, y=y))

ビューアの修正

AbsoluteMoveとRelativeMoveでは、移動量として渡すx, yの値が異なるので、
ビューアも以下のように修正しました。
※修正箇所のみ

ptz_viewer.py
# パンチルトパラメータ。移動量のみ。0~1の範囲とする。
X_STP = 0.1
Y_STP = 0.1
# ○ボタンで移動する定位置。絶対位置指定する。-1~1の範囲とする。
X_DEFALUT = -0.4
Y_DEFALUT =  0.2
ptz_viewer.py
    elif "-pt_" in event or inited_ptz:
        if is_streaming:
            # ○ボタン押下時は、定位置に移動
            if "-pt_center-" in event:
                x = X_DEFALUT
                y = Y_DEFALUT
                # 絶対位置指定用
		# Tapoアプリで「画像を反転」をONにしている場合は-を付ける。
                oreq = onvif_request.absolute_move(-x, -y)
            else:
                if "right" in event:
                    x = X_STP
                elif "left" in event:
                    x = - X_STP
                else:
                    x = 0

                if "top" in event:
                    y = Y_STP
                elif "btm" in event:
                    y = - Y_STP
                else:
                    y = 0
                # 相対位置指定用
		# Tapoアプリで「画像を反転」をONにしている場合は-を付けない。
                oreq = onvif_request.relative_move(x, y)
            req = urllib.request.Request(url, data=oreq.encode(), method='POST', headers=headers)
            try:
                with urllib.request.urlopen(req) as response:
                    pass
            except urllib.error.URLError as e:
                if e.reason == 'Bad Request':
                    print('カメラの範囲外です')
                else:
                    print(e)
            inited_ptz = False

Pan/Tiltの移動量の正・負について

absolute_move()にrelative_move()渡すx, yに-を付けるかどうかですが、
以下のようにするのが良いようです。

Tapoアプリで「画像を反転」をONにしている場合、
 absolute_move()には-を付ける。
 relative_move()には-を付けない。
 continuous_move()には-を付けない。
Tapoアプリで「画像を反転」をOFFにしている場合、
 absolute_move()には-を付けない。
 relative_move()には-を付ける。
 continuous_move()には-を付ける。

私は、カメラを天井に逆さに取り付けているため、「画像を反転」をONにしています。

Zoom操作の実現(できず)

これは元の記事にはありませんでしたが、
absolute_move(), relative_move(), continuous_move()にzを追加するだけで
Zoom機能も実現できると見込んでいました。
zの範囲は0~1。範囲外の値を与えるとBad Requestが返る…はずが、上手くいきません。

修正内容

引数、<Zoom x="">、request()関数に渡すcommandの3か所に、zを追加。

onvifreq.py
    def relative_move(self, x, y, z):
        command = """
        <RelativeMove xmlns="http://www.onvif.org/ver20/ptz/wsdl">
            <ProfileToken>profile_1</ProfileToken>
            <Translation>
                <PanTilt x="{x}" y="{y}" xmlns="http://www.onvif.org/ver10/schema" \
                    space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace"/>
                <Zoom x="{z}" xmlns="http://www.onvif.org/ver10/schema" \
                    space="http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace"/>
            </Translation>
        </RelativeMove>
        """
        return self.request(self.username, self.password, command.format(x=x, y=y, z=z))
ptz_viewer.py
# パンチルトパラメータ。X, Y, Zは0~1の範囲とする。
X_STP = 0.1
Y_STP = 0.1
Z_STP = 0.1
# ○ボタンで移動する初期位置。X, Yは-1~1の範囲、Zは0~1の範囲とする。
X_DEFALUT = -0.4
Y_DEFALUT =  0.2
Z_DEFALUT =  0
ptz_viewer.py
    elif "-pt_" in event or inited_ptz:
        #print(event, "x:", x, "y:", y)
        if is_streaming:
            # ○ボタン押下時は、定位置に移動
            if "-pt_center-" in event:
                x = X_DEFALUT
                y = Y_DEFALUT
                z = Z_DEFALUT
                # 絶対位置指定用
		# Tapoアプリで「画像を反転」をONにしている場合はx, yに-を付ける。
                oreq = onvif_request.absolute_move(-x, -y, z)
            else:
                if "right" in event:
                    x = X_STP
                elif "left" in event:
                    x = - X_STP
                else:
                    x = 0

                if "top" in event:
                    y = Y_STP
                elif "btm" in event:
                    y = - Y_STP
                else:
                    y = 0

                if "-pt_zoomin-" in event:
                    z = Z_STP
                elif "-pt_zoomout-" in event:
                    z = -Z_STP
                else:
                    z = 0

                # 相対位置指定用
		# Tapoアプリで「画像を反転」をONにしている場合はx, yに-を付けない。
                oreq = onvif_request.relative_move(x, y, z)
            req = urllib.request.Request(url, data=oreq.encode(), method='POST', headers=headers)
            try:
                with urllib.request.urlopen(req) as response:
                    pass
            except urllib.error.URLError as e:
                if e.reason == 'Bad Request':
                    print('カメラの範囲外です')
                else:
                    print(e.reason)
                    print(e)
            inited_ptz = False

テスト結果

No. Move 移動量 結果
1 relative_move() z=0, y=0, z=0 Bad Requestが返る
2 relative_move() z=0, y=0, z=0.1 Bad Requestが返る
3 relative_move() z=0, y=0, z=0.5 Bad Requestが返る
4 relative_move() z=0, y=0, z=0.9 Bad Requestが返る
5 relative_move() z=0, y=0, z=1 Bad Requestが返る
6 relative_move() z=0.1, y=0.1, z=0.5 Pan/Tiltは正常に動くが、Zoom In/Outしない
7 relative_move() z=-0.1, y=-0.1, z=0.5 Pan/Tiltは正常に動くが、Zoom In/Outしない
8 relative_move() z=0.1, y=0.1, z=-0.5 Pan/Tiltは正常に動くが、Zoom In/Outしない
9 relative_move() z=-0.1, y=-0.1, z=-0.5 Pan/Tiltは正常に動くが、Zoom In/Outしない
10 absolute_move() z=-0.5, y=0.2, z=1 Pan/Tiltは正常に動くが、Zoom In/Outしない
11 absolute_move() z=-0.5, y=0.2, z=0 Pan/Tiltは正常に動くが、Zoom In/Outしない
12 absolute_move() z=-0.5, y=0.2, z=0.5 Pan/Tiltは正常に動くが、Zoom In/Outしない

要約すると、こんな感じ。
・zだけ変更したときはBad Requestが返る。
・z以外も変更したときは、Pan/Tilt動作を行う。Pan/Tilt動作で範囲外を指定すると、Bad Requestが返る。

ONVIF Device Manager』というソフトを使っても、
Zoomのところは非アクティブ(0固定)になっていました。

メーカーの製品情報を見ても、私の購入したC210には「ズーム」の文字が無く、
C320WSには8倍までズームできるという記載があります。
また、価格.comの口コミによると、
C320WSの8倍ズームもカメラ自体のズーム機能ではなく、
Tapoアプリ上での拡大機能の事だそうです。

購入前に完全に見逃していました。

Zoom In/Outを行いたい場合は、カメラの機能を使うのではなく、
ビューアの機能として実現する必要があるようです。

Discussion