Расположение центроида (центра масс) сферических многоугольников

Я пытаюсь выяснить, как лучше всего найти центр тяжести произвольной формы, наложенной на единичную сферу, причем упорядоченные по порядку (по часовой стрелке или анти-cw) вершины для границы формы. Плотность вершин нерегулярна вдоль границы, поэтому длины дуг между ними обычно не равны. Поскольку формы могут быть очень большими (половина полушария), как правило, невозможно просто проецировать вершины на плоскость и использовать плоские методы, как описано в Википедии (извините, мне не разрешено более двух гиперссылок в качестве новичков). Немного лучший подход предполагает использование планарной геометрии, управляемой сферическими координатами, но опять же, с большими полигонами этот метод терпит неудачу, как хорошо иллюстрированный здесь. На этой же странице "Cffk" выделил эту статью, в которой описывается метод вычисления центроида сферических треугольников. Я попытался реализовать этот метод, но безуспешно, и я надеюсь, что кто-то может определить проблему?

Я сохранил определения переменных, похожие на те, что указаны в документе, чтобы было легче сравнивать. Вход (данные) представляет собой список координат долготы/широты, преобразованный в координаты [x, y, z] кодом. Для каждого из треугольников я произвольно фиксировал одну точку как + z-полюс, остальные две вершины состоят из пары соседних точек вдоль границы многоугольника. Код проходит вдоль границы (начиная с произвольной точки), используя каждый граничный сегмент многоугольника в качестве треугольной стороны по очереди. Субцентроид определяется для каждого из этих отдельных сферических треугольников, и они взвешиваются в соответствии с областью треугольника и добавляются для вычисления общего центроида полигонов. Я не получаю никаких ошибок при запуске кода, но возвращаемые итоговые центроиды явно неправильны (у меня есть некоторые очень простые формы, где местоположение центра тяжести однозначно). Я не нашел какой-либо разумной картины в месте возврата центроидов... поэтому на данный момент я не уверен, что происходит не так, как в математике, так и в коде (хотя подозрение - математика).

В приведенном ниже коде должен работать copy-paste, как если бы вы хотели его попробовать. Если у вас установлен 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))

Заранее благодарим за любые предложения или идеи.

EDIT: Вот цифра, показывающая проекцию единичной сферы с полигоном и итоговый центроид, который я вычисляю из кода. Ясно, что центроид ошибочен, поскольку многоугольник довольно мал и выпуклый, но центроид выходит за его периметр. polygon and centroid

EDIT: Здесь очень похожий набор координат, как указано выше, но в исходном формате [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

EDIT: Еще несколько примеров... с 4 вершинами, определяющими идеальный квадрат с центром в [1,0,0]. Я получаю ожидаемый результат: enter image description here Однако из несимметричного треугольника я получаю центроид, который нигде не близок... центроид действительно падает на дальнюю сторону сферы (здесь проецируется на лицевую сторону как антипод): enter image description here Интересно, что оценка центра тяжести выглядит "стабильной" в том смысле, что если я инвертирую список (иди от часовой стрелки до порядка против часовой стрелки или наоборот), то центроид точно инвертирует.

Ответ 1

Я думаю, что это будет сделано. Вы должны иметь возможность воспроизвести этот результат, просто скопировав код ниже.

  • Вам нужно будет иметь данные о широте и долготе в файле с именем longitude and latitude.txt. Вы можете скопировать-вставить исходные данные образца, которые включены ниже кода.
  • Если у вас есть mplotlib, он дополнительно произведет график ниже
  • Для неочевидных вычислений я включил ссылку, которая объясняет, что происходит
  • На графике ниже ссылочный вектор очень короткий (r = 1/10), так что 3d-центроиды легче видеть. Вы можете легко удалить масштабирование, чтобы максимизировать точность.
  • Примечание для op: я переписал почти все, поэтому я не уверен, где именно исходный код не работал. Однако, по крайней мере, я думаю, что он не принимал во внимание необходимость обработки вершин треугольника по часовой стрелке/против часовой стрелки.

Working Centroid

Условные обозначения:

  • (черная линия) опорный вектор
  • (маленькие красные точки) сферический треугольник 3d-центроиды
  • (большая красная/синяя/зеленая точка) 3d-centroid/проецируется на поверхность /, проецируемая на плоскость 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

Ответ 2

Я думаю, что хорошим приближением было бы вычисление центра масс с использованием взвешенных декартовых координат и проецирование результата на сферу (предполагая начало координат (0, 0, 0)^T).

Пусть (p[0], p[1], ... p[n-1]) n точек многоугольника. Аппроксимативный (декартовый) центроид может быть вычислен с помощью:

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|. 

Ответ 3

Чтобы уточнить: величина интереса представляет собой проекцию истинного 3d-центра тяжести (т.е. трехмерный центр масс, т.е. трехмерный центр области) на единичную сферу.

Поскольку все, о чем вы заботитесь, это направление от начала координат до 3d-центра, вам не нужно беспокоиться об областях; проще просто вычислить момент (т.е. область трех центроидов). Момент области слева от замкнутой траектории на единичной сфере составляет половину интеграла от единичного вектора влево, когда вы ходите по пути. Это следует из неочевидного применения теоремы Стокса; см. http://www.owlnet.rice.edu/~fjones/chap13.pdf Проблема 13-12.

В частности, для сферического многоугольника момент представляет собой половину суммы (a x b)/|| a x b || * (угол между 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]

Направления двух ответов примерно противоположны (так что я думаю, код КобеДжон решил взять область справа от пути в этом случае).