ベクトル形式の天気図から前線記号などを抜き出す(気象庁SVG形式天気図)
1. ベクトル形式の天気図
気象庁ではベクトル形式の天気図を発表しています。ベクトル形式というのは天気図として描画する要素(前線記号、高気圧・低気圧記号など)の通過する座標などをテキストで記載した形式です。描画ツール側でその書式を理解して図として表現します。
ピクセル単位に色が定義されているPNG形式などでは、図を拡大していくとボケていってしまいます。
これに対してベクトル形式では表示するサイズに応じて図形を描画することができるので、クオリティの高い図が作画できます。また、描画要素ごとに情報などを得ることができるため、目的に応じてこれを取り出すことができます。
例えば、私は天気図を気象画像のセマンティクセグメンテーションのマスク画像として使用しています。
これまでPNG形式のデータのピクセル値を利用していましたが、同じ色で描画されていた異なる要素が区別できないという悩みがありました。そこでベクトル形式データからマスク画像を作成してみました。
例えば日本の気象庁のデータでは下記のようになります。
左:ダウンロードした天気図 右:気象要素のみ切り出した画像
この投稿は、こういったベクトル形式の天気図データの取り扱いに関するものです。
2.気象庁のSVG形式の天気図
2.1 SVG形式天気図のありか
気象庁はベクトル形式の天気図として、SVG形式の天気図を公開しています。
このページから、目的の日の天気図をクリックすると、この様に日毎の天気図が取得できるページに進むことができます。それぞれの天気図の下に、「SVG」という表記があり、これをクリックすることでSVG形式の天気図を得ることができます。
ファイルはダウンロードすると圧縮形式となっていますが解凍すると、テキスト形式のファイルです。
今回対象とするのは、「速報天気図」、略称SPASです。
2.2 SVG形式天気図の書式
気象庁による書式の解説がここにあります。
この書式解説を参考にしながら、セマンティックセグメンテーションを行うため、このSVG形式の天気図ファイルから、前線や記号を抜き出してみます。
(1)ファイルの全体構造
XML形式での記載となっています。
SVGタグで囲まれた中に、グループタグ(で囲まれた書式指定)がいくつかあります。
重要なところは2箇所あります。
1)図の全体の座標
viewBoxで座標(-170,-30) (1105, 780)を左下隅、右上隅とする長方形が指定されています。
実際に表示されている天気図の最も外側の枠線は左下隅(0,0)右上隅(795.259,720.378)という指定ですので、実質的な描画範囲は(0,0)(795,720)という長方形です。
図のサイズは、height, widthのキーワードによって指定しています。高さ257mm, 幅364mmという指定がなされています。
2)気象要素の指定
気象要素については、id='WeatherChart'
で指定されたグループタグのノードの、さらに子ノードに注目します。この中に、class='high'
とかclass='warmFront'
といった指定になっているグループタグがあります。
つまり最上位から見ると2段目の子ノードの、特定のclassに着目します。
これを元に後にSVGを解読します。
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<svg baseProfile='tiny'
height='257mm'
version='1.2'
viewBox='-170 -30 1105 780' <-- viewBoxに描画範囲が記載されています
width='364mm'
xmlns:xlink='http://www.w3.org/1999/xlink'
xmlns='http://www.w3.org/2000/svg'>
<title dateTime='2023-09-29T18:00:00Z' kind='SPAS'/>
(省略)ここは特に見る必要はありません
<g id='map'>
(省略)地図に関する情報(海岸線、緯度経度線など)。ここも特に見る必要はありません
</g>
<g id='plots'/>
<g id='weatherChart'>
天気図情報の部<<<ここに着目します>>>
<g class='contour'> 等圧線情報
(省略)気象要素の取り出しとしては等圧線は無視します
</g>
<g class='high'> 高気圧情報
(中略)
</g>
<g class='low'> 低気圧情報
(中略)
</g>
<g class='warmFront'> 温暖前線
</g>
<g class='coldFront'> 寒冷前線
</g>
<g class='occFront'> 閉塞前線
</g>
</g>
(省略)
<g id='label'> ここも無視できます
(省略)
</g>
</g>
</svg>
(2) 高気圧・低気圧などの記号
最上位から見ると2段目の子ノードのclassが下表のものです。
# | 擾乱要素 | グループタグ |
---|---|---|
1 | 高気圧 | <g class='high'> ここに記載されるタグで線分の通過点を指定 </g> |
2 | 低気圧 | <g class='low'> ここに記載されるタグで線分の通過点を指定 </g> |
3 | 台風 | <g class='typhoon'> ここに記載されるタグで線分の通過点を指定 </g> |
4 | 熱帯低気圧 | <g class='td> ここに記載されるタグで線分の通過点を指定 </g> |
それぞれの要素を示すノードの下に、さらにいくつかのグループがあって、具体的な記号や文字を描画する指定がなされています(高気圧低気圧の文字、中心位置を示す「✖️」マーク、台風の番号、進行方向を示す矢印や速度)。
気象の世界ではこれら高低気圧や台風、熱帯低気圧のことを「擾乱」といいます。
(3) 前線記号
前線記号は次の4種類があります。
# | 要素 | グループタグ |
---|---|---|
1 | 寒冷前線 | <g class='coldFront'> 線分や円弧を描画する指定とその座標を指定 </g> |
2 | 温暖前線 | <g class='warmFront'> 線分や円弧を描画する指定とその座標を指定 </g> |
3 | 閉塞前線 | <g class='occFront'> 線分や円弧を描画する指定とその座標を指定 </g> |
4 | 停滞前線 | <g class='stnFront> 線分や円弧を描画する指定とその座標を指定 </g> |
こちらも、それぞれの要素を示すノードの下に、さらにいくつかのグループがあって、線や記号を描画する指定があります(前線の線、三角や丸による前線記号)。
2.3 記号抜き出し
オリジナルの天気図から前線と擾乱を示す記号を抜き出してみます。
元のSVGに対して以下の操作をすることを意味します。
・地図(海岸線・湖などの線分、緯度経度線や数値)を削除
・等圧線を削除
・ヘッダ部のサイズを必要に応じて変更
・前線、擾乱は必要な修正を加える
SVGの読み込みのためにはxml.dom.minidomから、Parserをimportしておきます。
書き出しは、必要な修正のみ行なってなるべくそのまま転記するようにします。
(1)ヘッダ部分
修正はheight, widthをmm指定ではなくビクセル指定に変更します。
最終的に256x256の画像にしたいので、heigth=256, width=256と指定します。
またviewBoxについても、必要な範囲に限定します。
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<svg baseProfile='tiny' (変更前:オリジナル)
height='257mm' (mm指定)
version='1.2'
viewBox='-170 -30 1105 780'
width='364mm' (mm指定)
xmlns:xlink='http://www.w3.org/1999/xlink' xmlns='http://www.w3.org/2000/svg'>
<svg baseProfile='tiny' (変更後)
height='256' (ピクセル数で指定)
version='1.2'
viewBox='29 18 707 664' (必要なviewBoxに変更)
width='256' (ピクセル数で指定)
xmlns:xlink='http://www.w3.org/1999/xlink' xmlns='http://www.w3.org/2000/svg'>
<g fill='none' fill-rule='evenodd' stroke='black' stroke-linecap='round'
stroke-linejoin='round' stroke-width='1'>
<g id='weatherChart'>
...これ以降に本体描画
</g>
</g>
</svg>
viewBox
の変更による描画範囲の変化は下記の様になります。
外側の線がオリジナルの天気図の範囲です(viewBoxの指定よりも小さな(0,0)(795,720)という長方形)。
内側の線の範囲が修正後の範囲(私が処理なので切り出した)範囲です。
(2) 本体の読み込み
本体のノードの読み取りは、XMLパーサを使うと便利です。
最上位から見ると2段目の子ノードのclassを取り出してclass
名を元に処理を行います。
目的のclass
であるならば、さらにその子ノードを取り出していきます。
from xml.dom import minidom
from xml.dom.minidom import parse
dom = parse(in_file) #ファイルの読み込み
nodeList = dom.getElementsByTagName("g") #タグがgである子ノードを選ぶ
"""
最上位から見ると2段目の子ノード(1段目の子ノードの、タグ名が'g'である子ノード)
について、class名を調べて処理を分岐させる
"""
for node in nodeList: #1段目子ノード群について
for cls in node.getElementsByTagName('g'): #グループタグを取り出す
attr = cls.getAttribute('class') #クラス名を取得
#class名がwarmFrontの場合(温暖前線の場合)
if attr == "warmFront" :
# さらに子ノード群を取得する
for p in cls.childNodes:
# ノード名がpathの場合class名を抜き出す
if p.nodeName == "path" :
ccls = p.getAttributeNode('class').nodeValue
if ccls == "redLine" : #赤ラインの座標等を抜き出す
"""
<path class='redLine' d='M309.468,175.767 L309.958,175.226 ...
L382.113,208.788' stroke-width='2'/>
Mx1,y1 Lx2,y2 Lx3,y4 ... 形式で、
始点(x1,y1)、通過点(x2,y2), (x3,y3)...に線を描画されている
"""
#必要な処理を行い、書き出しファイルに転記する
if ccls == "redSymbol" : #赤色の記号の指定を抜き出す
"""
<path class='redSymbol' d='M319.591,168.948
A6.1698 6.1698 0 0,1 331.929 168.774
L331.207,168.664 L331.207,168.664...
L319.591,168.948' stroke-width='2'/>
Mx1,y1, ARx Ry 0 0,1 x1e y1e Lx2,y2 ... 形式で、
始点(x1,y1) 円弧(半径Rx,Ry, 終点x1e,y1e)、通過点(x2,y2)
を指定して、前線記号が記載されている
"""
#必要な処理を行い、書き出しファイルに転記する
気象庁のSVG形式天気図では、前線は、SVGのd=' '
の指定を用いて、Mx1,y1 Lx2,y2 Lx3,y3...
というように、M
で始点(x1,y1)
を指定して、L
で次の位置(x2,y2), (x3,y3),...
を指定して線分を書かせています。前線記号は、A
によって描かせる円弧と、線分によって形を指定しています。
a) 前線記号に関する処理
今回の処理では、前線記号や線に関するスタイル指定を追加しました。
オリジナルのSVG形式天気図では、ヘッダ部でクラス毎の描画を纏めて定義しています。
これを各シンボルを描画するタグに直接色や線幅の指定する様に変更しました。
変更前:
<defs/><style type='text/css'><![CDATA[
...
.redSymbol { stroke:#ff0000; fill:#ff0000;}
.blueSymbol { stroke:#0000ff; fill:#0000ff;}
.occSymbol { stroke:#ff00ff; fill:#ff00ff;}
.redLine { stroke:#ff0000; fill:none;}
.blueLine { stroke:#0000ff; fill:none;}
.occLine { stroke:#ff00ff; fill:none; }
...]]></style>
<g class='warmFront'>
<path class='redSymbol'
d='M621.729,153.771 A6.10748 6.10748 0 0,1 632.598 159.346
... L621.729,153.771 L621.729,153.771' stroke-width='2'/>
<path class='redSymbol'
d='M664.327,177.683 A6.10808 6.10808 0 0,1 675.031 183.57
....L664.327,177.683 L664.327,177.683' stroke-width='2'/>
<path class='redLine'
d='M610.73,148.457 L611.738,148.922
....L758.294,233.97' stroke-width='2'/>
変更後:
<!-----スタイル指定部分を削除---->
<g class='warmFront'>
<!-----末尾に線幅等の指定を追加---->
<path class='redSymbol'
d='M621.729,153.771 A6.10748 6.10748 0 0,1 632.598 159.346
... L621.729,153.771 L621.729,153.771' stroke="#ff0000" fill="#ff0000"/>
<path class='redSymbol'
d='M664.327,177.683 A6.10808 6.10808 0 0,1 675.031 183.57
....L664.327,177.683 L664.327,177.683' stroke="#ff0000" fill="#ff0000"/>
<path class='redLine'
d='M610.73,148.457 L611.738,148.922
....L758.294,233.97' stroke="#ff0000" stroke-width='2'/>
寒冷前線、閉塞前線についてもこれと同様の処理を行います。
停滞前線については、オリジナルの天気図では赤色と青色の線・記号が混在しますが、別の色1色に変更します。
変更前
<defs/><style type='text/css'><![CDATA[
...
.redSymbol { stroke:#ff0000; fill:#ff0000;}
.blueSymbol { stroke:#0000ff; fill:#0000ff;}
.redLine { stroke:#ff0000; fill:none;}
.blueLine { stroke:#0000ff; fill:none;}
...]]></style>
<g class='stnFront'>
<path class='redSymbol' d='...' stroke-width='2'/>
<path class='blueSymbol' d='...' stroke-width='2'/>
変更後
<g class='stnFront'>
<!---スタイル定義を削除、redSymbol, blueSymbol共に色を#00ff00で記載----->
<path class='redSymbol' d='...' stroke="#00ff00" fill="#00ff00"/>
<path class='blueSymbol' d='...' stroke="#00ff00" fill="#00ff00"/>
b) 高気圧・低気圧等の記号
この日の天気図には全ての記号が登場しています。
これを見ると、高気圧、低気圧等の記号には以下があります。
# | 種別 | 気象庁ドキュメントの説明 | 補足 |
---|---|---|---|
1 | 記号本体 | 擾乱マーク | 「高」、「低」、「熱低」、「台13号」など。 今回の抜き出し対象 |
2 | 記号背景 | 擾乱マークの背景 中心気圧値の背景 移動速度の背景 |
地図に描画する際の背景 地図を消去しているので使用しない |
3 | 補助文字 | 中心気圧値 移動速度値 |
中心気圧値「1018」、移動速度「20km/h」 「ほとんど停滞」など |
4 | 補助記号 | 移動方向 中心を示すXマーク |
移動方向を表す矢印、高低気圧や台風の 中心位置を示す✖️。 その位置を#1記号本体の位置と入れ替える |
5 | class | high:高気圧 low:低気圧 llow:低圧部 td:熱帯低気圧 typhoon:台風 |
今回はllowは使用しない |
これらの記号はすべてSVGの線分(polyline
)によって表されています。
今回の処理の仕様的には、
・高気圧・低気圧等の「記号本体」を残して他は消去
・ただし、「記号本体」の描画位置を、中止位置を示す「補助記号」”✖️”の位置に変更
というようにします。
SVGの高気圧を表現はざっくり言うとclass='high'
のノードが担当していて、
・class='markH'
・class='markBK'
・class指定無し
という3種類のノードが存在していて、これらが文字や記号の数分登場します。
それぞれのノードではpolyline
を用いて何かの文字や図形を表現しています。
このせいで、それぞれのノードはかなりごちゃごちゃとpolyline
と数値が並んでいます。
<g class='high'>
<g fill='none' stroke-width='2' (ここがXマークを示す「polyline2つ」)
transform='matrix(0.980091,0.198546,-0.198546,0.980091,308.124,290.02)'>
(matrixの最後の数字2個が中心位置)
<polyline fill='none' points='-4.24,-4.24 4.24,4.24 '/>
<polyline fill='none' points='-4.24,4.24 4.24,-4.24 '/>
</g>
<g class='markBK' (背景記号)
transform='matrix(0.979804,0.199963,-0.199963,0.979804,309.889,276.498)'>
<polyline fill='none' points='-10.8,-20 10.4,-20 '/>
<polyline points='6.25849e-07,-20 6.25849e-07,-22.4 '/>
<polyline fill='none'
points='-10,-0.4 -10,-10.4 8.8,-10.4 9.6,-9.6
9.6,-1.2 8.8,-0.4 8,-0.4 5.2,-1.2 '/>
<polyline fill='none'
points='-6.8,-13.2 6.4,-13.2 6.4,-17.2 -6.8,-17.2 -6.8,-13.2 '/>
<polyline fill='none'
points='-6,-7.2 6,-7.2 6,-2.8 -6,-2.8 -6,-7.2 '/>
</g>
<g class='markH' (擾乱記号)
transform='matrix(0.979804,0.199963,-0.199963,0.979804,309.889,276.498)'>
<polyline fill='none' points='-10.8,-20 10.4,-20 '/>
<polyline points='6.25849e-07,-20 6.25849e-07,-22.4 '/>
<polyline fill='none'
points='-10,-0.4 -10,-10.4 8.8,-10.4 9.6,-9.6
9.6,-1.2 8.8,-0.4 8,-0.4 5.2,-1.2 '/>
<polyline fill='none'
points='-6.8,-13.2 6.4,-13.2 6.4,-17.2 -6.8,-17.2 -6.8,-13.2 '/>
<polyline fill='none' points='-6,-7.2 6,-7.2 6,-2.8 -6,-2.8 -6,-7.2 '/>
</g>
<g class='markBK' (略)>
<polyline (略) />
</g>
<g fill='none' stroke-width='2'
transform='matrix(0.980031,0.198846,-0.198846,0.980031,304.359,307.542)'>
<polyline (略)/>
...
</g>
<g class='markBK' (略)/>
...
<g fill='none' stroke-width='2' (ここがXマークを示す「polyline2つ」)
transform='matrix(0.821536,-0.570156,0.570156,0.821536,759.02,85.8867)'>
(matrixの最後の数字2個が中心位置)
<polyline fill='none' points='-4.24,-4.24 4.24,4.24 '/>
<polyline fill='none' points='-4.24,4.24 4.24,-4.24 '/>
</g>
<g class='markBK'
transform='matrix(0.82483,-0.56538,0.56538,0.82483,744.336,70.1699)'>
(略)
</g>
<g class='markH'
transform='matrix(0.82483,-0.56538,0.56538,0.82483,744.336,70.1699)'>
<polyline fill='none' points='-10.8,-20 10.4,-20 '/>
<polyline points='6.25849e-07,-20 6.25849e-07,-22.4 '/>
<polyline fill='none'
points='-10,-0.4 -10,-10.4 8.8,-10.4 9.6,-9.6
9.6,-1.2 8.8,-0.4 8,-0.4 5.2,-1.2 '/>
<polyline fill='none'
points='-6.8,-13.2 6.4,-13.2 6.4,-17.2 -6.8,-17.2 -6.8,-13.2 '/>
<polyline fill='none' points='-6,-7.2 6,-7.2 6,-2.8 -6,-2.8 -6,-7.2 '/>
</g>
<g class='markBK' fill='none'
transform='matrix(0.870039,-0.492982,0.492982,0.870039,696.704,78.6036)'>
(略)
</g>
</g>
地図を削除しているので、背景塗りつぶしは不要です。つまりclass='markBK'
は無視します。
記号本体「高」を表しているのは、class='markH'
となっているグループタグですので、ここが主要な抜き出し対象です。
そのほか、補助記号や補助数値も使用しません。ただ記号本体「高」などの位置を、中心位置を示す補助記号✖️の位置と置き換えます。そこでこの補助記号がSVGの中のどこなのか、ズバリ指定されているわけでもなく、調べる必要があります。
✖️がどの「class
指定無しノード」なのか、気象庁ドキュメントを見ても、「擾乱の中心位置を示すXマークを polyline2つで表現する。」としか記載がありません。
おそらく、ひとつの擾乱に対して、まず「中心位置を示すXマークをpolyline2つで表現する。」と言う意味かと思われます。他の文字や記号は複雑なので、polyline2つのグループが出てきたら新しい擾乱の始まりとであることが保証されているのだと思われます。
それでも、補助記号や補助文字が全て必ず登場するとは限らないので、
<polyline fill='none' points='-4.24,-4.24 4.24,4.24 '/>
<polyline fill='none' points='-4.24,4.24 4.24,-4.24 '/>
この文字列が出現したら新しい擾乱記述の始まりであると判断して、matrix
の引数に示された中心位置を取得する様にします。
✖️の位置は、polylineによって、matrix指定子が指定する中心座標からの相対位置で表現されているので、この文字列自体は毎回同じになります。
def getPosition(str_matrix):
#matrix指定文字列から最後の2フィールドを切り出す
str1 = str_matrix.replace("matrix","").replace(","," ")
str2 = str1.replace("(","").replace(")","")
s_list = str2.split(" ")
return s_list[4], s_list[5]
def replace_Position(str_matrix, pos1, pos2):
#matrix指定文字列から最後の2フィールドを切り出す
str1 = str_matrix.replace("matrix","").replace(","," ")
str2 = str1.replace("(","").replace(")","")
s_list = str2.split(" ")
str_ret =
"matrix(" + s_list[0] + "," + s_list[1] + "," \
+ s_list[2] + "," + s_list[3] \
+ "," + str(pos1) + "," + str(pos2) + ")"
return str_ret
# Xを示す文字列を定義しておく
cross_magick_numbers = "-4.24,-4.24 4.24,4.24 "
dom = parse(in_file) #ファイルの読み込み
nodeList = dom.getElementsByTagName("g") #タグがgである子ノードを選ぶ
#子ノードの、さらにタグ名が'g'である子ノードについて、class名を抜き出す
for node in nodeList:
for cls in node.getElementsByTagName('g'):
attr = cls.getAttribute('class')
#class名がwarmFrontの場合(温暖前線の場合)
if attr == "warmFront" :
(省略)
#class名がhighの場合(高気圧の場合)
if attr == "high":
for p in cls.getElementsByTagName('g'):
c_attr = p.getAttribute('class')
if c_attr == "": #class指定無し(補助記号、補助文字描画の部分)
getstr1 = p.getAttributeNode('transform').nodeValue
ppwk = p.getElementsByTagName('polyline')
if len(ppwk) > 0: # polylineの数がゼロよりおきいとき
getstr_wk = ppwk[0].getAttributeNode('points').nodeValue
if getstr_wk == cross_magick_numbers: # xの文字列と比較
pos1, pos2 = getPosition(getstr1) # xの位置を取得
else:
continue
if c_attr == "markH": # classが擾乱記号の場合
# transformの指定文字列を取得
getstr1 = p.getAttributeNode('transform').nodeValue
# 擾乱記号の記載位置を変更
str_rep = replace_Position(getstr1, pos1, pos2)
(必要に応じてtransformをファイルに書き出す)
# 擾乱記号の文字を描画
for pp in p.getElementsByTagName('polyline'):
getstr2 = pp.getAttributeNode('points').nodeValue
(必要に応じてファイルに書き出す)
高気圧、低気圧、台風、熱帯低気圧に対してこのような処理を行います。
この結果、以下のように記号が切り出されました。
これが目的の画像です。
停滞前線が1色になっています。
また、擾乱記号の位置が微妙に変わっているのが前線との位置関係でわかるかと思います。
セマンティックセグメンテーションのマスク画像として用いる時は、前線の種類、擾乱の種類毎に異なるクラスとして使用します。
3. まとめ
気象庁が公開しているベクトル形式の天気図(SVG形式)から、XMLparserであるPythonライブラリを用いて、気象要素(前線、擾乱)を抜き出しました。SVGから擾乱中心位置を取得する方法も掲載しました。
なお今回は補助記号(矢印など)や数値(中心気圧、速度)については無視しましたが、これが線分集合になっているだけでなく、SVGデータから数値や文字、方向などのデータとして取り出せる様になっていると、機械学習の利用などを想定した場合は意義がある場合もあるとも感じました。
次回は、アメリカ気象局による天気図について投稿予定です。