【 DJI TelloPy 】キーボード操作しながら、移動中のフレーム画像を遅延なくMacbookのウィンドウに出力する（成功）


__[前回の記事](https://qiita.com/electronics_diy721/items/bf70d8fca4dc7ba1f9e6)__では、DJITelloPyのmanual-control-opencv.pyを、MacbookのPython3環境で動かして、以下に成功しました。


- Tello離陸前に、Telloを手で持って動かしたり、カメラの前にかざした手を動かすと、遅延なく、リアルタイムに画像がウィンドウに表示される。

その一方で、キーボード入力しても、反応はありませんでした。

そこで、今回は、__*key = cv2.waitKey(1) & 0xff*__で取得した__*key*変数__ではなく、Python組込の__*input("")*メソッド__を使って、Terminalから標準入力されたキーボード入力を受け取るように変えてみました。

__ここで留意すべき__は、__*key*変数を使わない__のですが、__*key = cv2.waitKey(1) & 0xff*__のコードは消したり、コメントアウトせずに、残しておくことです。

このコードを消すと、画像を受信して、Macbookのウィンドウに出力表示できなくなりました。

__（ 参考 ）__

- [python: opencvのimshowで画像が表示されない](https://moimoiblog.com/programing/python-opencv-error2/)

###今回、作成したスクリプトファイル

```Python3:manual-control-opencv_2.py
from djitellopy import Tello
import cv2, math, time

tello = Tello()
tello.connect()

tello.streamon()
frame_read = tello.get_frame_read()

#tello.takeoff()

while True:
    # In reality you want to display frames in a seperate thread. Otherwise
    #  they will freeze while the drone moves.
    img = frame_read.frame
    cv2.imshow("drone", img)
    
    #次の行（key = cv2.・・・）を削除すると、画像が受信できなくなる。
    key = cv2.waitKey(1) & 0xff
    msg = ""
    msg = input("Tello1号機に送る指示コマンドを入力してください。：  ")
    print("操作コマンド ：　{0} が入力されました。".format(msg))
    if msg == "i":
        tello.takeoff()
    elif msg == "w":
        tello.move_forward(30)
    elif msg == "s":
        tello.move_back(30)
    elif msg == "a":
        tello.move_left(30)
    elif msg == "d":
        tello.move_right(30)
    elif msg == "e":
        tello.rotate_clockwise(30)
    elif msg == "q":
        tello.rotate_counter_clockwise(30)
    elif msg == "r":
        tello.move_up(30)
    elif msg == "f":
        tello.move_down(30)
    elif msg == "g":
        tello.land()
    

tello.land()

```

###動作検証の結果

動かしてみたところ、次の課題が浮かび上がりました。

- __*input()*メソッド__が呼び出されると、ユーザがTerminalでEnterキーを押すまで、次のフレーム画像が読み込まれずに、画面が止まる。

- TerminalからEnterキーを（時間を置かずに素早く）連打すると、次々と次のフレーム画像が読み込まれて、リアルタイムのストリーム動画のように、滑らかな画面が表示される。

####__（ 原因と対策 ）__

今回実施したスクリプトは、__Python3の*input()*メソッド__を用いて、ユーザからのキーボード入力を取得します。

__*While True:*__で始まるループ処理の中で、以下の2つの処理がまとめて走っていますので、

- （処理１） フレーム画像の取得 

> img = frame_read.frame

- （処理２） ユーザからのキーボード入力読取り

> msg = input("Tello1号機に送る指示コマンドを入力してください。：  ")

そのため、MacbookのTerminalから、文字列を打ち込んでEnterキーを叩くか、何も文字列を打ち込まずにEnterキーを押すまで、ループ処理は次の周に進みません。その結果、画像フレームは新しい画像が読み込まれずに、Macbookのウィンドウに出力される画像は固まったように見えてしまいます。

TerminalからEnterキーを連打すると、次々と次のフレーム画像が読み込まれて、Macbookのウィンドウに表示される画面は、Telloの移動にあわせて滑らかに動く、リアルタイムに近い映像になります。

今後の課題として、ユーザが、0.50.５秒以内に、TerminalでEnterキーを打ち込まないと、__*While True:__で始まるループ処理の次の周に行く （ ＝次の画像フレームを取りに行く）ように、工夫したいと思います。

__（ 参考 ）__

- [teratail pythonのinput関数ついてです](https://teratail.com/questions/92991)
- [timeout付きinputを作った](https://qiita.com/johejo/items/8e724110a008b27d6c1f)

__*key = cv2.waitKey(1) & 0xff*__で取得した__*key*変数__が、うまく認識されると、こうした苦労から開放されるのですが。なぜか、__*key*変数__が、Macbookでは、認識されませんでした。

__*input()*メソッド__を記述しないスクリプトファイルでは、フレーム画像が次々と読み込まれて、Macbookのウィンドウ画面に、遅延のないリアルタイムの単眼カメラ画像が表示されていたことは、__[前回の記事](https://qiita.com/electronics_diy721/items/bf70d8fca4dc7ba1f9e6)__で動作検証済みです。Tello内蔵カメラレンズの前で、手を動かすと、リアルタイムに遅延なく、手の動作が画面に表示されていました。（ただし、キーボードには反応しませんでした）


###改修後のスクリプトファイル

```Python3:anual-control-opencv_3.py
from timeout_decorator import timeout, TimeoutError
from djitellopy import Tello
import cv2, math, time

TIMEOUT_SEC = 0.5

@timeout(TIMEOUT_SEC)
def input_with_timeout(msg=None):
   return input(msg)


tello = Tello()
tello.connect()

tello.streamon()
frame_read = tello.get_frame_read()

#tello.takeoff()

while True:
    # In reality you want to display frames in a seperate thread. Otherwise
    #  they will freeze while the drone moves.
    img = frame_read.frame
    cv2.imshow("drone", img)
    
    #次の行（key = cv2.・・・）を削除すると、画像が受信できなくなる。
    key = cv2.waitKey(1) & 0xff
    
    try:
        msg = input_with_timeout('\n{}秒以内に操作コマンドを入力して下さい :'.format(TIMEOUT_SEC))
        print('\n操作コマンド：　{} を受信しました。\n'.format(msg))
        if msg == "i":
            tello.takeoff()
        elif msg == "w":
            tello.move_forward(30)
        elif msg == "s":
            tello.move_back(30)
        elif msg == "a":
            tello.move_left(30)
        elif msg == "d":
            tello.move_right(30)
        elif msg == "e":
            tello.rotate_clockwise(30)
        elif msg == "q":
            tello.rotate_counter_clockwise(30)
        elif msg == "r":
            tello.move_up(30)
        elif msg == "f":
            tello.move_down(30)
        elif msg == "g":
            tello.land()
    except TimeoutError:
        print('\n操作コマンド入力時間切れ。次のフレーム画像を読み込みます。\n')

tello.land()

```

###実行結果

今度は、以下のすべてに成功しました。

- Tello離陸前に、Telloを手で持って動かしたり、カメラの前にかざした手を動かすと、遅延なく、リアルタイムに画像がウィンドウに表示される。
- キーボード入力に反応して、意図した通りにTelloが動く。
- Telloが移動中の単眼カメラのフレーム画像が、滑らかにウィンドウに出力されている。

最後の項目は、Telloの移動が終わって、ホバリングしているときしか、フレーム画像が出力できない、という事象に直面しないことを意味しています。

```bash:Terminal
electron@diynoMacBook-Pro examples % python3 manual-control-opencv_3.py
[INFO] tello.py - 107 - Tello instance was initialized. Host: '192.168.10.1'. Port: '8889'.
[INFO] tello.py - 422 - Send command: 'command'
[ERROR] tello.py - 442 - 'utf-8' codec can't decode byte 0xcc in position 0: invalid continuation byte
[INFO] tello.py - 422 - Send command: 'command'
[INFO] tello.py - 446 - Response command: 'ok'
[INFO] tello.py - 422 - Send command: 'streamon'
[INFO] tello.py - 446 - Response streamon: 'ok'
[h264 @ 0x7fcfb4014c00] non-existing PPS 0 referenced
[h264 @ 0x7fcfb4014c00] non-existing PPS 0 referenced
[h264 @ 0x7fcfb4014c00] decode_slice_header error
[h264 @ 0x7fcfb4014c00] no frame!
[h264 @ 0x7fcfb4014c00] non-existing PPS 0 referenced
[h264 @ 0x7fcfb4014c00] non-existing PPS 0 referenced
[h264 @ 0x7fcfb4014c00] decode_slice_header error
[h264 @ 0x7fcfb4014c00] no frame!
[h264 @ 0x7fcfb4014c00] non-existing PPS 0 referenced
[h264 @ 0x7fcfb4014c00] non-existing PPS 0 referenced
[h264 @ 0x7fcfb4014c00] decode_slice_header error
[h264 @ 0x7fcfb4014c00] no frame!

0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。

（ 省略 ）

0.5秒以内に操作コマンドを入力して下さい :jhgj

操作コマンド：　jhgj を受信しました。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。

（ 省略 ）

0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


```

離陸命令 'i' を入力します。 

```bash:Terminal
0.5秒以内に操作コマンドを入力して下さい :i

操作コマンド：　i を受信しました。

[INFO] tello.py - 422 - Send command: 'takeoff'
[INFO] tello.py - 446 - Response takeoff: 'ok'

0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。
```

ここで、離陸しました。
しばらく、飛行を続けます。


着陸命令 'g' を入力します。 

```bash:terminal
0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :g
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :

操作コマンド：　g を受信しました。

[INFO] tello.py - 422 - Send command: 'land'
[INFO] tello.py - 446 - Response land: 'ok'

0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。
```

ここで、着陸しました。
TerminalでCTRL-Cして、強制終了。

```bash:terminal
0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :
操作コマンド入力時間切れ。次のフレーム画像を読み込みます。


0.5秒以内に操作コマンドを入力して下さい :^CTraceback (most recent call last):
  File "/Users/electron/Desktop/DJITelloPy/examples/manual-control-opencv_3.py", line 37, in <module>
    msg = input_with_timeout('\n{}秒以内に操作コマンドを入力して下さい :'.format(TIMEOUT_SEC))
  File "/usr/local/lib/python3.9/site-packages/timeout_decorator/timeout_decorator.py", line 82, in new_function
    return function(*args, **kwargs)
  File "/Users/electron/Desktop/DJITelloPy/examples/manual-control-opencv_3.py", line 16, in input_with_timeout
    return input(msg)
KeyboardInterrupt
[INFO] tello.py - 422 - Send command: 'streamoff'
[INFO] tello.py - 446 - Response streamoff: 'ok'

electron@diynoMacBook-Pro examples %
```
###発展検証

__*TIMEOUT_SEC = 0.1*に変えてみた__ところ、結果は以下でした。

- ウィンドウ表示される画像の滑らかさ： 0.５のときと、目視で見る限り、さほど変化なし
- Terminalでのキー入力の難易度： 0.5のときと変わらず。キーを叩くと、100%の精度で認識された。（0.1秒タイムアウトで、入力が認識されない、といった心配はなし）