🗺️

なぜ地理院地図の標高タイルは海を赤く染めるのか? 無効値(128, 0, 0)に隠された合理性

に公開
6

1.無効値の合理性と地理情報のモヤモヤ

疑問

地理情報に携わる諸兄諸姉、地理院地図の標高タイルに、疑問を抱いたことはありませんか。

  • 「なぜ海や湖は\color{red}\textsf{赤い (R, G, B)=(128, 0, 0)}のか?」
  • 「黒(0, 0, 0)ではない理由は?」
  • 「値128はどこから来るのか?」

かつては、私も男子中学生のようにモヤモヤしたものでした。

モヤモヤするだけではない

しかも、このモヤモヤは、気持ちだけでなく現実的な問題でもあります。その問題の一例を示すと、次のようなものです。

例えば、日本列島の立体地形図(https://youtu.be/c2vfUVLZJss)を描画するとします。陸地には地理院地図の標高データが使えますが、海の深度データはないので他のデータ(例えばJODCの水深)を組み合わせることになります。

その組み合わせの際、無効値が\mathbf{0}ならば標高と水深を単純に加算できるところ、(128, 0, 0)の場合には無効化する余計な条件分岐が必要になってしまいます。このような条件分岐は、GPU処理が流行りの昨今においては、最適化の妨げとなり得ます。

記事が目指すもの

本記事では、この一見、不合理に見える \color{red}\textsf{(128, 0, 0)} という無効値の謎を解き明かし、その背後にある合理性を詳らかにします。記事を読み終える頃には、標高タイルに対する見方が変わる、やもしれません。

先に結論

勿体を付けずに先に結論を述べますと、
「標高タイルの標高データは、RGB値を256進数で位取り変換した24bit符号なし整数を、2の補数表現により符号付き整数として解釈してスケーリングしたものであり、その2の補数表現の余り(一般には最小負数) である(128, 0, 0)が無効値に活用されたもの」
と言うことになります。本記事の本旨は、この結論を、わかりやすく詳説することにあります。

なお、上記結論は、独自の考察によるものであり、本記事の内容以外には根拠がありません。そのため、的が外れている可能性も否定できません。「真実はいつもひとつ!」、とはいきませんが、合理的な推論に基づく技術的な推理としてお楽しみください。


2. 標高タイルのPNG形式とは?基本をおさらい

まずは、用語と仕様を国土地理院のサイトから「抜粋」します。

  • 地理院地図(https://maps.gsi.go.jp
     地形図、写真、標高、地形分類、災害情報など、国土地理院が捉えた日本の国土の様子を発信するウェブ地図です。
  • 地理院タイル(https://maps.gsi.go.jp/development/ichiran.html)
     国土地理院が配信するタイル状の地図データです。
  • 標高タイル(https://maps.gsi.go.jp/development/demtile.html)
     地図タイルと同一のタイル座標とピクセル座標を用いてデータを整備しており、カンマ
    区切りのテキスト形式とPNG形式があります。
  • PNG形式
     24ビットカラーのPNG形式で...ピクセルの画素値(RGB値)から、当該ピク
    セル座標の標高値が算出できます。

画素値(RGB値)から標高値h(m)の計算式は下記のとおりです。

  • x = 2^{16}R+2^8G+B
  • x < 2^{23}の場合: h = xu
  • x = 2^{23}の場合: h = NA
  • x > 2^{23}の場合: h = (x-2^{24})u

uは標高分解能(0.01m)を表します。また、無効値は(R, G, B) = (128, 0, 0)です。

これらの式が何を意味しているのかを現時点で理解している必要はありません。ここでは、前提となる用語や計算式に齟齬がないように確認しただけです。

それでは、標高タイルの謎を解き明かしていきたいと思います。その鍵は、「位取り変換(radix conversion)」と「2の補数表現」です。


3. 謎を解く鍵その1:位取り変換

3種類の硬貨で金額を表現するには?

RGB値の説明の前に、より単純な硬貨の例で説明をします。

金貨、銀貨、銅貨の3種類の硬貨を各9枚づつ合計27枚を用意し、銅貨1枚を1円とします。

このとき、金額を一意的かつ網羅的に表現する方法を考えます。一意的とは金額から決まる硬貨の枚数が一通りであること(無駄がないこと)、網羅的とは範囲内の金額をすべて表せること(隙間がないこと)とします。

すぐに分かるように、その表現方法とは、

  • 銀貨1枚を銅貨10枚と等価(つまり、10円)
  • 金貨1枚を銀貨10枚と等価(つまり、100円)

とすることです。

なぜ、等価の単位が10枚であるかと言うと、例えば、銀貨1枚9円だと、9円を表すのに銀貨1枚または銅貨9枚の二通りで表せることになってしまい(一意的でない)、また、銀貨1枚11円だと、10円を表すことができないからです(網羅的でない)。

このように、「金貨X枚、銀貨Y枚、銅貨Z枚は、全部で何円になるか?」を求める方法が位取り変換になります(X, Y, Z \in[0, 9])。つまり、金貨を100の位、銀貨を10の位、銅貨を1の位と位取りして、金額(10進数)に変換する方法です。

このとき、単位となる枚数10は基数と呼ばれ、各位が取り得る状態(0枚から9枚)の数に一致します(これは、10進数の位取りになります)。また、各位は、硬貨の種類によって重み付けされており、その重みは基数の累乗となっています。

この位取り変換によれば、物理的な数(27枚)よりも多い数(0円から999円)を、無駄なくかつ隙間なく表現できます。

3チャンネルの8bitで符号なし整数を表現するには?

次に、RGB値から数値を表現する方法を考えます。その方法は、効率を考えるなら、位取り変換となります。

具体的には、まず、基数を求めます。各チャンネルの深さは8bitであり、その8bitで表せる値は0から255なので、基数は256になります。つまり、256進数の位取りになります。

次に位の重みを求めます。ここでは、Bを最小の位(硬貨の例では銅貨)、次の位(銀貨)をG、その次の位(金貨)をRと定めます。重みは、基数の累乗になることから、Gの重みが256、Rの重みが256\times 256になります。実際に、重みが255以下だと一意的にならず、256を越えると網羅的にならないことは、硬貨の例からも類推できるでしょう。

これが上述の計算式「x=2^{16}R+2^8G+B」の内容になります(256=2^8, 256\times 256=2^{16})。


4. 謎を解く鍵その2:2の補数表現と負数

補数と負数の二段階で行う

次に、負数の2の補数表現について説明します。説明では、負数を表現する方法を解説した後、その方法が標高タイルの仕様に合致することを示します。

また、説明は、グループ分けと正負の符号化の二つの段階に分けて行います。なお、24bitは桁が多くて説明が煩雑になるので、まずは3bitで説明して、後に拡張します。

符号なし整数を二つに分ける

第1の段階では、3bitの符号なし整数を、二つのグループに分けます(まだ符号を付けません)。これには色々な方法が考えられます。例えば、ビットの一つを識別に使う方法や、中央の値(3もしくは4)の上下で分ける方法です。ここでは、コンピュータで一般的な2の補数表現を使います。

3bitにおける2の補数表現

3bitの符号なし整数におけるxの2の補数とは、x+x'=2^3を満たすようなx'を言います(ただし、x'も3bitの符号なし整数に限ります)。

なぜx+x'=2^3なのか

x+x'=2^3について、これらのビット表現を用いて説明します。

まずは、xのビット表現(つまり3bit符号なし整数のビット表現)を以下に示します。

000 001 010 011 100 101 110 111
  0   1   2   3   4   5   6   7

次に、2^3=8のビット表現ですが、これは3bitでは表すことができません。1000であり4bitになります。

この1000の見方を少し変えてみましょう。1000を無理やり3bit符号なし整数として捉えるとどうなるでしょうか。最上位(左端)のビットは表現できないのでなかったことにしましょう。すると、4'b10003'b000となり、0と同じものとして捉えることがでます。

これを上式に当てはめると、

x\mathrel{++}x'=0

となります。

ただし、3bitしか考慮しない桁上がりを無視した加法になるので、'+'とは違う演算子'\mathrel{++}'を使いました。桁上がりを無視した加法によれば、x'xは互いに正負が逆の値になっています。

この逆の関係に基づいて正負のグループ分けを行います。

基になる数xと2の補数x'

それでは、具体的な値を求めていきます。x'=2^3-xから

x : 0, 1, 2, 3, 4, 5, 6, 7
x':  , 7, 6, 5, 4, 3, 2, 1

となります。これらからは以下のことがわかるでしょう。

  • x'についての2の補数はxである(特に、4についての2の補数は4である)
  • 0についてはx'が存在しない

グループ分け

これらx, x'を、正数の表現に使うグループPと負数の表現に使うグループNに分けます。x, x' を対にすると、

{0,  }, {1, 7}, {2, 6}, {3, 5},{4, 4}, {5, 3}, {6, 2}, {7, 1}

となります。

まず、「x'についての2の補数はxである」ことから、{x, x'}と{x', x}は区別の必要がありません。

{0,  }, {1, 7}, {2, 6}, {3, 5}, {4, 4}

次に、xx'は同じグループに入れることはできません。なぜなら、グループ分けを、上述したx\mathrel{++}x'=0の関係に基づいて行うからです。例えば、{1, 7}の1と7の両方を正数にしてしまうと、負数を表現できなくなってしまいます。

次に、正数グループPに入れる数はx, x'のうちより0に近い値が好ましいです。これは当たり前でしょう。例えば、{1, 7}の場合に、+1がなくて+7があるよりは、+1があって+7がないほうがより扱い易いでしょう。

以上から、正数の表現に使うグループPはP={1, 2, 3}, 負数の表現に使うグループNはN={7, 6, 5}になります。そして、相手がいない0と、自分自身が相手である4が「余り」になります。
 この「余り」の4が、正数表現のグループPと負数表現のグループNの境目になっている点が後に重要になります。

負数を割り振る

負数表現のグループNの負数を具体的に求めていきます。x\mathrel{++}x'=0と意味が合うように負数を割り振ると、

{1, 7}->{+1, -1}
{2, 6}->{+2, -2}
{3, 5}->{+3, -3}

となります。
 さらに、グループNの数x'を負数に変換する式を求めると、
 -1は、7の2の補数である1の正負を逆転したもの
 -2は、6の2の補数である2の正負を逆転したもの
 -3は、5の2の補数である3の正負を逆転したもの
であることから、

x'の負数 = -(2^3-x')= x'-2^3

となります。

以上をまとめると、

  • 4未満の数{1, 2, 3}は、正数{+1, +2, +3}である
  • 4を越える数{7, 6, 5}は、負数{-1, -2, -3}である
  • 0は、0である
  • 4は、正数と負数の境目となる
    となります。

24bitへの拡張

では、整数を3bitから24bitに拡張しましょう。

余りはだれだ?

まず、「余り」の4に相当する数を求めます。4の特徴は、4の2の補数が4であることでした。
つまり、x = x'です。これから

\begin{align*} x+x' = 2^{24}\\ 2x = 2^{24}\\ x = 2^{23} \end{align*}

となります。さらに、この「余り」の2^{23}が、「正数表現のグループPと負数表現のグループNの境目」になることから、

  • x < 2^{23}の場合、正数pは、p = x
  • x > 2^{23}の場合、負数nは、n = x-2^{24}

となります。

標高タイルの変換式

さらに、それぞれに標高分解能uを掛けると、上述の標高タイルの標高hの計算式
x < 2^{23}の場合: h = xu
x > 2^{23}の場合: h = (x-2^{24})u
が導かれます。
 また、境目となる2^{23}は、正数、負数のいずれにもならず、
x = 2^{23}の場合: h = NA
の如く、無効値として扱われます。而して、2^{23}を、(R, G, B)で表すと、

 10000000 | 00000000 | 00000000
    R          G          B

から、\color{red}\textsf{無効値(128, 0, 0)}になります。

謎解き

ここまでで、謎を解くは情報はすべて出揃いました。

まず、標高タイルの変換式は、RGB値を256進数の位取り変換で24bit符号なし整数に変換し、その符号なし整数を2の補数表現により符号付き整数に変換するものでした。その2の補数表現では、自身と自身の補数とが一致する「余り」2^{23}が存在していました。

ここで、「余り」を除いた、符号付き整数について考えます。「余り」を除いた場合、符号付き整数は、正数の個数と負数の個数が等しくなります。これは、符号付き整数が0を中心に対称に[-(2^{23}-1), 2^{23}-1]の値を取ることを意味しています。

したがって、これらの符号付き整数は、正負の偏りがなく、「余り」がなくても、標高を表すには過不足がないと考えられます。つまり、「余り」は標高を表す文脈においても、「余り」なのです。

そこで、「余り」である2^{23}を無効値として活用したのが標高タイルの仕様ではないか?、と言うのが私の推理になります。正誤は、読者の皆さんに委ねたいと思います。

なお、一般的な2の補数表現による符号付けでは、「余り」の数である4や2^{23}は計算式の都合から負数として処理されます(詳細は「7.補足」参照)。


5. この知見から何が得られるのか?

無効値の活用と意義

無効値の活用と意義について、もう少し深く考えてみましょう。

無効値が0の場合の問題

冒頭の「モヤモヤするだけではない」で、「無効値が0ならば」標高と水深を単純に加算できる、と述べました。では逆に、無効値を0にすると、どのような問題があるでしょうか?

無効値を0にしてしまうと、その変化は不可逆になります。一度0にしてしまうと、その0から「無効な値」と言う意味を取り戻すことはできません。

つまり、数値の0が、「標高データの0」と「無効な値」の二つの意味を持ってしまい、しかもそれらは区別できません

無効値の定義がないとどうなる?

さらに、無効値の定義そのものがないとどうなるでしょうか?

例えば、Terrain-RGBおよびTerrariumと言った標高タイルは、その仕様に無効値の定義を持ちません。これらの仕様で無効な値を表現する場合、有効な値のいずれかに「無効」を割り当てることになるでしょう。これは、その値に二つの意味(本来の標高と、無効)を持たせることなります。

しかし、問題はそれだけではありません。仕様で無効値が定義されていないことは、無効値の割り当てが標高タイルの作成者に依存することを意味します。つまり、有効な値の有効性が不安定になってしまいます。

無効値定義のアドバンテージ

これに対して、仕様で無効値が定義されている場合には、値の解釈から曖昧さが排除され、値は常に一つの意味しか持ちません

地理院地図の標高タイルの仕様(無効値の定義)は、無効な値を表現可能にするだけでなく、有効な値の一意性を担保していると言えるのではないでしょうか。

このように、地理院地図の標高タイルは、長期的な安定性と信頼性を備え、先を見据えた堅実な設計と言えるでしょう。

定義の重み

以上のように、「無効値の定義」は、一見些細に見えますが、実際には、仕様の明確性を大きく左右し、データの品質と信頼性に決定的な影響を与えることが分かります。

皆さんも、日々の仕事で何らかの「仕様」の設計を行ったり、あるいはそれに従って作業を行うことがあるでしょう。この事例は、「仕様」における定義の難しさと大事さを教えてくれるのではないでしょうか。

ナレッジ・シェアリングとレーゾンデートルのベネフィット

薀蓄マウント

同僚や後輩に知識をひけらかして気持ちよくなれます。

「実はさ、標高タイルの無効値って…」
「マウントうぜぇ(へぇーすごいですね)」

などのように、技術的な会話で盛り上がること間違いなしです。

独法師の存在意義

無効値をどのように決めたか振り返ってみましょう。無効値である2^{23}や4は、2の補数が自分自身であるような数でした。

これは、例えるなら、二人組を組んだときに、自分以外に相手にしてもらえず、(正数/負数)グループのいずれにも居場所がない悲しき存在です。

本来なら、このような存在は、いなかったものとして扱われるところです。

しかし、そのような惨めなものにも役割を与え、社会の一員として扱う、と言う希望を示したのが、標高タイルの無効値ではないでしょうか。

誰にも相手にされないような余りにも、無駄にならない場合がなくもない、そのような夢を騙る素晴らしい事例だと思います。


6. 結び:奥深きデータ形式の世界

標高タイルの無効値(128, 0, 0)の謎は、コンピュータサイエンスにおける二つの初歩的な概念、すなわち、位取り変換と2の補数表現とによって解き明かされました。

さらには、無効値を定義する意義も明らかになりました。地理院地図が海を赤く染めるのは、陸地を陸たらしめるためと言えるかもしれません。

私たちが普段、当たり前のように使っているデータ形式の裏には、このような巧妙で合理的な設計思想が隠されていることがあります。

この知見が、あなたの今後の地図開発やデータ解析において、新たな視点と深い理解をもたらす一助となればこれ幸いです。


7. 補足:一般的な2の補数表現における「余り」の扱い

一般的な変換式

上述しましたように、一般的には、nビット整数の「余り」の数2^{n-1}(上述の4や2^{23})は負数として扱われます。

その理由は、2^{n-1}を負数とすることで、nビットのビット列からnビット符号付き整数を求める式が
 

d = -b_{n-1}2^{n-1}+\sum_{i<n-1} b_i2^i

のように、簡素になるからです。

つまり、「余り」の数2^{n-1}を負数として解釈するようにすれば、一般的な2進数から10進数への変換式(ビットごとに重み付けを行う式)において、最上位ビットの重み付けをマイナスに変更するだけで、符号付き整数への変換式を求めることができます。

ねちっこく説明

天下り的で何を言っているのか分からない、と言う方のためにもう少し噛み砕いた説明をしましょう。ここでは、再び3bit整数を例にとり説明します。

負数についての符号なしと符号あり関係

まず、前提として、符号なし整数4は負数の表現に使います(つまり、-4に変換します)。この点を考慮して、元の数と負数との関係を整理すると、

  7   6   5   4
 -1  -2  -3  -4
111 110 101 100

となります。これから以下のことが分かります。

  • どの数も最上位ビットは1である
  • 負数は-1から順に1づつ減少している

共通する部分と異なる部分

負数については、逆に、-4から1づつ増加していると考えることができ、この考え方で、整理すると、

  -1   -2   -3   -4
-4+3 -4+2 -4+1 -4+0

となります。-4+yは、-4の部分が共通で、-4との差分yが異なります。
 そこで、差分yだけを抜き出すと、

   3    2    1   0

となり、これらをビット表現すると、

 011  010  001 000

となります。ここで、下位2bitのビット表現に注目すると、

  11   10   01  00
 111  110  101 100
   7    6    5   4

のように、元の符号なし整数の下位2bitと一致していることがわかります。

一度まとめる

ここまでを強引にまとめると以下のようになります。

  • 負数は最上位ビットが1である点が共通し、また-4+yと表したときは-4が共通する
    つまり、最上位ビット1を-4に変換することが望まれる。
  • 負数の元の数と下位2bitが一致する
    つまり、下位2bitについては、ビット列を符号なし整数に変換する式(2進数から10進数への変換式)がそのまま使えます。

変換式を求める

そこで、符号なし整数への変換式を基に符号付き整数への変換式を考えます。具体的には、3bit場合、ビット列をb_iとすると符号なし整数dへの変換式は、

d = \sum_{i<3}b_i2^i

となります。上述したようにi<2の場合は、これが、そのまま使えます。
\sum_{i<2}b_i2^i

問題は、i=2の場合ですが、なんと2^2=4のビット表現100は偶然にも最上位ビットだけが1です。そこで、最上位ビットb_2が1の場合に、マイナスを掛ければ-4に変換できます。

-4 = -b_22^2

さらに、最上位ビットb_2=0のときは0との掛け算なのでマイナスの影響がなく、結局、0か1かに拘らず最上位ビットにマイナスを掛けます。

以上から3bitにおけるビット列から符号付き整数への変換式は、

d' = -b_22^2+\sum_{i<2}b_i2^i

となります。

このように4を負数-4として扱うことで、変換式をすっきりとした形で書けます。

結論

さらに、上式を一般化すれば(添字に含まれる2をn-1に入れ替えれば)、上述した「一般的な変換式」に一致します。

以上が、一般的にはなぜ2^{n-1}が負数に変換されるのかの説明です。

なお、個人的には2の「補数」と言う名前は、失敗だと思っています。計算機科学における「2の補数」で示される概念は、上述の変換式で定義されるべきものであり、それには「補数」の概念は必要ありません。「2の補数」は、単位となるオブジェクト(2^{n})が、補数を定義付ける演算子の定義域および値域に含まれていません。この点が、「補」から連想される他の概念(補集合、補元、1の補数)と大きく異なっています。それにも拘らず、2の「補数」と名付けられていることは、ある種の参入障壁に...じゃあまあいいか。

以上

Discussion

y-nishiokay-nishioka

面白くて詳細な記事をありがとうございます.
個人的な感想なんですが,「固定小数点数」ですと,どうも2進固定小数点数を想像してしまいます.また分解能uは今は0.01が主流ですが将来は0.005とかになる可能性もあって,そうすると10進固定小数点数でもなくなってしまいます。uをかける前の整数でのお話として整理されたほうが良いような気がしました.

佐野聡佐野聡

コメントありがとうございます。
ご指摘のuについてですが、確かにこれはビット列の解釈における係数(小数点位置)と言うよりは、整数化された数値を補正、調整するための係数(fx+o)の一つと捉えるほうが、「分解能」という語感とも合致しており、より合理的だと思います(記事ではf=u, o=0です)。
また、固定小数点についても、ご指摘のように基数を明示せずに用い、さらに小数点位置が関係ないとなれば、もはや「固定小数点」と称する意味はなく、「整数」と称すべきものだと言えます。
y_nishiokaさんのご助言により、記事をより本質的な視点から見直すことができ、真実に一歩近づけた気がします。どうもありがとうございました。

y-nishiokay-nishioka

書き直してくれたんですね,ありがとうございます.
(自分的には大変すっきりしました)

y-nishiokay-nishioka

せっかく無効値にスポットライトを当てていただいたのでもう少しコメントさせてください.
地理院の標高タイルに無効値が定義されていることは,他の類似フォーマット(Terrain-RGBやTerrarium)に比べてかなりのアドバンテージだと思っています.単純に3D表示するだけなら,仕方がないので0mでいいか,でもいいんでしょうが,他の標高タイルと合成したり,演算して赤色立体地図を作ろうとしたりすると問題になってきます.そこにデータが存在するのか否か,0mなのかデータが無いだけなのかはかなり重要です.おそらく,Terrain-RGBの設計者はその辺には興味が無かったのでしょうけれども.

佐野聡佐野聡

コメントありがとうございます。
無効値の定義につきまして、記事を書いた当初は無効な値を表現できる程度に考えていたのですが、ご指摘の「データが存在するのか否か」の観点から見直すと、より深い意義の可能性に思い至りました。
「データが存在するのか否か」の区別がつかないフォーマットは、本質的に値の解釈が曖昧(標高と無効の二つの意味を持つ)になる虞があります。
なので、仕様で無効値を定義することのアドバンテージの一つは、値の解釈を明確(無効だけでなく有効も)にすることではないかと思います。
y_nishiokaさんのご助言により、記事に新たな視点を盛り込むことができ、締まりのなかった記事の結部に深みを持たせることができました。どうもありがとうございました。

y-nishiokay-nishioka

また自分の意図をしっかりくみ取っていただけでうれしいです,そのうでさらに整理されていて.
お役に立てたようで良かったです.