MotoJapan's Tech-Memo

技術めも

【Javascript to flask】base64で画像を送信/受信する方法 【お家IT#7】

本件の実装の一部 
motojapan.hateblo.jp

前回の続き 
motojapan.hateblo.jp

目次 

前々回、ChromeWebカメラを起動/撮影した後、Canvasに書き出すところまで進めた。
前回は、Ajaxを利用した単純なデータ通信をまとめた。
この流れでCanvasの画像をbase64化してサーバーに送信したい。

やり方がわかると実はすごく単純。

base64とは

データを["a-z", "A-Z", "0-9", "+", "/"]の64文字で表現するエンコード方式
(データは特に画像などに限らない。)

送信側実装(Javascript)

#ajax用モジュール読み込み
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
    ...

    function send_img(){
        //canvas elementを取得
        var canvas = document.getElementById('canvas');
        //base64データを取得(エンコード)
        var base64 = canvas.toDataURL('image/png');

        var fData = new FormData();
        fData.append('img', base64);

        #ajax送信
        $.ajax({
            //画像処理サーバーに返す場合
            url: 'https://192.168.0.100:12345/hoge_image_processing',   
            type: 'POST',
            data: fData ,
            contentType: false,
            processData: false,
            success: function(data, dataType) {
                //非同期で通信成功時に読み出される [200 OK 時]
                console.log('Success', data);
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {
                //非同期で通信失敗時に読み出される
                console.log('Error : ' + errorThrown);
            }
        });
    }
    ...
</script>

受信側実装(python : flask)

import base64
from PIL import Image
from io import BytesIO

...
#前回に追記
@app.route('/hoge_image_processing', methods=['POST'])
def set_data():
    enc_data  = request.form['img']
    #dec_data = base64.b64decode( enc_data )              # これではエラー  下記対応↓
    dec_data = base64.b64decode( enc_data.split(',')[1] ) # 環境依存の様(","で区切って本体をdecode)
    dec_img  = Image.open(BytesIO(dec_data))
...

enc_dataを詳しく見てみると次の通り。

data:image/png;base64,kpvdfkgspkhgsKOJIJgmfkdvmkkPOKPO
DSmkpogj/mKRFPVKMPOGKP+OSKPVKS++LKVOPKIGRPOIJpkvmplldk
iVBaasfdojOIAJHSUHFvDFNIOVEOIJobjtredpjORw0KGgoAAAANSU
hEUgAAAoAAAAHgCAYAAAA10dzkAAAgAElEQVR4Xky9W4+la5adNb/z
WisiI3Mfq6vbXY1E26ItVcsWssG ...

“data:image/png;base64,”以降がbase64エンコードされた画像情報(エンコードデータ)。
これをpythonで扱えるようにするためには、次の手順が必要。

  • 1. エンコードデータbase64.b64decodeでデコードして、byte列にする。
  • 2. byte列を、BytesIOでバイナリストリームに変換し、PILでpythonで扱える画像形式にする。

以上。

ここまで来てフロントエンドからは 「No 'Access-Control-Allow-Origin' header is present on the requested resource. 」というエラーがでているので、次回はこれを修正する。

あと、Javascript->flaskへの通信が多いが、画像処理結果をWebアプリに送り返したいこともある。
次々回は、flask->Javascriptjsonデータ通信を纏めたい。

【Javascript to flask】FormDataを送信/受信する方法 【お家IT#6】

本件の実装の一部 
motojapan.hateblo.jp

前回の続き 
motojapan.hateblo.jp

目次 


前回まででWebカメラで撮影した画像をCanvas領域にコピーするところまで終えた。
が、根本的にJavascript - python(flask)環境でのフロントエンド-バックエンド間通信について知識がないので手軽にサーバーに対してデータ送信する方法をメモ。


今回は簡単な文字列"hogehoge"と"fugafuga"を送受信する例。

送信側実装(Javascript)


「FormDataというデータコンテナ」と「Ajaxによる非同期通信」を利用して送信する方法が一番簡単に試せる。
JavascriptのFormDataは、append(,)することで追加し続けられる。

#ajax用モジュール読み込み
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
    ...

    function send_text(){
        var text1 = 'hogehoge'
        var text2 = 'fugafuga'

        #FormData初期化
        var fData = new FormData();

        fData.append('text1', text1);
        fData.append('text2', text2);

        #ajax送信
        $.ajax({
            //配信元のhttpsサーバーに返す場合
            url: '/register',                            
            //別サーバー(今回でいう画像処理サーバー)に返す場合
            //url: 'https://192.168.0.100:12345/register',   
            type: 'POST',
            data: fData ,
            contentType: false,
            processData: false,
            success: function(data, dataType) {
                //非同期で通信成功時に読み出される [200 OK 時]
                console.log('Success', data);
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {
                //非同期で通信失敗時に読み出される
                console.log('Error : ' + errorThrown);
            }
        });
    }
    ...
</script>

受信側実装(python : flask)

Flaskのrequest.formは、でアクセスする。
前回のスクリプトの差分で必要箇所を記載(非常に簡単)

from flask import Flask, render_template, request

...
#前回に追記
@app.route('/register', methods=['POST'])
def set_data():
    text1 = request.form['text1']
    text2 = request.form['text2']

...

対応関係

下記に対応関係を簡単にまとめる。

javascript python:flask
Formデータ var fData = new FormData(); request.form
Formデータ読み書き [Write] fData.append('text1','hogehoge'); [Read] request.form['text1']


以上。
これでデータの登録などの単純なやり取りができる。

次回はbase64を送受信する話。

【flask】https対応Webサーバーをバックグランド実行する方法 【お家IT#5】

本件の実装の一部
motojapan.hateblo.jp

前回の続き
motojapan.hateblo.jp

目次

標準入力をバックグラウンドで無視すると落ちる

前回 flaskをhttps化 (http over ssl)したが、実行時にはpass phraseの入力が必要。
サーバーなのでSSHで繋ぎ、バックグラウンドで実行してセッションを切断したいことはよくある。
ただし、前回のスクリプトを無理やりバックグラウンド実行すると次のエラーが出る。

$$ sudo python app.py &
$ Enter PEM pass phrase:
    Traceback (most recent call last):
  File "app.py", line 8, in <module>
    context.load_cert_chain('cert.crt', 'server_secret.key')
IOError: [Errno 22] Invalid argument

バックグラウンド実行対策

load_cert_chainの第3引数にパスワードを入れてあげると可能

#app.py
from flask import Flask, render_template, request
import ssl

app = Flask(__name__)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)

# >>> 対策前
#context.load_cert_chain('cert.crt', 'server_secret.key')
# <<<

# >>> 対策後
with open('./pwd.txt') as f:
    #平文で書くのは怖いのでローカル管理するファイルを呼び出す
    pwd = f.read()
context.load_cert_chain('cert.crt', 'server_secret.key', pwd)
# <<<

@app.route("/")
def index():
    return render_template('index.html')

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=800, ssl_context=context, threaded=True, debug=True)

下記参考資料のAPI referenceにも次の通りの記載。

password 引数に、秘密鍵を復号するためのパスワードを返す関数を与えることができます。その関数は秘密鍵が暗号化されていて、なおかつパスワードが必要な場合にのみ呼び出されます。その関数は引数なしで呼び出され、string, bytes, または bytearray を返さなければなりません。戻り値が string の場合は鍵を復号化するのに使う前に UTF-8エンコードされます。string の代わりに bytes や bytearray を返した場合は password 引数に直接供給されます。秘密鍵が暗号化されていなかったりパスワードを必要としない場合は、指定は無視されます。

password が与えられず、そしてパスワードが必要な場合には、OpenSSL 組み込みのパスワード問い合わせメカニズムが、ユーザに対話的にパスワードを問い合わせます。


Python 標準ライブラリ」「18.2. ssl — ソケットオブジェクトに対する TLS/SSL ラッパー」より
URL:
https://docs.python.jp/3/library/ssl.html

これでめでたく、PC版Android版でもChromeWebカメラが動くので、次回からは撮影した画像をbase64化してjavascriptで送る部分を進める。

【flask】Webサーバーをhttps対応する方法 【お家IT#4】

本件の実装の一部
motojapan.hateblo.jp

前回の続き
motojapan.hateblo.jp

目次

JavascriptからWebカメラを起動しようとしたら下記コードで怒られた。
非Secure通信下でのgetUserMediaはdeprecatedとのこと。

[Deprecation] getUserMedia() no longer works on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gl/rStTGz for more details.

SSL対応の手順

本来はSSLサーバー証明書を購入する必要があるがオレオレ証明書を作成することも可能。(通常利用は避ける)

秘密鍵を作成

まずサーバー側の鍵を作成

$ openssl genrsa -aes128 2048 > server_secret.key

Generating RSA private key, 2048 bit long modulus
.............+++
...............................................................................+++
e is xxxxx (0xxxxxx)
Enter pass phrase: (パスワード入力)
Verifying - Enter pass phrase:(パスワード再入力)

秘密鍵から公開鍵を作成

クライアント側の公開鍵を作成

$ openssl req -new -key server_secret.key > server_pub.csr
Enter pass phrase for server_secret.key: (パスワード入力)
・・・
↓必要事項入力
Country Name (2 letter code) [AU]:(国名)
State or Province Name (full name) [Some-State]:(都道府県名)
Locality Name (eg, city) []:(市区町村名)
Organization Name (eg, company) [Internet Widgits Pty Ltd]:(企業名)
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

デジタル証明書を作成

デジタル証明書とは

  • 秘密鍵から作った)公開鍵を正真であることを証明するデータ
  • 本来はCAを通して発行される

配布用証明書を作成

$ openssl x509 -in server_pub.csr -days 365000 -req -signkey server_secret.key > cert.crt

flask上でHTTPSサーバー起動

#app.py
from flask import Flask, render_template, request
import ssl

app = Flask(__name__)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain('cert.crt', 'server_secret.key')

@app.route("/")
def index():
    return render_template('index.html')

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=800, ssl_context=context, threaded=True, debug=True)

実行

$ sudo python app.py
Enter PEM pass phrase: (パスワード入力)
 * Running on https://0.0.0.0:800/ (Press CTRL+C to quit)
 * Restarting with stat
Enter PEM pass phrase: (再パスワード入力)
 * Debugger is active!
 * Debugger PIN: 123-456-789


無事起動(めでたしめでたし)

おまけ

ちなみに回避方法もあるにはあるが、
Chrome on Androidの場合、回避すらできないケースもあるので
折角ならhttps対応すべき。

【Javascript】Webカメラを起動&撮影する 【お家IT#3】

本件の実装の一部
motojapan.hateblo.jp

Webアプリ上でHTML+JSでWebカメラを動作させたい。
私の場合、それほど特殊なWebカメラの使い方をするわけでもないし、こういった類の記事は山のようにあるので自分が参考にした記事をメモ。

カメラ起動

今回はWebRTC(Web Real Time Communications)でカメラを起動する。
キーワードはUserMedia。

参考にした記事①

シンプルなサンプルコード
html5experts.jp
html5experts.jp

参考にした記事②

HTML5 での映像と音声の取得(細かい説明で一番良い)
https://www.html5rocks.com/ja/tutorials/getusermedia/intro/

カメラ撮影

カメラのプレビューを押す or 撮影トリガー(テキストやボタン)を押し、videoタグの画像をcanvasに保存には下記

<!-- HTML -->
<!-- カメラの準備 -->
<video id="camera" width="720px" autoplay></video>
<!-- 記録用canvas -->
<canvas id="canvas"></canvas>
<!-- 記録用img -->
<img id="img">
<!-- 撮影トリガー 1-->
<a id="rec" href="#">TAKE PICTURE</a>
<!-- 撮影トリガー 2-->
<input type="button" value="REC" onclick="take_picture()">
//Javascript
//video element
var video = document.getElementById('camera');

//video element(プレビュー画面)をクリックして撮影
video.addEventListener("click", function() {
    take_picture()
});

//テキストクリック撮影(撮影トリガー 1)
$("#rec").click(function() {
    take_picture()
});

//撮影関数
function take_picture() {
    //videoのstreamをcanvasに書き出す方法
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    //videoの縦幅横幅を取得
    var w = video.offsetWidth;
    var h = video.offsetHeight;    
    canvas.setAttribute("width", w);
    canvas.setAttribute("height", h);
    ctx.drawImage(video, 0, 0, w, h);

    //canvasを更にimgに書き出す方法
    var img = document.getElementById('img');
    img.src = canvas.toDataURL('image/png');
}

とりあえず参考資料を使うと、explorerでは動いた。

下記エラーでchromeだと見事に動かない。

[Deprecation] getUserMedia() no longer works on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gl/rStTGz for more details.

chromeセキュアな通信(HTTPS, SSL)じゃないとカメラ起動できないとのこと。
ということで次回は、flaskサーバーをHTTPSします。

【お家IT#2】Webアプリと画像認識で家の食品名と賞味期限を簡単に登録したい(構想編)

下記記事の続きです。
motojapan.hateblo.jp

構想

Google Spreadsheetを探して、食品名と賞味期限を手入力するのは、実は結構めんどくさい。
なので、Webアプリ開発と画像認識の勉強も兼ねて、下のような構成を構想。
写真を撮影すると商品情報が取得でき、自動で登録されるようにしたい。

右側データ管理、通知は過去の記事でGASで実装済み。今回は左側webアプリサーバー、画像認識、登録などの一連のフローを実装予定。
f:id:motojapan:20171026004433p:plain

つまりはこんなイメージ。(みんな幸せ)
f:id:motojapan:20171016003018p:plain

環境

想定環境はRaspberry piをサラから使うので、下記の通り。

実装項目

ざっと思いつくやるべき項目を列挙。(超雑)
青字がフロントエンドとバックエンドの通信周り
赤字が最終的に求めたいデータ

バックエンド

  • Webサーバーを立ち上げる
  • Webサーバーをバックグラウンド実行する
  • HTTPS対応する(必要に応じて)
  • formを受信する(単純なPOSTデータの受信)
  • base64で画像を受信する(解析用画像の受信)
  • jsonを送信する(結果の送信)
  • pythonからgoogle spread sheetを読み書きする

フロントエンド

  • Webカメラを起動する
  • カメラを切り替える(フロント/バック)
  • Webカメラを起動後のvideoタグのレイアウトサイズを調べる
  • formを送信する(単純なPOSTデータの送信)
  • base64で画像を送信する(解析用画像の送信)
  • jsonを受信する(結果の受信)
  • CSSでレイアウト整理する

画像解析

  • 画像からバーコード情報を読み取る(画像⇒数列)
  • バーコード情報から商品名を調べる(数列⇒商品情報)
  • 画像から賞味期限を読み取る(画像⇒文字列)


以降は、この項目の開発メモ、ハマった話を書き残す予定。

興味がある人は次回以降の記事をご覧ください。

【Android】TensorFlow LiteのMobilenetサンプルコードをビルド&実機インストール・実行する方法と雑感 (Android Studio 3.0)

待ちに待っていたTensorFlow Liteが先日リリースされた。
Preview版ということでgithubに公開されていたので、触ってみた。
(そもそも本家GithubのTfLiteCameraDemo.apkというPre-build binaryのapkがエラーでインストールできない(17/11/18現在))

Nexus5実機で実行した結果はこんな感じ。
「ボールペン」と「絆創膏」のリアルタイム認識。
f:id:motojapan:20171118031641g:plain

仕組み

既存のTensorFlow学習済みを、TensorFlow Liteモデル(tflite)に変換。
この変換済みモデルをAndroid / iOSのモバイルプラットフォームに組み込み、推論が可能。
f:id:motojapan:20171118024153p:plain
(画像は、本家サイトの「TensorFlow Lite Architecture」抜粋)

今回はGitHub上で提供されるAndroid側。
Android StudioのMobilenetサンプルプロジェクトをビルドし、実機インストール、実行、デバックする。

主な手順

本家に書いてあるものは下記。

Building in Android Studio using TensorFlow Lite AAR from JCenter
The simplest way to compile the demo app, and try out changes to the project code is to use AndroidStudio.

  • Install the latest version of Android Studio 3 as specified here.
  • Make sure the Android SDK version is greater than 26 and NDK version is greater than 14 (in the Android Studio Settings).
  • Import the tensorflow/contrib/lite/java/demo directory as a new Android Studio project.
  • Click through installing all the Gradle extensions it requests.
  • Download the quantized Mobilenet TensorFlow Lite model from here
    • unzip and copy mobilenet_quant_v1_224.tflite to the assets directory: tensorflow/contrib/lite/java/demo/app/src/main/assets/
  • Build and run the demo app


https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/liteより抜粋。

かなりシンプル。
これを1つずつ追っていく。

前提環境

1. Android Studio 3 インストールする

TensorFlow Liteを使う場合、Android Studio 3が必要。
本家ここからダウンロードとインストール。
f:id:motojapan:20171118035336p:plain

2. TensorFlow Repositoryをcloneする

好きな場所にcloneする。

$ git clone https://github.com/tensorflow/tensorflow

サンプルプロジェクトは、「tensorflow/contrib/lite/java/demo」にある。

3. サンプルプロジェクトをAndroid Studio上で開く

Android Studioの「File」->「Open」で「tensorflow/contrib/lite/java/demo」を開く。
f:id:motojapan:20171118040750p:plain

上記で開いた後、足りない各種ライブラリやbuild-toolsのインストール、gradle syncを進める。
IDEが通知してくるので基本的にはぽちぽちするだけ)
f:id:motojapan:20171118041024p:plain

4. 実機インストール・実行する(モデル無し)

上記が進むと、緑三角形の「Run」が有効になり、USB接続した実機が認識されるようになる。
f:id:motojapan:20171118041331p:plain
実機が認識されたら「OK」で実行する。

まず、インストール後、起動して見事に落ちる。
f:id:motojapan:20171118041647p:plain

アプリ一覧からTfLiteCameraDemoを再度起動する。
f:id:motojapan:20171118041747p:plain

学習済みモデルがないので「Uninitialized Classifier or invalid context.」が表示される。

5. モデルをダウンロードし、プロジェクトに配置する

本家ここからzipダウンロード。

zipを解凍すると2ファイルある。

  • labels.txt : ラベル情報(文字列ベタ書き)
  • mobilenet_quant_v1_224.tflite : tensorflow lite学習済みモデル

f:id:motojapan:20171118041941p:plain

学習済みモデルファイルの配置場所は、androidプロジェクトのasset直下。(tensorflow/contrib/lite/java/demo/app/src/main/assets/mobilenet_quant_v1_224.tflite)

Android Studioのプロジェクト上にも反映されていることを確認する。
f:id:motojapan:20171118042508p:plain

6. インストール・実行を実行する(モデルあり)

5.で晴れてモデル有りで実機にインストールしてみると結果は下記のように表示される。

f:id:motojapan:20171118031922p:plain
ボールペン(ballpoint)を認識した。
万年筆(fountain pen)にも似ているらしい。

f:id:motojapan:20171118031929p:plain
絆創膏(Band Aid)も認識した。
かなりconfidenceが高い。

実機評価

処理速度

手元にあるデバイス Google Nexus5, Huawei P9 liteで確認。
入力画像サイズはMobilenetなので224 x 224、これに対する処理速度。

Google Nexus5(2013) Huawei P9 lite(2016)
CPU Qualcomm Snapdragon 800 MSM8974 2.26GHz(クアッドコア) kirin 650 (オクタコア(2.0GHz×4コア+1.7GHz×4コア))
Android version 5.0.1 6.0
RAM 2.0 [GB] 2.0 [GB]
mobilenet処理速度 (per 1 frame) 210-260 [msec] (best 140 [msec]) 150-160 [msec]

Nexus5の速度が安定しない。(瞬間最大風速的に早くなる時もある)
実際OSの忙しさで待たされたりすることもあるので、ここら辺は揺らぎがあるのかなぁ。
動画でリアルタイムに見ている分には思ったよりサクサク動くし、気にならない。うまくやれば何かしら使えそう。

モデルサイズ

ここが妥当な評価になるかわからないが、TensorFlow Lite v1 model とTensorFlow Mobilenet v1 model を比較してみる。

TensorFlow Lite TensorFlow TensorFlow
mobilenet_quant_v1_224 MobileNet_v1_0.25_224 MobileNet_v1_1.0_224
バイナリサイズ 4.3 [MB] 7.6 [MB] 67.9 [MB]

サイズは小さくなっているように見える。

メモリ使用量

サンプルアプリのメモリ使用量を調べる。

$ adb shell dumpsys meminfo | grep tflitecamerademo
    91689 kB: android.example.com.tflitecamerademo (pid 22383 / activities)

カメラも動いているが、

  • 稼働時はメモリ使用量は90[MB]近い
  • 待機時は38[MB]付近。

ちなみに標準カメラ単体では、

  • 稼働時は18[MB]付近
  • 待機時は4[MB]付近。

雑感

個人的な感想として、

  • 今回程のモデルで、エンジョイユースであれば、リアルタイム性は気にならず快適。
  • モデルのフットプリントは小さくなっている模様。
    • 精度差はわからない(ちゃんと評価してないので)
  • サンプルアプリバイナリでも22.0[MB]は単純にかなり大きいアプリだなという印象。
    • モデル更新の度にアプリ更新が必要?
  • メモリもそれなりに食っている。
  • エッジとサーバーはどう協調していくかこれから大きなテーマになりそう。

近々、自作モデルをビルドしたい。