Pythonでカメラのストリームサーバーを作ってみます。
いやまずGrpcってなんぞや?ていうのは他記事に任せるとして、動くプログラムを作る方に専念します。
さっくり説明するとGoogleが開発した通信プロトコルでProtocolBuffersというやり方を用いて高速通信を可能にしたものです。一回のやりとりで4MBまでという制約があります。
XMLやJSONのようなスキーマ言語で".proto"という拡張子のファイルに、事前にサーバー側とクライアント側でやりとりするデータの形式を決めておきます。
また、C++、C#、Python、Java、NodeJS、PHP、GO、などなど様々な言語に対応しているので使い勝手がかなりいいです。
これ以上の説明は他記事に任せるとして(本人もざっくりとしかわかってない)、実際に動くプログラムを見ていきましょう。
今回はサーバー側もクライアント側もPython3.7で動作させます。
なぜPythonかって?環境構築が楽だったから、ただそれだけ。
また、今回のプロジェクトはGithubにあげてあるので、何か困ったらそちらも参考あそばせ。
https://github.com/Iwanaka/Python_Grpc_VideoStream_Sample
protoファイルとgen.pyの作成
Datas.proto
syntax = "proto3";
// リクエストデータ
message Request{
string msg = 1;
}
// リプライデータ
message Reply{
bytes datas = 1;
}
// サーバー
service MainServer{
rpc getStream (stream Request) returns (stream Reply) {}
}
gen.py
# Grpcモジュールのインポート
from grpc.tools import protoc
# 生成オプション
protoc.main(
(
'',
'-I.',
'--python_out=.', #書き出し先指定
'--grpc_python_out=.', #書き出し先指定
'./Datas.proto' #書き出し元のファイル指定
)
)
Datas.protoが、Grpcを用いてやりとりするためのデータ型を決める、いわば通信一覧表になります。
そして、gen.pyが、そのprotoファイルを用いてPython用にモジュールを作り出すための指示書になっています。
パッケージをインストールしてgenファイルのコンパイルする
'grpcio'と'grpcio-tools'というパッケージが必要な為、まだインストールしていない方は、
$ pip install grpcio
$ pip install grpcio-tools
でパッケージをインストールしちゃいましょう。
そして、先ほど作ったファイルを使って、
$ python gen.py
して、エラーなく2つのDatas_pb2_grpc.pyとDatas_pb2.pyが書き出されれば正しい挙動です。
この生成された2つのファイルが、自分のプロジェクトで直接使うことになるファイルです。(主にDatas_pb2_grpc.pyの方)。
サーバー、クライアントのスクリプト作成
Server.py
#============================================================
# import packages
#============================================================
#必要パッケージのインポート
from concurrent import futures
import grpc
import Datas_pb2
import Datas_pb2_grpc
import time
import cv2
import base64
import sys
#============================================================
# property
#============================================================
#カメラ情報の取得、準備
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
#キャプチャデータ確保用の変数
captureBuffer = None
#============================================================
# class
#============================================================
# .protoを使って生成したクラスを継承して、独自のメソッドを持たせたサーバーを用意
class Greeter(Datas_pb2_grpc.MainServerServicer):
#==========
def __init__(self):
pass
#==========
def getStream(self, request_iterator, context):
for req in request_iterator:
# リクエストメッセージのプリント
print("request message = ", req.msg)
# なんとなくグレイスケールにする
gray = cv2.cvtColor(captureBuffer, cv2.COLOR_BGR2GRAY)
# 画像をjpg形式にする
ret, buf = cv2.imencode('.jpg', gray)
if ret != 1:
return
# base64形式にエンコード
b64e = base64.b64encode(buf)
# print("base64 encode size : ", sys.getsizeof(b64e))
# クライアントにデータを返す
yield Datas_pb2.Reply(datas = b64e)
#============================================================
# functions
#============================================================
def serve():
# スレッドを用いてサーバーを起動、同時アクセス数は10に制限
server = grpc.server(futures.ThreadPoolExecutor(max_workers = 10))
# 先ほど作ったGreeterクラスを引数にする
Datas_pb2_grpc.add_MainServerServicer_to_server(Greeter(), server)
# ポート番号を指定
server.add_insecure_port('[::]:50051')
# サーバースタート
server.start()
print('server start')
while True:
try:
ret, frame = cap.read()
if ret != 1:
continue
# キャプチャデータを格納する
global captureBuffer
captureBuffer = frame
cv2.imshow('Capture Image', captureBuffer)
k = cv2.waitKey(1)
if k == 27:
break
time.sleep(0)
except KeyboardInterrupt:
server.stop(0)
#============================================================
# main
#============================================================
if __name__ == '__main__':
serve()
#============================================================
# after the App exit
#============================================================
cap.release()
cv2.destroyAllWindows()
Client.py
#============================================================
# import packages
#============================================================
#必要パッケージのインポート
from concurrent import futures
import time
import cv2
import grpc
import base64
import numpy as np
import Datas_pb2
import Datas_pb2_grpc
import sys
#============================================================
# class
#============================================================
#============================================================
# property
#============================================================
#============================================================
# functions
#============================================================
def run():
# アクセス先を指定、今回はローカルにサーバーが立つのでローカルIPを指定する
channel = grpc.insecure_channel('127.0.0.1:50051')
# スタブ
stub = Datas_pb2_grpc.MainServerStub(channel)
while True:
try:
message = []
message.append(Datas_pb2.Request(msg = 'give me the stream!!'))
# サーバーにリクエストを投げてレスポンスを取得
responses = stub.getStream(iter(message))
for res in responses:
# print(res)
# 元データがbase64になっているのでデコードする
b64d = base64.b64decode(res.datas)
# バイナリデータをuint8形式に変換
dBuf = np.frombuffer(b64d, dtype = np.uint8)
# cv2で扱えるように更に変換
dst = cv2.imdecode(dBuf, cv2.IMREAD_COLOR)
#print("dst size : ", sys.getsizeof(dst))
cv2.imshow('Capture Image', dst)
k = cv2.waitKey(1)
if k == 27:
break
# 何かしらのエラーがあった場合はプリント
except grpc.RpcError as e:
print(e.details())
#break
#============================================================
# Awake
#============================================================
#============================================================
# main
#============================================================
if __name__ == '__main__':
run()
#============================================================
# after the App exit
#============================================================
cv2.destroyAllWindows()
実行
$ python Server.py
$ python Clinet.py
で順番に起動すると、映像がストリームで来ていると思います。当然Webカメラが繋がっていないと動かないのでご注意を。
[Python]Grpcでカメラ映像をストリームで共有するhttps://t.co/lm0SrgosLE pic.twitter.com/C7wuCJDNs9
— iwax (@iwax51101141) October 6, 2019
お疲れさまでした
はい、Grpc便利ですね。4MBという結構大きな容量までいけるのであらゆるところで活躍しそうです。
今回はWebカメラの映像を共有するというサンプルを用いました。
"いやいや、だったら普通に産業用とかのIPカメラ使ったほうが高fpsだし解像度高いじゃん"、と思われた方、その通りです。でもそれは産業用カメラという高価なものを購入できる前提のお話ですね。今回のサンプルを用いれば、ラズパイにサーバーとなるカメラを持たせるとして、Webカメラとラズパイ本体だけで考えれば一万円以内で済みます。
もちろん、中fps、中解像度でニーズに応えられるなら、ですが。
既にある高機能高価格のものを買えば早く済む話であっても、金銭面等のある程度制約のある中でどうやってシステムを動かすかを考える方のも、以外と楽しかったりするものです。
そのために世にある色んな技術に常に興味を示していきたいなと思う小生であるのでした。