Python websocket-clientでバイナリデータの送受信

websocket-clientでWebSocket通信してみました。
Python websocket-clientでBottleフレームワークと通信する

バイナリデータの送受信を試してみます。


バイナリデータの送信



サンプルとして画像のバイナリデータを送信。
確認用にサーバー側でファイルに復元。
クライアントには送信されてきたバイナリデータをそのまま返却してみます。

こちらが参考になりました。
Python websocket client: sending binary content


・app.py


  1. import json
  2. import bottle
  3. import gevent
  4. from bottle.ext.websocket import GeventWebSocketServer
  5. from bottle.ext.websocket import websocket
  6. app = bottle.Bottle()
  7. @app.route('/wsbin', apply=[websocket])
  8. def wsbin(ws):
  9.     while True:
  10.         body = ws.receive()
  11.         if not body:
  12.             break
  13.         
  14.         # 確認用に受信したデータをファイルに出力
  15.         with open('debug_app.jpg', 'wb') as f:
  16.             f.write(body)
  17.         # そのまま送り返す
  18.         ws.send(body)
  19.         
  20. app.run(host='localhost', port=8080, reloader=True, debug=True, server=GeventWebSocketServer)




・client.py


  1. import json
  2. import time
  3. import urllib.request
  4. import urllib.parse
  5. import websocket
  6. url = 'ws://localhost:8080/wsbin'
  7. # 画像データ読み込み
  8. with open('images.jpg', 'rb') as f:
  9.     data = f.read()
  10. ws = websocket.create_connection(url)
  11. # sendではなくsend_binaryでデータ送信
  12. ws.send_binary(data)
  13. body = ws.recv()
  14. # 確認用に受信データをファイルに保存
  15. with open('debug_client.jpg', 'wb') as f:
  16.     f.write(body)
  17. ws.close()




ポイントは、sendではなくsend_binaryでデータ送信する点でしょうか。
受信部分はrecvのまま変更しなくてもOKでした。

適当な画像ファイルimages.jpgを用意して実行してみると、同じ内容の
debug_app.jpg
debug_client.jpg
が出力されることが確認できました。


【参考URL】
Python websocket client: sending binary content

Python websocket-clientでBottleフレームワークと通信する

BottleフレームワークでWebSocketが使用できるよう構成し、JavaScriptで通信してみました。
BottleフレームワークでWebsocket通信を行う

Pythonからの通信を試してみます。


websocket-client



こちらを使用して接続することにしました。
https://github.com/websocket-client/websocket-client

pipでインストールしておきます。


$ pip3 install websocket-client





http通信のサンプル



jsonデータを送信しサーバー側で解析。
jsonデータの応答を返すサンプルを動かしてみることにします。

まず、http通信でのサンプル。

・app.py


  1. import json
  2. import bottle
  3. import gevent
  4. from bottle.ext.websocket import GeventWebSocketServer
  5. from bottle.ext.websocket import websocket
  6. app = bottle.Bottle()
  7. @app.route('/httpecho', method='POST')
  8. def httpecho():
  9.     body = bottle.request.body
  10.     data = body.read().decode('utf-8')
  11.     json_data = json.loads(data)
  12.     return json.dumps({'status':'ok', 'message': 'hello ' + json_data['name']})
  13. app.run(host='localhost', port=8080, reloader=True, debug=True, server=GeventWebSocketServer)




・client.py


  1. import json
  2. import urllib.request
  3. import urllib.parse
  4. url = 'http://localhost:8080/httpecho'
  5. data = json.dumps({'name': 'symfoware'})
  6. response = urllib.request.urlopen(url, data.encode('utf-8'))
  7. body = response.read().decode('utf-8')
  8. result = json.loads(body)
  9. print(result['message'])




http通信でjson通信できました。


$ python3 client.py
hello symfoware



これをWebSocket版に変更してみます。




WebSocket



WebSocket版のサンプルは以下のようになりました。

・app.py


  1. import json
  2. import bottle
  3. import gevent
  4. from bottle.ext.websocket import GeventWebSocketServer
  5. from bottle.ext.websocket import websocket
  6. app = bottle.Bottle()
  7. @app.route('/wsecho', apply=[websocket])
  8. def wsecho(ws):
  9.     while True:
  10.         body = ws.receive()
  11.         if not body:
  12.             break
  13.         
  14.         json_data = json.loads(body)
  15.         ws.send(json.dumps({'status':'ok', 'message': 'hello ' + json_data['name']}))
  16. app.run(host='localhost', port=8080, reloader=True, debug=True, server=GeventWebSocketServer)




・client.py


  1. import json
  2. import websocket
  3. url = 'ws://localhost:8080/wsecho'
  4. data = json.dumps({'name': 'symfoware'})
  5. ws = websocket.create_connection(url)
  6. ws.send(data)
  7. body = ws.recv()
  8. result = json.loads(body)
  9. print(result['message'])
  10. ws.close()




実行結果に変化はありませんが、WebSocketでの通信を確認できました。


$ python3 client.py
hello symfoware






通信速度の比較



http、WebSocket各々1,000回通信した際の速度を比較してみました。

・app.py


  1. import json
  2. import bottle
  3. import gevent
  4. from bottle.ext.websocket import GeventWebSocketServer
  5. from bottle.ext.websocket import websocket
  6. app = bottle.Bottle()
  7. @app.route('/httpecho', method='POST')
  8. def httpecho():
  9.     body = bottle.request.body
  10.     data = body.read().decode('utf-8')
  11.     json_data = json.loads(data)
  12.     return json.dumps({'status':'ok', 'message': 'hello ' + json_data['name']})
  13. @app.route('/wsecho', apply=[websocket])
  14. def wsecho(ws):
  15.     while True:
  16.         body = ws.receive()
  17.         if not body:
  18.             break
  19.         
  20.         json_data = json.loads(body)
  21.         ws.send(json.dumps({'status':'ok', 'message': 'hello ' + json_data['name']}))
  22. app.run(host='localhost', port=8080, reloader=True, debug=True, server=GeventWebSocketServer)




・client.py


  1. import json
  2. import time
  3. import urllib.request
  4. import urllib.parse
  5. import websocket
  6. def http_call():
  7.     start_time = time.perf_counter()
  8.     url = 'http://localhost:8080/httpecho'
  9.     data = json.dumps({'name': 'symfoware'})
  10.     for i in range(1000):
  11.         response = urllib.request.urlopen(url, data.encode('utf-8'))
  12.         body = response.read().decode('utf-8')
  13.         result = json.loads(body)
  14.     print('http: %.4f sec' % (time.perf_counter() - start_time))
  15.     
  16. def ws_call():
  17.     start_time = time.perf_counter()
  18.     url = 'ws://localhost:8080/wsecho'
  19.     data = json.dumps({'name': 'symfoware'})
  20.     ws = websocket.create_connection(url)
  21.     
  22.     for i in range(1000):
  23.         ws.send(data)
  24.         body = ws.recv()
  25.         result = json.loads(body)
  26.     ws.close()
  27.     print('ws: %.4f sec' % (time.perf_counter() - start_time))
  28. if __name__ == '__main__':
  29.     http_call()
  30.     ws_call()




実行結果


$ python3 client.py
http: 0.4717 sec
ws: 0.1240 sec



WebSocketの方が4倍程度高速になりました。

BottleフレームワークでWebsocket通信を行う

Python製軽量Webフレームワーク Bottle
https://bottlepy.org/docs/dev/

単体ではWebsocket通信を行うことはできませんが、プラグインを導入することで対応できるようです。
BottleでWebsocketする
bottle-websocket


ライブラリのインストールとサンプル



bottle-websocketと、依存しているgevent、gevent-websocketをpipでインストール。


$ pip3 install gevent
$ pip3 install gevent-websocket
$ pip3 install bottle-websocket




サンプルを参考にテキストに文字を入力したら「echo:」という文字列を付与した応答を送信するプログラムを作成しました。

・app.py


  1. import bottle
  2. from bottle.ext.websocket import GeventWebSocketServer
  3. from bottle.ext.websocket import websocket
  4. app = bottle.Bottle()
  5. @app.route('/')
  6. def main():
  7.     return bottle.static_file('index.html', root='./')
  8. @app.route('/websocket', apply=[websocket])
  9. def echo(ws):
  10.     while True:
  11.         msg = ws.receive()
  12.         if msg is not None:
  13.             ws.send('echo:' + msg)
  14.         else:
  15.             break
  16. app.run(host='localhost', port=8080, reloader=True, debug=True, server=GeventWebSocketServer)




・index.html


  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  5.     <title>WebSocket Sample</title>
  6.     <meta charset="utf-8">
  7.     <meta name="viewport" content="width=device-width, initial-scale=1">
  8.     <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  9. <script>
  10. $(function() {
  11.     if (!window.WebSocket) {
  12.         if (window.MozWebSocket) {
  13.             window.WebSocket = window.MozWebSocket;
  14.         } else {
  15.             $('#result').append("Your browser doesn't support WebSockets.");
  16.         }
  17.     }
  18.     
  19.     ws = new WebSocket('ws://127.0.0.1:8080/websocket');
  20.     ws.onopen = function(evt) {
  21.         $('#result').append('<li>WebSocket connection opened.</li>');
  22.     }
  23.     ws.onmessage = function(evt) {
  24.         $('#result').append('<li>' + evt.data + '</li>');
  25.     }
  26.     ws.onclose = function(evt) {
  27.         $('#result').append('<li>WebSocket connection closed.</li>');
  28.     }
  29.     $('#send').submit(function() {
  30.         ws.send($('input:first').val());
  31.         $('input:first').val('').focus();
  32.         return false;
  33.     });
  34.     $('#message').on('keyup', function() {
  35.         ws.send($('#message').val());
  36.     });
  37. })
  38. </script>
  39. </head>
  40. <body>
  41.     <h3>WebSocket Sample</h3>
  42.     <div>
  43.         <input type="text" id="message" />
  44.     </div>
  45.     <ul id="result"></ul>
  46. </body>
  47. </html>




狙い通りの動作です。

a15_01.png


動画をMotion JPEG(multipart jpeg)に変換し、Bottleフレームワークで配信する

こちらの記事で知ったのですが、
OpenCV – Stream video to web browser/HTML page
Motion JPEGという連続して画像を配信する規格があるそうです。

動画ファイルから画像を切り出して加工。
ブラウザに配信するということが簡単に実現できますね。

参考サイトではwebフレームワークにFlaskが使用されていますが、
Bottleを使用して同様の配信を試してみます。

動画ファイルの読み込みにはOpenCVを利用します。



サンプル



サンプルはこのようになりました。

・app.py


  1. import time
  2. import bottle
  3. import cv2
  4. app = bottle.Bottle()
  5. def gen():
  6.     
  7.     cam = cv2.VideoCapture('travelpockets_iceland_land_of_fire_and_ice.mp4')
  8.     while True:
  9.         # 動画から1フレーム分の画像を読み込み
  10.         ret_val, image = cam.read()
  11.         if not ret_val:
  12.             break
  13.         # jpg形式に変換
  14.         flag, frame = cv2.imencode('.jpg', image)
  15.         yield b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + bytearray(frame) + b'\r\n\r\n'
  16.         # 連続再生されるのでwaitを入れる
  17.         time.sleep(1/60)
  18. @app.route('/')
  19. def main():
  20.     return bottle.static_file('index.html', root='./')
  21. @app.route('/video_feed')
  22. def video_feed():
  23.     bottle.response.content_type = 'multipart/x-mixed-replace;boundary=frame'
  24.     return gen()
  25. app.run(host='localhost', port=8080, reloader=True, debug=True)




・index.html


  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  5.     <title>mjpeg Sample</title>
  6.     <meta charset="utf-8">
  7.     <meta name="viewport" content="width=device-width, initial-scale=1">
  8. </head>
  9. <body>
  10.     <h3>mjpeg Sample</h3>
  11.     <div>
  12.         <img src="./video_feed">
  13.     </div>
  14. </body>
  15. </html>




再生する動画はこちらからお借りしました。
https://mazwai.com/video/iceland-%7C-land-of-fire-and-ice/455108


サーバーを起動


$ python3 app.py



ブラウザでhttp://localhost:8080/にアクセスすると、動画のように連続して画像が表示されました。

a14_01.png


【参考URL】
OpenCV – Stream video to web browser/HTML page
https://github.com/mrxmamun/camera-live-streaming/blob/master/app.py
Python Live Video Streaming Example


Python3 ExifのOrientation属性による画像の回転と縮小(PIL使用)

画像認識システムの落とし穴となる「ExifのOrientation属性」とは?

元記事
The dumb reason your fancy Computer Vision app isn’t working: Exif Orientation

Pythonの画像ライブラリ、ExifのOrientation属性が反映されないものが多いから
AIでの画像認識時は気をつけてねとのこと。

記事中で紹介されているExifのOrientation属性が付与された画像があるリポジトリ。
exif-orientation-examples

ダウンロードしてlabelImgで表示してみます。
Ubuntu 18.04にlabelImgをインストールして、学習用画像のラベル付を行う

左がlabelImg、右側は画像ビューワーで同じ画像を表示しています。
確かにExif情報が反映されていませんね。

a12_01.png



画像の回転リサイズ



指定ディレクトリにある画像すべてのOrientationを反映。
同時に画像のリサイズを行いたい場合はこんな処理になりました。

元画像:images
変換後:convertedに出力


  1. import PIL.Image
  2. import PIL.ImageOps
  3. import numpy as np
  4. import os
  5. def exif_transpose(img):
  6.     if not img:
  7.         return img
  8.     exif_orientation_tag = 274
  9.     # Check for EXIF data (only present on some files)
  10.     if hasattr(img, "_getexif") and isinstance(img._getexif(), dict) and exif_orientation_tag in img._getexif():
  11.         exif_data = img._getexif()
  12.         orientation = exif_data[exif_orientation_tag]
  13.         # Handle EXIF Orientation
  14.         if orientation == 1:
  15.             # Normal image - nothing to do!
  16.             pass
  17.         elif orientation == 2:
  18.             # Mirrored left to right
  19.             img = img.transpose(PIL.Image.FLIP_LEFT_RIGHT)
  20.         elif orientation == 3:
  21.             # Rotated 180 degrees
  22.             img = img.rotate(180)
  23.         elif orientation == 4:
  24.             # Mirrored top to bottom
  25.             img = img.rotate(180).transpose(PIL.Image.FLIP_LEFT_RIGHT)
  26.         elif orientation == 5:
  27.             # Mirrored along top-left diagonal
  28.             img = img.rotate(-90, expand=True).transpose(PIL.Image.FLIP_LEFT_RIGHT)
  29.         elif orientation == 6:
  30.             # Rotated 90 degrees
  31.             img = img.rotate(-90, expand=True)
  32.         elif orientation == 7:
  33.             # Mirrored along top-right diagonal
  34.             img = img.rotate(90, expand=True).transpose(PIL.Image.FLIP_LEFT_RIGHT)
  35.         elif orientation == 8:
  36.             # Rotated 270 degrees
  37.             img = img.rotate(90, expand=True)
  38.     return img
  39. def load_image_file(file, mode='RGB'):
  40.     # Load the image with PIL
  41.     img = PIL.Image.open(file)
  42.     if hasattr(PIL.ImageOps, 'exif_transpose'):
  43.         # Very recent versions of PIL can do exit transpose internally
  44.         img = PIL.ImageOps.exif_transpose(img)
  45.     else:
  46.         # Otherwise, do the exif transpose ourselves
  47.         img = exif_transpose(img)
  48.     img = img.convert(mode)
  49.     return img
  50. def main():
  51.     for file in os.listdir('images'):
  52.         # exif Orientation
  53.         img = load_image_file(os.path.join('images', file))
  54.         # 1/2にリサイズ
  55.         width, height = img.size
  56.         img = img.resize((int(width / 2), int(height / 2)))
  57.         # 結果を保存
  58.         img.save(os.path.join('converted', file))
  59.     
  60. if __name__ == '__main__':
  61.     main()



画像の向き変換と縮小が一括で行えました。

a12_02.png

プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
Symfoware まとめ

PR




検索フォーム

月別アーカイブ