flask+ajaxでスライダーの値を読み取り動的になんやかんやする

# やりたいこと
- flaskを使ってウェブアプリケーションを作成したい
- スライダー(`<input type="range">`)の値を読み取り、値に応じて要素をなんやかんや動的に変更したい
- その際ページ更新はしたくない(ajax通信で解決したい)

調べれば似たような記事はあるにはあるものの、勉強も兼ねて記事にすることにした。

# やったこと
flask + jQuery + ajaxで解決した。
今回**なんやかんや**に関しては、`cv2.addWeighted()`による画像合成を行うこととした。

今回のソースはすべて下記のリポジトリで公開しています。
https://github.com/ikentoh/flask-ajax

![blend.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529606/89be6209-45bb-29a8-1509-62480665c84d.gif)



## スライダーを定義する
画像合成のアルファ値の単位はパーセンテージなので、0~100の値となるように設定する。

```template/index.html
    <div>
        <img src="{{ src_left }}">
        <img src="{{ src_right }}">
    </div>
    <div>
        <img id="blend-image">
        <input type="range" id="blend-slider" min="0" max="100" value="50">
    </div>
```

## スライダー操作時にajax通信が行われるようにする

```static/js/blend.js
$(function(){
  // ページ読み込み時にも描画されるようにする
  updateBlend();

  // スライダーを操作したときにblend画像を更新する
  $('#blend-slider').on('change', function() {
    updateBlend();
  });
});

function updateBlend() {
  // flaskの/blendエンドポイントにリクエストする
  $.ajax({
    type: 'POST',                     // リクエストメソッドはPOSTでいくぜ、という宣言
    url: '/blend',                    // flaskサーバの/blendというエンドポイントにリクエストする
    data: $('#blend-slider').val(),   // flaskのrequest.get_data()で取得できるデータ 
    contentType: 'application/json',  // よく分からなければおまじないの認識でいい
  }).then(
    data => $('#blend-image').attr('src', data),  // flaskとの通信がうまくいったときの処理
    error => console.log(error)                   // flaskとの通信がエラーになったときの処理
  );
};
```

ajaxなんもわからん、て場合に見るべき場所は以下の3箇所。
- `data: $('#blend-slider').val()` → スライダーの値をリクエストのdataに格納し、flaskに渡す
- `url: '/blend'` → flaskの`@app.route('/blend')`へリクエストを行う
- `data => $('#blend-image').attr('src', data)` → flaskからの返り値で`<img id="blend-image">`のsrc属性を更新する


## エンドポイント `/blend` を定義する

ajax通信とは言っても、結局のところクライアントが工夫をこらしてリクエストを送りつけてきているだけなので、flask側から特に工夫することはない。
今回のケースでは、返り値を直接`<img id="blend-image">`のsrc属性にぶち込みたいので、`data:image/png;base64,{base64 encodeされた画像}`という返り値になるようにしている。

個人的な躓きとして「`@app.route()`でデコレートしたからには `return render_template()`でなければならないのでは？」という勘違いをしていたが、全然そんなことはないのでここに供養しておく。
(javascriptにhtmlを送りつけても困り果てるだけ)
なおブラウザから`localhost:8050/blend`にアクセスしようとすると、リクエストメソッドGETを受け付けない設定になっているため、 `Method Not Allowed`の表示になる。


```src/endpoints/index.py
    @app.route(r'/blend', methods=['POST'])
    def get_blend():
        """
        blend画像を生成し、ajax通信に応じて返却する
        """
        SRC_HEAD = 'data:image/png;base64,'

        # ajax通信のdataに格納された、スライダーの値を取得する
        blend_alpha = int(request.get_data()) / 100

        # b64_left, b64_rightはglobal変数で定義されていることに注意, github上のソース全文を要確認)
        b64_blend = blend.generate(b64_left, b64_right, blend_alpha)

        src_blend = SRC_HEAD + b64_blend

        return src_blend
```

## openCVを使って合成画像を生成する
今回の本筋とはすこし外れるが、以下のように実現しているので一応紹介しておく。
宗教上の理由により画像をbase64で扱っているなので、画像の読み込みに関しては都度うまいこと読み替えること。


```blend.py
import base64

import cv2
import numpy as np


#
# 合成画像の生成
#

def generate(b64_left, b64_right, alpha):
    """
    教師(期待値)と検証値の合成画像を出力する
    """
    image_left = b64_to_ndarray(b64_left)
    image_right = b64_to_ndarray(b64_right)

    image_blend = cv2.addWeighted(
        src1=image_left,
        alpha=(1-alpha),
        src2=image_right,
        beta=alpha,
        gamma=0)

    b64_blend = ndarray_to_b64(image_blend)

    return b64_blend


def b64_to_ndarray(b64_dist):
    """
    base64 -> ndarray
    """
    np_dist = np.frombuffer(base64.b64decode(b64_dist), np.uint8)
    dist = cv2.imdecode(np_dist, cv2.IMREAD_ANYCOLOR)

    return dist


def ndarray_to_b64(ndarray_dist):
    """
    ndarray -> base64
    """
    _, bytes_dist = cv2.imencode('.png', ndarray_dist)
    bytes_b64_dist = base64.b64encode(bytes_dist)
    str_b64_dist = bytes_b64_dist.decode()

    return str_b64_dist
```

## まとめ
つまるところ、

1. スライダーが操作されるたびに
2. ajax通信によって `/blend`へリクエストが行われ
3. `cv2.addWeighted()`が実行されて合成画像が生成され
4. その合成画像がレスポンスとして返却され
5. 要素`#blend-image`のsrc要素が更新され
6. ウェブページ上の画像に更新が反映される
7. その間 `/`へのリクエストは行われず、 `return render_template()`が実行されないので、ページの再読み込みは行われない

という順番の処理を行っている。