19
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FlaskとAjaxで複数データをリアルタイムに更新する ~湯婆婆もあるよ~

Posted at

はじめに

今回の記事は私がこれまで知らなかった技術に出会って感動しつつ実装したものです。
ググればそれなりにヒットするので新規性を主張するつもりはありませんが、ニーズは確実にある一方で今回のやり方が定番であるとはとても思えません。
「そんな面倒くさいことしなくても○○を使えば一発じゃん」といった知見をお持ちの方はご教示願います。

Ajaxで非同期通信

Flaskの限界(下から目線で)

Flaskで動的にウェブページを作るにはrender_template()を使うのが普通だ(というか、それしか知らない)。だが、画面遷移するときはいいとしても、同一画面内である部分のみ変更させたいときにページをゼロから作り直すのはイマイチだと感じていた。
JavaScriptのようにhtml要素の一部のみ変更することはできないだろうか。そう考えて見つけた技術がAjaxだった。
これだよ、私がやりたかったことは。

湯婆婆ふたたび

かつて私は書いた。解読できる程度に簡単で、だがしっかりとしたサンプルコードが必要だと。

だからこそ私は示そう。Ajaxがデータを送り、Pythonが受け取って処理し、その結果をAjaxに送り返してhtmlの一部の要素のみ更新するサンプルを。令和のHello world(by everylittleさん)、湯婆婆を。

コード

yubaba.py
from flask import Flask, render_template, request
import random
import json

app = Flask(__name__)

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

@app.route("/call_from_ajax", methods = ["POST"])
def callfromajax():
    if request.method == "POST":
        # ここにPythonの処理を書く
        try:
            name = request.form["data"]
            new_name = random.choice(name)
            message = f"フン。<b>{name}</b>というのかい。贅沢な名だねぇ。<br>"
            message += f"今からお前の名前は<b>{new_name}</b>だ。<br>"  
            message += f"いいかい、<b>{new_name}</b>だよ。"
            message += f"分かったら返事をするんだ、<b>{new_name}</b>!!"
        except Exception as e:
            message = str(e)
        dict = {"answer": message}      # 辞書
    return json.dumps(dict)             # 辞書をJSONにして返す

if __name__ == "__main__":
    app.run(debug=True)
templates/yubaba.html
<html>
<head>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
契約書だよ。そこに名前を書きな。<br>
<input type="text" id="inputbox">
<input type="button" id="submit" value="送信" onclick="send_to_python()"><br>
<br>
<div id="result" style="border: 1px solid black; width: 500px; height: 100px;"></div>
<input type="button" id="reset" value="リセット" onclick="reset()"><br>

<script type="text/javascript">
function send_to_python() {
    var send_data = $("#inputbox").val();
    $.ajax("/call_from_ajax", {
        type: "post",
        data: {"data": send_data},              // 連想配列をPOSTする
    }).done(function(received_data) {           // 戻ってきたのはJSON(文字列)
        var dict = JSON.parse(received_data);   // JSONを連想配列にする
        // 以下、Javascriptで料理する
        var answer = dict["answer"];
        $("#result").html(answer);              // html要素を書き換える
    }).fail(function() {
        console.log("失敗");
    });
};

function reset(){
    //これは普通のJavaScript(jQuery)
    $("#inputbox").val("");
    $("#result").text("");
};
</script>
</body>
</html>

結果

以下のようにhtmlの中の特定要素のみ変更される。
例外も拾っているのでエラーメッセージを読むこともできる。

名前を入力 ヌルストリング
yubaba.png yubaba2.png

Ajaxをストリーミングに使う

既存技術の組み合わせとはいえここから先はオリジナルなので、実用性を認めていただけたら嬉しいです。

参考:従来のストリーミング

カメラ映像をストリーミングするにはGStreamerを使うことが多いが、OpenCVでもできないことはない。
無限ループの中でジェネレータを使い画像を送りつづけるというやり方だ。先ほどrender_template関数しか知らないと書いたがさっそくResponseクラスが登場して頭を抱えている。

以下のコードはネットで拾ったものを自分で読み解いていろいろ小変更したもの。httpレスポンスの部分以外は理解できている、と思う。

stream.py
from flask import Flask, render_template, Response
import cv2

app = Flask(__name__)

class Camera():
    def __init__(self):
        self.video = cv2.VideoCapture(0)

    def read(self):
        while True:
            _, frame = self.video.read()
            # 必要ならここで画像処理する
            _, encoded = cv2.imencode(".jpg", frame)
            bin = b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + encoded.tobytes() + b"\r\n"
            yield bin

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

@app.route("/video")
def video():
    return Response(camera.read(), mimetype="multipart/x-mixed-replace; boundary=frame")

if __name__ == "__main__":
    camera = Camera()
    app.run(debug=True)
templates/stream.html
<html>
<body>
<img src="{{ url_for('video') }}">
</body>
</html>

あれとこれとそれをリアルタイムで送りたい

せっかくだからカメラ映像だけでなくさまざまなデータを送りたい。
ニーズは少なからずあるように思う。画像とセンサーデータがリアルタイムで更新されたら嬉しいでしょ。

試行錯誤

リストを使ってFlaskの戻り値を複数にしても、jinjaテンプレートに対応させることができなかった。render_template()ではなくResponse()だからプログラム的な記述はできなくても仕方がない。

一方、「無限ループの中のyield」というテクニックをAjaxに使えないかと試してみたところ、たとえ中身がJSONであってもジェネレータは駄目というエラーになった。

そこで、Python内で無限ループするのではなく、JavaScript内で無限ループすることにした。
すると。みごと成功した!

成果物

Ajaxから呼ばれるたびにPythonから画像2個と時刻を送っている。
Chromeだと、ずっと動かしているとnet::ERR_INSUFFICIENT_RESOURCESエラーが発生してフレームレートが低下してしまうことがある。
以下はFirefoxでの実行例。今更アニメGIFを作り直すのも面倒なので言葉で書いておくと、diffの値はミリ秒だ。

yobikomi.gif

技術トピック

Ajax

湯婆婆のサンプルをじっくり読んでください。

画像をbase64で表示する

ファイル名でなく以下で変換した文字列をimgタグのsrcに指定することで画像を表示できる。
これによりファイルとして保存する必要はなくなる。

Python
def img2base64(image):
    _, imgEnc = cv2.imencode(".jpg", image)                     # メモリ上にエンコード
    imgB64 = base64.b64encode(imgEnc)                           # base64にエンコード
    strB64 = "data:image/jpg;base64," + str(imgB64, "utf-8")    # 文字列化 
    return strB64

無限ループ内で一瞬スリープする

Excelマクロにはループ処理に時間がかかるとその間動かせなくなってしまうのでDoEventsを挟むというテクニックがある。
また、OpenCVでループ内で画像を表示するにはcv2.imshow()のあとcv2.waitKey(1)する必要がある(例として適切でないけど)。
それらと同様、Ajaxをぐるぐる回すだけではhtml要素を書き換えても描画されない。実際に描画しなおすためにsleep関数が必要になる。この解釈が正しいかどうかはわからないけど。

以下のコードでは念のため処理中かどうかのフラグも使っている。この記事を読むと無くても良いのかもしれない。

JavaScript
async function loop(){
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
    var isBusy = false;
    while (true) {
        if (! isBusy) {
            isBusy = true;
            send_to_python();   // この先でAjaxとhtml要素書き換えをおこなう(詳細略)
            isBusy = false;
            await sleep(1);     // 単位はミリ秒
        }
    };
};

全体コード

こちらに示す。
湯婆婆などのサンプルも含まれている。最終的な成果物のソースコードはstream_with_ajax.pyだ。

終わりに

これと前回の記事を組み合わせれば表現力に富んだ実用的なWebアプリを作ることができだろう。
気がかりなのは、よりエレガントな方法があるのではないかということ。
大いに経験を積んだので決して無駄ではないが。

19
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?