Streamlit で機械学習, 画像処理系の UI を作るメモ

https://docs.streamlit.io/library/api-reference

Imgui みたいに Declarative UI でいい感じです. 

## 開発前の一般的な注意点

* 開発時 `.py` リロードは `from` あたりで取り込んだソースコードには反映されないので注意です.
  * 効率化やパッケージ化(モジュール化)などのために `.py` を分けている場合ですね 
  * https://github.com/streamlit/streamlit/issues/1131
  * https://github.com/streamlit/streamlit/issues/358  
  * 正確には `streamit run` したディレクトリ以外にある `.py` はウォッチしない模様(どれが自作のか既存パッケージなのかわからないからですかね). `PYTHONPATH` にウォッチしたいパスを設定でリロードできるかもしれません https://docs.streamlit.io/knowledge-base/using-streamlit/streamlit-watch-changes-other-modules-importing-app 
* UI の値を保存したり編集したい場合は `st.session_state` に保存します. 
* ボタン押したらリロードしたい場合などは, `experimental_rerun()` を明示的に呼ぶ必要があります.

## ページロードするとへんなところにスクロールしてしまう.

https://discuss.streamlit.io/t/page-scrolling-on-opening-and-reload/29938

header(`#`, `##`) が重複しているからになります.
`st.markdown("#")` で改行していたりとか...
(筆者は streamlit 利用のライブラリでそのようなコードがあって原因突き止めるの面倒でした)

重複チェックは面倒そうですが, 頑張って unique な header 割当るようにしましょう.

tab 化していて, レイアウト調整したい場合は `&nbsp;` や css で頑張る感じですかね.

## ページロードで top にこさせたい

無理やり top にこさせる手もあるようです.

https://discuss.streamlit.io/t/no-way-to-set-focus-at-top-of-screen-on-page-reload-really/15474/14

## checkbox

https://docs.streamlit.io/library/api-reference/widgets/st.checkbox

value には初期値を指定するので, 


```py
st.session_state.check0 = st.checkbox("check0", value=st.session_state.check0)
```

みたいなことをすると挙動が怪しくなるので注意です.
(チェックしても元に戻ったり...)
value は実行中固定であるのが想定されているが, 変化したらそれで再初期化するからのようです.

後述しますが, `on_change` でコールバック仕掛けるなどを考えた場合, 
key 指定して, implicit に session_state に保存しそれを取得がいいかもしれません.

```py
st.checkbox("check0", value=True, key="check0_key")


st.write(st.session_state.check0_key)
```

これは number_input などにも当てはまります.
on_change と連携したい場合は, `st.session_state` には初期値を入れらせないので, 初期値は別途管理しないといけずちょいめんどうですね...
 
## ファイルブラウザ

サーバー側のファイルをブラウズして, 処理するファイルを指定とかしたい...
標準ではありません.

https://github.com/streamlit/streamlit/issues/120#issuecomment-1248532008

こんな感じで自前で頑張ります...

Linux(or macOS) でローカル動作前提であれば, tk を使う手もあるかもしれません.
(リモートで動かすとリモート側で tk のウィンドウが出てしまう)


### サーバ側にフォルダを作る.

標準ではありません.
自前でがんばります...

## ファイルのアップロード

https://docs.streamlit.io/library/api-reference/widgets/st.file_uploader

ただ, フォルダを指定してアップロードはできないようです.

## widget の値を操作したい

たとえばスライダー A の値を元に別のスライダー B の値を変更したいなど.

`on_change` コールバックと session_state でがんばります.

最近(2022/10)だと, widget を `key` 付きでつくると, `session_state` に自動でステートが生成されるようですので, 明示的に作成

```
if 'age' not in st.session_state:
    st.session_state['age'] = 10
```

なのは不要です(昔の streamlit の discussion だと明示的に作るのをやってたりする).

https://docs.streamlit.io/knowledge-base/using-streamlit/widget-updating-session-state

```
 def plus_one():
      if st.session_state["slider"] < 10:
         st.session_state.slider += 1
      else:
          pass
      return
  
  # when creating the button, assign the name of your callback
  # function to the on_click parameter
  add_one = st.button("Add one to the slider", on_click=plus_one, key="add_one")
  
  slide_val = st.slider("Pick a number", 0, 10, key="slider")

```

ただ  UI 全体にリフレッシュをかけるのかちょっと反応わるいです.
(コールバックで `experimental_rerun()` すると多少反応よくなるかも)

`session_state["key"]` も `session_state.key` もどちらも同じです.

### on_click, on_change コールバック

ボタンが押されてたときなどにコールバックをしかけることができます.
ipywidgets などと同じで引数は設定できません.

UI 要素固有のメソッド名にするか, lambda で頑張るか, `functool.partial` で対応になります.

https://github.com/jupyter-widgets/ipywidgets/issues/2103
 
### st.session_state.text_input cannot be modified after the widget with key `***` is instantiated.

text_input や number_input などの一部?の widget は `st.session_state` 直書き換えではタイトルのようなエラーがでてしまいます.
https://zenn.dev/alivelimb/books/python-web-frontend/viewer/about-streamlit#%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%82%92%E4%BD%BF%E3%81%86%E3%82%B1%E3%83%BC%E3%82%B9

ありがとうございます. コールバック経由で変更が必要でした.

また, 先に session_state で値をセットしておくと反映されないので注意です!

```
# NG
st.session_state["myval"] = 0.0

st.number_input("val", key="myval")

def update():
  # エラーは出ないが反映されない
  st.session_state.myval = 1.0

st.button("click", on_click=update)
```

## Image crop, 領域選択

標準ではありません.

https://github.com/mitsuse/streamlit-image-crop

あたりでがんばります.

## Table

`st.dataframe`, `st.table` がありますが, エディットできません.

https://docs.streamlit.io/library/api-reference/data/st.dataframe

aggrid を streamlit に対応したものでがんばります.

Streamlit st_aggrid でデータテーブル表 UI の調整メモ
https://qiita.com/syoyo/items/1b4027299a97fdf45728

### DataFrame でデータを追加するときの注意

ボタン押したりしたら行追加したい...

https://discuss.streamlit.io/t/adding-a-new-row-to-a-dataframe-with-each-button-click-persistent-dataframe/12799/9

Pandas append or loc を使います.

append の場合, Pandas の新しいオブジェクトを作るようで, 変数を使うとうまく反映されないので注意です!
(Python の場合変数は基本リファレンス(のはず)と思って処理するとテーブル反映されない...)

```py
df = st.session_state.df

df = df.append({"Title": "Bora"}, ignore_index=True)

# テーブルは反映されない...
st.dataframe(st.session_state.df)

# `st.session_state.df` に結果を返せば反映される
st.session_state.df = df
```

また,


```py

st.dataframe(st.session_state.df)

if st.button("Update"):
  st.session_state.df = ...

```

のようにするとボタン押してもすぐには反映されません. ↑にあるように `experimental_rerun()` でアップデートさせるか, 後述のように placeholder で対応します.

さらに, `st.dataframe` と `st.table` は `key` パラメータがありません.
(pandas のオブジェクトのアドレスあたりで判定しているんじゃろか?)

### stdqm

https://github.com/Wirg/stqdm

stqdm で iteration ごとに row を追加して dataframe アップデートしたいときもあります.
ただ, stqdm 内で append して `st.dataftame` すると新しく dataframe widget が生成されて追加となってしまいます(dataframe は `key` が指定できないため, widget を固定化?できない).

```py

for _ in stqdm(range(10)):
  st.session_state.df = st.session_state.df.append(...)
  st.dataframe(st.session_state.df)
  # 複数 dataframe が生成されてしまう. loc で追加の場合も同等
```

experimental_rerun だと tqdm のイテレーションが終了してしまいますので...
empty() で placeholder を作って対応します!

https://discuss.streamlit.io/t/delete-widgets/7596

```py

placeholder = st.empty()

placeholder.dataframe(st.session_state.df)

for _ in stqdm(range(10)):
  st.session_state.df = st.session_state.df.append(...)
  
  # widget 作り直し
  placeholder.empty()
  placeholder.dataframe(st.session_state.df)
```

## plot

https://blog.amedama.jp/entry/streamlit-tutorial#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E5%8F%AF%E8%A6%96%E5%8C%96

ありがとうございます.
組み込みで plot 機能ありました.
matplotlib(pyplot)もあります!

## その他 UI

React を使っているので, React or JavaScript でがんばります...