今回は、Pythonでヒルベルト曲線を描いてみます。
ヒルベルト曲線の性質上、再帰アルゴリズムで解く方法がありますが、今回は再帰アルゴリズムを使わず、そのまま通常ループで解いていこうと思います(個人的にはそのほうが分かりやすいので)。
追記:再帰アルゴリズムを後半に追加しました。
環境: Python 3.8.5、Jupyter notebook
上画像、ORDER=6の場合(4096個の点をつないだ線)。
ORDERを上げていけば、面を埋め尽くしてしまうという空間充填曲線の一つ。
また拡大縮小しても相似形となるフラクタルな性質も持ち合わせています。
アルゴリズム:
まずヒルベルト曲線は4点からなるコの字を基準として、それの組み合わせで描いていきます。
コの字の組み合わせとして、90回転が4パターン、そして時計回り、反時計回りがあるので、合計4x2=8パターンありますが、実際はそのうちの4パターンしか使いません。
今回は、座標(0,0)を開始点とし移動方向が上右下のコの字を基本形とし、そのほか右上左、左下右、下左上の4パターンを使います。
ORDER=1の場合、上画像のような4点になります。
ルール1:(X座標が右向き、Y座標が上向きの場合)
- 4点を(0, 1, 2, 3)の順番とする。
- 原点0:(0, 0)を左下にする。
- 原点(0, 0)から上(0, 1)、右(1, 1)、下(1, 0)という順番で描く。
*尚、プログラミング環境によってY座標が下向きの場合は、原点が左上となり、下、右、上(上下反転)。
ルール2:
- 点の数は4n個(コの字4点の組数:2n個)、n=ORDER
- コの字(4点)ごとに(0, 1, 2, 3)の番号をつける。
- 前回のORDER=1(赤破線)を参照し、青(0,1,2,3)の順番を入れ替える。
- 赤0のエリア: 青(0,1,2,3)→青(0,3,2,1)、青1と青3を入れ替え
- 赤1のエリア: 青(0,1,2,3)→青(0,1,2,3)、入れ替えなし
- 赤2のエリア: 青(0,1,2,3)→青(0,1,2,3)、入れ替えなし
- 赤3のエリア: 青(0,1,2,3)→青(2,1,0,3)、青0と青2を入れ替え
- 前回のORDER=1(赤破線)を参照し、その順番で青コの字を配置する。
- 赤0:(0,0)のエリア内に青1組目を配置する。
- 赤(0, 0)に赤単位長2を掛けた値(0,0)を青の点に加算する(赤0の場合は結局0)。
- 赤1:(0,1)のエリア内に青2組目を配置する。
- 赤(0, 1)に赤単位長2を掛けた値(0,2)を青の点に加算する。
- 赤2:(1,1)のエリア内に青3組目を配置する。
- 赤(1, 1)に赤単位長2を掛けた値(2,2)を青の点に加算する。
- 赤3:(1,0)のエリア内に青4組目を配置する。
- 赤(1, 0)に赤単位長2を掛けた値(2,0)を青の点に加算する。
ORDER=Nの内容を得るには、ORDER=N-1を参照し、ORDER=N-1の内容を得るには、ORDER=N-2を参照するように遡っていくので、たしかに再帰アルゴリズムが向いていることが分かります。しかし、今回はこのまま続行。
上記手続きのコード:
N = 2**ORDER
X,Y = [], []
for i in range(N * N):
U = [[0, 0], [0, 1], [1, 1], [1, 0]]
px, py = 0.5, 0.5
for j in reversed(range(ORDER)):
ID = i // 4**j % 4
L = 2**j
if ID == 0:
U[1], U[3] = U[3], U[1]
elif ID == 3:
U[0], U[2] = U[2], U[0]
px += U[ID][0] * L
py += U[ID][1] * L
X.append(px)
Y.append(py)
- ORDER=3のため合計23×23=64点(0〜63)を順番に一つずつforループ処理させる。
- 基本となるコの字型(ORDER=1)の座標を、U=[[0,0],[0,1],[1,1],[1,0]]という配列にしておく。
- ORDER=3のとき、縦横に8個ずつ点があるため単位長を1とすれば全体のサイズは8x8となり、2のORDER乗だけ大きくなる(最終的に正規化し1×1のサイズに変更可)。
- px, pyは原点座標であり、最終的に最小単位長1の半分(0.5)のオフセットがあるため、最初にずらしておく。
- ネストされたfor文でORDERの数だけループさせる。reversed()で反転させたループとなり、j=2,1,0となる。
- ID=i//4**j%4は、ORDER=jによる各点の位置関係を0〜3の値で返す。ORDER=3の場合は4進数の3ビットとなる(以下に「4進数」の説明あり)。
- L=2**jは、ORDER=jに応じた単位長。
- j=2: L=4 (ORDER=1)
- j=1: L=2 (ORDER=2)
- j=0: L=1 (ORDER=3)
- ID=0の時U[1]とU[3]、ID=3の時U[0]とU[2]の入れ替えてコの字の向きを変える。
- px+=U[ID][0]*Lは、入れ替えによって得られたUの要素とORDER数に応じた単位長を掛け合わせた値が加算され、最終的な座標が得られる(pyについても同様)。
4進数:
再帰アルゴリズム(追加):
import sys
sys.setrecursionlimit(10 ** 4) # Increase recursive functions memory(default: 1000)
def hilbert(i, L, U):
if i == 4**Order:
return P + 0.5 # Final output when the iteration finishes, offset(x, y)=(0.5, 0.5)
if L >= 0:
ID = i // 4**L % 4 # Quarternary index number
if ID == 0:
U[1], U[3] = U[3].copy(), U[1].copy() # Turn the U-shape in clockwise and steps in anti-clockwise
elif ID == 3:
U[0], U[2] = U[2].copy(), U[0].copy() # Turn the U-shape in anti-clockwise and steps in anti-clockwise
P[:, i] += U[ID] * 2**L # add each step to the array for the output
return hilbert(i, L-1, U) # go to the next L(L=Order-1, Order-2 ... 0, -1)
U = array([[0,0],[0,1],[1,1],[1,0]]) # initialize U and L
L = Ord - 1
return hilbert(i+1, L, U) # go to the next i
Order = 4 # Level of Order (fixed value)
i = 0 # The number of iteration(i=0, 1 ... 4**Order-1)
L = Ord - 1 # Length of U-shape(L=Order-1, Order-2 ... 0, -1)
U = array([[0,0],[0,1],[1,1],[1,0]]) # Initial U-shape coodinates
P = zeros((2, 4**Order)) # Initial empty array for the output
P = hilbert(i, L, U) # X, Y = P[0], P[1]
def hilbert(i):
if i == 4**Order:
return P + 0.5 # Final output when the iteration finishes, offset(x, y)=(0.5, 0.5)
U = array([[0,0],[0,1],[1,1],[1,0]]) # initialize U and L
for j in reversed(range(Order)): # loop for each Order-scale steps: j=Order-1, Order-2 ... 0
ID = i // 4**j % 4 # Quarternary index number
if ID == 0:
U[1], U[3] = U[3].copy(), U[1].copy() # Turn the U-shape in clockwise and steps in anti-clockwise
elif ID == 3:
U[0], U[2] = U[2].copy(), U[0].copy() # Turn the U-shape in anti-clockwise and steps in anti-clockwise
P[:, i] += U[ID] * 2**j # add each step to the output array
return hilbert(i+1) # go to the next i
Order = 5 # Level of Order (fixed value)
P = zeros((2, 4**Order)) # Initial empty array for the output
i = 0 # The number of iteration(i=0, 1 ... 4**Order-1)
P = hilbert(i) # X, Y = P[0], P[1]
def hilbert(n, L, U):
for i in range(n, 4**Order):
if L >= 0:
ID = i // 4**L % 4 # Quarternary index number
if ID == 0:
U[1], U[3] = U[3].copy(), U[1].copy() # Turn the U-shape in clockwise and steps in anti-clockwise
elif ID == 3:
U[0], U[2] = U[2].copy(), U[0].copy() # Turn the U-shape in anti-clockwise and steps in anti-clockwise
P[:, i] += U[ID] * 2**L # add each step to the array for the output
return hilbert(n, L-1, U) # go to the next L(next Order: 1, 2 ... Order-1)
U = array([[0,0],[0,1],[1,1],[1,0]]) # initialize U and L
L = Order - 1
n += 1
return P
Order = 4 # Level of Order (fixed value)
P = zeros((2, 4**Order)) # Initial empty array for the output
n = 0 # The number of iteration(i=0, 1 ... 4**Order-1)
L = Order - 1 # Length of U-shape(L=Order-1, Order-2 ... 0)
U = array([[0,0],[0,1],[1,1],[1,0]]) # Initial U-shape coodinates
P = hilbert(n, L, U) # X, Y = P[0], P[1]
0 件のコメント:
コメントを投稿