Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

ラズパイでカメラキャプチャから年齢・性別予測

先日Tensorflowにて顔画像から年齢や性別が推定できるコードを動かしてみました。
elsammit-beginnerblg.hatenablog.com

環境構築に手こずりましたが結果はまぁまぁ。
ただ、せっかくなのでカメラからキャプチャした人物の判定を行いたい!!
と思い立ち、今回のコードを改良してキャプチャから動的に年齢・性別が予測できるようにしました。

今回はこちらをまとめておこうと思います!!

ゼロから作るDeep Learning ?Pythonで学ぶディープラーニングの理論と実装




■とりあえず動かしてみる

コードは前回と同様にこちらの方のコードを利用します。
こちらのコードは画像判別なので、カメラキャプチャに置き換えました。

img = cv2.imread("input10.jpg")

capture = cv2.VideoCapture(0)

while(True):
    ret, frame = capture.read()

に置き換えた感じですね。

ただこちらを実行したところ、カクカクに動く。。。どころの話ではない!!
かなりの時間待たされてやっと1フレーム分の測定結果が表示される感じ🤣
このままでは使い物にならない。。
ということでまずはどこに時間を要しているか確認!!

■現状の処理時間測定

今回の制御は大きく分けると、
 ①顔検知や顔輪郭収集
 ②検知した顔から年齢・性別予測
 ③アウトプット画像生成
の3つになります。

それぞれの時間を測定したところ、、、
N = 3で平均が、
 ①顔検知や顔輪郭収集          :7.63秒
 ②検知した顔から年齢・性別予測     :5.20秒
 ③画像に四角形の輪郭を重畳の上画像表示 :0.06秒
となっておりました。

ということで、①と②に時間を要していました。
まぁ、ほぼほぼ想定通りだのですが、、それにしても合計で12秒越え。。
流石にこのままではカメラキャプチャで動的判定は無理だな🤣

そこで、①、②の時間を短縮させていきたいと思います!!
一旦制御を簡単にするために検知人数は1人にします。

■顔検知や顔輪郭収集時間短縮

今回用いていた制御ではMTCNNを用いていました。

【MTCNNとは】
画像の中から、顔を検出するための深層学習モデルです。
顔領域を四角いフレームで囲います。

3つの畳み込みネットワークを使用して画像内の顔を検出します。
3つの畳み込みネットワークとは、

P-net:顔を検出する
R-net:顔の位置を改善する
O-net:顔器官点(目とか鼻とか口)を検出する
※参考:https://www.souya.biz/blog/2019/04/17/mtcnn%E3%81%A8%E3%81%AF/

結構正確に顔検出できるモデルではあるのですが、、、
処理時間が結構かかってしまうので、精度が落ちるがOpenCVの検出を用いることにしました!!

コードはこちらの通り。

def FaceDetection(FaceCascadePath, InputImg):
    gry_img = cv2.cvtColor(InputImg, cv2.COLOR_BGR2GRAY)
    
    face_cascade = cv2.CascadeClassifier(FaceCascadePath)
    facerect = face_cascade.detectMultiScale(gry_img,scaleFactor=1.5,minNeighbors=2,minSize=(30,30))

    face = None
    if len(facerect) > 0:    
        x = facerect[0][0]
        y = facerect[0][1]
        w = facerect[0][2]
        h = facerect[0][3]

        face = InputImg[y:y+h,x:x+w]

    return face,facerect

こちらのコードは、以前githubに公開した顔検知APIの必要な部分のみを抽出して持ってきました。
https://github.com/Elsammit/ImageAPI

上記関数は引数に、
・FaceCascadePath:顔検知用カスケードパス
・InputImg:キャプチャイメージ
を与えることで、
・face:顔抽出画像
・facerect:顔検知時の4点の座標値
が返ってくる関数になります。
返り値は元のMTCNNと同じにしました。

では、置き換えた後の時間を測定!!
結果、
①顔検知や顔輪郭収集          :0.15秒
となりました!!
検知時間が1/50以下になりましたね。
かなり短縮できました👍

■検知した顔から年齢・性別予測の短縮

では次に年齢・性別予測の短縮です。

予測用のコードはこちらです。

def age_gender_predict(faces):    
    if len(faces) > 0:   
        # モデルの設定
        if os.isdir("model") == False:
            pre_model = "https://github.com/yu4u/age-gender-estimation/releases/download/v0.5/weights.28-3.73.hdf5"
            modhash = 'fbe63257a054c1c5466cfd7bf14646d6'
            weight_file = get_file("weights.28-3.73.hdf5", pre_model, cache_subdir="model",
                                   file_hash=modhash, cache_dir=str(Path(__file__).resolve().parent))
        else:
            weight_file = "model/weights.28-3.73.hdf5"            

        img_size = np.asarray(faces.shape)[1]
        model = WideResNet(img_size, depth=16, k=8)()
        model.load_weights(weight_file)
        
        # 予測
        results = model.predict(faces)
        Genders = results[0]
        ages = np.arange(0, 101).reshape(101, 1)
        Ages = results[1].dot(ages).flatten()

    return Ages, Genders

こちらのコードのうち、実際に入力イメージから予測をするコードは、

        results = model.predict(faces)
        Genders = results[0]
        ages = np.arange(0, 101).reshape(101, 1)
        Ages = results[1].dot(ages).flatten()

であり、このコードの前は初期化処理になります。

そこで、、、

# 性別・年齢予測モデル初期化.
def init_age_gender():
    if os.isdir("model") == False:
        pre_model = "https://github.com/yu4u/age-gender-estimation/releases/download/v0.5/weights.28-3.73.hdf5"
        modhash = 'fbe63257a054c1c5466cfd7bf14646d6'
        weight_file = get_file("weights.28-3.73.hdf5", pre_model, cache_subdir="model",
                                   file_hash=modhash, cache_dir=str(Path(__file__).resolve().parent))
    else:
        weight_file = "model/weights.28-3.73.hdf5"            

    img_size=64
    model = WideResNet(img_size, depth=16, k=8)()
    model.load_weights(weight_file)   

    return model


# 性別・年齢予測
def age_gender_predict(faces, model):    
    if len(faces) > 0:   
        # 予測
        results = model.predict(faces)
        Genders = results[0]
        ages = np.arange(0, 101).reshape(101, 1)
        Ages = results[1].dot(ages).flatten()
        
        gender = ""
        if Genders[0][0] < 0.5:
            gender ="Male"
        else:
            gender = "FeMale"

    return Ages, gender

とし、モデルの初期化と性別・年齢予測を分割しました。

そしてmain関数はこちらの通り、
モデル初期化は事前に実施した後、
キャプチャ画像から年齢・性別予測をループさせることにしました。

capture = cv2.VideoCapture(0)
img_size = 64
model = init_age_gender()
while(True):
    ret, frame = capture.read()

    # resize the window
    windowsize = (800, 600)
    frame = cv2.resize(frame, windowsize)
    face,facerect = FaceDetection("haarcascade_frontalface_default.xml", frame)
    if len(facerect) > 0: 
        for rec in facerect: 
            cv2.rectangle(frame, tuple(rec[0:2]), tuple(rec[0:2]+rec[2:4]), (0,255,0), 2)
        aligned = misc.imresize(face, (img_size, img_size), interp='bilinear')
        test = np.zeros((1, img_size, img_size, 3), dtype = "uint8")
        test[0, :, :, :] = cv2.cvtColor(aligned, cv2.COLOR_BGR2RGB)

        Ages, Genders = age_gender_predict(test, model)

        label = "{}, {}".format(int(Ages[0]), Genders)
        draw_label(frame, (facerect[0, 0], facerect[0, 1]), label)

    cv2.imshow('title',frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

こちらで時間測定実行!!
結果、、、
 ②検知した顔から年齢・性別予測     :1.04秒
となりました。
ちょっと初期化時間伸びましたが、予測実行時間としては1/5以下に出来ました!!
こちらも上々👍

■実際に動かしてみる

変更後の時間は、
 ①顔検知や顔輪郭収集          :0.15秒
 ②検知した顔から年齢・性別予測     :1.04秒
 ③画像に四角形の輪郭を重畳の上画像表示 :0.06秒
となり、合計1.21秒!!
ちょっとカクカクしちゃいますが、、、これぐらいなら使い物になりそうです!!

ということでカメラキャプチャから動的に年齢・性別判定してみます!!
まずは男性。
f:id:Elsammit:20210305212859g:plain

次は女性。
f:id:Elsammit:20210305213742g:plain

年齢が少し高めに出てしまっている気が。。🤔
性別はまぁまぁかな?

と思ったのですが、、、ちょっと性別も怪しい感じでした😥
f:id:Elsammit:20210305214110g:plain

顔検知出来ていない時もありましたが、なにより性別・年齢ともに。。。
全然違う!!

他の写真も試してみましたが、、
恐らく髪の毛が顔にかかっている分判定を誤ってしまっていそうだな。と思いました。
ちょっと調査・検証が必要ですね😅

■最後に

今回は1人のみの検知でしたが、動的に年齢・性別が行えるように出来ました!!
複数人検知できるようにすると時間伸びちゃうと思いますが、、、どの程度伸びるのだろう🤔
もう少し検証してみます。
後は精度を上げるための策を考えてみたいと思います!!


ラズパイでのTensorflow2.3インストール手段と人物画像から年齢予測

興味本位で顔検出からの年齢や性別予測してみたいな🤔、
と思い立ち、さっそく実施してみることにしました!!

AWSを用いればS3に保存された画像から顔検出、年齢、性別予測が出来ることは知っているのですが、、、
AWSの無料期間使い切ってしまっているし、どうせだったら無料の方法でやりたいよな。
と思い、さっそく調べてみることに。

そこで、ローカル環境で年齢や性別予測することが出来る方法が見つかったので試しに使ってみることにしました!!



■利用環境

手頃なサーバが見つからなかったので、お試しで
Raspberry Pi 4
を用いてみました。

■ローカルで年齢、性別を予測する方法

こちらの記事にて紹介されておりました。
qiita.com

FacenetのMTCNNにて顔検出を行い、kerasで年齢や性別を予測しているようです。
こちらの方がgithubにて環境一式を用意されているようでしたので、試しにこちらを実行してみました。

そうしたところ、Tensorflowのバージョンが古いために実行できませんでした😥
こちらのログが出力されてしまったのです。。。

ImportError: Keras requires TensorFlow 2.2 or higher. Install TensorFlow via `pip install tensorflow

そこで、Tensorflowのバージョンを上げることに!!
しかしながら、バージョンアップを実施する上で少し躓いたのでまとめておくことにします!!

■対策1

pipにてアップグレードを試みてみました。
コマンドはこちら。

pip3 install --user --upgrade tensorflow

ですが、1.13.1までしかアップグレードされませんでした。
簡単にはいきませんね😥

■対策2

pipでインストールできないのなら、、、
自分でソースからビルドするしかない!!
、と言うことで早速実施してみました。

まず、

wget "https://raw.githubusercontent.com/PINTO0309/Tensorflow-bin/master/tensorflow-2.3.0-cp37-none-linux_armv7l_download.sh"

にてダウンロードスクリプトを取得。
そして、

sudo chmod +x tensorflow-2.3.0-cp37-none-linux_armv7l_download.sh
./tensorflow-2.3.0-cp37-none-linux_armv7l_download.sh

にてダウンロードスクリプトを実行!!

次に先ほどのダウンロードスクリプトにて取得したtensorflowをインストール!!
の前にすでにインストール済みのtensorflowを削除。

pip3 uninstall tensorflow

最後にtensorflowをインストール!!

pip3 install tensorflow-2.3.0-cp37-none-linux_armv7l.whl

これでインストールは完了。
念のため、インストールされたことを確認するためにこちらを実行。

pip3 list | grep tensorflow
tensorflow                       2.3.0
tensorflow-estimator             2.3.0

が出力されればOKです!!

■その他のエラーについて

tensorflowを2.3にするとこちらのようなエラーが発生。
エラーはこちら。。

'tensorflow._api.v2.nn' has no attribute 'xw_plus_b'

どうやらtensorflowを2.x.xと1.x.xでAPIが異なるようで、使用できなくなってしまうようです。
そこで、

tf.nn.xw_plus_b

のコードをこちらに変更。

tf.compat.v1.nn.xw_plus_b

他にも
・tf.div
・tf.GPUOptions
なども同様にエラーとなってしまうので、
compat.v1
を追記いたします。

ほかにも、kerasやscipyが最新版ではなくてはいけなかったので、

pip3 install モジュール名 --upgrade

を実行してアップグレードを実行。

これで、環境構築完了。
環境を作るだけで一苦労でした。。

■実行

せっかく構築できたので、人のコードですがお試しで実行してみました!!

face_age_gender.py
の106行目あたり、

img = cv2.imread("input.jpg") #入力画像

jpgファイル名を実行したいファイル名に変更。
そして、実行!!
結果はこちら。

【入力画像】
f:id:Elsammit:20210302222358j:plain

【出力画像】
f:id:Elsammit:20210302222421j:plain

※画像はこちらを利用。
https://www.pakutaso.com/20210259056post-33541.html

性別はOK。
年齢も恐らくOKですね!!

他の画像も試してみましたが大体判定出来ていました。

■最後に
今回は有識者が作成したコードをそのまま使ってみました。
ローカル環境でも結構精度が出るんですね。

ちょっと判定結果の表記が小さかったり、動画に対応してみたいとも思ったりしているので、
これから自分アレンジしていきたいと思います!!


■参考
https://qiita.com/oHjm/items/677b57d4714de3cf83ca
https://itnext.io/installing-tensorflow-2-3-0-for-raspberry-pi3-4-debian-buster-11447cb31fc4
https://github.com/keras-team/keras/issues/12649

機械学習による着座中か否か判定 part2

昨日学習結果を保存するまでの処理をまとめました。
elsammit-beginnerblg.hatenablog.com

今回はこちらの保存した学習データを用いてWeb上に着座中か否かの判定結果を表示させてみたいと思います!!
f:id:Elsammit:20210227105017p:plain



■学習結果を使用する

学習結果はpickleにて保存いたしました。
では、保存した結果を使用してみます。

使用する場合にはこちらのように、pickleファイルを読み出せば学習結果を利用することが出来ます。

    with open('model.pickle', mode='rb') as f:
        lr = pickle.load(f)
    ans = lr.predict(row_f.reshape(1, -1))

pickleを呼び出して使用する場合には、

import pickle
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

の通り、インポートをする必要があります。

■学習結果を用いて赤外線アレイセンサから予測する

では実際に保存した学習結果を用いて予測していきます。
構成はこちらを参照ください。
elsammit-beginnerblg.hatenablog.com

赤外線アレイセンサ値を収集して学習結果より予測するためのコードはこちら。

from flask import *
from flask import Flask, jsonify, make_response, request, Response
import time
import busio
import board
import adafruit_amg88xx
import numpy as np
from flask_cors import CORS
import csv
import pickle
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

app = Flask(__name__)
cors = CORS(app, resources={r"/*": {"origins": "*"}})

@app.route("/")
def main():
    return "Hello World"    

@app.route('/post', methods=['POST'])
def post_json():
    #赤外線アレイセンサ値取得.
    i2c_bus = busio.I2C(board.SCL, board.SDA)
    sensor = adafruit_amg88xx.AMG88XX(i2c_bus, addr=0x68)
    time.sleep(.1)
    array = np.array(sensor.pixels)
    array1616 = array.repeat(2, axis=0).repeat(2, axis=1) 
    data = array1616.reshape(-1)
    llist = data.tolist()
    row_f = np.array(llist)
    
    #学習結果を呼び出し.
    with open('model.pickle', mode='rb') as f:
        lr = pickle.load(f)
    
    #学習結果を用いて赤外線アレイセンサ値から予測.
    ans = lr.predict(row_f.reshape(1, -1))
    Msg = ""
    if ans == 1:
        Msg = "Sit down"
    else:
        Msg = "None or Stand up"

    result = {
        "data": {
        "ret": Msg,
        "message":llist
        }
    }

    return jsonify(result)

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

こちらのコードにてセンサ値を取得し、8x8 ⇒16x16に変換。
そして、list型からnumpy配列に変換。

    i2c_bus = busio.I2C(board.SCL, board.SDA)
    sensor = adafruit_amg88xx.AMG88XX(i2c_bus, addr=0x68)
    time.sleep(.1)
    array = np.array(sensor.pixels)
    array1616 = array.repeat(2, axis=0).repeat(2, axis=1) 
    data = array1616.reshape(-1)
    llist = data.tolist()
    row_f = np.array(llist)

そして次に、先ほどの通り、学習結果を呼び出してその結果を用いて予測します。

    with open('model.pickle', mode='rb') as f:
        lr = pickle.load(f)
    ans = lr.predict(row_f.reshape(1, -1))

ansには予測結果が格納されています。
ansは、
 ・1:着座状態
 ・0:着座以外の状態
で返ってきます。
こちらの結果からWeb上に表示する文字列に変換しjson形式でresponseします。

webブラウザ上で予測結果表示

では次に表示側の修正です。
前回のブラウザ表示と異なる点はこちらです。

【HTML】

    <body>
        <link rel="stylesheet" href="heatmap.css">
        <div id="result"></div>
        <div style="height:800px; width:800px">
            <canvas id="heatMap" style="height:800px; width:800px"></canvas>
        </div>
        
        <script type="text/javascript" src="heatmap.js"></script>
    </body>

javascript

const interval = function(){
    var result = document.getElementById("result");
    $.post("ipアドレス")
    .always(function(data){
        datalist = data.data.message
        result.innerHTML = data.data.ret;
    })
    
    heatMap.data = {
        datasets: generateDatasets(),
        labels: generateLabels()
    }
    Chart.defaults.global.animation.duration = 0
    heatMap.update();
}
setInterval(interval, 1000);

css

#result{
    font-size: 52px;
    color:red;   
}

追加した部分が少し多いように見えますが、、、
実施していることはとても単純で、javascriptに記載した

    var result = document.getElementById("result");
    $.post("ipアドレス")
    .always(function(data){
        datalist = data.data.message
        result.innerHTML = data.data.ret;
    })

にてresponseされた結果をresultのinnerHTMLに表示。
このresultは、HTMLの

<div id="result"></div>

の部分に表示されます。
cssはこちらのresultに表示される文字サイズや文字色を変更しているのみです。

■結果表示

では実際に学習結果を用いてリアルタイムで予測・結果表示を行ってみます。
結果はこちら!!


着座⇒席を立ってみる⇒着座
の順で動いてみたのですが、リアルタイムに状態の表示が行えておりました!!
上出来、上出来!!

■最後に

機械学習を用いてみましたが精度高く実現できたのでうれしい!!
だけど、、、どういった判定を行って結果を出しているのでしょう🤔
機械学習ですと過程が分かりにくいのが難点ですね。。。



機械学習による着座中か否か判定

先日お伝えしました通り、
着席か席を外しているかを判定
機械学習で行ってみましたのでまとめてみました!!
f:id:Elsammit:20210227105017p:plain



■判定手段

今回は
 ①着座中
 ②席を立っている or 席を外している
のどちらか一方を判定します。

今回も赤外線アレイセンサを用いて着座か否かの状態を取得いたします。
赤外線アレイセンサにより、
着座中の場合、席を立っている場合、席を外している場合、
それぞれの温度マップはこちらの通り。
【着座中】
f:id:Elsammit:20210227095932p:plain

【席を立っている】
f:id:Elsammit:20210227095832p:plain

【席を外している】
f:id:Elsammit:20210227100041p:plain

ここまで温度マップが異なるので、
①、②を自分でアルゴリズム作成して判定してもいいのですが、、、
機械学習でやってみたいと思います!!

機械学習に向けたデータ収集

まずはデータ収集!!
それぞれのパターン毎に300程度の温度マップを作成していきます。
要するに、、、
 ・着座中:300データ
 ・席を立っている:300データ
 ・席を外している:300データ
の計900データを収集しておきます。
そして、それぞれcsvファイルとして保存。

機械学習モデル作成

では実際に機械学習用のモデルを作成していきます!!
入力となる温度マップは16x16(256)の1次元データになります。
先ほど収集しておいたデータを用いて機械学習していきます。
コードはこちら。

import csv
import codecs
import pandas as pd
import numpy as np
from pandas import plotting  
import codecs

import pickle
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

none = []
standup = []
sitdown = []
status = []

#席を外している場合.
csv_file = open('none.csv')
f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
for row in f:
    row_f = [float(row_s) for row_s in row]
    row_f.append(0)
    none.append(row_f)
    status.append(row_f)

#着座中
csv_file = open('sitdown.csv')
f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
for row in f:
    row_f = [float(row_s) for row_s in row]
    row_f.append(1)
    sitdown.append(row_f)
    status.append(row_f)

#席を立つ
csv_file = open('standup.csv')
f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
for row in f:
    row_f = [float(row_s) for row_s in row]
    row_f.append(0)
    standup.append(row_f)
    status.append(row_f)

status = np.array(status)

X = status[:,0:256]
y = np.ravel(status[:,256:257])

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

lr = LogisticRegression(C=1.0,max_iter=300)
lr.fit(X_train, y_train)

print("-------------------------------------------------")
print("train score: %.3f" % lr.score(X_train, y_train))
print("test score: %.3f" % lr.score(X_test, y_test))
print("-------------------------------------------------")

実施していることは各パターン毎にcsvファイルよりデータを収集。

csv_file = open('sitdown.csv')
f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)

そして、

for row in f:
    row_f = [float(row_s) for row_s in row]
    row_f.append(1)
    sitdown.append(row_f)
    status.append(row_f)

にて各データ群(16x16の温度マップデータ)に対して、csvファイルで取得したデータをfloat型に変換し、
257個目のデータとして、
 ①着座中:1
 ②席を立っている or 席を外している:0
を追加します。

これらの動作を3パターン全て実施した上で、
機械学習実行!!
使用した機械学習モデルはロジスティック回帰です。

X = status[:,0:256]
y = np.ravel(status[:,256:257])

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

lr = LogisticRegression(C=1.0,max_iter=300)
lr.fit(X_train, y_train)

■学習結果確認

こちらの学習結果をtestデータを用いてtestを実施しました。
結果はこちらの通り!!

-------------------------------------------------
train score: 1.000
test score: 0.992
-------------------------------------------------

かなり高い!!
これなら問題なく判定に用いることが出来そうです!!

■学習結果を保存

こちらの学習結果を保存してみます。
保存にはpickleをもちいました。
pickleは下記でインストールできます。

pip install pickle-mixin

学習結果を保存するためにこちらのコードを
lr.fit(X_train, y_train)
以降に追加します。

with open('model.pickle', mode='wb') as f:
    pickle.dump(lr,f,protocol=2)

実行すると、
model.pickle
というファイルが生成されます。

■最後に

ちょっと長くなってしまったので今回はここまで!!
明日には今回学習・保存した学習結果を用いて着座判定結果をWeb上に表示するサービスを表示させた結果をまとめたいと思います!!



python Flaskを使ってみました

先日赤外線アレイセンサの結果をWeb上に表示させてみました。
elsammit-beginnerblg.hatenablog.com

そこで、サーバーサイドはgo Ginを用いました。
が、どうせなら着席か席を外しているかを判定してみたいな🤔と考えました。
着席か、席を外しているかは赤外線アレイセンサのマップを見ると明らかに異なっているため、
自分で判定アルゴリズムを作成してもよいのですが、、、

やるなら機械学習でも使ってみよう!!

と思い立ちました。

機械学習と言えばpython!!
というこで、サーバーサイドをpythonに置き換えることにしました!!

■Flaskとは?
カスタマイズ性が高く、シンプルなWebアプリケーションに適しているものがFlaskというフレームワーク
です。
※参考:https://udemy.benesse.co.jp/development/system/flask.html
f:id:Elsammit:20210225231910p:plain
https://flask.palletsprojects.com/en/1.1.x/https://admin-it.xyz/python/flask/flask-blueprint-directory-mistakes/

■なぜFlask??
pythonでサーバーサイドを構築する上で、
 ・Django
 ・Flask
 ・FastAPI
といったフレームワークが思い浮かびます。
この中で、Django
フルスタックフレーム
と呼ばれる、多くの機能がデフォルトで用意されているフレームワークになります。
多くの機能が用意されているため、色々な機能・サービスを容易に構築することが出来る一方、
・簡単なWebサーバーの場合でも多くの機能が動いてしまうため無駄である
・学習コスト増
である欠点があります。

FastAPI、Flaskはどちらもマイクロフレームワークと呼ばれる、簡易機能のみが搭載されたフレームワークになります。
Djangoとは逆で学習コストは少ない一方、多くの機能を用意する場合には自分で手を動かして実装することになります。

今回は単純なWebサービスの実現になるので、マイクロフレームワークを選択しました。
FlaskかFastAPIかの検討ですが、、、
とりあえず有名なFlaskにしよう!!と思い、Flaskを選択しましたww。
まったくもって安易w

■Flask導入手順
Flaskですがpythonがインストール済みであれば下記コマンドを実行すれば完了です!!

pip install flask

■Flaskを使ってみる
では、動作確認も兼ねて簡単なWebサーバーを立ち上げてみます。
コードはこちら。

from flask import *

app = Flask(__name__)

@app.route("/")
def main():
    return "Hello World"    

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

こちらを実行して、
http://IPアドレス:8888
でアクセスしてみると、

Hello world

というメッセージがWebブラウザ上に表示されるかと思います。

■Flaskでpost request受信
では次にpost request受信のためのコードを紹介いたします。
コードはこちら。

from flask import *

app = Flask(__name__)

@app.route('/post', methods=['POST'])
def post_json():
    json = request.get_json()  # requestで渡されたjsonデータを取得.
    NAME = json['name']  #jsonデータ内name keyを取得.
   #response用json作成. 
   result = {
        "data": {
        "id": 1,
        "name": NAME,
        }
    }
    
    # jsonデータをresponse.
    return jsonify(result)

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

ではこちらのコードを実行し、コマンドプロンプトにて

curl -X POST -H "Content-Type: application/json" -d '{"name":"hogehoge"}' http://IPアドレス:8888/post

を実行。
すると、

{
  "data": {
    "id": 1,
    "name": "hogehoge"
  }
}

と言った結果が得られるかと思います。

■赤外線アレイセンサをpost responseしてみる
では最後にgo Ginで行ったのと同様にpost requestに伴い、赤外線アレイセンサ値をresponseしてみたいと思います!!
コードはこちら。

from flask import *
import time
import busio
import board
import adafruit_amg88xx
import numpy as np

app = Flask(__name__)

@app.route('/post', methods=['POST'])
def post_json():
    i2c_bus = busio.I2C(board.SCL, board.SDA)
    sensor = adafruit_amg88xx.AMG88XX(i2c_bus, addr=0x68)
    time.sleep(.1)
    array = np.array(sensor.pixels)
    data = array.reshape(-1)
    llist = data.tolist()

    result = {
        "message":llist
    }

    return jsonify(result)

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

実施していることは、

    i2c_bus = busio.I2C(board.SCL, board.SDA)
    sensor = adafruit_amg88xx.AMG88XX(i2c_bus, addr=0x68)
    time.sleep(.1)

にて赤外線アレイセンサamg8833からセンサ値を取得し、

    array = np.array(sensor.pixels)
    data = array.reshape(-1)
    llist = data.tolist()

    result = {
        "message":llist
    }

    return jsonify(result)

にてjsonデータをresponseしています。

こちらを実行すると前回作成したWebサービスから
f:id:Elsammit:20210221181836g:plain
といった結果が得られます。

せっかくなので、8x8⇒16x16に拡張してみます。
コードですが、

    array = np.array(sensor.pixels)
    data = array.reshape(-1)

    array = np.array(sensor.pixels)
    array1616 = array.repeat(2, axis=0).repeat(2, axis=1) 
    data = array1616.reshape(-1)
    llist = data.tolist()

に置き換えればOKです!!
結果はこんな感じになります。
f:id:Elsammit:20210225230725g:plain

16x16の温度マップが表示されていますね!!

■最後に
今回はpythonのwebフレームワークである、Flaskについての紹介でした。
pythonで実装できたので、今度は機械学習を行っていきたいと思います!!
完成したらブログとしてまとめていきたいと思います。

■参考
https://nearprog.com/webframework-python/
https://udemy.benesse.co.jp/development/system/flask.html
https://qiita.com/nagataaaas/items/3116352da186df102d96

Linuxで挿抜したUSBメモリやSDカードをwindowsに挿入するとスキャンして修復しますか?と聞かれる原因とその対策

よくLinuxで挿抜したUSBメモリwindowsに挿入するとこちらのように
「スキャンして修復しますか?」
というメッセージが表示されます。
f:id:Elsammit:20210223101924p:plain

今回このメッセージ表示する原因と対策方法が分かりましたのでまとめておこうと思います!!



■メッセージ発生原因

どうやら、USBメモリやSDカード内にVolumeDirty フラグ(ダーティ・ビット)と呼ばれるビットが存在し、このビットが1(ON)を検知してwindowsにて
「スキャンして修復しますか?」
というメッセージを表示させていたようです。

【VolumeDirty フラグ(ダーティ・ビット)とは?】
各ボリュームごとに備えられている特別なステータス・ビットであり、OSの起動時(正確には、ボリュームのマウント時)にセットされ、OSの終了時にクリアされる。もしシステムが突然の障害や電源断、リセットなどにより、稼働途中で強制的に終了、再起動したりすると、ダーティ・ビットはセットされたままになる。
この状態でシステムを起動すると、ダーティ・ビットがセットされたままなので、システムに異常が発生したことが分かる(正常終了していれば必ずクリアされているはずだから)。
するとOSはchkdskを実行し、ファイル・システムの整合性、一貫性を検査する。
※参考:https://ameblo.jp/55saori/entry-12113543690.html

要するに書き込み・読み出し中に何等かの原因により、ダーティ・ビットがONになったままになってしまい、windows側に挿入するとこのようなメッセージが表示されるようです。

■なぜダーティ・ビットがONになったままなのか?

ですが、、、
なぜLinuxで挿入するとダーティ・ビットがONになったままになってしまうのでしょうか??

どうやらダーティ・ビットをON/OFFのタイミングがLinuxWindowsで異なるようです。
 ・Windowsの場合:ファイル書き込み・読み出し時にダーティ・ビットをONにし、ファイルを閉じたり書き込みが完了次第ダーティ・ビットをOFFにする
 ・Linuxの場合:USBメモリやSDカードマウント時にダーティ・ビットをONし、アンマウント時にダーティ・ビットをOFFにする

要するに、Linux側でちゃんとアンマウント処理を実施しなかった場合に次回windowsに挿入するとダーティ・ビットが立ってしまっているためにスキャンが必要になってしまいます。

Windowsの場合には確かにファイルが壊れている可能性があるので、ダーティ・ビットがONであればチェックするのは当然ですね😅
windowsLinuxでON/OFFのタイミングが全く異なりますね。。

ではなぜLinuxWindowsと同様のON/OFFとしないのでしょうか?🤔
あくまで想定の域ですが、Windowsのように読み書き時にダーティ・ビットをON/OFFしようとするとLinuxカーネルがファイルの読み書きに対して追従しなければならず複雑な処理が必要になってしまいます。
一方、Linuxの場合にはマウント/アンマウントしなければUSBメモリ・SDカードへの書き込み・読み出しが行えません。
ですので、
「マウント/アンマウントで切り替えてしまえばよくない? こっちの方が制御簡単だよね?」
となったのではないかな?と思います。
※参考:https://unix.stackexchange.com/questions/230181/why-does-linux-mark-fat-as-dirty-simply-due-to-mounting-it

■解決策

上でも記載した通り、
USBメモリやSDカードをしっかりとアンマウントしましょうね
というのが解決策になります!!

だけど、
ちゃんとやっているんだけどメッセージが出てしまうよ
という方。
自分もそうでした。。。なぜかアンマウントしているはずなのですがメッセージが出てしまうのですよね😥

自分の場合には自動マウントが原因でした。
どうやら自動マウントを行う上でsystemdがUSBメモリやSDカードのデバイスをマウントした状態にしてしまっているようでした。
このため、別途下記を実行することで解決させることが出来ました。

sudo umount /dev/sd*

■最後に

別にファイルが壊れているわけではなかったし、そこまで作業に問題があったわけではなかったので問題なかったのですが、
以前から疑問だった内容が分かりかなりすっきりしました。

やはりLinuxwindowsで異なる点は多いですね。
Linuxwindowsを両方使用しているので、しっかりと差分を理解していきたいと思います!!



赤外線アレイセンサの結果をWeb上で表示

先日、javascriptでヒートマップを表示する方法をまとめました。
elsammit-beginnerblg.hatenablog.com

今回は以前から用いている赤外線アレイセンサ値をヒートマップで表示してみたいと思います!!



■構成

今回もこちらの赤外線アレイセンサをラズパイに接続して動作させていきます。
用いている赤外線アレイセンサはいつも通りこちら。

AMG8833 IR 8 * 8サーマルイメージャーアレイ温度センサーモジュール8x8赤外線カメラセンサー

構成はこちら。
f:id:Elsammit:20201031171813p:plain

また、ラズパイ側はgo言語を用いて赤外線アレイセンサの受信とpost responseを行っていきます。

■サーバサイド実装

ではラズパイ側での実装を進めていきます。
先ほど記載した通り、go言語で実装いたします。
PCからのpost requestを受け取り、赤外線アレイセンサのセンサ値をjsonでresponseさせるため、
go言語ライブラリのGinを用います。

コードはこちら。

package main

import (
	"time"
	"github.com/gin-gonic/gin"
	"github.com/jweissig/amg8833"
)

func Do_Post() {
	r := gin.Default()
	r.POST("/post/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": gridpost,
		})
	})
	r.Run()
}

var gridpost []float64

func main() {
	amg, err := amg8833.NewAMG8833(&amg8833.Opts{
		Device: "/dev/i2c-1",
		Mode:   amg8833.AMG88xxNormalMode,
		Reset:  amg8833.AMG88xxInitialReset,
		FPS:    amg8833.AMG88xxFPS10,
	})
	if err != nil {
		panic(err)
	}

	ticker := time.NewTicker(1 * time.Second)
	go Do_Post()
	for {
		gridpost = amg.ReadPixels()
		<-ticker.C
	}
}

実施していることですが、
Ginでpost受信するためのサーバをスレッドで立ち上げ、
そのままメイン関数で赤外線アレイセンサ値を1秒ごとに取得しています。
サーバ立ち上げ時の関数はDo_Post()です。
赤外線アレイセンサ値は

{
      "message": gridpost,
}

というようにjsonデータ化してresponseとして返しております。

■フロントエンド実装

次にWeb上でヒートマップを表示するための実装をしていきます!!
post送信にはjQueryを用います。
ではコードです。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Test</title>
        <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.css'>
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js"></script>
        <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    </head>
    <body>
        <div style="height:800px; width:800px">
            <canvas id="heatMap" style="height:800px; width:800px"></canvas>
        </div>
        <script type="text/javascript">
            'use strict'

            const mapHeight = 8;
            const mapWidth = 8;

            var datalist = [];


            // データセットの生成
            const generateDatasets = function(){
                const datasets = []
                for(let i=0; i<mapHeight; i++){
                    datasets.push({
                        data: new Array(mapWidth).fill(1),
                        borderWidth: 0.2,
                        borderColor: "#FFFFFF",
                        backgroundColor: generateColor(i)   // 変更
                    })
                }
                return datasets    
            }

            // 定期的に赤外線アレイセンサ値を取得.
            const interval = function(){
                $.post("パス")
                .always(function(data){
                    datalist = data.message
                })
                heatMap.data = {
                    datasets: generateDatasets(),
                    labels: generateLabels()
                }
                Chart.defaults.global.animation.duration = 0
                heatMap.update();
            }
            setInterval(interval, 1000);

            const generateColor = function(y){
                const datasetColors = []
                for(let x=0; x<mapWidth;x++){
                    const opa = ((datalist[x + (mapHeight-y-1) * mapWidth] - 16)*0.08).toFixed(2);
                    datasetColors.push("rgb(235,10,10,"+opa+")")
                }
                return datasetColors;
            }

            // データラベルの生成
            const generateLabels = function(){
                let labels = []
                for (var i=1; i<mapWidth+1; i++){
                    labels.push(i)
                }
                return labels
            }

            const ctx = document.getElementById('heatMap').getContext('2d')
            const heatMap = new Chart(ctx, {
                type: 'bar',
                data: {
                    datasets: generateDatasets(),
                    labels: generateLabels()
                },
                options: {
                    title: {
                    display: true,
                    text: 'Heat Map Sample',
                    fontSize: 18,
                },
                animation: false,
                animation: {
                    duration: 0
                },
                legend: {
                    display: false
                },
                scales: {
                    xAxes: [{
                        gridLines: {
                            color: '#FFFFFF',
                        },
                        barPercentage: 0.99,
                        categoryPercentage: 0.99,
                        stacked: true,
                        ticks: {
                            min: 0,
                            display: false,
                        }
                    }],
                    yAxes: [{
                        gridLines: {
                            color: '#FFFFFF',
                            zeroLineWidth: 0
                        },
                        stacked: true,
                        ticks: {
                            min: 0,
                            stepSize: 1,
                            display: false
                        }
                    }]
                },
            }
        });
        </script>
    </body>
</html>

ちょっと長いですが、、、
先日のヒートマップをランダムに表示するコードと異なる点は2つ。
1つ目は、
赤外線センサ値取得のための定期的なjQueryでpost送信部。
post responseで得られたjsonのmessageに赤外線アレイセンサ値が格納されているので、
datalist変数に値を格納し、heatmapとして表示するためにgenerateDatasets()実行+heatMap.update()を行っています。

const interval = function(){
    $.post("パス")
    .always(function(data){
        datalist = data.message
    })
    heatMap.data = {
        datasets: generateDatasets(),
        labels: generateLabels()
    }
    heatMap.update();
}
setInterval(interval, 1000);

2つ目はヒートマップ表示のためのRGBAの値の調整。

const generateColor = function(y){
    const datasetColors = []
    for(let x=0; x<mapWidth;x++){
        const opa = ((datalist[x + (mapHeight-y-1) * mapWidth] - 16)*0.08).toFixed(2);
        datasetColors.push("rgb(235,10,10,"+opa+")")
    }
    return datasetColors;
}

■実行してみる part1

では先ほどのコードを実行してみます!!
真ん中あたりに人を配置して実行させてみました!!
実行結果はこちら。
f:id:Elsammit:20210221180658g:plain

あまり濃淡がはっきりせず人がどこにいるのか分かりにくいですね😅
よく見ると濃淡があって分かりますが。。。
こちらの計算により濃淡を変化させているのですが、、、
こちらのような、センサ値に対して濃淡を連続値にしてしまうときれいな濃淡を出すのが難しそう。。。

const opa = ((datalist[x + (mapHeight-y-1) * mapWidth] - 16)*0.08).toFixed(2);

■濃淡表現式の変更

ということで、先ほどの式を変更し、非連続で濃淡(というかRGB値)を変更してみました!!
コードはこちら。

const generateColor = function(y){
    const datasetColors = []
    for(let x=0; x<mapWidth;x++){
        if(datalist[x + (mapHeight-y-1)*mapWidth] >= 30){
            datasetColors.push("rgb(235,10,10,"+1.0+")")
        }else if(datalist[x + (mapHeight-y-1)*mapWidth] >= 23){
            datasetColors.push("rgb(235,10,10,"+0.5+")")
        }else{
            datasetColors.push("rgb(5,10,235,"+0.8+")")
        }
    }
    return datasetColors;
}

レイセンサの各画素毎に、
 ・28以上:RGB値(235,10,10,1.0)
 ・28より小さく18以上:RGB値(235,10,10,10,0.5)
 ・18より小さい:RGB値(5,10,235,0.8)
としました。

■実行してみる part2

では変更したコードで実行してみます。
先ほどと同様に真ん中あたりに人を配置して実行。
結果はこちら。
f:id:Elsammit:20210221181836g:plain

うん!!人がいる部分といない部分ではっきりしていますね。
見やすくなりました!!

■最後に

まだとりあえず表示出来ただけですが、、、
赤外線アレイセンサ値を取得し、ヒートマップとしてWeb上に表示させることが出来ました。

ただしこちらのコードはまだまだで、、
 ・最初の1秒はヒートマップに赤外線アレイセンサ値が表示されない(真っ黒なヒートマップが表示される)
 ・1秒前の赤外線アレイセンサ値がWeb上のヒートマップに表示される
等々、まだまだな部分があるので、修正していきたいと思います!!
とりあえず今回は表示出来たよ。という報告で