均匀地在球体上分布n个点

时间:2012-03-07 11:39:06

标签: python algorithm math geometry uniform

我需要一个算法,可以给我一个球体周围的位置N点(可能小于20)模糊地展开它们。没有必要“完美”,但我只需要它,所以它们都不会聚在一起。

  • This question提供了良好的代码,但我找不到制作这种制服的方法,因为这似乎是100%随机化。
  • This blog post建议有两种方法允许在球体上输入点数,但Saff and Kuijlaars算法完全在我可以转录的伪代码中,而我发现的code example包含“节点[k]“,我无法解释并破坏了这种可能性。第二个博客的例子是黄金分割螺旋,它给了我奇怪的,褶皱的结果,没有明确的方法来定义恒定的半径。
  • 来自This algorithm
  • this question似乎可能有效,但我无法将该页面上的内容拼凑成伪代码或任何内容。

我遇到的一些其他问题主题是随机均匀分布,这增加了我不关心的复杂程度。我很抱歉这是一个如此愚蠢的问题,但我想表明我真的很努力,但仍然很短暂。

所以,我正在寻找的是简单的伪代码,可以在单位球体周围均匀分布N个点,以球形或笛卡尔坐标返回。更好的是,如果它甚至可以通过一点随机分布(想想一颗恒星周围的行星,分散得很好,但还有余地)。

15 个答案:

答案 0 :(得分:110)

Fibonacci球体算法非常适用于此。它速度快,结果一目了然很容易愚弄人眼。 You can see an example done with processing随着时间的推移会显示结果。 Here's another great interactive example由@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

答案 1 :(得分:80)

这被称为球体上的包装点,并且没有(已知的)通用的完美解决方案。但是,有很多不完美的解决方案。最受欢迎的三个似乎是:

  1. 创建模拟。将每个点视为约束到球体的电子,然后运行模拟一定数量的步骤。电子的排斥力自然会使系统处于一个更稳定的状态,在这种状态下,这些点的距离彼此相差甚远。
  2. 超立方体拒绝。这种花哨的方法实际上非常简单:你在球体周围的立方体内统一选择点(远远超过它们的n,然后拒绝球体外的点。将剩余的点视为向量,并将它们标准化。这些是你的“样本” - 使用某种方法(随机,贪婪等)选择n
  3. 螺旋近似。您在球体周围追踪螺旋线,并均匀分布螺旋周围的点。由于所涉及的数学,这些比模拟更复杂,但更快(并且可能涉及更少的代码)。最受欢迎的似乎是Saff, et al
  4. lot 可以找到有关此问题的更多信息here

答案 2 :(得分:9)

this example code node[k]中只是第k个节点。您正在生成数组N个点,node[k]是第k个(从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

如果你绘制它,你会发现在极点附近的垂直间距更大,因此每个点位于大约相同的空间区域(靠近极点的地方空间更小)水平“,所以它提供更多”垂直“)。

这与所有与他们的邻居有相同距离的点(这是我认为你的链接所讨论的)不一样,但它可能足以满足您的需要并且仅仅制作一个统一的lat就可以了/ lon grid。

答案 3 :(得分:7)

您要找的是球形遮盖物。球形覆盖问题非常困难,除了少量的点之外,解决方案是未知的。有一点可以肯定的是,给定球体上的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个距离,拒绝的概率取决于松弛;很难说是怎么做的,所以运行一个模拟来了解相关的统计数据。

答案 4 :(得分:6)

这个答案基于this answer

概述的相同“理论”

我将这个答案添加为:
- 没有其他选项适合“均匀性”需要“点亮”(或者显然不是那么明显)。 (注意在原始问题中让行星看起来像分布看起来特别需要的行为,你只是随机地从k统一创建的点的有限列表中拒绝(随机地回到k项中的索引计数)。)
- 最接近的另一个impl迫使你通过'角轴'来决定'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等)进行测试,看起来效果很好

答案 5 :(得分:4)

尝试:

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 loop total和k loop current iteration。

它基于向日葵种子图案,除了向日葵种子弯曲成半圆顶,再次变成球形。

这是一张照片,除了我把相机放在球体的一半,所以它看起来是2d而不是3d,因为相机与所有点的距离相同。 http://3.bp.blogspot.com/-9lbPHLccQHA/USXf88_bvVI/AAAAAAAAADY/j7qhQsSZsA8/s640/sphere.jpg

答案 6 :(得分:2)

Healpix解决了一个密切相关的问题(用等面积像素对球体进行像素化):

http://healpix.sourceforge.net/

这可能有点矫枉过正,但也许在看完之后你就会意识到其他一些不错的属性对你来说很有意思。它不仅仅是一个输出点云的函数。

我降落在这里试图再次找到它; “healpix”这个名字并不能完全唤起人们的注意......

答案 7 :(得分:1)

N的两个最大因素,如果N==20,那么两个最大因素是{5,4},或者更一般地{a,b}。计算

dlat  = 180/(a+1)
dlong = 360/(b+1})

将你的第一个点放在{90-dlat/2,(dlong/2)-180},将你的第二个点放在{90-dlat/2,(3*dlong/2)-180},将你的第三个点放在{90-dlat/2,(5*dlong/2)-180},直到你绕过世界一次,到那时你必须到达当你走到{75,150}旁边时,大约{90-3*dlat/2,(dlong/2)-180}

显然,我在球形地球表面上以度为单位进行此操作,通常用于将+/-转换为N / S或E / W.显然,这会给你一个完全非随机的分布,但它是统一的,并且点不会聚集在一起。

要添加一定程度的随机性,您可以生成2个正态分布(适当的平均值为0和std dev {dlat / 3,dlong / 3})并将它们添加到均匀分布的点。

答案 8 :(得分:1)

点数少,你可以进行模拟:

from random import random,randint
r = 10
n = 20
best_closest_d = 0
best_points = []
points = [(r,0,0) for i in range(n)]
for simulation in range(10000):
    x = random()*r
    y = random()*r
    z = r-(x**2+y**2)**0.5
    if randint(0,1):
        x = -x
    if randint(0,1):
        y = -y
    if randint(0,1):
        z = -z
    closest_dist = (2*r)**2
    closest_index = None
    for i in range(n):
        for j in range(n):
            if i==j:
                continue
            p1,p2 = points[i],points[j]
            x1,y1,z1 = p1
            x2,y2,z2 = p2
            d = (x1-x2)**2+(y1-y2)**2+(z1-z2)**2
            if d < closest_dist:
                closest_dist = d
                closest_index = i
    if simulation % 100 == 0:
        print simulation,closest_dist
    if closest_dist > best_closest_d:
        best_closest_d = closest_dist
        best_points = points[:]
    points[closest_index]=(x,y,z)


print best_points
>>> best_points
[(9.921692138442777, -9.930808529773849, 4.037839326088124),
 (5.141893371460546, 1.7274947332807744, -4.575674650522637),
 (-4.917695758662436, -1.090127967097737, -4.9629263893193745),
 (3.6164803265540666, 7.004158551438312, -2.1172868271109184),
 (-9.550655088997003, -9.580386054762917, 3.5277052594769422),
 (-0.062238110294250415, 6.803105171979587, 3.1966101417463655),
 (-9.600996012203195, 9.488067284474834, -3.498242301168819),
 (-8.601522086624803, 4.519484132245867, -0.2834204048792728),
 (-1.1198210500791472, -2.2916581379035694, 7.44937337008726),
 (7.981831370440529, 8.539378431788634, 1.6889099589074377),
 (0.513546008372332, -2.974333486904779, -6.981657873262494),
 (-4.13615438946178, -6.707488383678717, 2.1197605651446807),
 (2.2859494919024326, -8.14336582650039, 1.5418694699275672),
 (-7.241410895247996, 9.907335206038226, 2.271647103735541),
 (-9.433349952523232, -7.999106443463781, -2.3682575660694347),
 (3.704772125650199, 1.0526567864085812, 6.148581714099761),
 (-3.5710511242327048, 5.512552040316693, -3.4318468250897647),
 (-7.483466337225052, -1.506434920354559, 2.36641535124918),
 (7.73363824231576, -8.460241422163824, -1.4623228616326003),
 (10, 0, 0)]

答案 9 :(得分:1)

编辑:这不会回答OP想要提出的问题,请将其保留在此处以防人们发现它有用。

我们使用概率的乘法规则,结合无穷的动物。这导致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(θ)。因此,我们通过将切片的区域从南极到北极进行积分来计算要从中采样的区域的cumulative distribution

enter image description here(其中stuff = 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)

答案 10 :(得分:1)

或......放置20个点,计算二十面体面的中心。对于12个点,找到二十面体的顶点。对于30点,二十面体边缘的中点。你可以用四面体,立方体,十二面体和八面体做同样的事情:一组点位于顶点上,另一组位于面的中心,另一组位于边缘的中心。但是,它们不能混合在一起。

答案 11 :(得分:1)

根据 fnord 的回答,这是一个 Unity3D 版本,增加了范围:

代码:

// golden angle in radians
static float Phi = Mathf.PI * ( 3f - Mathf.Sqrt( 5f ) );
static float Pi2 = Mathf.PI * 2;

public static Vector3 Point( float radius , int index , int total , float min = 0f, float max = 1f , float angleStartDeg = 0f, float angleRangeDeg = 360 )
{
    // y goes from min (-) to max (+)
    var y = ( ( index / ( total - 1f ) ) * ( max - min ) + min ) * 2f - 1f;

    // golden angle increment
    var theta = Phi * index ; 
        
    if( angleStartDeg != 0 || angleRangeDeg != 360 )
    {
        theta = ( theta % ( Pi2 ) ) ;
        theta = theta < 0 ? theta + Pi2 : theta ;
            
        var a1 = angleStartDeg * Mathf.Deg2Rad;
        var a2 = angleRangeDeg * Mathf.Deg2Rad;
            
        theta = theta * a2 / Pi2 + a1;
    }

    // https://stackoverflow.com/a/26127012/2496170
    
    // radius at y
    var rY = Mathf.Sqrt( 1 - y * y ); 
    
    var x = Mathf.Cos( theta ) * rY;
    var z = Mathf.Sin( theta ) * rY;

    return  new Vector3( x, y, z ) * radius;
}

要点:https://gist.github.com/nukadelic/7449f0872f708065bc1afeb19df666f7/edit

预览:

MP4

答案 12 :(得分:0)

# create uniform spiral grid
numOfPoints = varargin[0]
vxyz = zeros((numOfPoints,3),dtype=float)
sq0 = 0.00033333333**2
sq2 = 0.9999998**2
sumsq = 2*sq0 + sq2
vxyz[numOfPoints -1] = array([(sqrt(sq0/sumsq)), 
                              (sqrt(sq0/sumsq)), 
                              (-sqrt(sq2/sumsq))])
vxyz[0] = -vxyz[numOfPoints -1] 
phi2 = sqrt(5)*0.5 + 2.5
rootCnt = sqrt(numOfPoints)
prevLongitude = 0
for index in arange(1, (numOfPoints -1), 1, dtype=float):
  zInc = (2*index)/(numOfPoints) -1
  radius = sqrt(1-zInc**2)

  longitude = phi2/(rootCnt*radius)
  longitude = longitude + prevLongitude
  while (longitude > 2*pi): 
    longitude = longitude - 2*pi

  prevLongitude = longitude
  if (longitude > pi):
    longitude = longitude - 2*pi

  latitude = arccos(zInc) - pi/2
  vxyz[index] = array([ (cos(latitude) * cos(longitude)) ,
                        (cos(latitude) * sin(longitude)), 
                        sin(latitude)])

答案 13 :(得分:0)

@robert king这是一个非常好的解决方案,但其中包含一些草率的错误。我知道它对我有很大帮助,所以不要介意草率。 :) 这是一个清理的版本。...

from math import pi, asin, sin, degrees
halfpi, twopi = .5 * pi, 2 * pi
sphere_area = lambda R=1.0: 4 * pi * R ** 2

lat_dist = lambda lat, R=1.0: R*(1-sin(lat))

#A = 2*pi*R^2(1-sin(lat))
def sphere_latarea(lat, R=1.0):
    if -halfpi > lat or lat > halfpi:
        raise ValueError("lat must be between -halfpi and halfpi")
    return 2 * pi * R ** 2 * (1-sin(lat))

sphere_lonarea = lambda lon, R=1.0: \
        4 * pi * R ** 2 * lon / twopi

#A = 2*pi*R^2 |sin(lat1)-sin(lat2)| |lon1-lon2|/360
#    = (pi/180)R^2 |sin(lat1)-sin(lat2)| |lon1-lon2|
sphere_rectarea = lambda lat0, lat1, lon0, lon1, R=1.0: \
        (sphere_latarea(lat0, R)-sphere_latarea(lat1, R)) * (lon1-lon0) / twopi


def test_sphere(n_lats=10, n_lons=19, radius=540.0):
    total_area = 0.0
    for i_lons in range(n_lons):
        lon0 = twopi * float(i_lons) / n_lons
        lon1 = twopi * float(i_lons+1) / n_lons
        for i_lats in range(n_lats):
            lat0 = asin(2 * float(i_lats) / n_lats - 1)
            lat1 = asin(2 * float(i_lats+1)/n_lats - 1)
            area = sphere_rectarea(lat0, lat1, lon0, lon1, radius)
            print("{:} {:}: {:9.4f} to  {:9.4f}, {:9.4f} to  {:9.4f} => area {:10.4f}"
                    .format(i_lats, i_lons
                    , degrees(lat0), degrees(lat1)
                    , degrees(lon0), degrees(lon1)
                    , area))
            total_area += area
    print("total_area = {:10.4f} (difference of {:10.4f})"
            .format(total_area, abs(total_area) - sphere_area(radius)))

test_sphere()

答案 14 :(得分:-1)

这很有效,而且很简单。尽可能多的要点:

    private function moveTweets():void {


        var newScale:Number=Scale(meshes.length,50,500,6,2);
        trace("new scale:"+newScale);


        var l:Number=this.meshes.length;
        var tweetMeshInstance:TweetMesh;
        var destx:Number;
        var desty:Number;
        var destz:Number;
        for (var i:Number=0;i<this.meshes.length;i++){

            tweetMeshInstance=meshes[i];

            var phi:Number = Math.acos( -1 + ( 2 * i ) / l );
            var theta:Number = Math.sqrt( l * Math.PI ) * phi;

            tweetMeshInstance.origX = (sphereRadius+5) * Math.cos( theta ) * Math.sin( phi );
            tweetMeshInstance.origY= (sphereRadius+5) * Math.sin( theta ) * Math.sin( phi );
            tweetMeshInstance.origZ = (sphereRadius+5) * Math.cos( phi );

            destx=sphereRadius * Math.cos( theta ) * Math.sin( phi );
            desty=sphereRadius * Math.sin( theta ) * Math.sin( phi );
            destz=sphereRadius * Math.cos( phi );

            tweetMeshInstance.lookAt(new Vector3D());


            TweenMax.to(tweetMeshInstance, 1, {scaleX:newScale,scaleY:newScale,x:destx,y:desty,z:destz,onUpdate:onLookAtTween, onUpdateParams:[tweetMeshInstance]});

        }

    }
    private function onLookAtTween(theMesh:TweetMesh):void {
        theMesh.lookAt(new Vector3D());
    }