Leaflet地図、鮮明に生まれ変わる:Laravel + Fast APIを活用した高画質化

こんにちは❗フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、完全に個人的な趣味なんですけど、私はマップでいろいろなところを「地図旅行」するのが好きです。

特に国土地理院が提供してくれている過去の航空写真(1960年代とか)の「時間旅行&地図旅行」が最高だったりします。

しかし、それにはひとつ問題があって、

画質が荒い

んですよね…(もちろん、当時としてはすごいのですが😅)

そして、「うーん、もうちょっと拡大して見てみたい」と何度も思ってたら1つアイデアが浮かんできました。

地図画像を高画質化すればいいじゃないか❗

と。

というのも、やはり科学はすごいですね。

なんと、Real-ESRGAN という技術を使うとPythonで画像を高画質化することができるんですね。(しかもこのパッケージも2年ぐらい前のものです)

そこで!

今回は「Laravel + Fast API + Real-ESRGAN」で「クリックするだけで画像を高画質化する」機能をつくってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😄✨

※ ちなみにFast APIは私が使ってみたかったからです。PHPからでもコマンド実行できるならいけると思います。

「夏のサイクリング用に
取り付け式の空調ファン
を購入❗今年も行くぞ 🚲✨」

開発環境: Laravel 10.x、Fast API

全体の構成

今回は、LaravelPHP)だけでなく、Pythonも使うので全体像を先にご紹介します。

Laravel(PHP)Fast API(Python)Real-ESRGAN(Python)

つまり、手順としては次のとおりです。

  1. ブラウザで地図を表示する
  2. ボタンをクリックすると Fast API に画像をアップロード
  3. Fast API から Real ESRGAN を実行し画像を高画質化
  4. 保存された画像にブラウザからアクセス

では、楽しんでやっていきましょう❗

Real-ESRGAN をインストールする

まずは、画像を高画質化する部分Real-ESRGANを用意します。

※ なお、おそらくGPUはなくてもOKだと思いますが、環境によっては実行できない場合もあるかもです。事前に こちら から確認しておいてください。

(お好きな)Pythonのフォルダで移動して以下のコマンドを実行します。

git clone https://github.com/xinntao/Real-ESRGAN.git

すると、Real-ESRGANというフォルダが作成されますので、以下のコマンドでその中に移動してください。

cd Real-ESRGAN

あとは、以下のコマンドをそれぞれ実行すれば自動的にインストールが完了します。

pip install basicsr
pip install facexlib
pip install gfpgan
pip install -r requirements.txt
python setup.py develop

では、この状態で以下のコマンドを実行してみましょう。
サンプルの画像を高画質化することができます。

python inference_realesrgan.py -n RealESRGAN_x4plus -i inputs

すると、resultsフォルダの中に高画質化された画像が保存されることになります。ぜひご自身でチェックしてみてください。

Fast API をインストールする

続いて、Pythonで簡単にAPIを作成することができるFast APIを使って「画像アップロード&高画質化」する部分を作っていきましょう。

まずは必要なパッケージをインストールしておきます。
以下のコマンドを実行してください。

pip install fastapi
pip install uvicorn

では、今回は「high_resolution_map」というフォルダをつくり、その中へコードをつくっていくことにします。

では、「high_resolution_map」フォルダの中へ以下のコードを保存してください。

high_resolution_map/api.py

from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
import os, uuid


app = FastAPI()
origins = [
    "http://(Laravel側のドメイン)",

    # 必要なだけ追加できます
]

app.add_middleware(
    CORSMiddleware, # クロスドメイン対策
    allow_origins=origins,
)

@app.post("/high_resolution")
async def create_upload_file(map_image: UploadFile = File(...)):
    content = await map_image.read()

    # 画像をディスクに保存
    random_filename = uuid.uuid4().hex
    save_dir = "./uploaded_images"
    save_filename = random_filename + ".jpg"
    os.makedirs(save_dir, exist_ok=True)

    with open(os.path.join(save_dir, save_filename), "wb") as f:
        f.write(content)

    # 高画質化
    original_image_dir = os.getcwd() + "/uploaded_images"
    generated_image_dir = "(Laravel までのパス)/public/images/high_resolution_map" # Laravel の公開フォルダ
    result = os.system(
        "python /(Python プロジェクトまでのパス)/python/Real-ESRGAN/inference_realesrgan.py "+
        "-n RealESRGAN_x4plus "+
        "-i  "+ original_image_dir +" "+
        "-o "+ generated_image_dir
    )
    os.remove(original_image_dir + "/" + save_filename) # 元画像は削除する
    
    result_value = True if result == 0 else False
    generated_filename = random_filename + "_out.jpg"

    return {
        "result": result_value,
        "filename": generated_filename,
    }

この中では、送信されてきた画像を一旦フォルダに保存し、その画像をReal-ESRGANで高画質化し、さらにその画像をLaravelのフォルダに保存しています。

また、重要なところは以下のとおりです。

クロスドメイン対策

とてもシンプルに言うと、セキュリティ上の理由から「http://example.com」からAjaxで「http://example.net」にアクセスはできません。

でも、これだと自分の別サイトにもアクセスできなくなってしまうので、回避する方法が用意されています。それを行っているのが以下の部分です。

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins, # クロスドメイン対策
)

つまり、「このoriginsの中に入っているURLからのアクセスは許可するよ😉」という意味になります。

高画質化

先ほどReal-ESRGANを実行したときは相対パスを使っていたのでコマンドが短かったですが、Fast APIとはまた別のフォルダになりますので、ここでは絶対パスが必要になります。

つまり、コード内の以下2つはご自身のパスの置き換えてください

  • (Laravel までのパス)
  • (Python プロジェクトまでのパス)

例: /home/*******/pytyhon

Laravel で地図を表示し、高画質化できるようにする

では、やっとLaravelにたどり着きました。
では、以下のファイルを作成してください。

※ ちなみに地図の場所は、1960年ごろの東京スカイツリーの場所です。
CDNを使っているのでパッケージのインストールは不要です

resources/views/high_resolution_map/index.blade.php

<!DOCTYPE html>
<html>
<head>
    <title>地図の高解像度化テスト</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@5.2.3/dist/css/bootstrap.min.css" />
    <style>

        .loader {
            border: 6px solid #f3f3f3;
            border-top: 6px solid #3498db;
            border-radius: 50%;
            width: 50px;
            height: 50px;
            animation: spin 2s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

    </style>
</head>
<body class="p-4">
    <h3 class="mb-3">地図の高解像度化テスト</h3>

    <div class="row">
        <div class="col-6">
            <div id="map" class="mb-3" style="width:500px;height:500px;"></div>
            <button id="button" type="button" class="btn btn-primary btn-sm">高解像度化する</button>
        </div>
        <div class="col-6">
            <!-- 高解像度化した画像を表示する -->
            <img id="generate-image" src="" class="d-none w-100">
            <div id="loader" class="loader d-none"></div>
        </div>
    </div>

    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet-src.js"></script>
    <script src="https://unpkg.com/leaflet-simple-map-screenshoter@0.5.0/dist/leaflet-simple-map-screenshoter.js"></script>
    <script src="https://unpkg.com/axios@1.4.0/dist/axios.min.js"></script>
    <script>

        // マップ
        const initialLocation = [35.70990329289358, 139.81077267654356]; // 東京スカイツリー
        const map = L.map('map').setView(initialLocation, 17);

        L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/ort_old10/{z}/{x}/{y}.png', {
            minZoom: 10,
            maxZoom: 17,
            attribution: '<a href="https://www.gsi.go.jp/kikakuchousei/kikakuchousei40182.html">国土地理院</a>'
        })
        .addTo(map);

        // プラグイン
        const pluginOptions = { hidden: true }; // ボタンは表示しない
        const screenShot = L.simpleMapScreenshoter(pluginOptions).addTo(map);
        const screenShotOptions = { mimeType: 'image/jpeg' };

        // 高解像度化
        const button = document.getElementById('button');
        button.addEventListener('click', () => {

            screenShot.takeScreen('blob', screenShotOptions)
                .then(blob => {

                    const loaderEl = document.getElementById('loader');
                    const imageEl = document.getElementById('generate-image');
                    loaderEl.classList.remove('d-none');
                    imageEl.classList.add('d-none');

                    const file = new File([blob], 'map.jpg', { type: 'image/jpeg' });
                    const url = '(Fast API のURL)/high_resolution'; // Fast API の URL
                    const headers = { 'Content-Type': 'multipart/form-data' };
                    const formData = new FormData();
                    formData.append('map_image', file);

                    axios.post(url, formData, { headers })
                        .then(response => {

                            if(response.data.result === true) {

                                const filename = response.data.filename;

                                imageEl.src = `/images/high_resolution_map/${filename}`;
                                imageEl.classList.remove('d-none');

                            } else {

                                console.log('高解像度化に失敗しました。');

                            }

                        })
                        .finally(() => {

                            loaderEl.classList.add('d-none');

                        })

                });

        });

    </script>
</body>
</html>

この中で重要なのが「地図(航空写真)の画像データ化」です。

ありがたいことに地図ライブラリLeafletには simple-map-screenshoter というプラグインがあり、これを使うと自分で実装せずに画像データが取得できるんですね。

そして、その場所が以下の部分です。

screenShot.takeScreen('blob', screenShotOptions)
    .then(blob => {
    
        // 省略
    
    });

そして、取得されたblobデータをFileデータに変換しています。(このFileデータは、<input type="file">でファイル選択されたときのものと同じです)

const file = new File([blob], 'map.jpg', { type: 'image/jpeg' });

最後に、このデータをパラメータとしてFast APIURLへ送信し、高画像化が完了したらそのファイル名が取得できるので、それを使って画像を表示するという流れになっています。

ちなみに、(Fast API のURL)となっているところは、Fast APIを起動したときに表示されるURLをセットします。(ポートの変更は「テストしてみる」をご覧ください)

では、これで作業は完了です。
お疲れ様でした😄✨

テストしてみる

では、実際にテストしてみましょう❗

まずは、high_resolution_mapフォルダにコマンド移動し、以下のコマンドを実行します。

uvicorn api:app --reload

apiという部分はapi.pyから来ています。ファイル名が別の場合は適宜変更してください。

なお、このコマンドだと127.0.0.1:8000がアクセスできるURLになりますが、私の場合は別のプロセスが実行中でしたので、以下のようにポート番号4989(四苦八苦😂)を使いました。

uvicorn api:app --port 4989 --reload

なお、--reloadは便利なオプションで、もしファイルに変更があったら自動的にサーバーを再起動してくれる(つまり、いちいちストップ&スタートしなくていい)というものになっています。

では、Fast APIが起動したら次はブラウザで「http://******/high_resolution_map」へアクセスします。

※ アクセスするドメインは「クロスドメイン対策」で許可したドメインです。

はい❗予定通り地図が表示されています。
では、「高解像度化する」ボタンをクリックしてみましょう。

すると・・・・・・

処理中のローディングが表示されています。

そして・・・・・・・・・・

はい❗
高解像度化された画像が表示されました。

成功です😄✨

ちなみに以下が元画像と高解像度化した画像(縮小してますが、その他の加工はしてません)

【元画像】

【高画質化した画像】

※ この画像は国土地理院が提供しているものです

だいぶくっきりはっきりしていますね。
なんと画像サイズは「500px→1980px」です。

Real-ESRGANすごいですね❗

企業様へのご提案

今回は地図(航空写真)の高画質化でしたが、もちろん以下のようなケースでも利用ができます。

  • 過去の写真を高画質化する
  • 小さな集合写真を高画質化する
  • 通常の素材写真を高画質化して大きくする
  • フォーカスがぼけた写真を高画質化する

などなど。

もしこういった機能をご希望でしたらお問い合わせからお気軽にご相談ください。

お待ちしております。😄✨

おわりに

ということで、今回は「Laravel + Fast API + Real-ESRGAN」で地図画像の高画質化をしてみました。

今回の機能自体はあまり実用的ではないかもしれませんが、いろいろなテクノロジーを組み合わせると面白いことができたりすることを体験していただけると嬉しいです。

それにしてもFast APIは初めて使いましたがホントに「Fast」でした。パッケージをインストールして関数をつくるだけでOKですからね👍

今後、Pythonとの連携が必要な場合はFast APIを使っていきたいと思います。(好みでしょうが、djangoはクセがあまりに強いので…😫)

ではでは〜❗

「合体式マットを立体にして
頭を囲み、
質のいい睡眠をゲット❗」

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ