Google Colaboratory でリアルタイムレンダリング


Jupyter notebook ならば、 ipywidgets と IPython.display で処理しつつ、結果を動画としてモニタすることが可能

こんな感じ

```shell-session
%% bash
git clone https://github.com/NVIDIA-AI-IOT/jetcam.git
cd jetcam
pip3 install -e .
```

```python
import ipywidgets
from IPython.display import display
from jetcam.utils import bgr8_to_jpeg
import gym
env = gym.make('Pong-v0')                         # GUI環境の開始(***)
img = env.reset()
# img = env.render(mode="rgb_array")
image_widget = ipywidgets.Image(format='jpeg')
image_widget.value = bgr8_to_jpeg(img)
display(image_widget)
```

![Untitled](https://user-images.githubusercontent.com/5820803/100499567-f08fef00-31ad-11eb-99ec-ee8924cae89f.gif)

```python
for episode in range(20):
  observation = env.reset()                             # 環境の初期化

  for _ in range(100):
    # img = env.render(mode = 'rgb_array')
    action = env.action_space.sample()                  # 行動の決定
    observation, reward, done, info = env.step(action) 
    image_widget.value = bgr8_to_jpeg(observation[:,:,::-1])
```

おお、これなら Colab でも、って期待したんだけど、 Colab は jupyter notebook とは仕様が違うらしくて、動いてくれない。

[gym-notebook-wrapper](https://gitlab.com/ymd_h/gym-notebook-wrapper) も試してみたのだけれど、いずれもあまり速くなく、自分で作ったラッパーをかけると動かないケースが多い。

最後（かどうかわからんけど）に行き着いた方法をメモしておく。

参考にしたのは、[webCamGoogleColab](https://github.com/a2kiti/webCamGoogleColab)という、Webcam のモニタ画面を Colab 上に表示する方法。

方法を２つ公開してくださっていて、ひとつは Colab だけで閉じた方法、もう一つは ngrok の転送を使う方法。意外なことに、後者の方が速い。

WebCam の画面と、それをリアルタイムに処理した結果を表示するようなサンプルを書いてくださっているのだが、処理結果の方を自分のプログラムの処理結果に置き換えて、カメラ入力の画面を表示しなければいいんじゃないか、というのがアイデア。

それができるんなら、きっと転送も不要なのではないかと思うし、無駄なコードが残っているに違いないんだけど、目的は達成できているのでよしとして、記録に残す。

![battlezone](https://user-images.githubusercontent.com/5820803/100510012-7f523b00-31b1-11eb-92a3-379ed77bdb2f.gif)

```python
wget -q https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip > /dev/null
unzip ngrok-stable-linux-amd64.zip
pip install bottle > /dev/null
pip install bottle_websocket > /dev/null
```
カーネルを起動したあとでインストールしたライブラリは機能しないので、ここで一旦ランタイムをリセットする。

```python
from IPython.display import display, Javascript
from google.colab.output import eval_js
from time import sleep

def game_mon(url, quality=0.8):
  sleep(3)
  print("start monitor")  # このプリント文は必須
  js = Javascript('''
    async function gameMon(url, quality) {
      
      const div = document.createElement('div');
      document.body.appendChild(div);
      
      const canvas = document.createElement('canvas');
      canvas.width  = 240;
      canvas.height = 315;
      const canvasCtx = canvas.getContext('2d');
      div.appendChild(canvas);

      
      //exit button　（効かないです）
      const btn_div = document.createElement('div');
      document.body.appendChild(btn_div);
      const exit_btn = document.createElement('button');
      exit_btn.textContent = 'Exit';
      var exit_flg = false
      exit_btn.onclick = function() {exit_flg = true};
      btn_div.appendChild(exit_btn);
      
      //log
      let jsLog = function(abc) {
        document.querySelector("#output-area").appendChild(document.createTextNode(`${abc} `));
      }
      google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true);

      //for websocket connection.
      var connection = 0
      var flag_count = 0
      var send_flg = false

      // loop
      _canvasUpdate();
      jsLog("Connect_start");
      async function _canvasUpdate() {
            flag_count += 1
            
            // 少し（1カウント）待ってから WebSocket を実行
            if (flag_count == 10){
                connection = new WebSocket(url); 
                jsLog("Connect_start");
            }
            if (flag_count == 100){
                print("100");
                connection.onmessage = function(e) {
                    var image = new Image()
                    image.src = e.data;
                    image.onload = function(){canvasCtx.drawImage(image, 0, 0, 160, 210, 0, 0, canvas.width, canvas.height)}
                    send_flg=false
                };
                jsLog("Set_recieve")
            }
            if(flag_count > 100){
                const img = canvas.toDataURL('image/jpeg', quality);
                
                if (send_flg==false){
                    connection.send(img);
                    send_flg = true
                }
            }
          if (!exit_flg){
              requestAnimationFrame(_canvasUpdate);   
          }else{
              connection.close();
          }
      };
    }
    ''')
  display(js)
  data = eval_js('gameMon("{}", {})'.format(url, quality))

```

画面サイズが、openAI gym[atari] 用に決め打ちになってて生の数値が埋め込まれていますが、実験的プログラムなので、ご勘弁。

ngrok の Web 公開用 url を取得。　サインイン不要。

```python
get_ipython().system_raw('./ngrok http 6006 &')
"""
! curl -s http://localhost:4040/api/tunnels | python3 -c \
    "import sys, json;url = json.load(sys.stdin)['tunnels'][0]['public_url'];print(url);f = open('url.txt', 'w');f.write(url);f.close()"
"""
import subprocess
url = "wss"
while len(url)==3:
  result = subprocess.run(["curl -s http://localhost:4040/api/tunnels | python3 -c \"import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])\""], encoding='utf-8',shell=True, stdout=subprocess.PIPE)
  url = "wss"+result.stdout[5:-1]
url
```

参考にしたプログラムでは Linux シェルで ngrok の url を一旦テキストに書き出して、後で python で読み込んでいたが、全部 pythonで書いてみた。

```python
import numpy as np
import cv2
import bottle
from bottle.ext.websocket import GeventWebSocketServer
from bottle.ext.websocket import websocket
from multiprocessing import Pool
import base64
import gym

env = gym.make('BattleZone-v0') 

socket = bottle.Bottle()
@socket.route('/', apply=[websocket])
def wsbin(ws):
    kbreak = False
    while not kbreak:
        done = False
        env.reset()
        while not done:
            try:
                action = env.action_space.sample()                  # ランダムな行動選択
                out_img, reward, done, info = env.step(action)      # １ステップ実行
                if done: 
                  break
            
                #encode to string
                _, encimg = cv2.imencode(".jpg", out_img, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
                img_str = encimg.tostring()
                img_str = "data:image/jpeg;base64," + base64.b64encode(img_str).decode('utf-8')
                ws.send(img_str)
            except:
                kbreak = True
                done = True
                pass

if __name__ == '__main__':
    with Pool(2) as p:
      p.apply_async(game_mon, (url, 0.8)) # 
      socket.run(host='0.0.0.0', port=5000, server=GeventWebSocketServer)
```

もともとが cam からの画像を表示するプログラムをなぞっているので、その名残が残っているが、ご容赦。

-> [ColabHowto/RealTime_Rendering_on_Colab.ipynb](https://github.com/aquapathos/ColabHowto/blob/master/RealTime_Rendering_on_Colab.ipynb)