【Flask・初心者】高専生が初めて自作画像切り抜きAIWebアプリを作ってみた話(Web編)

## 本記事について
本記事は、軽量フレームワーク**Flask**を使用した**自作画像切り抜きAIを搭載したWebアプリ**の**Web編**となります。
**Webアプリを作ってみたい方**, **Flaskを使いたい方**向けの記事となります。
前提として、Pythonの基本文法がわかっている方向けですのでご了承ください。

AI編については、以下の記事をぜひご覧ください。
[【AI・初心者】高専生が自作画像切り抜きAI搭載Webアプリを作ってみた話(AI編)](https://qiita.com/osame083007/items/54e77204bd63ca631164)


## 要件整理・使用策定
- Webアプリ形式で自作したAIを搭載し、「**画像をアップロードすると、切り抜いた画像を表示する**」ようにしたい。
- 小規模な開発なので、サーバーサイド(DB等)には深く触れずに、勉強中のフロントエンドを使いたい。

以下の要件を踏まえ、使った言語等はこちらです。
Webアプリを作るのは初めてでしたので、軽量フレームワークの**Flask**を使用しました。

- **フロントエンド**
    - **HTML**
    - **CSS**
    - **JavaScript**
- **バックエンド**
    - **Flask(Python)**


## 準備 -Flaskのディレクトリ構造と仮想環境-
Flaskは、ディレクトリ構造が厳しいということを知りました。
以下に必要なディレクトリ構造を書いておきます。
modelディレクトリ以下は、仮想環境関連のディレクトリやgithub関連のファイルです。(仮想環境の立て方については下を)

::: note alert
mypage, app.py以外はディレクトリの名前, 階層を変えてはいけない！！
:::

```
mypage/
    ┝ app.py
    ┝ templates/
    |     ┝ index.html
    |     └ DownLoad.html
    ┝ static/
    |     ┝ css/
    |        ┝ style.css
    |        └ style_download.css
    |     ┝ img/
    |     ┝ js/
    |     |  └ sctipt.js
    |     └ video/
    ┝ model/
    |     └ Fafnercut.pt
    |
    ┝ __pycache__/
    ┝ .vscode/
    ┝ venv/
    ┝ .gitattributes
    ┝ github.gitignore
    ┝ README.md
    └ requirements.txt
```

index.html/style.css/script.jsがホームページ, DownLoad.html/style_download.cssがダウンロードページのマークアップ, 
model/Fafnercut.ptがAIです。
img, videoディレクトリには、ホームページの画像と動画(AI・Webサイトについて解説した動画)を入れておきました。

### 仮想環境
仮想環境の立て方です。
1. 作業ディレクトリを作成したら、ターミナルを起動し`cd`コマンドで作業ディレクトリ(app.py)と同じ層に移動
2. `python -m venv venv`を実行して、venvフォルダ等が作成されることを確認
3. `venv\Scripts\activate`で仮想環境を有効化しましょう
   (ちなみに無効化は単に`deactivate`)


## コーディング -app.py-
具体的なコードは趣旨から外れてしまいますので、[GitHub](https://github.com/Samemaru07/AI_jikken)のstatic, css, jsディレクトリを参考にしてください。
また、授業の趣旨とは程遠かったので、凝ったデザインでもありません。

::: note
実際にAIを作ったので組み込んでみたいという方は、仮想環境がアクティベートの状態で、ローカルに必要なライブラリを入れてください。
:::

ルーティング処理は、`app.py`に記入しました。解説はコードの下で行っています。
``` app.py
import os
import torch
import numpy as np
from PIL import Image
from flask import Flask, render_template, request, url_for
from werkzeug.utils import secure_filename
import torchvision.transforms as transforms
import torch.nn as nn

app = Flask(__name__)

UPLOAD_FOLDER = "static/uploads"
OUTPUT_FOLDER = "static/processed"
MODEL_PATH = "model/ゴルディアス.pt"

app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["OUTPUT_FOLDER"] = OUTPUT_FOLDER

os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(OUTPUT_FOLDER, exist_ok=True)


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(16)
        self.act1 = nn.ReLU()
        self.pool1 = nn.MaxPool2d(kernel_size=2)

        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(32)
        self.act2 = nn.ReLU()
        self.pool2 = nn.MaxPool2d(kernel_size=2)

        self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.bn3 = nn.BatchNorm2d(64)
        self.act3 = nn.ReLU()

        self.conv4 = nn.ConvTranspose2d(64, 32, kernel_size=2, stride=2)
        self.conv5 = nn.ConvTranspose2d(32, 16, kernel_size=2, stride=2)
        self.conv6 = nn.Conv2d(16, 1, kernel_size=3, padding=1)

        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        out1 = self.pool1(self.act1(self.bn1(self.conv1(x))))
        out2 = self.pool2(self.act2(self.bn2(self.conv2(out1))))
        out3 = self.act3(self.bn3(self.conv3(out2)))
        out = self.conv4(out3)
        out = self.conv5(out)
        out = self.sigmoid(self.conv6(out))
        return out


def load_model(model_path):
    model = Net()
    model.load_state_dict(torch.load(model_path, map_location=torch.device("cpu")))
    model.eval()
    return model


def process_image(image_path, model, output_path, threshold=0.8):
    image = Image.open(image_path).convert("RGB")
    transform = transforms.Compose([transforms.Resize((256, 256)), transforms.ToTensor()])
    input_tensor = transform(image).unsqueeze(0)

    with torch.no_grad():
        mask = model(input_tensor).squeeze().numpy()

    mask = (mask > threshold).astype(np.uint8) * 255
    mask = Image.fromarray(mask).resize(image.size, Image.LANCZOS)

    transparent_image = Image.new("RGBA", image.size, (0, 0, 0, 0))
    cutout = Image.composite(image.convert("RGBA"), transparent_image, mask.convert("L"))
    cutout.save(output_path, format="PNG")


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


@app.route("/upload", methods=["POST"])
def upload():
    file = request.files.get("image")
    if not file or file.filename == "":
        return "No file uploaded", 400

    filename = secure_filename(file.filename)
    input_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
    file.save(input_path)

    output_filename = f"processed_{filename}"
    output_path = os.path.join(app.config["OUTPUT_FOLDER"], output_filename)
    process_image(input_path, model, output_path)

    output_url = url_for("static", filename=f"processed/{output_filename}")
    return render_template("DownLoad.html", output_url=output_url)


if __name__ == "__main__":
    model = load_model(MODEL_PATH)
    app.run(debug=True)
```

アップロードされた画像と、切り抜いた画像を保存しておく用のディレクトリを設けました。
また、AI開発に使ったレイヤーを記述し、`evalモード`で、アップロードされた画像の切り抜きを予測するようにしました。

画像切り抜きの大まかなプロセスは、
1. 画像をAIが予測可能な型(テンソル型)に変換し前景を予測。
2. process_image関数の引数のthresholdが、閾値です。このコードだと、0.8, つまり「前景である確率が80%以上のピクセル」のみを白, それ以外を黒とする"**マスク画像**"を生成。
3. 元画像と照らし合わせ、黒い部分を透明にして元画像と合成し、透過PNGとして保存する

です。

この画像切り抜きのマスク生成の前に、"リサイズ"という作業を行うのですが、生成されたマスク画像は、元画像より小さいサイズとなります。マスク画像と元画像を照らし合わせる際に、画像サイズをそろえなければなりません。
そこで、**LANCZOS**(ランチョス)フィルタを使用し、マスク画像の高品質な画像補間を実現しました。

DownLoad.htmlに、切り抜かれた画像をダウンロードできる記述もしました。


## 実演
最初のページはこちらです。いたってシンプルで、HTML/CSSで基本中の基本となるタグのみを使って作りました。
![サイト1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970907/942f2128-bf3b-4163-8bc6-6d80161e9062.png)

"ファイルを選択"部分を押すと、エクスプローラーが開き、画像を選択して"Upload"ボタンを押すと...

![サイト2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970907/14bf6501-2be5-4d0a-b796-2c99321a52b3.png)

前景だけ切り抜かれた画像が表示されました。
下の"Download Image"を押すと、自動でダウンロードが開始されます。

ちなみに、保存された画像は
![サイト3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3970907/f2b84c56-091f-4bac-874e-4b420c63dbe3.png)

こんな風に、ちゃんと背景は透明になっています！


## 感想・改善点
初めてフレームワークというのを使ってWebアプリを作ってみました。
urlの使い方が難しかったですが、何とか実現できました。
Flaskは、HTMLのaタグと相性がいいなと思いました。直感的にルーティング処理をかけるからです。

切り抜かれた画像の保存時のファイル名を変えられるようにするなどまだまだ改善点はありそうです。
ほかにも、今回はDB等のサーバーサイドにはあまり深く触れていません。
ユーザーごとにアカウントを作って、PHPでDBを操作し履歴等の管理も実装してみたいと思いました！

最後までご覧いただき、ありがとうございました！

## 編集者のGitHub
- [GitHub](https://github.com/Samemaru07/AI_jikken)
- [AI編の記事](https://qiita.com/osame083007/items/54e77204bd63ca631164)