作ったもの
Real-ESRGANを使って画像を最大4倍に拡大できるWebサイトを作りました。
登録不要・無料で、画像をアップロードするだけです。大したものではないですが、構成をメモがてら書いておきます。
構成
| 項目 | 技術 |
|---|---|
| フロントエンド | HTML / CSS / JS(素のやつ) |
| バックエンド | Python / FastAPI |
| AI処理 | Real-ESRGAN(VPS上でCPU推論) |
| サーバー | VPS + Nginx |
| SSL | Cloudflare |
ReactもNext.jsも使ってません。ページ数が少ないので素のHTMLで十分でした。
バックエンド
FastAPIでAPIを1本生やして、Real-ESRGANで画像を処理して返すだけです。
モデルの読み込み
_upsampler = None
def get_upsampler():
global _upsampler
if _upsampler is None:
from realesrgan import RealESRGANer
from basicsr.archs.rrdbnet_arch import RRDBNet
model = RRDBNet(
num_in_ch=3, num_out_ch=3, num_feat=64,
num_block=23, num_grow_ch=32, scale=4
)
_upsampler = RealESRGANer(
scale=4,
model_path="/path/to/weights/RealESRGAN_x4plus.pth",
model=model,
tile=256,
tile_pad=10,
pre_pad=0,
half=False,
device='cpu'
)
return _upsampler
初回リクエスト時にモデルをロードしています。起動時にやるとsystemdのタイムアウトに引っかかることがあったので。
tile=256はVPSだとメモリが足りなくなるので必須。half=FalseはCPU推論なのでFP16が使えないため。
画像処理
def _process_upscale(contents, scale):
nparr = np.frombuffer(contents, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is None:
return None
try:
upsampler = get_upsampler()
output, _ = upsampler.enhance(img, outscale=scale)
except Exception:
return None
_, buffer = cv2.imencode('.png', output)
return buffer.tobytes()
@app.post("/upscale")
async def upscale_image(
file: UploadFile = File(...),
scale: int = 4
):
contents = await file.read()
if len(contents) > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="10MB以下にしてください")
png_bytes = await asyncio.to_thread(_process_upscale, contents, scale)
if png_bytes is None:
raise HTTPException(status_code=503, detail="処理に失敗しました")
return Response(content=png_bytes, media_type="image/png")
Real-ESRGANの処理はCPUバウンドで1枚数十秒かかるので、asyncio.to_threadでスレッドに逃がしています。これをやらないとuvicornのイベントループが止まって、処理中にヘルスチェックすら返せなくなります。
レスポンスはバイナリPNGをそのまま返しています。最初はbase64で返していたんですが、サイズが33%増えるだけで何のメリットもなかったのでやめました。
フロントエンド
バイナリPNGを受け取ってBlob URLで表示するだけです。
const response = await fetch(`${API_BASE_URL}/upscale?scale=${scale}`, {
method: 'POST',
body: formData
});
const blob = await response.blob();
// 前回のBlob URLを解放してから新しいのを作る
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = URL.createObjectURL(blob);
resultImage.src = currentBlobUrl;
downloadBtn.href = currentBlobUrl;
revokeObjectURLしないとメモリリークするので注意。
あとAI処理は時間がかかってタイムアウトしやすいので、失敗したら2秒待って最大3回リトライするようにしています。
let resultBlob = null;
for (let i = 0; i < 3; i++) {
try {
resultBlob = await doUpscale();
break;
} catch (err) {
if (i < 2) await new Promise(r => setTimeout(r, 2000));
}
}
Nginx
静的ファイルの配信とAPIへのリバースプロキシです。
server {
listen 80;
server_name ai-kakudai.com;
root /home/user/ai-kakudai/frontend;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:8000/;
proxy_read_timeout 300s;
}
}
proxy_read_timeoutはデフォルト60秒だとAI処理がタイムアウトするので長めにしています。
ハマったところ
Cloudflare Flexible SSLのリダイレクトループ
Cloudflareの「Flexible SSL」はCloudflare↔サーバー間がHTTPです。Nginx側でHTTP→HTTPSリダイレクトを書いていると、Cloudflareからの通信が常にHTTPなので無限ループします。
Nginx側のリダイレクトを消して解決。
CPU推論が遅い
GPUなら数秒のところ、CPUだと数十秒〜数分。対策はフロントのリトライとNginxのタイムアウト延長くらいしかやってません。GPUサーバーを借りればいいんですが、コストが跳ね上がるので今のところCPUで我慢しています。
tileパラメータ
tileを設定しないと大きい画像でメモリ不足になります。最初知らなくてOOMで落ちました。
コスト
外部のAI APIは使っていないので、VPSのサーバー代とドメイン代だけで動いています。リクエスト数が増えてもコストは変わりません。逆に言うとCPU推論なので処理速度は出ませんが、個人サイトなのでそこまで同時アクセスもないので問題なし。
おわりに
やっていることはReal-ESRGANをFastAPIで包んでNginxで配信しているだけなので、技術的には特に目新しいことはしていません。似たようなことをやろうとしている人のメモになれば。