Jetson Nano + Realsense D455 の映像を、ストリーミングしてみる。（カラーカメラのみ、Socket、Jpeg編）

# はじめに
　前回は、HTTPでストリーミングを行って、Webブラウザで映像を見た。今回は、HTTPより低レイヤーを用いてデータの送信・受信をおこない、ローカルのアプリケーションのウィンドウに画像を表示してみる。

# Socketとは
　技術解説は先達にお任せ。
　　
　　知ったかぶりをしていたソケット通信の基礎を改めて学んでみる
　　https://qiita.com/megadreams14/items/32a3eed4661e55419e1c

# pythonでストリーミングを行う、Socketプログラミングの参考
　こちらも先達の情報を参照・・・。

　　Pythonでネットワーク越しにカメラの映像をストリーミング
　　https://qiita.com/tokoroten-lab/items/bb27351b393f087650a9

　しかし、こちらの情報をまんま書き写したところでいろいろと動作しない。まず、connection.iniの情報を自分の環境用に書き換えないといけない。そして、サンプルが書き漏らしているであろう設定情報などを自分なりにアレンジして書き込む。この設定のうえで、ストリーミングサーバーを起動させ、[前回と同様、サーバーはrealsenseカメラを用いるし、IPアドレスはサーバー側の環境で自動取得のうえPrintでコンソールに出力（ターミナルに表示）させる](https://qiita.com/tomofu74/items/bb9afe48ccbb7088d963)。

できあがったファイルがこちら。

```connection.ini
[server]
ip = AUTO
port = 12345

[packet]
# [bytes]
header_size = 4

[camera]
id = 0
fps = 15
image_width = 1280
image_height = 960

[pixels]
image_width = 640
image_height = 480
```
```streaming_server.py
# coding:utf-8
import socket
import numpy as np
import cv2
import time
import configparser
from realsensecv import RealsenseCapture
import getIpAddress as gIA

config = configparser.ConfigParser()
config.read('./connection.ini', 'UTF-8')

# 全体の設定
FPS = int(config.get('camera','fps'))
INDENT = '    '

# カメラ設定
CAMERA_ID = int(config.get('camera','id'))
CAMERA_FPS = FPS
CAMERA_WIDTH = int(config.get('camera','image_width'))
CAMERA_HEIGHT = int(config.get('camera','image_height'))

# 画像設定
IMAGE_WIDTH = int(config.get('pixels', 'image_width'))
IMAGE_HEIGHT = int(config.get('pixels', 'image_height'))
IMAGE_QUALITY = 30

# カメラ設定適用
try:
    cam = RealsenseCapture()
    # プロパティの設定
    cam.WIDTH = CAMERA_WIDTH
    cam.HEIGHT = CAMERA_HEIGHT
    cam.FPS = CAMERA_FPS
    # cv2.VideoCapture()と違ってcap.start()を忘れずに
    cam.start()
    CAMERA_ID = "RealsenseCapture"
except:
    cam = cv2.VideoCapture(CAMERA_ID, cv2.CAP_V4L)
    cam.set(cv2.CAP_PROP_FRAME_WIDTH, CAMERA_WIDTH)
    cam.set(cv2.CAP_PROP_FRAME_HEIGHT, CAMERA_HEIGHT)
    cam.set(cv2.CAP_PROP_FPS, CAMERA_FPS)
    if not cam.isOpened():  # ビデオキャプチャー可能か判断
        print("Not Opened Video Camera")
        exit()

# カメラ情報表示
print('Camera {')
print(INDENT + 'ID    : {},'.format(CAMERA_ID))
print(INDENT + 'FPS   : {},'.format(CAMERA_FPS))
print(INDENT + 'WIDTH : {},'.format(CAMERA_WIDTH))
print(INDENT + 'HEIGHT: {}'.format(CAMERA_HEIGHT))
print('}')

# サーバ設定
SERVER_IP = str(config.get('server', 'ip'))
SERVER_PORT = int(config.get('server', 'port'))
if SERVER_IP == "AUTO":
    l = [d.get('name') for d in gIA.get_ip()]
    print(l)
    for k in gIA.get_ip():
        if (k.get('name') == 'wlan0'):
            HOST_wlan0 = k['address']
        elif (k.get('name') == 'eth0'):
            HOST_eth0 = k['address']

    if HOST_wlan0 is not None:
        SERVER_IP = HOST_wlan0
    else:
        SERVER_IP = HOST_eth0

if SERVER_IP == "AUTO" or SERVER_IP is None:
    print("SERVER_IP is {}".format(SERVER_IP))
    cam.release();
    exit()

# パケット設定
HEADER_SIZE = int(config.get('packet', 'header_size'))

# クライアント接続 listen
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((SERVER_IP, SERVER_PORT))
s.listen(1)
soc, addr = s.accept()

print('Server {')
print(INDENT + 'IP   : {},'.format(SERVER_IP))
print(INDENT + 'PORT : {}'.format(SERVER_PORT))
print('}')

# クライアント情報表示
print('Client {')
print(INDENT + 'IP   : {},'.format(addr[0]))
print(INDENT + 'PORT : {}'.format(addr[1]))
print('}')

# メインループ
while True:
    loop_start_time = time.time()

    # 送信用画像データ作成
    flag, frames = cam.read()
    color_frame = frames[0]
    depth_frame = frames[1]
    resized_img = cv2.resize(color_frame, (IMAGE_WIDTH, IMAGE_HEIGHT))
    (status, encoded_img) = cv2.imencode('.jpg', resized_img, [int(cv2.IMWRITE_JPEG_QUALITY), IMAGE_QUALITY])

    # パケット構築
    packet_body = encoded_img.tostring()
    packet_header = len(packet_body).to_bytes(HEADER_SIZE, 'big')
    packet = packet_header + packet_body

    # パケット送信
    try:
        soc.sendall(packet)
    except socket.error as e:
        print('Connection closed.')
        break

    # FPS制御
    time.sleep(max(0, 1 / FPS - (time.time() - loop_start_time)))

s.close()
```
　これらのファイルをを、前回も使用したrealsensecv.pyとgetIpAddress.pyのファイルと一緒に任意のフォルダに置き、python3で処理させる。

```
$ python3 streaming_server.py 
pipline start
Camera {
    ID    : RealsenseCapture,
    FPS   : 15,
    WIDTH : 1280,
    HEIGHT: 960
}
['eth0', 'l4tbr0', 'wlan0']
```
このような表示がでて、サーバー側はclientの接続待ちになる。

client側は、別のPC、Macで実行する。connection.iniは、サーバーに置いたものと同じ。
client側のスクリプトは、先達のものの83行目を次のように、img.tostring()をimg.tobytes()に変えただけ。

```streaming_client.pyの変更点
82: # 画像をバイナリに変換
83: img = img.tobytes()
```

client側で実行成功すると、次のようにアプリケーションウィンドウが表示される。
![スクリーンショット 2021-01-10 18.17.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699890/f561e86e-2aab-6dea-6d27-64598bb8f44c.png)

なお、Client側のアプリケーションウィンドウを閉じると、サーバー側のプログラムも停止する。また、ブラウザでの映像参照とは異なり、このスクリプトのSocket通信では多数のクライエントからの同時アクセスは想定していないようだ。

# 考察
Socketプログラミングで、ブラウザではなくアプリケーションでデータを受信し、ウィンドウ表示できることがわかった。１対１の通信ならばこれで良いのだろうが、１対多、多対１のストリーミングをおこないたいときは、どのようにSocketプログラミングすればよいのだろうか。
なお、GUIの表示にkivyを使っている。初めてKivyを使ったが、Tcl/Tkのライブラリを使うよりも、ウィンドウが開くのが早いことがわかった。今後GUI開発では合わせてkivyの利用について検討したい。

# まとめ
Socketプログラミングの参考情報を調べ、１対１の通信を成功させた。
今後は、1対多、多対1の通信の実現手段を調べて、機能を拡張したい。