40

我正在尝试找出如何最好地定位覆盖在单位球体上的任意形状的质心,输入为形状边界的有序(顺时针或逆顺时针)顶点。沿边界的顶点密度是不规则的,因此它们之间的弧长通常不相等。由于形状可能非常大(半个半球),因此通常不可能简单地将顶点投影到平面并使用平面方法,如 Wikipedia 中所述(抱歉,我不允许超过 2 个超链接作为新手)。稍微好一点的方法是使用在球坐标中操作的平面几何,但同样,对于大多边形,这种方法会失败,如此处所示。在同一页上,“Cffk”突出显示了这篇论文其中描述了一种计算球面三角形质心的方法。我试过实现这个方法,但没有成功,我希望有人能发现问题?

我将变量定义与论文中的定义保持相似,以便于比较。输入(数据)是经度/纬度坐标列表,由代码转换为 [x,y,z] 坐标。对于每个三角形,我任意将一个点固定为 +z 极,另外两个顶点由沿多边形边界的一对相邻点组成。代码沿着边界步进(从任意点开始),依次使用多边形的每个边界段作为三角形边。为这些单独的球形三角形中的每一个确定子质心,并根据三角形面积对它们进行加权并添加以计算总多边形质心。运行代码时我没有收到任何错误,但是返回的总质心显然是错误的(我已经运行了一些非常基本的形状,其中质心位置是明确的)。我没有在返回的质心位置找到任何合理的模式......所以目前我不确定在数学或代码中出了什么问题(尽管怀疑是数学)。

如果您想尝试一下,下面的代码应该可以复制粘贴。如果您安装了 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

编辑:再举几个例子……用 4 个顶点定义一个以 [1,0,0] 为中心的完美正方形,我得到了预期的结果: 在此处输入图像描述 但是,从非对称三角形中,我得到了一个离得很近的质心……质心实际上落在球体的远端(这里投影到正面作为对极): 在此处输入图像描述 有趣的是,如果我反转列表(从顺时针到逆时针顺序,反之亦然),质心估计看起来“稳定” -versa)质心相应地完全反转。

4

4 回答 4

14

任何发现此问题的人,请务必查看 Don Hatch 的答案,这可能会更好。


我认为这会做到。您应该能够通过复制粘贴下面的代码来重现此结果。

  • 您需要将纬度和经度数据保存在一个名为longitude and latitude.txt. 您可以复制粘贴代码下方包含的原始示例数据。
  • 如果你有 mplotlib,它还会产生下面的图
  • 对于非显而易见的计算,我提供了一个链接来解释正在发生的事情
  • 在下图中,参考向量非常短(r = 1/10),因此更容易看到 3d 质心。您可以轻松删除缩放以最大限度地提高准确性。
  • 操作注意事项:我几乎重写了所有内容,所以我不确定原始代码在哪里不起作用。但是,至少我认为它没有考虑处理顺时针/逆时针三角形顶点的需要。

工作重心

传奇:

  • (黑线)参考向量
  • (小红点)球形三角形 3d 质心
  • (大红/蓝/绿点)3d质心/投影到表面/投影到xy平面
  • (蓝/绿线)球面多边形和在 xy 平面上的投影

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
于 2013-11-18T19:58:42.900 回答
7

澄清一下:感兴趣的量是真正的 3d 质心(即 3d 质心,即 3d 面积中心)到单位球体上的投影。

由于您只关心从原点到 3d 质心的方向,因此您根本不需要关心区域;只计算时刻(即 3d 质心时间区域)更容易。当你绕着路径走时,单位球面上闭合路径左侧区域的矩是左侧单位矢量积分的一半。这是从斯托克斯定理的非显而易见的应用得出的;见http://www.owlnet.rice.edu/~fjones/chap13.pdf问题 13-12。

特别是,对于一个球面多边形,矩是 (axb) / ||axb|| 之和的一半。*(a 和 b 之间的角度)对于每对连续顶点 a,b。(这是路径左侧的区域;对路径右侧的区域取反。)

(如果你真的想要 3d 质心,只需计算面积并将矩除以它。比较面积也可能有助于选择两个区域中的哪一个称为“多边形”。)

这是一些代码;这真的很简单:

#!/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]

两个答案的方向大致相反(所以我猜 KobeJohn 的代码决定在这种情况下将区域带到路径的右侧)。

于 2016-07-05T10:47:50.817 回答
4

我认为一个很好的近似值是使用加权笛卡尔坐标计算质心并将结果投影到球体上(假设坐标原点是(0, 0, 0)^T)。

设 为(p[0], p[1], ... p[n-1])多边形的 n 个点。近似(笛卡尔)质心可以通过以下方式计算:

c = 1 / w * (sum of w[i] * p[i])

whilew是所有权重的总和,whilep[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|. 
于 2013-11-12T10:58:25.463 回答
3

抱歉,我(作为新注册的用户)不得不写一篇新帖子,而不是仅仅对 Don Hatch 的上述答案进行投票/评论。我认为唐的回答是最好最优雅的。当应用于球面多边形时,它以简单的方式计算质心(质心第一矩)在数学上是严格的。

科比约翰的答案是一个很好的近似值,但只适用于较小的区域。我还注意到代码中的一些小故障。首先,应将参考点投影到球面上以计算实际的球面面积。其次,函数 points_are_equivalent() 可能需要改进以避免被零除。

科比方法的近似误差在于球面三角形的质心计算。次质心不是球面三角形的质心,而是平面质心。如果要确定单个三角形(符号可能翻转,见下文),这不是问题。如果三角形很小(例如多边形的密集三角剖分),这也不是问题。

一些简单的测试可以说明近似误差。例如,如果我们只使用四个点:

10 -20

10 20

-10 20

-10 -20

确切的答案是 (1,0,0),这两种方法都很好。但是,如果您在一条边上再添加几个点(例如,将 {10,-15},{10,-10}... 添加到第一条边),您会看到 Kobe 方法的结果开始发生变化。此外,如果将经度从 [10,-10] 增加到 [100,-100],您会看到 Kobe 的结果反转了方向。一个可能的改进可能是为子质心计算添加另一个级别(基本上细化/减小三角形的大小)。

对于我们的应用程序,球形区域边界由多个弧组成,因此不是多边形(即弧不是大圆的一部分)。但这只是在曲线积分中找到 n 向量的更多工作。

编辑:用Brock 的论文中给出的计算替换次质心计算应该可以解决 Kobe 的方法。但我没有尝试。

于 2018-03-07T21:10:09.470 回答