トップページ -> Pythonで世界地図に色を塗る・国旗を貼り付ける

Pythonで世界地図に色を塗る・国旗を貼り付ける

今回はPythonで世界地図にランダムに色を塗ることと国旗を貼り付けて画像のような世界地図を作ります. geopandasで境界データを読み込んでmatplotlibで描画するだけなのでmatplotlibに慣れ親しんでいる方からすると目新しい内容はないと思います.

  1. 1. 国境の境界データをダウンロードする
  2. 2. ランダムに色を塗る
  3. 3. 国旗をダウンロードする
  4. 4. 国旗を加工する
  5. 5. 国旗を貼り付ける
国旗を張り合わせて作った世界地図
国旗を張り合わせて作った世界地図

1. 国境の境界データをダウンロードする

国境の境界データをダウンロードします. World Bank Official Boundariesから「World Country Polygons - Very High Definition」というファイルをダウンロードします.

2. ランダムに色を塗る

以下のコードでgeopandasを使って境界データから世界地図を表示できます.ここでは色を指定していないのでデフォルトの青で描画されます.


# https://datacatalog.worldbank.org/search/dataset/0038272
import geopandas as gpd
import matplotlib.pyplot as plt

# ダウンロードした境界データをgeopandasで読み込む
gdf = gpd.read_file("WB_countries_Admin0_10m/WB_countries_Admin0_10m.shp")

# 地図をプロット
gdf.plot(figsize=(10, 10))
plt.show()
以下のコードで色を指定して世界地図を表示できます.カラーコードのリストをランダムに生成してplot時にcolor=colorsを指定しただけです.

import random
import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("WB_countries_Admin0_10m/WB_countries_Admin0_10m.shp")

# ランダムな16進カラーコードのリストを生成
colors = ['#' + ''.join(random.choices(list('0123456789ABCDEF'),k=6)) for _ in range(len(gdf))]

# gdfにcolor列を追加してもいい この場合,color=gdf['color']
# gdf['color'] = ['#' + ''.join(random.choices(list('0123456789ABCDEF'),k=6)) for _ in range(len(gdf))]

# colorに指定したい色のリストを与えて地図をプロット
fig, ax = plt.subplots(figsize=(10, 10))
gdf.plot(ax=ax, color=colors, legend=False)

plt.show()

3. 国旗のデータをダウンロードする

世界地図に国旗を貼り付けるために,まずは国旗の画像をダウンロードします. 以下のサイトでダウンロードできます. 1,2は一括でダウンロードできません. どのサイトを利用してもいいのですが,5でpngファイルを一括でダウンロードしてISO 3166-1 - Wikipediaの国コードに基づいて辞書を作る(またはファイル名を変える)のがやりやすいかもしれません. 私は以前1のサイトでダウンロードしていたので,足りない分をWikimedia Commonsからダウンロードしました.
以降のコードは5のファイル名に基づいて進めていきます.
※ 冒頭で紹介した画像は1 + Wikimedia Commonsでダウンロードした大きな国旗を使用しています.
【関連1(外部サイト)】FreeSozai.jp
サイズが大きいので綺麗に表示できます.足りない分はWikimedia Commonsで大きなサイズをダウンロードできます.
【関連2(外部サイト)】国旗のひろば
【関連3(外部サイト)】Github - lipis/flag-icons
【関連4(外部サイト)】Kaggle - Flags of the World with abbreviations
【関連5(外部サイト)】Github - yammadev/flag-icons

4. 国旗を加工する

Github - yammadev/flag-iconsでダウンロードしたファイルはISO 3166-1のAlpha-2に基づいて命名されています. 国コードはISO_A2列に格納されています.コードが欠損(-99)している行があるので修正します.また合衆国領有小離島などの飛び地を一意に定めるために後ろに _n を付けます.


import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
import os
from shapely.affinity import scale

# 境界データを読み込む
gdf = gpd.read_file("WB_countries_Admin0_10m/WB_countries_Admin0_10m.shp")

# 欠損しているISO Alpha-2コードの辞書を作る
iso_update = {
    "フランス": "FR",
    "ノルウェー": "NO",
    "グァンタナモ米軍基地": "US",  # 米軍基地なのでUS
    "クリッパートン島": "FR",  # フランス領
    "オーストラリア領インド洋地域": "AU"  # オーストラリア領
}

# ISO_A2が "-99" の行を更新
for idx, row in gdf[gdf["ISO_A2"] == "-99"].iterrows():
    name = row["NAME_JA"]
    if name in iso_update:
        gdf.loc[idx, "ISO_A2"] = iso_update[name]

# 合衆国領有小離島などを一意に定める必要がある (_n を付ける)
gdf["ISO_A2"] = gdf.groupby("ISO_A2").cumcount().astype(str).radd("_").radd(gdf["ISO_A2"])
南鳥島などの小さい領土が描画できなくなると以降の操作で位置がずれてしまうので境界データを拡大しておきます. 画像サイズによって適宜 xfact=40, yfact=40 を弄る必要があります.(5でダウンロードした場合は40で十分) 本当は画像データごとに描画できる十分なサイズを計算して拡大すればいいのですが,今回は一括で40倍としておきます.

# すべてのポリゴンをn倍にスケール スケーリングしないと南鳥島などの小さい島が描画できなくなってしまう
gdf["geometry"] = gdf["geometry"].apply(lambda geom: scale(geom, xfact=40, yfact=40, origin=(0, 0)))
すべての行に対して,(1) 国がすっぽり入る矩形を取得 → (2) 国旗を国の矩形に合わせてリサイズ → (3) 境界データからマスクを作る → (4) マスクを使って国旗から国の形を切り抜く

from shapely.geometry import box
from PIL import Image, ImageDraw

output_dir = "processed_flags/"  # 保存先ディレクトリ

for idx, row in gdf.iterrows():
    # ISO_A2に基づいて国旗画像の読み込み
    nation_id = row['ISO_A2'].split("_")[0]
    flag_path = f"flag-icons-master/flag-icons-master/png/{nation_id}@3x.png"
    flag_img = Image.open(flag_path).convert("RGBA")

    # (1) 国の境界から矩形を取得(minx:左端, miny:上端, maxx:右端, maxy:下端)
    minx, miny, maxx, maxy = row["geometry"].bounds
    nation_width = maxx - minx
    nation_height = maxy - miny
    # ===================================================================

    # (2) 画像の縦横比を保ちつつ,国の境界サイズにリサイズ  =================
    flag_aspect = flag_img.width / flag_img.height

    # 画像の幅 高さを0で初期化
    new_width = 0
    new_height = 0
    while new_width == 0 or new_height == 0:
        if nation_width / nation_height > flag_aspect:
            # 幅を基準にリサイズ
            new_width = int(nation_width)
            new_height = int(new_width / flag_aspect)
        else: 
            # 高さを基準にリサイズ
            new_height = int(nation_height)
            new_width = int(new_height * flag_aspect)

        # new_width==0 or new_height==0 である内は画像にできないので10倍に拡大
        if new_width==0 or new_height==0:
            # 10倍スケール
            minx, miny, maxx, maxy = minx*10, miny*10, maxx*10, maxy*10
            nation_width = maxx - minx
            nation_height = maxy - miny

    # 国旗の画像を国の矩形に合わせてリサイズ
    flag_img = flag_img.resize((new_width, new_height), Image.LANCZOS)
    # =========================================================================

    # (3) new_width, new_heightでマスクを作成 (L: グレースケールで画像を作成) =====
    mask = Image.new("L", (new_width, new_height), 0)  # 黒背景
    draw = ImageDraw.Draw(mask)

    # 境界データを PIL の座標に変換する MultiPolygon は Polygon の集合(飛び地や離島がある場合など)
    if row["geometry"].geom_type == "MultiPolygon":
        geoms = row["geometry"].geoms  # MultiPolygon の場合,各Polygon をリストとして扱う
    else:
        geoms = [row["geometry"]]  # Polygon の場合,リストにして統一的に扱う

    # 境界データの境界内を白で塗りつぶしたマスクを作る
    for geom in geoms:
        polygon = [(x - minx, maxy - y) for x, y in np.array(geom.exterior.coords)]  # 座標変換
        draw.polygon(polygon, fill=255)  # マスクに白で描画
    # =============================================================================

    # マスクを適用して切り抜き
    size = (int(max(nation_width,1)), int(max(nation_height,1)))
    cropped_flag = Image.new("RGBA", size) # 透明な背景を作る
    cropped_flag.paste(flag_img, (0, 0), mask) # マスクで切り抜いで左上(0,0)に貼り付け (マスクの黒部分を透明にする) 

    # 保存
    save_path = f"{output_dir}/{row['ISO_A2']}.png"
    cropped_flag.save(save_path, "PNG")
    print(f"{row['NAME_JA']} の国旗を保存: {save_path}")

5. 切り抜いた国旗を世界地図に貼り付ける

先ほど作った画像を世界地図上の国の位置に合わせて貼り付けていきます. 本来ならこの操作で大きさは一致すると思うのですが,結構ずれるのでimagebox = OffsetImage(img_resized, zoom=0.72, resample=True)で縮小してサイズを合わせています.


# zoom を調整する
zoom_x = pixel_width / img_width
zoom_y = pixel_height / img_height
実行すると合衆国領有小離島 height and width must be > 0 などと表示されますが,国が小さすぎて国旗を重ねられないだけなので無視して構いません.(仮に貼り付けられても小さすぎて見えません)
小さい画像から作ったので大きい国はぼやけていますが,大きい国旗の画像を使えば画質がいいものが作れます. 今回は単純に国の中心から機械的に国旗の画像を切り抜いたのでフランスやロシアが不自然になっていたり,ポルトガルやスペインの国章が入っていないなどの不都合が起きていますが,
表示される世界地図
国旗を張り合わせて作った世界地図

import geopandas as gpd
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
from PIL import Image
import numpy as np

# 境界データを読み込む ============================================================
gdf = gpd.read_file("WB_countries_Admin0_10m/WB_countries_Admin0_10m.shp")

# 変更する国・地域とISO Alpha-2コードのマッピング
iso_update = {
    "フランス": "FR",
    "ノルウェー": "NO",
    "グァンタナモ米軍基地": "US",  # 米軍基地なのでUS
    "クリッパートン島": "FR",  # フランス領
    "オーストラリア領インド洋地域": "AU"  # オーストラリア領
}

# ISO_A2が "-99" の行を更新
for idx, row in gdf[gdf["ISO_A2"] == "-99"].iterrows():
    name = row["NAME_JA"]
    if name in iso_update:
        gdf.loc[idx, "ISO_A2"] = iso_update[name]

# 合衆国領有小離島などを一意に定める必要がある
gdf["ISO_A2"] = gdf.groupby("ISO_A2").cumcount().astype(str).radd("_").radd(gdf["ISO_A2"])
# ========================================================================================

# 地図のサイズ(100,100)で描画
fig, ax = plt.subplots(figsize=(100, 100))
gdf.plot(ax=ax, color="lightgray", edgecolor="black")

# 各国に国旗を配置
for idx, row in gdf.iterrows():
    try:
        # 国旗画像の読み込み
        img = Image.open(f"processed_flags/{row['ISO_A2']}.png")  # 画像があるディレクトリを指定
        
        # 国の境界ボックスの中心を計算
        minx, miny, maxx, maxy = row.geometry.bounds
        center_x = (minx + maxx) / 2
        center_y = (miny + maxy) / 2
        
        # 地理座標をピクセル単位に変換
        x0, y0 = ax.transData.transform((minx, miny))
        x1, y1 = ax.transData.transform((maxx, maxy))

        # ピクセル単位のサイズ
        pixel_width = abs(x1 - x0)
        pixel_height = abs(y1 - y0)

        # 元画像のサイズ
        img_width, img_height = img.size

        # zoom を調整する
        zoom_x = pixel_width / img_width
        zoom_y = pixel_height / img_height

        # リサイズ(縦横別々に倍率を適用)
        new_width = int(img.width * zoom_x)
        new_height = int(img.height * zoom_y)
        img_resized = img.resize((new_width, new_height), Image.LANCZOS)

        # (???) zoom = 0.72 で縮小しないと位置が合わない
        imagebox = OffsetImage(img_resized, zoom=0.72, resample=True)
        
        # AnnotationBbox を国の中心に設定
        ab = AnnotationBbox(imagebox, (center_x, center_y), frameon=False)

        # 軸に追加
        ax.add_artist(ab)
    except Exception as e:
        print(f"{row['NAME_JA']}")
        print(e)

# 表示
plt.show()