単位球にドレープされた任意の形状の図心を、形状境界の入力が順序付けられた(時計回りまたは反cw)頂点で見つけるための最良の方法を見つけようとしています。頂点の密度は境界に沿って不規則であるため、頂点間の弧の長さは一般に等しくありません。形状が非常に大きい(半球の半分)可能性があるため、ウィキペディアで詳しく説明されているように、頂点を平面に投影して平面法を使用することは一般に不可能です(申し訳ありませんが、新規参入者として2つ以上のハイパーリンクは許可されていません)。少し良いアプローチは、球座標で操作される平面ジオメトリの使用を含みますが、ここでも、うまく説明されているように、大きなポリゴンではこの方法は失敗します ここ 。その同じページで、「Cffk」が強調表示されています この論文 これは球面三角形の重心を計算する方法を説明しています。私はこのメソッドを実装しようとしましたが、成功しませんでした。誰かが問題を見つけられることを望んでいますか?
比較しやすいように、変数の定義は論文と同様にしています。入力(データ)は経度/緯度座標のリストであり、コードによって[x、y、z]座標に変換されます。三角形のそれぞれについて、1つの点を+ z極に任意に固定し、他の2つの頂点は、ポリゴン境界に沿った1対の隣接する点で構成されています。コードは、ポリゴンの各境界セグメントを三角形の辺として順番に使用して、境界に沿って(任意のポイントから開始して)ステップします。サブセントロイドは、これらの個々の球面三角形のそれぞれに対して決定され、三角形の面積に従って重み付けされ、合計されてポリゴンの重心が計算されます。コードの実行時にエラーは発生しませんが、返される重心の合計が明らかに間違っています(重心の位置が明確な、非常に基本的な形状をいくつか実行しました)。返された重心の位置に適切なパターンは見つかりませんでした...したがって、現時点では、数学またはコードのいずれかで何が問題になっているのかわかりません(ただし、疑いは数学です)。
以下のコードは、試してみたい場合はそのままコピーアンドペーストで機能するはずです。 matplotlibとnumpyがインストールされている場合、結果がプロットされます(インストールされていない場合、プロットは無視されます)。コードの下の経度/緯度データをexample.txtというテキストファイルに入れるだけです。
from math import *
try:
import matplotlib as mpl
import matplotlib.pyplot
from mpl_toolkits.mplot3d import Axes3D
import numpy
plotting_enabled = True
except ImportError:
plotting_enabled = False
def sph_car(point):
if len(point) == 2:
point.append(1.0)
rlon = radians(float(point[0]))
rlat = radians(float(point[1]))
x = cos(rlat) * cos(rlon) * point[2]
y = cos(rlat) * sin(rlon) * point[2]
z = sin(rlat) * point[2]
return [x, y, z]
def xprod(v1, v2):
x = v1[1] * v2[2] - v1[2] * v2[1]
y = v1[2] * v2[0] - v1[0] * v2[2]
z = v1[0] * v2[1] - v1[1] * v2[0]
return [x, y, z]
def dprod(v1, v2):
dot = 0
for i in range(3):
dot += v1[i] * v2[i]
return dot
def plot(poly_xyz, g_xyz):
fig = mpl.pyplot.figure()
ax = fig.add_subplot(111, projection='3d')
# plot the unit sphere
u = numpy.linspace(0, 2 * numpy.pi, 100)
v = numpy.linspace(-1 * numpy.pi / 2, numpy.pi / 2, 100)
x = numpy.outer(numpy.cos(u), numpy.sin(v))
y = numpy.outer(numpy.sin(u), numpy.sin(v))
z = numpy.outer(numpy.ones(numpy.size(u)), numpy.cos(v))
ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w', linewidth=0,
alpha=0.3)
# plot 3d and flattened polygon
x, y, z = Zip(*poly_xyz)
ax.plot(x, y, z)
ax.plot(x, y, zs=0)
# plot the alleged 3d and flattened centroid
x, y, z = g_xyz
ax.scatter(x, y, z, c='r')
ax.scatter(x, y, 0, c='r')
# display
ax.set_xlim3d(-1, 1)
ax.set_ylim3d(-1, 1)
ax.set_zlim3d(0, 1)
mpl.pyplot.show()
lons, lats, v = list(), list(), list()
# put the two-column data at the bottom of the question into a file called
# example.txt in the same directory as this script
with open('example.txt') as f:
for line in f.readlines():
sep = line.split()
lons.append(float(sep[0]))
lats.append(float(sep[1]))
# convert spherical coordinates to cartesian
for lon, lat in Zip(lons, lats):
v.append(sph_car([lon, lat, 1.0]))
# z unit vector/pole ('north pole'). This is an arbitrary point selected to act as one
#(fixed) vertex of the summed spherical triangles. The other two vertices of any
#triangle are composed of neighboring vertices from the polygon boundary.
np = [0.0, 0.0, 1.0]
# Gx,Gy,Gz are the cartesian coordinates of the calculated centroid
Gx, Gy, Gz = 0.0, 0.0, 0.0
for i in range(-1, len(v) - 1):
# cycle through the boundary vertices of the polygon, from 0 to n
if all((v[i][0] != v[i+1][0],
v[i][1] != v[i+1][1],
v[i][2] != v[i+1][2])):
# this just ignores redundant points which are common in my larger input files
# A,B,C are the internal angles in the triangle: 'np-v[i]-v[i+1]-np'
A = asin(sqrt((dprod(np, xprod(v[i], v[i+1])))**2
/ ((1 - (dprod(v[i+1], np))**2) * (1 - (dprod(np, v[i]))**2))))
B = asin(sqrt((dprod(v[i], xprod(v[i+1], np)))**2
/ ((1 - (dprod(np , v[i]))**2) * (1 - (dprod(v[i], v[i+1]))**2))))
C = asin(sqrt((dprod(v[i + 1], xprod(np, v[i])))**2
/ ((1 - (dprod(v[i], v[i+1]))**2) * (1 - (dprod(v[i+1], np))**2))))
# A/B/Cbar are the vertex angles, such that if 'O' is the sphere center, Abar
# is the angle (v[i]-O-v[i+1])
Abar = acos(dprod(v[i], v[i+1]))
Bbar = acos(dprod(v[i+1], np))
Cbar = acos(dprod(np, v[i]))
# e is the 'spherical excess', as defined on wikipedia
e = A + B + C - pi
# mag1/2/3 are the magnitudes of vectors np,v[i] and v[i+1].
mag1 = 1.0
mag2 = float(sqrt(v[i][0]**2 + v[i][1]**2 + v[i][2]**2))
mag3 = float(sqrt(v[i+1][0]**2 + v[i+1][1]**2 + v[i+1][2]**2))
# vec1/2/3 are cross products, defined here to simplify the equation below.
vec1 = xprod(np, v[i])
vec2 = xprod(v[i], v[i+1])
vec3 = xprod(v[i+1], np)
# multiplying vec1/2/3 by e and respective internal angles, according to the
#posted paper
for x in range(3):
vec1[x] *= Cbar / (2 * e * mag1 * mag2
* sqrt(1 - (dprod(np, v[i])**2)))
vec2[x] *= Abar / (2 * e * mag2 * mag3
* sqrt(1 - (dprod(v[i], v[i+1])**2)))
vec3[x] *= Bbar / (2 * e * mag3 * mag1
* sqrt(1 - (dprod(v[i+1], np)**2)))
Gx += vec1[0] + vec2[0] + vec3[0]
Gy += vec1[1] + vec2[1] + vec3[1]
Gz += vec1[2] + vec2[2] + vec3[2]
approx_expected_Gxyz = (0.78, -0.56, 0.27)
print('Approximate Expected Gxyz: {0}\n'
' Actual Gxyz: {1}'
''.format(approx_expected_Gxyz, (Gx, Gy, Gz)))
if plotting_enabled:
plot(v, (Gx, Gy, Gz))
提案や洞察を事前に感謝します。
編集:これは、ポリゴンを使用した単位球の投影と、コードから計算した結果の重心を示す図です。明らかに、ポリゴンはかなり小さく凸状であるため、図心は間違っていますが、図心はその周囲から外れています。
編集:これは上記のものと非常によく似た座標のセットですが、私が通常使用する元の[lon、lat]形式です(更新されたコードによって[x、y、z]に変換されます)。
-39.366295 -1.633460
-47.282630 -0.740433
-53.912136 0.741380
-59.004217 2.759183
-63.489005 5.426812
-68.566001 8.712068
-71.394853 11.659135
-66.629580 15.362600
-67.632276 16.827507
-66.459524 19.069327
-63.819523 21.446736
-61.672712 23.532143
-57.538431 25.947815
-52.519889 28.691766
-48.606227 30.646295
-45.000447 31.089437
-41.549866 32.139873
-36.605156 32.956277
-32.010080 34.156692
-29.730629 33.756566
-26.158767 33.714080
-25.821513 34.179648
-23.614658 36.173719
-20.896869 36.977645
-17.991994 35.600074
-13.375742 32.581447
-9.554027 28.675497
-7.825604 26.535234
-7.825604 26.535234
-9.094304 23.363132
-9.564002 22.527385
-9.713885 22.217165
-9.948596 20.367878
-10.496531 16.486580
-11.151919 12.666850
-12.350144 8.800367
-15.446347 4.993373
-20.366139 1.132118
-24.784805 -0.927448
-31.532135 -1.910227
-39.366295 -1.633460
編集:さらにいくつかの例... [1,0,0]を中心とする完全な正方形を定義する4つの頂点で、期待される結果が得られます。 ただし、非対称の三角形からは、どこにも近くない図心が得られます...図心は実際には球の反対側にあります(ここでは対蹠地として前面に投影されています)。 興味深いことに、重心の推定は、リストを反転すると(時計回りから反時計回りに、またはその逆に)、重心が対応して正確に反転するという意味で「安定」しているように見えます。
これでうまくいくと思います。以下のコードをコピーして貼り付けるだけで、この結果を再現できるはずです。
longitude and latitude.txt
というファイルに保存する必要があります。コードの下に含まれている元のサンプルデータをコピーして貼り付けることができます。伝説:
from math import *
try:
import matplotlib as mpl
import matplotlib.pyplot
from mpl_toolkits.mplot3d import Axes3D
import numpy
plotting_enabled = True
except ImportError:
plotting_enabled = False
def main():
# get base polygon data based on unit sphere
r = 1.0
polygon = get_cartesian_polygon_data(r)
point_count = len(polygon)
reference = ok_reference_for_polygon(polygon)
# decompose the polygon into triangles and record each area and 3d centroid
areas, subcentroids = list(), list()
for ia, a in enumerate(polygon):
# build an a-b-c point set
ib = (ia + 1) % point_count
b, c = polygon[ib], reference
if points_are_equivalent(a, b, 0.001):
continue # skip nearly identical points
# store the area and 3d centroid
areas.append(area_of_spherical_triangle(r, a, b, c))
tx, ty, tz = Zip(a, b, c)
subcentroids.append((sum(tx)/3.0,
sum(ty)/3.0,
sum(tz)/3.0))
# combine all the centroids, weighted by their areas
total_area = sum(areas)
subxs, subys, subzs = Zip(*subcentroids)
_3d_centroid = (sum(a*subx for a, subx in Zip(areas, subxs))/total_area,
sum(a*suby for a, suby in Zip(areas, subys))/total_area,
sum(a*subz for a, subz in Zip(areas, subzs))/total_area)
# shift the final centroid to the surface
surface_centroid = scale_v(1.0 / mag(_3d_centroid), _3d_centroid)
plot(polygon, reference, _3d_centroid, surface_centroid, subcentroids)
def get_cartesian_polygon_data(fixed_radius):
cartesians = list()
with open('longitude and latitude.txt') as f:
for line in f.readlines():
spherical_point = [float(v) for v in line.split()]
if len(spherical_point) == 2:
spherical_point.append(fixed_radius)
cartesians.append(degree_spherical_to_cartesian(spherical_point))
return cartesians
def ok_reference_for_polygon(polygon):
point_count = len(polygon)
# fix the average of all vectors to minimize float skew
polyx, polyy, polyz = Zip(*polygon)
# /10 is for visualization. Remove it to maximize accuracy
return (sum(polyx)/(point_count*10.0),
sum(polyy)/(point_count*10.0),
sum(polyz)/(point_count*10.0))
def points_are_equivalent(a, b, vague_tolerance):
# vague tolerance is something like a percentage tolerance (1% = 0.01)
(ax, ay, az), (bx, by, bz) = a, b
return all(((ax-bx)/ax < vague_tolerance,
(ay-by)/ay < vague_tolerance,
(az-bz)/az < vague_tolerance))
def degree_spherical_to_cartesian(point):
rad_lon, rad_lat, r = radians(point[0]), radians(point[1]), point[2]
x = r * cos(rad_lat) * cos(rad_lon)
y = r * cos(rad_lat) * sin(rad_lon)
z = r * sin(rad_lat)
return x, y, z
def area_of_spherical_triangle(r, a, b, c):
# points abc
# build an angle set: A(CAB), B(ABC), C(BCA)
# http://math.stackexchange.com/a/66731/25581
A, B, C = surface_points_to_surface_radians(a, b, c)
E = A + B + C - pi # E is called the spherical excess
area = r**2 * E
# add or subtract area based on clockwise-ness of a-b-c
# http://stackoverflow.com/a/10032657/377366
if clockwise_or_counter(a, b, c) == 'counter':
area *= -1.0
return area
def surface_points_to_surface_radians(a, b, c):
"""build an angle set: A(cab), B(abc), C(bca)"""
points = a, b, c
angles = list()
for i, mid in enumerate(points):
start, end = points[(i - 1) % 3], points[(i + 1) % 3]
x_startmid, x_endmid = xprod(start, mid), xprod(end, mid)
ratio = (dprod(x_startmid, x_endmid)
/ ((mag(x_startmid) * mag(x_endmid))))
angles.append(acos(ratio))
return angles
def clockwise_or_counter(a, b, c):
ab = diff_cartesians(b, a)
bc = diff_cartesians(c, b)
x = xprod(ab, bc)
if x < 0:
return 'clockwise'
Elif x > 0:
return 'counter'
else:
raise RuntimeError('The reference point is in the polygon.')
def diff_cartesians(positive, negative):
return Tuple(p - n for p, n in Zip(positive, negative))
def xprod(v1, v2):
x = v1[1] * v2[2] - v1[2] * v2[1]
y = v1[2] * v2[0] - v1[0] * v2[2]
z = v1[0] * v2[1] - v1[1] * v2[0]
return [x, y, z]
def dprod(v1, v2):
dot = 0
for i in range(3):
dot += v1[i] * v2[i]
return dot
def mag(v1):
return sqrt(v1[0]**2 + v1[1]**2 + v1[2]**2)
def scale_v(scalar, v):
return Tuple(scalar * vi for vi in v)
def plot(polygon, reference, _3d_centroid, surface_centroid, subcentroids):
fig = mpl.pyplot.figure()
ax = fig.add_subplot(111, projection='3d')
# plot the unit sphere
u = numpy.linspace(0, 2 * numpy.pi, 100)
v = numpy.linspace(-1 * numpy.pi / 2, numpy.pi / 2, 100)
x = numpy.outer(numpy.cos(u), numpy.sin(v))
y = numpy.outer(numpy.sin(u), numpy.sin(v))
z = numpy.outer(numpy.ones(numpy.size(u)), numpy.cos(v))
ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w', linewidth=0,
alpha=0.3)
# plot 3d and flattened polygon
x, y, z = Zip(*polygon)
ax.plot(x, y, z, c='b')
ax.plot(x, y, zs=0, c='g')
# plot the 3d centroid
x, y, z = _3d_centroid
ax.scatter(x, y, z, c='r', s=20)
# plot the spherical surface centroid and flattened centroid
x, y, z = surface_centroid
ax.scatter(x, y, z, c='b', s=20)
ax.scatter(x, y, 0, c='g', s=20)
# plot the full set of triangular centroids
x, y, z = Zip(*subcentroids)
ax.scatter(x, y, z, c='r', s=4)
# plot the reference vector used to findsub centroids
x, y, z = reference
ax.plot((0, x), (0, y), (0, z), c='k')
ax.scatter(x, y, z, c='k', marker='^')
# display
ax.set_xlim3d(-1, 1)
ax.set_ylim3d(-1, 1)
ax.set_zlim3d(0, 1)
mpl.pyplot.show()
# run it in a function so the main code can appear at the top
main()
longitude and latitude.txt
に貼り付けることができる経度と緯度のデータは次のとおりです
-39.366295 -1.633460
-47.282630 -0.740433
-53.912136 0.741380
-59.004217 2.759183
-63.489005 5.426812
-68.566001 8.712068
-71.394853 11.659135
-66.629580 15.362600
-67.632276 16.827507
-66.459524 19.069327
-63.819523 21.446736
-61.672712 23.532143
-57.538431 25.947815
-52.519889 28.691766
-48.606227 30.646295
-45.000447 31.089437
-41.549866 32.139873
-36.605156 32.956277
-32.010080 34.156692
-29.730629 33.756566
-26.158767 33.714080
-25.821513 34.179648
-23.614658 36.173719
-20.896869 36.977645
-17.991994 35.600074
-13.375742 32.581447
-9.554027 28.675497
-7.825604 26.535234
-7.825604 26.535234
-9.094304 23.363132
-9.564002 22.527385
-9.713885 22.217165
-9.948596 20.367878
-10.496531 16.486580
-11.151919 12.666850
-12.350144 8.800367
-15.446347 4.993373
-20.366139 1.132118
-24.784805 -0.927448
-31.532135 -1.910227
-39.366295 -1.633460
重み付きデカルト座標を使用して重心を計算し、その結果を球に投影することをお勧めします(座標の原点が(0, 0, 0)^T
であると仮定)。
ポリゴンのn点を(p[0], p[1], ... p[n-1])
とします。近似(デカルト)重心は、次の方法で計算できます。
c = 1 / w * (sum of w[i] * p[i])
一方、w
はすべての重みの合計であり、p[i]
はポリゴンポイントであり、w[i]
はそのポイントの重みです。
w[i] = |p[i] - p[(i - 1 + n) % n]| / 2 + |p[i] - p[(i + 1) % n]| / 2
一方、|x|
はベクトルx
の長さです。つまりポイントは、前のポリゴンポイントの半分の長さと、次のポリゴンポイントの半分の長さで重み付けされます。
この図心c
は、次の方法で球に投影できます。
c' = r * c / |c|
一方、r
は球の半径です。
ポリゴンの方向を考慮すると(ccw, cw)
結果は次のようになります。
c' = - r * c / |c|.
明確にするために:関心のある量は、単位球への真の3D重心(つまり、3D重心、つまり3D重心)の投影です。
気になるのは原点から3D重心への方向だけなので、領域を気にする必要はまったくありません。モーメントを計算する方が簡単です(つまり、3D重心×面積)。単位球上の閉じたパスの左側の領域のモーメントは、パスを歩き回るときの左側の単位ベクトルの積分の半分です。これは、ストークスの定理の非自明な適用によるものです。 http://www.owlnet.rice.edu/~fjones/chap13.pdf 問題13-12を参照してください。
特に、球形ポリゴンの場合、モーメントは(a x b)/ || a x b ||の合計の半分になります。 *(aとbの間の角度)連続する頂点a、bの各ペア。 (これは、パスの左までの領域の場合です。パスの右までの領域の場合は否定します。)
(そして、本当に3D重心が必要な場合は、面積を計算してモーメントをそれで除算するだけです。面積を比較すると、2つの領域のどちらを「ポリゴン」と呼ぶかを選択するのにも役立ちます。)
ここにいくつかのコードがあります。それは本当に簡単です:
#!/usr/bin/python
import math
def plus(a,b): return [x+y for x,y in Zip(a,b)]
def minus(a,b): return [x-y for x,y in Zip(a,b)]
def cross(a,b): return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]]
def dot(a,b): return sum([x*y for x,y in Zip(a,b)])
def length(v): return math.sqrt(dot(v,v))
def normalized(v): l = length(v); return [1,0,0] if l==0 else [x/l for x in v]
def addVectorTimesScalar(accumulator, vector, scalar):
for i in xrange(len(accumulator)): accumulator[i] += vector[i] * scalar
def angleBetweenUnitVectors(a,b):
# http://www.plunk.org/~hatch/rightway.php
if dot(a,b) < 0:
return math.pi - 2*math.asin(length(plus(a,b))/2.)
else:
return 2*math.asin(length(minus(a,b))/2.)
def sphericalPolygonMoment(verts):
moment = [0.,0.,0.]
for i in xrange(len(verts)):
a = verts[i]
b = verts[(i+1)%len(verts)]
addVectorTimesScalar(moment, normalized(cross(a,b)),
angleBetweenUnitVectors(a,b) / 2.)
return moment
if __name__ == '__main__':
import sys
def lonlat_degrees_to_xyz(lon_degrees,lat_degrees):
lon = lon_degrees*(math.pi/180)
lat = lat_degrees*(math.pi/180)
coslat = math.cos(lat)
return [coslat*math.cos(lon), coslat*math.sin(lon), math.sin(lat)]
verts = [lonlat_degrees_to_xyz(*[float(v) for v in line.split()])
for line in sys.stdin.readlines()]
#print "verts = "+`verts`
moment = sphericalPolygonMoment(verts)
print "moment = "+`moment`
print "centroid unit direction = "+`normalized(moment)`
ポリゴンの例では、これは答え(単位ベクトル)を与えます:
[-0.7644875430808217, 0.579935445918147, -0.2814847687566214]
これは、@ KobeJohnのコードによって計算された回答とほぼ同じですが、より正確です。このコードは、サブセントロイドに対して大まかな公差と平面近似を使用します。
[0.7628095787179151, -0.5977153368303585, 0.24669398601094406]
2つの答えの方向はほぼ反対です(したがって、KobeJohnのコードは、この場合、領域をパスの右に移動することを決定したと思います)。
申し訳ありませんが、私は(新しく登録されたユーザーとして)ドンハッチによる上記の回答に投票/コメントするだけでなく、新しい投稿を書く必要がありました。ドンの答えは、私が思うに、最高で最もエレガントです。球形ポリゴンに適用する場合、簡単な方法で重心(最初の質量モーメント)を計算するのは数学的に厳密です。
神戸ジョンの答えは良い近似ですが、より小さな領域でのみ満足のいくものです。また、コードにいくつかの不具合があることに気づきました。まず、実際の球面面積を計算するために、基準点を球面に投影する必要があります。次に、ゼロ除算を回避するために、関数points_are_equivalent()を改良する必要がある場合があります。
神戸の方法の近似誤差は、球面三角形の重心の計算にあります。サブ図心は、球面三角形の重心ではなく、平面三角形の重心です。その単一の三角形を決定する場合、これは問題ではありません(符号が反転する可能性があります。以下を参照してください)。三角形が小さい場合(ポリゴンの密な三角形分割など)も問題にはなりません。
いくつかの簡単なテストで、近似誤差を説明できます。たとえば、4つのポイントだけを使用する場合:
10 -20
10 20
-10 20
-10 -20
正確な答えは(1,0,0)で、どちらの方法も適切です。ただし、1つのエッジに沿ってさらにいくつかのポイントを投入すると(たとえば、最初のエッジに{10、-15}、{10、-10} ...を追加すると)、神戸の方法の結果がシフトし始めることがわかります。さらに、経度を[10、-10]から[100、-100]に増やすと、神戸の結果が方向を反転することがわかります。考えられる改善は、サブセントロイド計算用に別のレベルを追加することです(基本的に三角形のサイズを調整/縮小します)。
このアプリケーションでは、球形の領域の境界は複数の円弧で構成されているため、ポリゴンではありません(つまり、円弧は大円の一部ではありません)。しかし、これは、曲線積分でnベクトルを見つけるためのもう少し作業になります。
編集:サブセントロイド計算を ブロックの論文 で与えられたものに置き換えると、神戸の方法が修正されるはずです。しかし、私は試しませんでした。