画像データをJSONに仕込んでFlaskサーバに送付する

C# で作成したアプリケーションから画像データを受け付けて Python で処理するには、ファイルを仲介する方法が一番簡単です。しかし、この方法は趣味の日曜プログラミング程度なら問題ありませんが、高速な業務用のシステムを構築することには向きません。

実際の業務アプリケーションの場合は、Python の Flask でサーバーを作成し、C# でクライアントアプリケーションを作成して、サーバーに HTTP で画像データを送付するのがよいと思います。

Flaskのインストール

まずは Flask をインストールします。

pip install --upgrade pip
pip install flask

Flask が正しくインストールされたか確認するにはコンソールで flask --version と入力します。

PS C:\tmp> flask --version
Python 3.10.4
Flask 2.1.3
Werkzeug 2.1.2

サーバー動作のコード

下記はポート番号5000番をつかって所望の動作を実施するコードです。JSON でリクエストされた 8bpp の画像データの濃度を反転して、クライアントにレスポンスします。

from flask import Flask, request, Markup, json

import base64
import numpy as np
from PIL import Image
import datetime
import time
import sys

MY_PORT_NUMBER = 5000

sys.path.append('..')

app = Flask(__name__)

KEY_00 = "Image"
KEY_01 = "ImageWidth"
KEY_02 = "ImageHeight"

#-----------------------------------------------------
@app.route('/')
def index():

	s0 = 'This is INDEX page, string.'
	s1 = str( datetime.datetime.now() )
	s2 = s0 + '\n' + s1
	s_out = s2.replace( '\n', '<br>' )
	return Markup(s_out)

#-----------------------------------------------------
@app.route('/ask', methods=['POST'])
def ask():

	pfmc_prv = time.perf_counter()

	# 現在の時刻を表示する.
	print( "" )
	print( datetime.datetime.now() )

	# base64 のリクエストJSON文字列をディクショナリに仕込む.
	dic_for_req = request.json

	# JSONからディクショナリに仕込む.
	v00 = dic_for_req[ KEY_00 ]
	v01 = dic_for_req[ KEY_01 ]
	v02 = dic_for_req[ KEY_02 ]
	print( v01 )
	print( v02 )

	# base64 で示された画像データを base64 デコードする.
	tmp = base64.b64decode( v00 )

	# tmp から 配列を取得する.
	data_src = np.frombuffer( tmp, dtype = np.uint8 )
	# 配列データを濃度反転する.
	data_dst = 255 - data_src

	# 配列データを base64 文字列にエンコードして戻す.
	s = base64.b64encode( data_dst.astype( np.uint8 ) )

	# メンバには utf-8 の文字列を仕込まないと json.dumps() でエラーになる.
	v10 = s.decode( "utf-8" ) # ココ.
	v11 = v01
	v12 = v02

	# レスポンス用のディクショナリに仕込む.
	dic_for_rsp = {}
	dic_for_rsp.setdefault( KEY_00, v10 )
	dic_for_rsp.setdefault( KEY_01, v11 )
	dic_for_rsp.setdefault( KEY_02, v12 )

	# ディクショナリをJSON文字列にダンプする.
	s_rsp = json.dumps( dic_for_rsp, ensure_ascii=False, indent=4, sort_keys=False )

	# 現在の時刻を表示する.
	print( datetime.datetime.now() )

	# 実行時間を表示する.
	pfmc_now = time.perf_counter()
	dt = pfmc_now - pfmc_prv
	print( "{0} msec".format( dt * 1000.0 ))

	# 空行を入れておく.
	print( "" )

	#JSON文字列をレスポンスする.
	return s_rsp

#-----------------------------------------------------
if __name__ == '__main__':
	app.debug=True
	app.run(host="0.0.0.0", port=MY_PORT_NUMBER)

サーバー動作を開始するにはパイソンで実行するだけです。
test_server.py というファイル名称ならば python test_server.py と打ち込むだけです。とても簡単ですね。

PS c:\tmp> python test.py

 * Serving Flask app 'test' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on all addresses (0.0.0.0)
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.10.55:5000 (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 973-717-886

ブラウザでサーバ動作の確認

まずは正しくサーバーが動いているかブラウザで確認します。ブラウザのアドレスバーに下記のURLを入力します。ポート番号 5000 番を使うことを明示します。

http://127.0.0.1:5000/

ブラウザで Flask サーバが動いているか確認する

上記のコード test_server.py の def index(): ファンクションが実行されていることがわかります。

画像データをJSONに仕込んでクライアントからサーバにリクエスト

HTTPのクライアントソフトを作成するには requests モジュールを用いるのが簡単です。JSON データを作成するには 辞書型 dict をあらかじめ作っておいてJSONに変換するのがおすすめです。

しかし、落とし穴もあります。ソースコードのコメントにもあるように辞書型からJSONに変換するときはメンバが utf-8 の文字列でないとエラーが出ます。

あらかじめ辞書型のメンバに仕込むときに base64 でエンコードした文字列を utf-8 にデコードして、それから JSON に変換するというややこしいことをしなくてはなりません。

リクエストする JSON は下記の仕様です。

  • Image: base64 でエンコードされた画像データの長大な文字列を utf-8 に変換した文字列
  • ImageWidth: 画像の幅
  • ImageHeight: 画像の高さ

下記の test_client.py のコードを実行する場合は、8bpp のグレースケールのビットマップをご用意ください。もし手元にないならば、この画像を使ってください。( 右クリックでローカルに保存してください )

8bpp のビットマップ画像
import requests
import json
import base64
import numpy as np
from PIL import Image
import datetime

# 画像ファイルを開く.
im_req = Image.open( "c:/tmp/0512_0512.bmp" )

# Pillow のデータから ndarray に変換する.
data_tmp = np.array( im_req )

# ndarray で uint8 を明示する.
data_bpp08 = data_tmp.astype( np.uint8 )

# ndarray を1次元配列に変換する.
data_bpp08 = data_bpp08.reshape( -1 )

# ndarray のデータをBASE64エンコードして文字列へ.
s = base64.b64encode( data_bpp08 )

# 文字列を utf-8 でデコードして辞書型のメンバに仕込む準備をする.
v00 = s.decode( "utf-8" )
v01 = im_req.width
v02 = im_req.height

KEY_00 = "Image"
KEY_01 = "ImageWidth"
KEY_02 = "ImageHeight"

# 辞書型を生成する.
dic_for_req = {}
dic_for_req.setdefault( KEY_00, v00 )
dic_for_req.setdefault( KEY_01, v01 )
dic_for_req.setdefault( KEY_02, v02 )

# 辞書からJSONのデータに変換する.
req_json = json.dumps( dic_for_req )

# URLを作成する.
adrs = "http://127.0.0.1" 
port = "5000"
api = "ask"
url = "{0}:{1}/{2}".format( adrs, port, api )

# サーバーにJSONデータを送ってレスポンスを待つ.
the_header = {'Content-Type': 'application/json'}
rsp = requests.post( url, headers=the_header, data=req_json )

# レスポンスを全部読む.
# ハマった場合は、戻り値を print( type( )) して確認したほうがよい.
dict_for_rsp = rsp.json()

# 辞書型からそれぞれのメンバーを読む.
s10 = dict_for_rsp[KEY_00]
s11 = dict_for_rsp[KEY_01]
s12 = dict_for_rsp[KEY_02]

# JSON の Image データは BASE64 文字列なので、デコードして配列に戻す.
data_tmp_rsp = base64.b64decode( s10 )

# 配列であって ndarray ではないので、frombuffer() で変換する.
data_bpp08_rsp = np.frombuffer( data_tmp_rsp, dtype=np.uint8 )

# JSON の ImageWidth 画像幅と、ImageHeight 画像高さ.
w = int( s11 )
h = int( s12 )

# ndarray は1次元なので、形を2次元に変換する、引数の h, w の順序に注意せよ.
data_bpp08_rsp = data_bpp08_rsp.reshape( h, w )

# ndarray から Pillow に変換する.
im_rsp = Image.fromarray( data_bpp08_rsp )

# 年月日時分秒ミリ秒のファイル名を生成する.
dtm_now = datetime.datetime.now()
y4 = dtm_now.year
m2 = dtm_now.month
d2 = dtm_now.day
hh = dtm_now.hour
mm = dtm_now.minute
ss = dtm_now.second
ms = int( dtm_now.microsecond / 1000 )
FMT_DTM = "{0:4d}_{1:2d}{2:2d}_{3:2d}{4:2d}{5:2d}_{6:3d}"
str_dtm = FMT_DTM.format( y4, m2, d2, hh, mm, ss, ms )

# 画像を保存するファイルパス.
filepath = "c:/tmp/0512_0512_{0}.bmp".format( str_dtm )

# ファイルパスに従って画像ファイルを保存する.
im_rsp.save( filepath )

# 終了メッセージ.
print( dtm_now )
print( "finish." )

上記のコードで Flask サーバにリクエストすると、問題がなければサーバー側では下記のようなコンソール出力が得られます。

2022-07-23 20:39:42.801993
512
512
2022-07-23 20:39:42.806476
4.76469995919615 msec

127.0.0.1 - - [23/Jul/2022 20:39:42] "POST /ask HTTP/1.1" 200 -

問題がなければクライアント側では下記のようなコンソール出力が得られます。

PS C:\tmp> python test_client.py
2022-07-18 14:50:55.606153
finish.

上記の test_client.py のコードにおいて、サーバーから戻ってきた後は下記のような流れです。

サーバーから戻ってきた JSON を辞書型に変換する。
→ 辞書型のメンバをBASE64デコードで配列(1次元)に変換する。base64.b64decode()
→ → 配列(1次元)から ndarray(1次元)に変換する。np.frombuffer()
→ → → ndarray(1次元)から2次元に形を変える。reshape()
→ → → → ndarray(2次元) から Pillow のデータに変換する。Image.fromarray()
→ → → → → Pillow の機能でファイルを保存する。

という流れです。

リクエストする画像
レスポンスされた文字列をデコードした画像

クライアントソフトを C# で作った場合

test_server.py が動作する Flask サーバーに画像データを送るための C# で作った HTTP クライアントソフトをZIPにまとめて下記に提供しておきます。

リクエストする画像も上記の ZIP ファイルにひとまとめにしてあります。ファイル名は 0512_0512.bmp です。