【ポケモン風】自分の”好き”が集まる世界「オレオレ地方」のマップを作ろう【自己紹介で注目間違いなし】

# はじめに

※ 掲載したAI生成画像は、一部特定のキャラクターとわかってしまう部分があったため権利上一部を切り取っています。

あるところに、通っている学校の先生から「スライドを好きに作って自己紹介してね～」と言われ、何を書けばよいかわからず途方に暮れるポケモントレーナーがいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/c93d6ac1-e6cc-8c9b-4d61-f5b9a8a1cce2.png)


どんなスライドを作ろうか悩んでいたら、ふと自分の好きなことや趣味を「地図」として紹介したら面白くね？と思いつき、 **「オレオレ地方」** の地図を作ることにしたのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/87f6dc94-a66e-9f57-7f9f-ca2403d2823c.png)

オレオレ地方のシティとタウンを自分で命名し、自己紹介でこの仮想世界を紹介したところ、周りのポケモントレーナーから注目を集めましたとさ。


本プログラムで画像を生成し、Stable Diffusionで地形をリアル化するとこんな地図を作成できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/3129c53c-27d0-45ae-bff8-0d326fc8ce40.png)


そんな記事です。

実装はこちらにおいてあります。ぜひご活用ください。

https://github.com/keisuke-okb/oreore-chiho-map

# 実装

自分の好きなもの・推しのキーワードリストからポケモン風の仮想世界「オレオレ地方」を生成するプログラム

以下のデータ分析手法、機械学習によく出てくる技術を活用して実現しました（データ分析学習コンテンツとしても使えるかもしれません）

- **埋め込みモデル：キーワードのベクトル化（キーワードから固定長のベクトルを算出。類似するキーワードは似た場所に集まる。地図を作るうえでの最も主要な処理）**
- 主成分分析：２次元の地図を描画するために次元削減
- クラスタ分析（独自）：互いに近いキーワード同士は大きな「街」としてまとめる
- 最小全域木：各クラスタ（シティ・タウン）から任意のクラスタに移動できる最小の道を構成
- Torchのモデルを使った推論（GPU）
- matplotlibによるグラフの描画
- pillowを使った画像処理
- キーワードリストから固有シードの算出：ハッシュ関数を利用
- 固有シードによる砂漠、岩地などのランダム地形の生成：入力キーワードの組み合わせが同じなら同じ地形が生成される

## 実装のポイント

###  距離が近く似たキーワードはクラスタ（街）としてまとめる

```python
# クラスタリングと街サイズの計算
def cluster_points(reduced_vectors, map_width, map_height):
    clusters = []
    for vector in reduced_vectors:
        placed = False
        for cluster in clusters:
            if np.linalg.norm(cluster['center'] - vector) < min(map_height, map_width) * 0.15:
                cluster['points'].append(vector)
                cluster['center'] = np.mean(cluster['points'], axis=0)
                placed = True
                break
        
        if not placed:
            clusters.append({'center': vector, 'points': [vector]})
    
    cluster_sizes = [len(cluster['points']) for cluster in clusters]
    return clusters, cluster_sizes

df_embeddings = pd.read_csv("embeddings.csv")

embeddings = df_embeddings[["0", "1"]].values
embeddings -= embeddings.min(axis=0)
embeddings /= embeddings.max(axis=0)
embeddings[:, 0] *= (map_width * 0.8 - 1)
embeddings[:, 1] *= (map_height * 0.8 - 1)
embeddings[:, 0] += map_width * 0.1
embeddings[:, 1] += map_height * 0.1

clusters, cluster_sizes = cluster_points(embeddings, map_width, map_height)
```


### キーワードリスト固有のシードを用いた地形の生成

- クラスタ位置を基に作成した地形に加え、地図がよりバリエーションに富んだものになるようシード値からランダムな地形を作るようにしました。

- 入力するキーワードリストの組み合わせが同一であれば、生成される地形は変化しません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/11308d51-b3dd-7d3b-35ac-53f85abe7580.png)


```python
# シード起伏画像の作成
def create_seed_relief_image(map_width, map_height, seed):
    random.seed(seed)
    pixels = np.zeros((map_width, map_height))
    
    for _ in range(40):
        x, y = random.randint(0, map_width-1), random.randint(0, map_height-1)
        radius = random.randint(int(map_height * 0.02), int(map_height * 0.2))
        for dx in range(-radius, radius+1):
            for dy in range(-radius, radius+1):
                if (0 <= x + dx < map_width) and (0 <= y + dy < map_height) and (dx*dx + dy*dy <= radius*radius):
                    pixels[x + dx, y + dy] += 1

    pixels -= 1
    pixels = pixels.clip(-1 , 2)
    return pixels.T

seed_relief_img = create_seed_relief_image(map_width, map_height, int(unique_seed, 16))
plt.imshow(seed_relief_img)
```


### 最小全域木を活用した道路生成

各クラスタ（シティ・タウン）から任意のクラスタに移動できる最小のグラフを構成

```python
# 道路のデータ生成
import numpy as np
from scipy.spatial import distance_matrix
from itertools import combinations

def find(parent, i):
    if parent[i] == i:
        return i
    else:
        return find(parent, parent[i])

def union(parent, rank, x, y):
    rootX = find(parent, x)
    rootY = find(parent, y)
    
    if rank[rootX] < rank[rootY]:
        parent[rootX] = rootY
    elif rank[rootX] > rank[rootY]:
        parent[rootY] = rootX
    else:
        parent[rootY] = rootX
        rank[rootX] += 1

def generate_road_data(clusters):
    num_clusters = len(clusters)
    positions = [cluster['center'] for cluster in clusters]
    # すべてのクラスタ間の距離行列を算出
    dist_matrix = distance_matrix(positions, positions)
    
    # すべての辺を作成し、それに対応する距離を配列に格納
    edge_list = []
    for i, j in combinations(range(num_clusters), 2):
        edge_list.append((dist_matrix[i][j], i, j))
    
    # 距離に基づいてエッジをソート
    edge_list.sort()

    parent = list(range(num_clusters))
    rank = [0] * num_clusters
    roads = []

    for dist, i, j in edge_list:
        root_i = find(parent, i)
        root_j = find(parent, j)
        
        # サイクルが生じない場合にのみ、辺を追加
        if root_i != root_j:
            roads.append((clusters[i]['center'], clusters[j]['center']))
            union(parent, rank, root_i, root_j)

    return roads

roads = generate_road_data(clusters)
```


### 道路は上下左右移動のランダム移動（固有シード値）

直線でクラスタを結ぶと道路感が出ないため、上下左右の移動を固有シード値で並び替え描画

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/46d425bb-0fdb-e12c-55fd-6092e6b58b04.png)

```python
for road in roads:
    start = tuple(road[0].astype(int))
    end = tuple(road[1].astype(int))
    # 直線描画の場合
    # draw.line([start, end], fill=(255, 255, 255, 150), width=1)

    # マンハッタン式描画
    x1, y1 = start
    x2, y2 = end
    delta_x = x2 - x1
    delta_y = y2 - y1
    x_moves = [(1, 0)] * delta_x if delta_x > 0 else [(-1, 0)] * abs(delta_x)
    y_moves = [(0, 1)] * delta_y if delta_y > 0 else [(0, -1)] * abs(delta_y)
    moves = x_moves + y_moves
    random.seed(seed)
    random.shuffle(moves)

    x, y = start
    draw.line([start, start], fill=(255, 255, 255, 150), width=1)
    for x_d, y_d in moves:
        x += x_d
        y += y_d
        draw.line([(x, y), (x, y)], fill=(255, 255, 255, 150), width=1)
        if 1 <= relief_pixels[x, y] <= 2:
            terrain_pixels[x, y] = color_map.get(6, (0, 0, 255))
```


# 生成サンプル

## 休日はクラスメイトとお出かけし、平日は授業後家で動画や漫画を満喫する大学生(list_sample1.csv)

- 好きなものリスト
```csv
車でドライブ
海岸線の散歩
山道のハイキング
漫画の一気読み
カフェ
読書
夜景スポット巡り
ラーメン食べ歩き
観光地の写真撮影
YouTube
旅動画
キャンプ用品
ゲーム実況
地元の隠れた名所探し
コーヒー豆
観光ガイドブック
音楽を聴きながらドライブ
アウトドアグッズ
小旅行の計画立て
アニメの一気見
温泉巡り
```


- 次元削減結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/6dd010c7-5c4b-c5ca-404a-43db19bbd7d8.png)

- ピクセル地形
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/a222433f-c4fd-52b0-f304-31710ff40c88.png)

- テクスチャ描画地図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/8fa97fad-5c96-9802-6a69-eb1fa20f8d69.png)

- 道路
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/2eaa3ffc-fbb7-50cb-9b58-0037c58deb91.png)

- (オプション) Stable Diffusion変換後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/f6387f7f-c2e3-5394-fbdc-be67cf3767d5.png)

- 次元削減結果から各タウン・シティを命名
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/a3666394-3576-e387-328d-9c49e1b5af89.png)

- 休日のドライブ、旅行と、平日のアニメ・漫画が離れた島になっているのが面白いですね。
- 奇跡的にアウトドアタウンが森に囲まれた場所になりました。迷いそうですね。


## 平日の仕事終わりは酒や飲み会で楽しみ、休日は小さい子供と過ごす家族の時間が好きな30代のビジネスマン(list_sample2.csv)

- 好きなものリスト
```csv
仕事終わりのビール
居酒屋
飲み会
同僚との雑談
子供と一緒にプリキュア鑑賞
家族でピクニック
子供と公園遊び
動物園
水族館
ディズニーリゾート
カメラで家族写真
お弁当作り
車で近場の旅行
家族でショッピングモール
子供の成長アルバム作り
温泉旅行の計画立て
一眼レフカメラ
早朝ランニング
家族との団らん
休日ドライブ
地元イベント
```


- 次元削減結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/7063cceb-cd28-e29a-546f-7611f29015de.png)


- ピクセル地形
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/bd26f24b-ab0b-8e7f-6ae7-5e8a8de8a60b.png)

- テクスチャ描画地図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/208414c9-28f3-1633-8efc-02c3d2a70ca1.png)

- 道路
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/661af359-1ea2-1e94-64b5-84eae0a8b2c0.png)

- (オプション) Stable Diffusion変換後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/7f5cbdd5-1123-5275-cd13-57a3acb79bce.png)

- 次元削減結果から各タウン・シティを命名
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1373557/f7007f93-2c5e-4d73-6348-b850a35bd53d.png)

- 家族と過ごしたり、どこかに遊びに行ったりする街が集まりました。
- 右上に飲み関連の街が固まっており、それぞれの島になっています。渡り歩くのが楽しそうですね！（笑）

# さいごに

自分の好きなことから地図「オレオレ地方」を作るプログラムを紹介しました。
ぜひ自己紹介に迷ったらオレオレ地方の地図を資料に貼り付けて他人に紹介してみてください！