🪙

【凸包】GISを作るデータ構造とアルゴリズム【計算幾何学】

に公開

概要

地理空間情報システム(GIS)は, いまや都市計画, 防災, 経済学, 生物学...さまざまなドメインで不可欠な地理空間情報の可視化・分析ツールとなっている.

有名なGISツールではArcGISがあるし, 無料で利用できるQGISはお世話になったひとも多いだろう.
https://www.esrij.com/products/arcgis/
https://qgis.org

GISを使ってできる地理空間情報を扱うが, もう少し定義を広げ, 「地理空間情報を幾何学的構造」[1]とみなすと, 今後は「計算幾何学」という研究分野がみえてくる.

逆に, GISはこの「計算幾何学」の理論やアルゴリズムを活かし, 幾何学的構造をもつ地理空間情報に基づく問題を効率的に解いている.

いまとなっては,内部のそういった処理をユーザーが直接見る機会はないかもしれない.[2]

ただ, 今一度**GISが実装している機能を支えるデータ構造やアルゴリズムを学ぼう!!**という記事である.

この手の話を, 日本語で読める書籍は以下の「コンピュータ・ジオメトリ 第3版 計算幾何学:アルゴリズムと応用」だけだと思っている.[3]

https://www.kindaikagaku.co.jp/book_list/detail/9784764903883/

この本の内容に添いつつ, 有名どころの幾何学アルゴリズムをわかりやすく, かつ実装例をまとめたいいとおもう.

今回は, 基本的な処理である「凸包計算」である.

凸包とは

凸包計算は,
"ある点集合Pに対し,それらを内包する最小の凸領域" を求めることである.

イメージをもってもらうため, 「輪ゴムと釘」の例を持ち出す.

板に打ちつけた複数の釘に対し, それらをすべて囲むように輪ゴムを広げて手を離す.
輪ゴムが元のサイズに戻るときに, いくつかの釘にゴムがひっかかるはずだ. この時できたゴムの形こそすべての釘を囲む最小凸包領域である.

凸包計算の最終成果物のイメージができたところで,計算的にはどんな入出力なるのか考えよう.
幾何学アルゴリズムなので, 入力に対し, どのような処理で, 出力があるという3つの定義が必要だ.

入力は点集合であり, 出力は最小凸包領域,,,であるが, もう少し厳密に定義したい.
図を見ると, 実は最小凸包領域は元の点集合の部分集合で示せることがわかる.

つまり,

  1. 入力: 点集合{p_1, p_2, ..., p_9}
  2. 出力:最小凸包領域{p_1, p_2, p_6,p_9,p_8,p_4}

よって, 処理にあたる凸包計算は, 入力となる点集合のなかから, 複数の点を結ぶことで最小凸包領域を構成できる元の点集合の部分集合を探す, こととなる.

最後, もう少し概念的な部分を説明する.
そもそも「凸(convex)とは🧐?」という部分だ.

本の言葉を持ってくると,

平面の部分集合Sが凸(convex)であるための必要十分な条件は, Sの中の任意の点p,qに対し, 線分\bar{pq}Sに完全に含まれることである.

次に説明するアルゴリズムでは点と点を結んでいく処理が入るが, その際に破ってはいけないルールがこの凸性である.

アルゴリズム

では, 具体的に凸包を求めるアルゴリズムを説明していく.
ここでは, 逐次構成法(incremental algorithm) による, ある点集合Pを内包する最小の凸領域を求める.

以前, 「凸包計算は入力となる点集合のなかから, 複数の点を結ぶことで最小凸包領域を構成できる元の点集合の部分集合を見つける」と説明した.

逐次構成アルゴリズムでは, 入力となる点集合Pから一点ずつ解に加えていく, という逐次的な処理を導入する. 途中, 凸性の判定を行いながら, 解に含める点を更新していく.

擬似プログラミング


入力: 平面上の点P
出力: CH(P)の頂点を時計回りの順に並べたリスト

  1. 点をx座標値の順にソートし, 得られたソート列をp_1,p_2,...,p_nとする.
  2. リストL_{upper}[p_1, p_2]とする.
  3. for i <- 3 to n:
  4. ---- do p_iL_{upper}に加える
  5. -------- while L_{upper}が3点以上含んでいて、しかもL_{upper}の最後の3点が右回りではない
  6. -------- do 最後の3点のうちの中央の点をリストL_{upper}から削除
  7. リストL_{lower}[p_n, p_{n-1}]とする.
  8. for i <- n-2 downto 1:
  9. ---- do p_iL_{lower}に加える
  10. -------- while L_{lower}が3点以上含んでいて、しかもL_{lower}の最後の3点が右回りではない
  11. -------- do 最後の3点のうちの中央の点をリストL_{lower}から削除
  12. L_{upper}L_{lower}の両端の点のダブりを削除
  13. L_{lower}L_{upper}を連結して, 得られたリストをLとする.
  14. return L

まず凸包の計算では,入力となる二次元上の点をx座標でソートする(1).この方がのちの処理が便利になるからだ.

ソート後の点集合のうち, x座標が最小と最大のものは必ず凸包に含まれる.[4]

そこで, x座標が最小と最大の2点に対し, 上側に凸な領域を結ぶ処理(2-6)と下側に凸な領域を結ぶ処理(7-11)をそれぞれもとめ, 最後にそれぞれのリストを結合している(13,14).

見ればわかるが, 一点一点を処理し, 凸包の解を更新していることがわかる.
更新時, (5,6)のように, "最後の3点が右回りではない"という判定による点の除去もされている.

この判定による点の除去のイメージを共有しておこう.

上の図は, L_{upper} = [p_1,p_2,p_3,p_4]まで解を更新して, 次にp_5を加えようとしているケースだ.
まず,p_5L_{upper}に加え, L_{upper} = [p_1,p_2,p_3,p_4, p_5]とする.そして,L_{upper}の最後の3点[p_3,p_4, p_5]に注目する.
ここで"右回り(=時計回り)"といっているのは, ベクトル\bar{p_{3}p_{4}}とベクトル\bar{p_{4}p_{5}}の角度の話で,

ベクトル\bar{p_{3}p_{4}}から見て右向き,つまり時計回りにベクトル\bar{p_{4}p_{5}}が向いていれば凸性が破られていないとして判定.
逆に, ベクトル\bar{p_{3}p_{4}}から見て左向き,つまり反時計回りにベクトル\bar{p_{4}p_{5}}が向いていれば凸性が破られたとして判定し, 中央点p_4L_{upper}から排除する. これを凸性が破られていないまで行う.

結果として, L_{upper} = [p_1,p_2,p_3,p_4, p_5]は, L_{upper} = [p_1,p_2, p_5]にまで更新される.

Pythonコード

さてPythonでの簡単な実装例を挙げておこう.

import random
import numpy as np
import matplotlib.pyplot as plt

P = np.random.uniform(0, 10, (10,2)) # 0-10の範囲で10個の点をランダム生成

xP = np.argsort(P[:,0], axis=0)
SP = P[xP]# X座標でソート

def judge_clockwise(L): # 右回り判定処理
    # p,q,r:numpy.narray = [x1,y1],[x2, y2],[x3,y3]
    p,q,r = L[-3], L[-2], L[-1]
    v = q - p
    u = r - q
    o = v[0]*u[1] - v[1]*u[0] # 外積の計算
    if o >= 0:
        return True
    else:
        return False

# 上側凸
Lu = [SP[0], SP[1]] 
step = 2

while step < len(SP):
    Lu.append(SP[step]) # 点の追加
    while len(Lu) >=3 and judge_clockwise(Lu):
        Lu.pop(-2) # 後ろから2番目を排除
    step += 1

# 下側凸
np = len(SP)
Ll = [SP[np-1], SP[np-2]] 
step = 2

while  step < len(SP):
    Ll.append(SP[np-step-1]) # 点の追加
    while len(Ll) >=3 and judge_clockwise(Ll):
        Ll.pop(-2) # 後ろから2番目を排除
    step += 1

# プロット
fig = plt.figure(figsize=(10,10))

ax = fig.add_subplot(1,1,1)

ax.scatter(P[:,0],P[:,1])

for i in range(len(Lu)-1):
    ax.plot([Lu[i][0],Lu[i+1][0]],[Lu[i][1],Lu[i+1][1]], color='orange',  linestyle='solid')
for i in range(len(Ll)-1):
    ax.plot([Ll[i][0],Ll[i+1][0]],[Ll[i][1],Ll[i+1][1]], color='green',  linestyle='solid')

ax.set_xlim([0,10])
ax.set_ylim([0,10])
ax.set_title('xy-points')
ax.set_xlabel('x')
ax.set_ylabel('y')

ax.set(aspect=1) # 縦横比の指定
ax.grid(True) # グリッドの表示
fig.show()

まとめ

今回は, GISで使用されている基本処理のひとつ「凸包」を実装した. GISで使用されているデータは, 幾何学アルゴリズムにより効率的かつ頑健に処理できる.

次回は, GISで頻繁に使用される, 「地図の重ね合わせ」処理を支える「平面走査法」を紹介できればと思う.

脚注
  1. 学校の位置データは「点」、道路や鉄道車線は「ライン=点と点を結んだもの」、建物や敷地の形は「ポリゴン=複数のラインにより閉じた構造」 ↩︎

  2. 見なくてよいほどに,昨今のGISツールは優れているのだ ↩︎

  3. いい本が他にもあれば教えてください🥹 ↩︎

  4. 背理法でこれは証明できる ↩︎

Discussion