手軽に作れるstreamlitとdiffusersによる簡単な画像生成AIアプリ


この記事ではstreamlitとdiffusersモジュールを使ってPythonだけで画像生成AIのウェブアプリを作る例を説明します。

# 前置き

最近streamlitで作ったウェブアプリのプロジェクトを引き継ぐことになったので、streamlitの使い方を勉強する機会がありました。

streamlitを使ったら、JavaScriptもhtmlもcssも必要せずにPythonだけでフロントエンドからバックエンドまで全部書けるから便利で魅力的だと思っています。

私は今までウェブアプリを作る時主にvue.jsを使っていて、Pythonに拘る必要がないと思って今まで興味を持っていなかったが、確かにウェブサイトを作るのにJavaScript+html+cssを勉強しなければならないというところはやはりコストが高いとわかっています。

特にAIプログラマーにとってやはりPythonですね。アプリを作る時もPythonでけで済むのならそれが便利でしょう。

以前私はこの記事を書きました。

https://qiita.com/phyblas/items/d25315a2fdab744117ae

これはちゃんとしたフレームワークを使う本格的なウェブアプリです。やはりもっと手軽に作れる方法があったらいいと思っています。

だから今回はstreamlitを使って同じようにウェブアプリを作ることにしました。

streamlitを使うとこのアプリはたった1つのPythonファイルで完成できます。凄い簡単ですね。


# できたアプリ

まず作ったアプリはどんなものか紹介しておきます。

アプリを起動したらこのようにブラウザの中はこの画面で始まります。

![strdif01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/1b44fdee-0731-7d9e-d9c1-c439812f1403.png)

ではまず言葉だけで生成してみます（つまりtxt2img）。生成したい内容（つまりプロンプト）を入力して、出力サイズも好きなように設定して、「生成開始」ボタンを押したら生成が始まって、プログレスバーも表示されます。

![strdif02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/e3bd30be-5543-4366-1679-2115036be21b.png)

そして生成が終わったら下の方に現れます。

![strdif03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/0698888a-9951-b541-7e99-49abca8471c8.jpeg)

もう一度ボタンを押せばもう一回画像生成が始まって違う画像ができます。

又、生成モードのラジオボタンを変えて既存の画像を元に生成するモード（つまりimg2img）にしたら右の方の表示が変わって、画像ファイル入力が現れます。ここに好きなファイルをドラッグすることもできます。

![strdif04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/1de3753d-425c-e0db-cf6c-be410c2f1ac9.png)

先ほど生成した画像を入力として使ってゴッホ風に変えてみましょう。

![strdif05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/000f4035-ed19-7d3e-46db-f0e870ce372c.png)

出来上がり。

![strdif06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/a9ca8b0e-4990-365a-99d5-b862525a7c26.jpeg)

こんな感じで地味で簡単な生成AIアプリです。でもこれを作ることで色々勉強になると思います。


# 環境設定

今回のアプリで使うモジュールは主にstreamlitとdiffusersです。その他に、プロンプトを日本語から英語に翻訳するためにtranslateモジュールも必要です。どれも簡単にpipでインストールできます。
```
pip install streamlit diffusers transformers translate
```

尚、diffusersの使い方に関してはこの入門記事に書いてあるので参考に。

https://qiita.com/phyblas/items/00f750b8277f66fb9b13

今回使うチェックポイントモデルもその記事でも紹介した「gsdf/Counterfeit-V2.5」です。個別にモデルを準備必要なく、自動でダウンロードされて便利だからです。ただし最初に実行する時モデルがダウンロードされるのでその一回だけ時間がかかります。

streamlitもdiffusersも活発に機能の追加と更新が行われていて、バージョンの違いによって仕様が違う場合もあるので、今このアプリを書いた時点で使ったライブラリのバージョンを記述しておきます。

<table>
<tr><td><strong>python</strong></td><td>3.12.2</td></tr>
<tr><td>streamlit</td><td>1.40.2</td></tr>
<tr><td>diffusers</td><td>0.29.2</td></tr>
<tr><td>pytorch</td><td>2.3.1</td></tr>
<tr><td>transformers</td><td>4.41.2</td></tr>
<tr><td>translate</td><td>3.6.1</td></tr>
<tr><td>PIL</td><td>10.2.0</td></tr>
</table>

パソコンはメモリーMacBook Air M3です。GPUも使えるので早いです。でも実装のコードはGPUがなくてもCPUで動けるように書いてあります。ただしCPUだと遅いですね。


# 実装

環境などの準備が整ったら次は実装のコードです。コード全体は長くないので、全部一つのファイルに纏めます。
```python:strdif.py
import time
from PIL import Image
import torch
from diffusers import StableDiffusionPipeline,StableDiffusionImg2ImgPipeline
from translate import Translator
import streamlit as st


# 生成モードのリスト
lis_mode = ['言葉だけで生成する','元画像から生成する']
# 使える元画像の拡張子のリスト
lis_kakuchoushi = ['jpg','png','tiff','gif','ppm']
# 生成に関わる設定のパラメータ
dic_param = {
    'プロンプト': '',
    '元画像': None,
    '広さ': 512,
    '高さ': 512,
    '変化の強さ': 0.5,
    '生成モード': lis_mode[0],
}

# GPUが使えたらGPUを使うように
dtype = torch.float16
if(torch.cuda.is_available()):
    device = 'cuda'
elif(torch.backends.mps.is_available()):
    device = 'mps'
else:
    device = 'cpu'
    dtype = torch.float32

honyaku = Translator('en','ja').translate # プロンプトを日本語から英語に翻訳するための関数

# 生成を実行する関数
def seisei_suru():
    prompt = honyaku(dic_param['プロンプト'])
    # 言葉だけによる生成（txt2img）の場合
    if(dic_param['生成モード']==lis_mode[0]):
        pipe = StableDiffusionPipeline.from_pretrained('gsdf/Counterfeit-V2.5',torch_dtype=dtype).to(device)
        dekita_gazou = pipe(
            prompt,
            width = dic_param['広さ'],
            height = dic_param['高さ'],
            num_inference_steps = 20,
            callback_on_step_end = callback
        ).images[0]
    # 元画像からの生成（img2img）の場合
    else:
        pipe = StableDiffusionImg2ImgPipeline.from_pretrained('gsdf/Counterfeit-V2.5',torch_dtype=dtype).to(device)
        dekita_gazou = pipe(
            prompt,
            Image.open(dic_param['元画像']),
            strength = dic_param['変化の強さ'],
            num_inference_steps = 30,
            callback_on_step_end = callback
        ).images[0]
    
    # できた画像を返してウェブページに表示させる
    return dekita_gazou

# プログレスバーを表示するために、各ステップが終わった後実行する関数
def callback(pipe,step_index,timestep,kw):
    pbar.progress(step_index/pipe.num_timesteps,text='%d　/　%d　完成'%(step_index,pipe.num_timesteps))
    return kw


# ウェブページの表示
st.header('生成AIで好きな画像を')

dic_param['プロンプト'] = st.text_area('どんな絵が欲しいですの？',placeholder='何か書いてみて')
c1,c2 = st.columns([0.3,0.7])
with c1: ## txt2imgかimg2imgにするという生成モードを決めるラジオボタン
    dic_param['生成モード'] = st.radio('生成モード',lis_mode)
with c2:
    if(dic_param['生成モード']==lis_mode[0]):
        # txt2imgの場合は出力サイズの設定
        cl1,cl2,cl3 = st.columns([0.2,0.4,0.4],vertical_alignment='center')
        with cl1:
            st.text('画像サイズ')
        with cl2:
            dic_param['広さ'] = st.number_input('広さ（px）',64,1024,512,8)
        with cl3:
            dic_param['高さ'] = st.number_input('高さ（px）',64,1024,512,8)
    else:
        # img2imgの場合は変化の強さの設定と元画像入力
        cl1,cl2 = st.columns([0.25,0.75])
        with cl1:
            dic_param['変化の強さ'] = st.slider('変化の強さ',0.,1.,0.5)
        with cl2:
            dic_param['元画像'] = st.file_uploader('元画像のファイル',type=lis_kakuchoushi)

# 押したら生成が始まるボタン
seisei_botan = st.button('生成開始',disabled=(dic_param['生成モード']==lis_mode[1]) & (not dic_param['元画像']))

if(seisei_botan): # ボタンが押されたら実行する
    with st.spinner('生成中なのです……'):
        t0 = time.time() # ここで生成開始
        pbar = st.progress(0,text='準備') # 生成中プログレスバーを表示する
        dekita_gazou = seisei_suru() # 生成する
        # 生成終了
        st.text('生成完了。%.2f秒かかっちゃったね'%(time.time()-t0))
        st.image(dekita_gazou) # 生成が終わった後、できた画像を表示f
        pbar.empty() # 完成後プログレスバーを消す
```

コマンドラインでこのように打って実行します。
```
streamlit run strdif.py
```

そうしたら自動的に既定のブラウザで`http://localhost:8501/`が開かれて、アプリの画面が表示されます。これで完成です。


# 感想

こんな簡潔なコードで簡単にウェブアプリを作れるなんてstreamlitは凄いですね。その他に、今回は使っていないが、pandasのデータフレームやmatplotlibのグラフを簡単にアプリに表示できて便利です。

しかし実はstreamlitには色々欠点もあって、場合によってあまりいいとは言えません。

ここからは私自身が仕事でstreamlitを使った後の感想です。ただ経験則による個人の意見なので、参考程度に。

streamlitの一番大きな欠点はやはり「自由度」です。あまり融通が利かないから。もし見た目はどうでもよくて拘りなくて、ただ決められたテンプレで作りたいだけならstreamlitはいいかもしれませんが、見映えや詳しい設定が欲しいならstreamlitは寧ろ面倒くさいです。

丁度streamlitのデフォルト機能をそのまま使いたいなら、それは簡単に実装できて便利であるが、その一方ただ一部だけ改変したり消したりするのは困難です。

直接htmlとcssを触れずに実装できてしまうから、見た目は殆ど決められたもので、変更できることは少ないです。

自分で書いたhtmlとcssを混ぜることもできますが、書き方が意外と汚く感じて、そうしたら複雑になって「簡潔」というstreamlitの特徴が崩壊してしまいます。そして表示の不具合の原因にもなります。それに手動で沢山htmlを書く必要があるくらいなら、最初からちゃんとしたウェブフレームワークで書いた方が書きやすいでしょう。

更にセキュリティ方面やパフォーマンスの方面も申し分がありますね。

だからもし一般に公開する本格的なウェブアプリを作るなら、やはりFastAPIとvue.jsを使う方法の方を選ぶでしょう。

JavaScript/html/css経験者ならやはり無理にstreamlitを使うことはないと思います。streamlitは主にPythonに拘る人のためだという印象が大きいです。

ただし社内で使うための簡単なツールなどを作りたいならやはりstreamlitはいい選択かもしれません。


# 参考
今回のアプリを作るにあたって色んな記事を参考にして勉強したので、それらの記事のリンクをここに載せておきます。
- [streamlitを使ったお手軽Webアプリ開発](https://qiita.com/Aguuu0903/items/33c9f00dffdb379a2056)
- [Streamlit まとめ](https://qiita.com/not13/items/dcd8c12d64982dc0e819)
- [初心者でも簡単にPythonでWebアプリが作れる「Streamlit」を紹介](https://qiita.com/matsuda_tkm/items/b721f39736027b1caa34)
- [streamlit のデータの流れまとめ](https://qiita.com/munaita_/items/9c7f5c5e0341ecdd1187)
- [StreamlitとFastAPIを使ってメモアプリを作ってみた](https://qiita.com/nanndot/items/03f986059309d386eeae)
- [[Streamlit入門] よく使うコンポーネントまとめ](https://qiita.com/pani_py/items/d1bcb4a487da0a81158b)
- [Webアプリ開発～超初心者編～](https://qiita.com/r9miyakawa/items/572144f252c759f69be4)
- [見える安心感！streamlitのプログレスバーでストレスフルな待ち時間にさよなら！](https://note.com/hondi/n/n9f2a5404f463)
- [Streamlitで長期積立投資シミュレーションのWebアプリを作成](https://qiita.com/yasuyuki-s/items/4d22c9ab1f5005b64ae5)
- [streamlitで遊ぼう！](https://qiita.com/irisu-inwl/items/9d49a14c1c67391565f8)
- [Streamlitに入門してみる](https://qiita.com/goooospuuuurs/items/cacfd5946d39c8445e3d)
- [Streamlit入門＋応用 ～ データ分析Webアプリを爆速で開発する](https://qiita.com/tamura__246/items/366b5581c03dd74f4508)