「全球及び日本域150年連続実験データ」を可視化する その6-回る地球上に地上気温変化を描く。

はじめに

前回は、QGIS上で全球の地上気温変化アニメーションを作成しました。

cci-labo.hateblo.jp

ただやはり、本物の地球を観察しているような視点で描きたい! ということで(画面上で)回る地球儀にデータを貼り付けてアニメーションを作ってみます。

温度変化マップの作成

前回作成した年平均気温の変化データからcartpyを用いて正距円筒図法のマップを作成します。

# ライブラリのインポート
import os
import pandas as pd
import xarray as xr
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from matplotlib.colors import LinearSegmentedColormap, Normalize
from matplotlib.colorbar import ColorbarBase

# カラーマップの作成
colors = ["blue", "#ffffe1", "red", "black"]
nodes = [0.0, 0.2, 0.6, 1.0]
TC_cmap =  LinearSegmentedColormap.from_list("tccmap", list(zip(nodes, colors)))

# データの読み込み
os.chdir("/mnt/c/Users/hoge/ONEFIFTY")
ds = xr.open_dataset("GCM60_RCP85_tmpchange_sfc_avr_year.nc")
relief = plt.imread("../natural_earth/MSR_50M/MSR_50M.tif")

# 正距円筒図法のマップを作成
for h_date,h_arr in ds.groupby("time"):
    fig = plt.figure(figsize=(24, 12))
    ax = fig.add_subplot(1,1,1,projection=ccrs.PlateCarree())
    h_arr["temperature"].plot.imshow(ax=ax, cmap=TC_cmap, vmin=-5.0,vmax=20.0,
                                     transform=ccrs.PlateCarree(),add_colorbar=False,add_labels=False)
    ax.imshow(relief,alpha=0.2, cmap="gray",extent=(-180,180,-90,90))
    ax.coastlines(resolution='50m',linewidth=.1)
    ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, linewidth=1, alpha=0.8)

    plt.axis('tight')
    plt.axis('off')
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1)

    #保存
    plt.savefig(pd.to_datetime(h_date).strftime("./TCanu_latlon_png/anu_%Y.png"))
    plt.clf()
    plt.close()

# カラーバーを描く
fig = plt.figure(figsize=(1,7))
ax = fig.add_axes([0.05, 0.05, 0.9, 0.9])
norm = Normalize(vmin=-5.0, vmax=20.0)
cb = ColorbarBase(ax, orientation='vertical', 
                           cmap=TC_cmap,norm=norm, extend='both')
cb.ax.yaxis.set_tick_params(color="white")
cb.ax.set_yticklabels(["Cold","0","","","","Warm"])
cb.outline.set_edgecolor("white")
plt.setp(plt.getp(cb.ax.axes, 'yticklabels'), color="white",fontsize=20)

fig.patch.set_facecolor("black")   

plt.savefig('TC_cb_var.png', bbox_inches='tight', facecolor="black")

アニメーション作成

アニメーションロゴの作成ではGIMP上でGIFを作成しましたが、GIFを使うと256階調のインデックスカラーに変換されてしまい、グラデーションがうまく出ない・・・

qiita.com

そこで1フレームずつpngを作成し、連番画像からffmpegを使ってmp4を作ることにしました。

タイトル−キャプション画像の作成

GIMPを用いて、アニメーション動画の背景とタイトル・キャプションを作成し、pngで保存しておきます。

フレーム画像の作成

作成したマップをGIMPを使って地球儀に変換し、タイトル−キャプション画像に貼り付けてからpngに書き出します。ファイル名等の文字列の扱いはPythonでやったほうが楽なので、一連の処理をするスプリクトを作成し、Pythonで実行します。

処理スクリプト

1枚のマップを読み込んで地球儀に貼り付け、回転角を調節してから背景画像に貼り付け、pngに書き出すScript-fu

(define (latlonmap-to-flame base_png inFile outDir yr_str rotate1 rotate2 sq-width)
    (let* 
        (
            ;変数の設定
            (base_img (car (gimp-file-load RUN-NONINTERACTIVE  base_png  base_png)))
            (globe_img (car (gimp-file-load RUN-NONINTERACTIVE inFile inFile)))
            (src-layer (car (gimp-image-get-active-layer globe_img)))
            (globe_layers (cadr (gimp-image-get-layers globe_img)))
            (rotate_y rotate1)
            (new_globelayer 0)
            (l 0)
            (l_name "fl_")
            (layer_id 0)           
        ) 

        ;レイヤー名(fl-{year}_{number})を返す関数
        (define (res_layer_name yr_str layer_id)
            (let* (
                (s (string-append "0000" (number->string layer_id)))
                (n (string-length s))
                )
                (string-append "fl-" yr_str "_" 
                    (substring s (- n 2) n)
                )
            )
        )

        ;球体に変換したレイヤを背景に結合し、pngで書き出す関数
        (define (create_flame l)
            (let* (
                (new_flame (car (gimp-image-duplicate base_img )))
                (new_flamelayer (car (gimp-layer-new-from-drawable l new_flame)))
                (l_name (car (gimp-layer-get-name l)))
                (outFile (string-append outDir "/" l_name ".png"))
                (tx_layer 0) 
                (d_able 0)
                )
                (gimp-image-add-layer new_flame new_flamelayer 0)
                (gimp-layer-set-offsets new_flamelayer 50 0)
                (set! tx_layer (car (gimp-text-fontname new_flame new_flamelayer 10 0 
             yr_str -1 TRUE 100 POINTS "Rounded-L M+ 2p Heavy")))
                (gimp-floating-sel-to-layer tx_layer)
                (gimp-text-layer-set-color tx_layer '(255 255 255))
                (gimp-image-merge-visible-layers new_flame CLIP-TO-BOTTOM-LAYER )
                (set! d_able (car (gimp-image-get-active-drawable new_flame) ))
                (file-png-save-defaults RUN-NONINTERACTIVE new_flame d_able outFile outFile)
                (gimp-image-delete new_flame)
            )
        )

        ;マップを縦横同じサイズの画像に変換
        (gimp-image-scale globe_img sq-width sq-width)

       ;球体に変換し0.5刻みで回転させたレイヤを作成
        (while (< rotate_y rotate2) 
            (set! new_globelayer (car(gimp-layer-copy src-layer TRUE)))
            (set! l_name (res_layer_name yr_str layer_id))
            (gimp-layer-set-name new_globelayer l_name)
            (gimp-image-add-layer globe_img new_globelayer 0)
            (plug-in-map-object 1 globe_img new_globelayer 1 0.5 0.5 20.0 0.5 0.5 0.0 
            0 0 0 0 0 0 0 rotate_y 35 1 '(255 255 255) 0.5 0.5 10 0 0 1 
            0.4 0.9 0.4 0 27.0 TRUE FALSE FALSE TRUE 0.48 0.5 0.5 0.5 
            1.0 -1 -1 -1 -1 -1 -1 -1 -1)
            (set! rotate_y (+ rotate_y 0.5))
            (set! layer_id (+ layer_id 1))
        )
        (gimp-image-remove-layer globe_img src-layer)

        ;レイヤごとに背景と結合し、保存する。
        (set! globe_layers (cadr (gimp-image-get-layers globe_img)))
        (for-each 
            (lambda (l)
                (create_flame l)
            )
            (vector->list globe_layers)
        )

        (gimp-image-delete base_img)
        (gimp-image-delete globe_img)

    )
)

;GIMPへの登録設定
(script-fu-register
 "latlonmap-to-flame"                        ;func name
 "latlonmap-to-flame"                                  ;menu label
 "月平均値アニメーションのフレームを作成" ;description
 "ccilobo"                             ;author
 "copyright 2023, ccilabo"        ;copyright notice
 "Sep 14, 2023"                          ;date created
 ""                     ;image type that the script works on
 )

(script-fu-menu-register "latlonmap-to-flame" "<Image>/Script-Fu/CCILabo")
1年づつ処理する

pythonのsubprocessを用いて 1ファイルづつ処理し、最後にファイル名に連番を振ります。

# ライブラリの読み込み
import os
import pathlib
import numpy as np
import pandas as pd
import subprocess

# 処理ファイルのリストを作成
maps_df = pd.DataFrame({"f_path":list(pathlib.Path("/mnt/c/Users/hoge/ONEFIFTY/TCanu_latlon_png").glob("*.png"))})
maps_df["f_name"] = maps_df["f_path"].map(lambda x: x.name)
maps_df["yr_str"] = maps_df["f_name"].str.extract("anu_(\d{4}).png")

# 30年で1回転(1年あたり24フレーム)するように開始角:終了角を設定する
maps_df["rotate1"] = maps_df.index.values*24%720/2 - 180
maps_df["rotate2"] = maps_df.index.values*24%720/2 - 168

# 1ファイルずつ処理
for ind,hrow in maps_df.iterrows():
    cmd_str = 'gimp-2.10 -d -i -b \'(latlonmap-to-flame \"/mnt/c/Users/hoge/ONEFIFTY/annually_animebase.png\" \"{0}\" \"/mnt/c/Users/hoge/ONEFIFTY/TCanu_flame_png\" \"{1}\" {2} {3} 918)\' -b \'(gimp-quit 0)\''.format(str(hrow["f_path"]),hrow["yr_str"],hrow["rotate1"],hrow["rotate2"])
    subprocess.run(cmd_str,shell=True)

# ファイル名を連番に変更
flame_df = pd.DataFrame({"f_path":list(pathlib.Path("/mnt/c/Users/hoge/ONEFIFTY/TCanu_flame_png").glob("*.png"))})
flame_df["f_stem"] = flame_df["f_path"].map(lambda x: x.stem)
flame_df = flame_df.sort_values("f_stem").reset_index(drop=True)
flame_df["f_serial"] =flame_df.apply(lambda x: "fl-{:04}.png".format(x.name),axis=1)

for ind,hrow in flame_df.iterrows():
    hrow["f_path"].rename(hrow["f_path"].parent / hrow["f_serial"])
動画に変換

ffmpegを使ってmp4動画に変換します

qiita.com

cd /mnt/d/Users/kazuh/ONEFIFTY/
ffmpeg -framerate 30 -i ./TCanu_flame_png/fl-%04d.png -vcodec libx264 -pix_fmt yuv420p -r 60 TC_globe_annualy_150yr.mp4

mp4動画ができました。



youtu.be

「全球及び日本域150年連続実験データ」の全球60km150年連続実験データの温度変化を”回る地球儀”上でアニメーションする方法を紹介しました。試してみてくださいね~。

※本記事では文部科学省「統合的気候モデル高度化研究プログラム」において、地球シミュレータを用いて作成されたデータを使用しました。