【猫監視システムへの道】3.Tapo C210のPTZ(Pan/Tilt/Zoom)操作
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」が返ります。
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だと気付き、
# パンチルトパラメータ
X_MIN, X_MAX, X_STP = -170, 170, 10
Y_MIN, Y_MAX, Y_STP = -30, 30, 5
を、下記のように書き換えたら動きました。
# パンチルトパラメータ(最小座標、最大座標、移動量)
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()の下に、下記を追加。
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は使わない想定なので、そこまでは確認していません。
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の値が異なるので、
ビューアも以下のように修正しました。
※修正箇所のみ
# パンチルトパラメータ。移動量のみ。0~1の範囲とする。
X_STP = 0.1
Y_STP = 0.1
# ○ボタンで移動する定位置。絶対位置指定する。-1~1の範囲とする。
X_DEFALUT = -0.4
Y_DEFALUT = 0.2
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を追加。
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))
# パンチルトパラメータ。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
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