Равномерно распределяя n точек по сфере - PullRequest
99 голосов
/ 07 марта 2012

Мне нужен алгоритм, который может дать мне позиции вокруг сферы для N точек (вероятно, меньше 20), которые расплывчато распределяют их. Нет необходимости в «совершенстве», но мне просто нужно, чтобы ни один из них не был сгруппирован вместе.

  • Этот вопрос предоставил хороший код, но я не смог найти способ сделать эту униформу, так как это казалось на 100% рандомизированным.
  • В этом посте рекомендуется два способа, позволяющих вводить количество точек на сфере, но алгоритм Saff и Kuijlaars точно соответствует psuedocode, который я мог бы транскрибировать, и Пример кода Я обнаружил, что в нем содержится "узел [k]", который я не смог понять, объяснил и разрушил эту возможность. Второй пример блога - «Спираль золотого сечения», которая дала мне странные, сгруппированные результаты без четкого способа определения постоянного радиуса.
  • Этот алгоритм из этот вопрос может показаться, что он может сработать, но я не могу собрать воедино то, что на этой странице, в psuedocode или что-то в этом роде.

Несколько других вопросов, с которыми я сталкивался, говорили о рандомизированном равномерном распределении, что добавляет уровень сложности, который меня не беспокоит. Я прошу прощения за то, что это такой глупый вопрос, но я хотел показать, что я действительно выглядел суровым и все еще терпел неудачу.

Итак, я ищу простой псевдокод для равномерного распределения N точек вокруг единичной сферы, который возвращается в сферических или декартовых координатах. Еще лучше, если он может даже распределяться с небольшой долей случайности (подумайте о планетах вокруг звезды, прилично раскинувшихся, но с достаточной свободой).

Ответы [ 14 ]

110 голосов
/ 30 сентября 2014

Алгоритм сферы Фибоначчи отлично подходит для этого.Это быстро и дает результаты, которые с первого взгляда легко обмануть человеческий глаз. Вы можете увидеть пример с обработкой , который будет показывать результат с течением времени по мере добавления точек. Вот еще один замечательный интерактивный пример , сделанный @gman.А вот быстрая версия Python с простой опцией рандомизации:

import math, random

def fibonacci_sphere(samples=1,randomize=True):
    rnd = 1.
    if randomize:
        rnd = random.random() * samples

    points = []
    offset = 2./samples
    increment = math.pi * (3. - math.sqrt(5.));

    for i in range(samples):
        y = ((i * offset) - 1) + (offset / 2);
        r = math.sqrt(1 - pow(y,2))

        phi = ((i + rnd) % samples) * increment

        x = math.cos(phi) * r
        z = math.sin(phi) * r

        points.append([x,y,z])

    return points

1000 сэмплов дает вам:

enter image description here

81 голосов
/ 07 марта 2012

Это называется упаковочными точками на сфере, и не существует (известного) общего идеального решения. Однако есть много несовершенных решений. Три самых популярных, кажется, являются:

  1. Создать симуляцию . Рассматривайте каждую точку как электрон, ограниченный сферой, затем запустите симуляцию для определенного количества шагов. Отталкивание электронов естественным образом приведет систему к более устойчивому состоянию, когда точки находятся как можно дальше друг от друга, насколько они могут получить.
  2. Отклонение гиперкуба . Этот необычно звучащий метод на самом деле очень прост: вы равномерно выбираете точки (намного больше, чем n из них) внутри куба, окружающего сферу, затем отклоняете точки вне сферы. Рассматривайте оставшиеся точки как векторы и нормализуйте их. Это ваши «образцы» - выберите n из них, используя какой-либо метод (случайно, жадный и т. Д.).
  3. Спиральные приближения . Вы проводите спираль вокруг сферы и равномерно распределяете точки вокруг спирали. Из-за математики они более сложны для понимания, чем симуляция, но гораздо быстрее (и, вероятно, требуют меньше кода). Наиболее популярными, по-видимому, являются Сафф и др. .

A лот более подробную информацию об этой проблеме можно найти здесь

59 голосов
/ 24 мая 2017

Золотой метод спирали

Вы сказали, что не можете заставить работать метод золотой спирали, и это позор, потому что это действительно очень хорошо. Я хотел бы дать вам полное понимание этого, чтобы, возможно, вы могли понять, как не допустить, чтобы это было «сгруппировано».

Итак, вот быстрый, неслучайный способ создания решетки, которая приблизительно верна; как обсуждалось выше, ни одна решетка не будет идеальной, но это может быть «достаточно хорошо». По сравнению с другими методами, например на BendWavy.org , но он просто выглядит красиво и красиво, а также гарантирует равномерный интервал в пределе.

Грунтовка: спирали подсолнуха на диске агрегата

Чтобы понять этот алгоритм, я сначала приглашаю вас взглянуть на алгоритм 2D спирали подсолнечника. Это основано на том факте, что самым иррациональным числом является золотое сечение (1 + sqrt(5))/2, и если кто-то выбрасывает точки при подходе «встаньте в центре, поверните золотое сечение целых поворотов, а затем испустите другую точку в этом направлении» естественным образом строит спираль, которая, когда вы получаете все большее и большее количество точек, тем не менее отказывается иметь четко определенные «столбики», на которых эти точки располагаются в ряд. (Примечание 1.)

Алгоритм равномерного разделения на диске:

from numpy import pi, cos, sin, sqrt, arange
import matplotlib.pyplot as pp

num_pts = 100
indices = arange(0, num_pts, dtype=float) + 0.5

r = sqrt(indices/num_pts)
theta = pi * (1 + 5**0.5) * indices

pp.scatter(r*cos(theta), r*sin(theta))
pp.show()

и выдает результаты, которые выглядят следующим образом (n = 100 и n = 1000):

enter image description here

Радиальное расстояние между точками

Ключевая странная вещь - формула r = sqrt(indices / num_pts); как я дошел до этого? (Примечание 2.)

Ну, я использую здесь квадратный корень, потому что я хочу, чтобы они имели равномерное пространство вокруг сферы. Это то же самое, что сказать, что в пределе больших N я хочу маленький регион R ∈ ( r , r + d r ), Θ ∈ ( θ , θ + d θ ), чтобы содержать число точек, пропорциональных его площадь, которая составляет r d r d θ . Теперь, если мы притворимся, что речь идет о случайной переменной здесь, это будет иметь прямую интерпретацию, заключающуюся в том, что общая плотность вероятности для ( R , Θ ) составляет всего к для некоторой константы c . Затем нормализация на диске устройства приводит к принудительному выполнению c = 1 / & pi;.

Теперь позвольте мне представить трюк. Это происходит из теории вероятностей, где она известна как выборка обратного CDF : предположим, что вы хотели сгенерировать случайную величину с плотностью вероятности f ( z ) и у вас есть случайная переменная U ~ Uniform (0, 1), как и random() в большинстве языков программирования. Как ты это делаешь?

  1. Сначала превратите вашу плотность в кумулятивную функцию распределения F ( z ), которая, помните, монотонно увеличивается от 0 до 1 с производной F (* * г тысяча девяносто-один * +1092 *). * * 1 093
  2. Затем вычислите обратную функцию CDF F -1 ( z ).
  3. Вы обнаружите, что Z = F -1 ( U ) распределяется в соответствии с целевой плотностью. (Примечание 3).

Теперь спиральный трюк с золотым сечением расставляет точки равномерно и равномерно для θ , поэтому давайте интегрируем это; для единичного круга у нас осталось F ( r ) = r 2 . Таким образом, обратная функция равна F -1 ( u ) = u 1/2 , и поэтому мы будет генерировать случайные точки на сфере в полярных координатах с r = sqrt(random()); theta = 2 * pi * random().

Теперь яВместо случайной выборки этой обратной функции мы равномерно выбираем ее, и приятная вещь о равномерной выборке состоит в том, что наши результаты о том, как точки распределены в пределе больших N будет вести себя так, как будто мы случайным образом взяли его. Эта комбинация - хитрость. Вместо random() мы используем (arange(0, num_pts, dtype=float) + 0.5)/num_pts, так что, скажем, если мы хотим взять 10 точек, они равны r = 0.05, 0.15, 0.25, ... 0.95. Мы равномерно выбираем r , чтобы получить интервал равной площади, и мы используем приращение подсолнечника, чтобы избежать ужасных "полос" точек на выходе.

Сейчас занимаемся подсолнухом на сфере

Изменения, которые нам нужно внести, чтобы расставить точки по точкам, просто включают в себя переключение полярных координат на сферические. Радиальная координата, конечно, не входит в это, потому что мы находимся на единичной сфере. Чтобы здесь все было более согласованно, хотя я и был физиком, я буду использовать координаты математиков, где 0 & le; φ & le; &число Пи; широта, падающая с полюса и 0 & le; θ & le; 2 & пи; это долгота. Таким образом, отличие от вышеизложенного состоит в том, что мы в основном заменяем переменную r на & phi; .

Наш элемент площади, который был r d r d θ , теперь становится не намного более сложным грехом ( φ ) д φ д θ . Таким образом, наша плотность соединения для равномерного расстояния является грехом ( φ ) / 4π. Интегрируя θ , мы находим f ( φ ) = sin ( φ ) / 2, таким образом F ( φ ) = (1 & минус; cos ( φ )) / 2. Инвертируя это, мы можем видеть, что равномерная случайная величина будет выглядеть как acos (1 - 2 u ), но мы выбираем равномерно, а не случайно, поэтому вместо этого используем φ k = acos (1 & минус 2 ( k + 0,5) / N ). А остальная часть алгоритма просто проецирует это на координаты x, y и z:

from numpy import pi, cos, sin, arccos, arange
import mpl_toolkits.mplot3d
import matplotlib.pyplot as pp

num_pts = 1000
indices = arange(0, num_pts, dtype=float) + 0.5

phi = arccos(1 - 2*indices/num_pts)
theta = pi * (1 + 5**0.5) * indices

x, y, z = cos(theta) * sin(phi), sin(theta) * sin(phi), cos(phi);

pp.figure().add_subplot(111, projection='3d').scatter(x, y, z);
pp.show()

Опять же, при n = 100 и n = 1000 результаты выглядят так: enter image description here enter image description here

Примечания

  1. Эти "столбцы" образованы рациональными приближениями к числу, и наилучшие рациональные приближения к числу получаются из выражения его непрерывной дроби, z + 1/(n_1 + 1/(n_2 + 1/(n_3 + ...))), где z - целое число, а n_1, n_2, n_3, ... - либо конечная или бесконечная последовательность натуральных чисел:

    def continued_fraction(r):
        while r != 0:
            n = floor(r)
            yield n
            r = 1/(r - n)
    

    Поскольку дробная часть 1/(...) всегда находится между нулем и единицей, большое целое число в непрерывной дробной части дает особенно хорошее рациональное приближение: «деленное на что-то между 100 и 101» лучше, чем «деленное на что-то» между 1 и 2 ". Поэтому самым иррациональным числом является число, равное 1 + 1/(1 + 1/(1 + ...)) и не имеющее особо хороших рациональных приближений; можно решить φ = 1 + 1 / φ , умножив на φ , чтобы получить формулу для золотого сечения.

    1. Для людей, которые не очень знакомы с NumPy - все функции «векторизованы», так что sqrt(array) совпадает с тем, что могут написать другие языки map(sqrt, array). Так что это приложение за компонентом sqrt. То же самое относится и к делению на скаляр или сложению со скалярами - они применяются ко всем компонентам параллельно.

    2. рКрыша проста, если вы знаете, что это результат. Если вы спросите, какова вероятность того, что z <<em> Z <<em> z + d z , это то же самое, что спросить, какова вероятность того, что z <<em> F -1 ( U ) <<em> z + d z , примените F ко всем трем выражениям, отметив, что это монотонно возрастающая функция, поэтому F ( z ) <<em> U <<em> F ( z + d z ), разверните правую сторону, чтобы найти F ( z ) + f ( z ) d z , а поскольку U равномерно, эта вероятность равна f ( z ) d z как и было обещано.

9 голосов
/ 07 марта 2012

В этот пример кода node[k] является просто k-м узлом. Вы генерируете массив из N точек, а node[k] - это kth (от 0 до N-1). Если это все, что вас смущает, надеюсь, вы можете использовать это сейчас.

(другими словами, k - это массив размера N, определенный до запуска фрагмента кода и содержащий список точек).

В качестве альтернативы , опираясь на другой ответ здесь (и используя Python):

> cat ll.py
from math import asin
nx = 4; ny = 5
for x in range(nx):
    lon = 360 * ((x+0.5) / nx)
    for y in range(ny):                                                         
        midpt = (y+0.5) / ny                                                    
        lat = 180 * asin(2*((y+0.5)/ny-0.5))                                    
        print lon,lat                                                           
> python2.7 ll.py                                                      
45.0 -166.91313924                                                              
45.0 -74.0730322921                                                             
45.0 0.0                                                                        
45.0 74.0730322921                                                              
45.0 166.91313924                                                               
135.0 -166.91313924                                                             
135.0 -74.0730322921                                                            
135.0 0.0                                                                       
135.0 74.0730322921                                                             
135.0 166.91313924                                                              
225.0 -166.91313924                                                             
225.0 -74.0730322921                                                            
225.0 0.0                                                                       
225.0 74.0730322921                                                             
225.0 166.91313924
315.0 -166.91313924
315.0 -74.0730322921
315.0 0.0
315.0 74.0730322921
315.0 166.91313924

Если вы наметите это, вы увидите, что вертикальный интервал больше у полюсов, так что каждая точка находится примерно в одной и той же общей области пространства (около полюсов меньше места по горизонтали) ", так что это дает больше" по вертикали ").

Это не то же самое, что все точки, находящиеся примерно на одинаковом расстоянии от своих соседей (о чем, я думаю, ваши ссылки говорят), но этого может быть достаточно для того, что вы хотите, и улучшается просто создание равномерного широты. / длинная сетка.

7 голосов
/ 16 апреля 2015

То, что вы ищете, называется сферическим покрытием .Задача сферического покрытия очень сложна, и решения неизвестны, за исключением небольшого числа точек.Одна вещь, которая наверняка известна, состоит в том, что при заданных n точках на сфере всегда существуют две точки расстояния d = (4-csc^2(\pi n/6(n-2)))^(1/2) или ближе.

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

Random r = new Random();
double[] p = { r.nextGaussian(), r.nextGaussian(), r.nextGaussian() };

Затем спроецируйте точку на сферу, нормализуя ее расстояние от начала координат

double norm = Math.sqrt( (p[0])^2 + (p[1])^2 + (p[2])^2 ); 
double[] sphereRandomPoint = { p[0]/norm, p[1]/norm, p[2]/norm };

Гауссово распределение по n измерениям равносферически симметричный, так что проекция на сферу равномерна.

Конечно, нет гарантии, что расстояние между любыми двумя точками в наборе равномерно сгенерированных точек будет ограничено снизу, поэтому вы можете использовать отклонение для принудительного применения любоготакие условия, которые могут у вас возникнуть: вероятно, лучше всего создать всю коллекцию, а затем отклонить всю коллекцию, если это необходимо.(Или используйте «раннее отклонение», чтобы отклонить всю сгенерированную вами коллекцию; просто не сохраняйте одни точки и отбрасывайте другие.) Вы можете использовать формулу для d, приведенную выше, за вычетом некоторого провисания, чтобы определитьминимальное расстояние между точками, ниже которого вы будете отклонять набор точек.Вам нужно будет рассчитать n выбрать 2 расстояния, и вероятность отклонения будет зависеть от провисания;Трудно сказать, как, поэтому запустите симуляцию, чтобы получить представление о соответствующей статистике.

6 голосов
/ 21 апреля 2013

Этот ответ основан на той же «теории», которая хорошо изложена этим ответом

Я добавляю этот ответ как:
- Ни один из других вариантов не подходит для «единообразия» и требует «точного» (или явно не однозначного). (Принимая во внимание, что в первоначальном запросе получилось поведение, похожее на планету, как распределение, вы просто отбрасываете из конечного списка k равномерно созданных точек случайным образом (случайным образом по индексу в k элементах).)
- Ближайший другой подтолкнул вас к определению «N» по «угловой оси», а не по «одному значению N» по обеим значениям угловой оси (что при малых значениях N очень сложно узнать, что может, или может не иметь значения (например, вы хотите 5 баллов - получайте удовольствие))
- Кроме того, очень трудно «уловить», как провести различие между другими вариантами без каких-либо изображений, поэтому вот как выглядит этот параметр (ниже), и готовая к запуску реализация, сопровождающая его.

с N в 20:

enter image description here
, а затем N на 80: enter image description here


вот готовый к запуску код на python3, где эмуляция - это тот же источник: "http://web.archive.org/web/20120421191837/http://www.cgafaq.info/wiki/Evenly_distributed_points_on_sphere", найденный другими. (График, который я включил, который запускается при запуске как «основной», взят из: http://www.scipy.org/Cookbook/Matplotlib/mplot3D)

from math import cos, sin, pi, sqrt

def GetPointsEquiAngularlyDistancedOnSphere(numberOfPoints=45):
    """ each point you get will be of form 'x, y, z'; in cartesian coordinates
        eg. the 'l2 distance' from the origion [0., 0., 0.] for each point will be 1.0 
        ------------
        converted from:  http://web.archive.org/web/20120421191837/http://www.cgafaq.info/wiki/Evenly_distributed_points_on_sphere ) 
    """
    dlong = pi*(3.0-sqrt(5.0))  # ~2.39996323 
    dz   =  2.0/numberOfPoints
    long =  0.0
    z    =  1.0 - dz/2.0
    ptsOnSphere =[]
    for k in range( 0, numberOfPoints): 
        r    = sqrt(1.0-z*z)
        ptNew = (cos(long)*r, sin(long)*r, z)
        ptsOnSphere.append( ptNew )
        z    = z - dz
        long = long + dlong
    return ptsOnSphere

if __name__ == '__main__':                
    ptsOnSphere = GetPointsEquiAngularlyDistancedOnSphere( 80)    

    #toggle True/False to print them
    if( True ):    
        for pt in ptsOnSphere:  print( pt)

    #toggle True/False to plot them
    if(True):
        from numpy import *
        import pylab as p
        import mpl_toolkits.mplot3d.axes3d as p3

        fig=p.figure()
        ax = p3.Axes3D(fig)

        x_s=[];y_s=[]; z_s=[]

        for pt in ptsOnSphere:
            x_s.append( pt[0]); y_s.append( pt[1]); z_s.append( pt[2])

        ax.scatter3D( array( x_s), array( y_s), array( z_s) )                
        ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Z')
        p.show()
        #end

проверено на низких счетах (N в 2, 5, 7, 13 и т. Д.) И, кажется, работает "хорошо"

4 голосов
/ 31 декабря 2013

Попробуйте:

function sphere ( N:float,k:int):Vector3 {
    var inc =  Mathf.PI  * (3 - Mathf.Sqrt(5));
    var off = 2 / N;
    var y = k * off - 1 + (off / 2);
    var r = Mathf.Sqrt(1 - y*y);
    var phi = k * inc;
    return Vector3((Mathf.Cos(phi)*r), y, Mathf.Sin(phi)*r); 
};

Вышеприведенная функция должна выполняться в цикле с общим количеством N циклов и текущей итерацией k циклов.

Это основано на структуре семян подсолнечника, за исключением того, что семена подсолнечника изогнуты вокруг в половину купола, и снова в сферу.

Вот изображение, за исключением того, что я поместил камеру наполовину внутри сферы, чтобы она выглядела 2d вместо 3d, потому что камера находится на одинаковом расстоянии от всех точек. http://3.bp.blogspot.com/-9lbPHLccQHA/USXf88_bvVI/AAAAAAAAADY/j7qhQsSZsA8/s640/sphere.jpg

2 голосов
/ 17 июня 2017

Healpix решает тесно связанную проблему (пикселирование сферы с пикселями равной площади):

http://healpix.sourceforge.net/

Вероятно, это излишне, но, возможно, после просмотра вы пойметеэто другие приятные свойства, которые вам интересны.Это гораздо больше, чем просто функция, которая выводит облако точек.

Я приземлился здесь, пытаясь найти его снова;название «healpix» не совсем напоминает сферы ...

1 голос
/ 22 февраля 2014

ИЛИ ... чтобы разместить 20 точек, вычислите центры граней икосаэдра. Для 12 точек найдите вершины икосаэдра. Для 30 точек - средняя точка краев икосаэдра. вы можете сделать то же самое с тетраэдром, кубом, додекаэдром и октаэдром: один набор точек находится на вершинах, другой - в центре грани, а другой - в центре ребер. Однако их нельзя смешивать.

1 голос
/ 07 марта 2012

edit: Это не отвечает на вопрос, который ОП хотел задать, оставляя его здесь на случай, если люди найдут его полезным.

Мы используем правило вероятности умножения в сочетании с бесконечно малыми числами. В результате получается 2 строки кода для достижения желаемого результата:

longitude: φ = uniform([0,2pi))
azimuth:   θ = -arcsin(1 - 2*uniform([0,1]))

(определено в следующей системе координат:)

enter image description here

Ваш язык обычно имеет единый примитив случайных чисел. Например, в Python вы можете использовать random.random(), чтобы вернуть число в диапазоне [0,1). Вы можете умножить это число на k, чтобы получить случайное число в диапазоне [0,k). Таким образом, в python uniform([0,2pi)) будет означать random.random()*2*math.pi.


Доказательство

Теперь мы не можем назначить θ равномерно, иначе мы бы получили комкование на полюсах. Мы хотим назначить вероятности, пропорциональные площади поверхности сферического клина (θ на этой диаграмме фактически φ):

enter image description here

Угловое смещение dφ на экваторе приведет к смещению dφ * r. Каким будет это смещение при произвольном азимуте θ? Итак, радиус от оси z равен r*sin(θ), поэтому длина дуги этой «широты», пересекающей клин, равна dφ * r*sin(θ). Таким образом, мы вычисляем кумулятивное распределение области для выборки из нее путем интегрирования площади среза от южного полюса до северного полюса.

enter image description here (где вещи = dφ*r)

Теперь мы попытаемся получить инверсию CDF для выборки из него: http://en.wikipedia.org/wiki/Inverse_transform_sampling

Сначала мы нормализуем, разделив наш почти-CDF на его максимальное значение. Это имеет побочный эффект отмены dφ и r.

azimuthalCDF: cumProb = (sin(θ)+1)/2 from -pi/2 to pi/2

inverseCDF: θ = -sin^(-1)(1 - 2*cumProb)

Таким образом:

let x by a random float in range [0,1]
θ = -arcsin(1-2*x)
...