はじめに
※ 掲載したAI生成画像は、一部特定のキャラクターとわかってしまう部分があったため権利上一部を切り取っています。
あるところに、通っている学校の先生から「スライドを好きに作って自己紹介してね~」と言われ、何を書けばよいかわからず途方に暮れるポケモントレーナーがいました。
どんなスライドを作ろうか悩んでいたら、ふと自分の好きなことや趣味を「地図」として紹介したら面白くね?と思いつき、 「オレオレ地方」 の地図を作ることにしたのです。
オレオレ地方のシティとタウンを自分で命名し、自己紹介でこの仮想世界を紹介したところ、周りのポケモントレーナーから注目を集めましたとさ。
本プログラムで画像を生成し、Stable Diffusionで地形をリアル化するとこんな地図を作成できます。
そんな記事です。
実装はこちらにおいてあります。ぜひご活用ください。
実装
自分の好きなもの・推しのキーワードリストからポケモン風の仮想世界「オレオレ地方」を生成するプログラム
以下のデータ分析手法、機械学習によく出てくる技術を活用して実現しました(データ分析学習コンテンツとしても使えるかもしれません)
- 埋め込みモデル:キーワードのベクトル化(キーワードから固定長のベクトルを算出。類似するキーワードは似た場所に集まる。地図を作るうえでの最も主要な処理)
- 主成分分析:2次元の地図を描画するために次元削減
- クラスタ分析(独自):互いに近いキーワード同士は大きな「街」としてまとめる
- 最小全域木:各クラスタ(シティ・タウン)から任意のクラスタに移動できる最小の道を構成
- Torchのモデルを使った推論(GPU)
- matplotlibによるグラフの描画
- pillowを使った画像処理
- キーワードリストから固有シードの算出:ハッシュ関数を利用
- 固有シードによる砂漠、岩地などのランダム地形の生成:入力キーワードの組み合わせが同じなら同じ地形が生成される
実装のポイント
距離が近く似たキーワードはクラスタ(街)としてまとめる
# クラスタリングと街サイズの計算
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)
キーワードリスト固有のシードを用いた地形の生成
-
クラスタ位置を基に作成した地形に加え、地図がよりバリエーションに富んだものになるようシード値からランダムな地形を作るようにしました。
-
入力するキーワードリストの組み合わせが同一であれば、生成される地形は変化しません。
# シード起伏画像の作成
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)
最小全域木を活用した道路生成
各クラスタ(シティ・タウン)から任意のクラスタに移動できる最小のグラフを構成
# 道路のデータ生成
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)
道路は上下左右移動のランダム移動(固有シード値)
直線でクラスタを結ぶと道路感が出ないため、上下左右の移動を固有シード値で並び替え描画
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)
- 好きなものリスト
車でドライブ
海岸線の散歩
山道のハイキング
漫画の一気読み
カフェ
読書
夜景スポット巡り
ラーメン食べ歩き
観光地の写真撮影
YouTube
旅動画
キャンプ用品
ゲーム実況
地元の隠れた名所探し
コーヒー豆
観光ガイドブック
音楽を聴きながらドライブ
アウトドアグッズ
小旅行の計画立て
アニメの一気見
温泉巡り
-
休日のドライブ、旅行と、平日のアニメ・漫画が離れた島になっているのが面白いですね。
-
奇跡的にアウトドアタウンが森に囲まれた場所になりました。迷いそうですね。
平日の仕事終わりは酒や飲み会で楽しみ、休日は小さい子供と過ごす家族の時間が好きな30代のビジネスマン(list_sample2.csv)
- 好きなものリスト
仕事終わりのビール
居酒屋
飲み会
同僚との雑談
子供と一緒にプリキュア鑑賞
家族でピクニック
子供と公園遊び
動物園
水族館
ディズニーリゾート
カメラで家族写真
お弁当作り
車で近場の旅行
家族でショッピングモール
子供の成長アルバム作り
温泉旅行の計画立て
一眼レフカメラ
早朝ランニング
家族との団らん
休日ドライブ
地元イベント
-
家族と過ごしたり、どこかに遊びに行ったりする街が集まりました。
-
右上に飲み関連の街が固まっており、それぞれの島になっています。渡り歩くのが楽しそうですね!(笑)
さいごに
自分の好きなことから地図「オレオレ地方」を作るプログラムを紹介しました。
ぜひ自己紹介に迷ったらオレオレ地方の地図を資料に貼り付けて他人に紹介してみてください!