计算无向线段平均方向的算法

5

我有一组由两个端点组成的二维无向线段,其中大部分都呈现出基本相同的方向。

我希望计算的是这组线段的平均方向(例如,如果整组线段总体上是南北方向,它应该返回大约0°之类的东西...)。请注意,我不关心返回哪个实际方向(0°或180°都可以)。

将每个线段的方向限制在[0..180°[范围内并计算平均值并不起作用(例如两个线段,一个为1°,另一个为-1°:第二个将被限制为179°,平均值是错误的,这里是90°,应该是0°)。

我也考虑过将“标准化线段”端点聚集成两组,并计算由2个簇中点组成的线段的方向,但这似乎对于任务来说有些复杂。通过“标准化线段”,我指的是具有单位圆上的两个端点和原点的中间点的线段。

是否有已知的算法/公式可用于此?

5个回答

3

据我理解,分段的位置并不重要,只有它们的方向才重要。

因此,我们可以稍微改变一下问题:我们有一组向量,我们想在它们上面拟合一条直线。

我们可以采用不同的标准来完成这个任务。一个常用的标准是最小二乘法。

根据这个标准,解决方案为:

double dvx=0,dvy=0;
for(const auto &direction:directions)
{
    dvx+=2*direction.dx*direction.dy;
    dvy+=squared(directions.dx)-squared(directions.dy);
}
return std::atan2(dvx,dvy)/2;//or may be +pi/2

注意:对于此实现,方向将根据其长度加权,如果要分配相同的权重,则应标准化方向向量。
这种方法有时用于指纹识别中确定线条方向:http://jmit.us.edu.pl/cms/jmitjrn/22/28_Wieclaw_4.pdf 理解此方法有几种方式。其中一种是几何方法:
我们有一组向量,角度为 alpha[i] 与 X 轴。我们不平均这些向量。而是构建角度为 2*alpha[i] 的向量,对它们进行平均并取结果角度的一半。诀窍在于,如果相反的方向差异为 pi 并且经过加倍后它们将因为没有差异而相等。

据我所知,这将计算向量的平均值。我有无向线段,因此应该将线段及其相反方向(交换两个端点)视为同一条线段。 - Laurent Grégoire
@LaurentGrégoire,请查看我添加到结尾的解释。 - maxim1000
非常好,的確訣竅在於平均方位角乘以2,然後將這個平均值除以2。順帶一提,為了我的需求,我使用了正弦和餘弦的簡單加權平均值。 - Laurent Grégoire

2

有一种方法可以找到角度的平均值(在完整的圆形范围内)

MeanAngle = ArcTan2(Sum{i=1..n}(Sin(Alpha[i])), Sum{i}(Cos(Alpha[i])))

看起来对于你的情况,你可以计算方向向量的余弦平均值(因为Cos(-alpha) = Cos(alpha)),并得到ArcCos(范围在0..Pi之间)。
MeanAngleWithoutDir = ArcCos(1/n * Sum{i=1..n}(Cos(Alpha[i])))

可能应该将角度夹在(0..Pi)或(-Pi/2..Pi/2)范围内,以避免歧义。

这行代码不起作用。对于仅有两个线段的简单情况,一个在10度,另一个在-10度,你的公式给出的平均值是10度,而期望的解决方案是0度。 - Laurent Grégoire
@Laurent Grégoire,我再次对问题的表述表示怀疑:Mean(-10.10) = 0。但是根据您的要求,-10 = 170Mean(170, 10) = 90。这是矛盾的...这个问题能够解决吗? - MBo
这个问题肯定是可以解决的,看看我的答案。诀窍确实是要处理好模180度的情况。对角度进行模180度平均显然是错误的(-10度和10度就是一个简单的例子,期望的答案是0度,而不是90度)。 - Laurent Grégoire

2
注:本答案计算所给线条的“中位数”。 MBo的另一个答案计算所给线条的“平均值”。
为了更好地阐述问题,我们将问题形式化如下: 给定一组线段,我们要找到一条直线p,使得该直线与所有给定线段之间的夹角和尽可能小。 这里,两条直线之间的夹角是它们相交点处夹角的最小值,如果它们平行或重合,则夹角为0。 因此,两条直线之间的夹角始终在0到90度之间。
为了简化问题,将所有线段平移,使它们都通过原点。 显然,这不会影响答案。
为了解决这个问题,让我们研究所述总和的导数。 假设我们有一个答案线p。 让有x条线段从p顺时针方向0-90度,y条线段从p逆时针方向0-90度(x+y=n,n为给定线段总数)。
现在,将p顺时针旋转一个小角度α。 答案将减少x*α并增加y*α。 因此,如果x>y,则答案将减少,如果x
有两种情况下x和y会改变。
1.线p与给定线之一重合。 2.直线q垂直于给定线之一。
在圆上的任意两个相邻点之间,我们的总和的导数将是常数x-y。 因此,最小值将在某些“感兴趣的角度”之一:与某些给定线平行或垂直。 这导致O(n^2)算法:对于每个O(n)感兴趣的角度,仅在O(n)中计算总和,并选择给出最小总和的角度。
这可以进一步加速到O(n log n)。
  1. O(n)的时间复杂度生成2n个感兴趣的角度。

  2. O(n log n)的时间复杂度将它们排序。

  3. O(n)的时间复杂度计算答案,并计算第一个角度的xy值。

  4. 沿着圆周移动,保持当前答案和xy值,在O(n)步中,每次运算可以在O(1)的时间内完成。


这个中位数的方法看起来很有趣,但无法轻松适应分段加权。对于大数据集,O(n^2)是行不通的。 - Laurent Grégoire
@LaurentGrégoire 但是它可以。我们将拥有总重量为_X_的顺时针段和总重量为_Y_的逆时针段,而不是权重为1的_x_和_y_段。其余的论点保持完全相同。 - Gassa
@LaurentGrégoire O(n^2) 是一种简单的实现方式。如下所述,如果需要,它可以加速到 _O(n log n)_。如果不清楚,请指出需要进一步解释的内容。 - Gassa
@LaurentGrégoire 另外一点,请在问题中清楚地说明您的要求(加权分段、可行的渐近行为等)。 - Gassa
Segment weight是可选的,许多类算法可以轻松地进行调整以适应它(如果需要)。我没有包含它是为了不在我的问题中增加太多复杂性。你的算法很好;只是O(n)的另一个解决方案更快,可能更容易实现。 - Laurent Grégoire
@LaurentGrégoire 更简单,更可靠:MBo的解决方案只有几行代码,而这个可能需要10-30行。至于它是否更快,很可能是的,但在大多数情况下,_O(n log n)_排序和_O(n)_附加处理也可以。我猜结果的相关性比速度和简单性更重要。这些解决方案计算不同的数量(“平均值”与“中位数”),而重要的是哪一个更接近您实际需要的内容。 - Gassa

2
统计数据显示,它们大部分都朝着或多或少相同的方向。这个关键信息在算法设计中至关重要。如果你知道所有向量都在一个90度圆锥内,可以使用非常简单的方法: 1. 将第一个标准化方向向量与其他所有向量进行点积。 2. 翻转任何返回负点积的向量。 3. 像往常一样平均结果向量。
如果需要处理更广泛的分布,可以稍微修改一下: 1. 逐个对标准化方向向量求和。 2. 在添加向量之前,计算其与当前总和的点积。 3. 如果点积为负数,请先翻转向量再将其加入总和。 4. 对总和进行标准化。
这是一个串行算法,但如果需要更高的性能,则可以很容易地将其制定为并行归约: 1. 对于归约树中的每一对标准化向量:
a. 取出这两个向量的点积。 b. 如果点积为负数,请翻转其中一个向量并将它们加起来。 c. 将总和传递到归约的下一阶段。
2. 对最终总和进行标准化。
任何这些方法都可以轻松加权,因为你只关心点积的符号。

不幸的是,并非所有的数据都在90°锥体内。但其中大部分可能会相对集中在平均值周围。我的数据集在平均值周围具有类似于高斯分布的形态,标准差在10-20°之间。 - Laurent Grégoire

1
以下是一个 O(n) 的解决方案,还可以为每个段指定可选的权重。
我们通过它与 X 轴的角度(a)和权重(w)来模拟每个段。此时,段的方向并不重要,任何值模 180° 都可以。思路是循环每个段,并跟踪到目前为止计算出的平均方向;并使用更接近平均自身的方向模 180 来调整此平均值。
伪代码(所有角度以度为单位):
aa = 0
ww = 0
for a, w in segments:
    // Compute delta between angles in range [-180°..+180°[
    da = a - aa
    if da < -180:
       da += 360
    if da >= 180:
       da -= 360
    // Optional direction swap, delta in [-90°..+90°[
    if da < -90:
       da += 180
    if da >= 90:
       da -= 180
    // The following formula also make sure aa = a mod 180
    // when ww = 0 (first iteration).
    aa += da * w / (w + ww)
    ww += w
    // Clamp result to [0°..+360°[
    if aa >= 360:
       aa -= 360
    if aa < 0:
       aa += 360
// Clamp final result aa to [0..+180°[ (optional step)
if aa > 180:
    aa -= 180

我没有证明结果与迭代顺序无关,但是根据算法的第一眼印象,它应该是无关的。


关于算法与输入迭代顺序的依赖性

对于规范的输入数据,无论迭代顺序如何,该算法都非常稳定。

然而,一旦输入数据没有明确的主方向,该算法的结果将强烈依赖于迭代顺序,以难以预测的混沌模式出现。

数值模拟表明,对于标准差小于20°(中位数左右)的随机方向,该算法似乎总是稳定的。当标准差大于20°时,数值不稳定性开始出现,结果强烈依赖于迭代顺序(在20°到30°之间,差异可能足够小可以忽略,超过30°后差异变得很大)。

我没有精确计算混沌/稳定标准差截止值,因此将这个20°值作为初始猜测。一个精确的数学解决方案留给读者作为练习。

以下是数值模拟的结果(对于每个标准差从0到45°,在给定标准差的各种随机数据上运行1000次算法,并测量10次运行之间的平均差异)。

Average deviation between runs vs input data standard deviation around mean

因此,为了获得最佳结果,如果您的输入数据不能保证具有小的标准偏差,则最好按稳定的关键字排序输入数据(首先是更大的权重,或者根据您的输入选择任何其他关键字)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接