首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

8.4 移动凸多边形
(Translating a Convex Polygon)

当机器人形状为凸多边形时,我们会遇到一个棘手的问题。从一个位置移动到另一个位置可能需要旋转机器人。 关于旋转的讨论我们放到 8.5 节。在这里,我们仅考虑平移运动。 这项任务仍然比移动圆盘更复杂,但幸运的是,通过闵可夫斯基和(Minkowski sums)来膨胀障碍物的想法仍然适用。在讨论算法细节之前,我们先通过一个例子来解释其基本思想。

8.4.1. 闵可夫斯基和示例 (Minkowski Sum Example)

设机器人 \(R\) 为一个正方形,并选择其左下角作为参考点 \(r\)。考虑一个多边形 \(P\),如图 8.5 所示的五边形。 当 \(R\) 沿着 \(P\) 的边界 \(\partial P\) 移动时,参考点 \(r\) 描绘出 \(P^+\) 的边界,\(P^+\) 是膨胀后的障碍区域,定义了 \(r\) 无法进入的范围。 请注意,这种情况与圆盘稍有不同,因为我们将参考点选在了 \(R\) 的一个角上。\(P\) 在不同边上膨胀量各不同。例如,沿边 \(e_0\) 移动时, \(P\) 和 \(P^+\) 的边界重合,因为参考点 \(r\) 有可能接触到 \(e_0\)。而且 \(P^+\) 相对于 \(P\) 的边 \(e_1\) 和 \(e_4\) 的偏移量是不同的。 相对于边 \(e_1\) 的膨胀量由 \(R\) 的水平宽度决定,相对于 \(e_4\) 取决于 \(R\) 的垂直高度。 还要注意在凹顶点(reflex vertex)附近的情况。如果我们沿着与之相连的两条边的全长移动,\(r\) 会描绘出一条自相交的路径,尽管如此,该路径的外侧仍然精确地表征了 \(R\) 靠近 \(P\) 的几何极限。

8.4.2. 闵可夫斯基加法 (Minkowski Addition)

当 \(R\) 是圆盘时,我们曾论证 \(P^+ = P \oplus R\)。但这显然不适用于图 8.5 的情况。例如,\(P \oplus R\) 会凸出 \(P\) 的边 \(e_0\) 之外,但 \(P^+\) 并不会。 合适的计算方法应该是取 \(P\) 与 \(R\) 关于参考点 \(r\) 的反射的闵可夫斯基和。take the Minkowski sum of \(P\) with a reflection of \(R\) through the refence point \(r\). 由于 \(r\) 在闵可夫斯基和的公式中作为原点,所以 \(R\) 的反射版本就是 \(-R\),即 \(R\) 中的每一点都取反。 需要反射的原因是,\(R\) 中的任意点 \(p\),比如说 \(R\) 的右上角,都有将 \(r\) 推离 \(\partial P\) 距离 \(-p\) 的效果。 由此可见,图 8.5 中的 \(P^+\) 就是 \(P \oplus -R\)。注意,因为圆盘关于其圆心是中心对称的,所以 \(R = -R\),因此这个新公式与我们在上一节中的表述是一致的。 鉴于"闵可夫斯基剪发(Minkowski subtraction)"这个术语有时用于指代另一个概念(Guggenheimer 1977),我们将继续称之为闵可夫斯基加法。

我们用一个稍显通用的命题来形式化上述讨论。

定理 8.4.1. 设 \(R\) 是一个区域(机器人),\(r \in R\) 是一个参考点。\(P\) 是一个障碍物。那么区域 \(P^+ = P \oplus -R\) 是参考点 \(r\) 的禁区,具体表现为:

  1. 若平移 \(R\) 使得 \(r\) 严格位于 \(P^+\) 内部,那么 \(R\) 与 \(P\) 就会穿模,或者说是有重叠区域。
  2. 若平移 \(R\) 使得 \(r\) 位于 \(\partial P^+\) 上,那么 \(\partial R\) 与 \(\partial P\) 会接触上,或者说是相切。
  3. 若平移 \(R\) 使得 \(r\) 严格位于 \(P^+\) 外部,那么 \(R \cap P = \emptyset\),即不相交。

这是一种更一般的表述,因为 \(R\) 和 \(P\) 都不需要是凸的,甚至不需要是多边形。但在本节中,我们仍然假设两者都是多边形,且 \(R\) 是凸的。为了方便起见,我们将取 \(r\) 设为原点。

8.4.3. 构建闵可夫斯基和 (Constructing the Minkowski Sum)

我们在此概述一种构建两个多边形闵可夫斯基和的方法,其中部分内容将在下文 8.4.4 节中实现。为了将重点保持在运动规划本身,而不迷失于由此引发的各种有趣问题, 这里不提供完整细节,而是将其留作习题。

我们继续使用图 8.5 中的例子。图 8.6(b) 展示了 \(P^+ = P \oplus -R\),其中 \(P\) 的边标记为 \(0, \dots, 4\),\(-R\) 的边标记为 \(a, \dots, d\),两者均按逆时针遍历顺序标记, 而 \(P^+\) 的边则根据是由 \(P\) 或 \(-R\) 的哪条边"生成",来标记为 \(\{0, 1, 2, 3, 4, a, b, c, d\}\) 中的元素。因此,当 \(R\) 沿 \(P\) 的边 2 滑动时, 参考点 \(r\) 描绘出 \(P^+\) 的一条平行边,我们将其标记为 2。而当 \(R\) 在 \(P\) 的边 1 和 2 交点处沿 \(c\) 垂直滑动时,我们将 \(P^+\) 的那条边标记为 \(c\)。

注意,我们已经标记了包围 \(P^+\) 的整条自相交多边形路径 \(\tau\),包括在凹顶点(reflex vertex)附近位于 \(P^+\) 内部的边。构造 \(P^+\) 最简单的方法是先构建 \(\tau\)。 它有时被称为 \(P\) 和 \(-R\) 的卷积(convolution)(Guibas, Ramshaw & Stolfi 1983)。

\(\tau\) 各边的标记规律,可以通过图 8.6(a) 所示的 \(P\) 和 \(-R\) 边向量的星形图(star diagram)来直观理解。 如果我们像在当前示例中那样,将 \(P\) 视为较大的多边形,\(R\) 视为较小的多边形。那么粗略地说,\(\tau\) 包含对应于 \(P\) 各边的边,其间穿插着一些 \(-R\) 的边。 实际上,人们可以看到 \(\tau\) 的标签序列 \((0, b, 1, c, d, 2, a, b, 3, c, d, 4, a)\) 包含了 \((0, 1, 2, 3, 4)\) 作为一个子序列。星形图提供了一种预测 \(-R\) 穿插标签的机制。

将每条边视为按逆时针遍历方向的向量,并将它们都移动到一个公共原点,如图 8.6(a) 所示,并归一化为单位长度。这种边向量的排列方式被称为星形图(star diagram)。 现在从 0 开始,绕星形图逆时针旋转。在 \(P\) 边的索引 \(i\) 和 \(i+1\) 之间,写下遇到的所有 \(-R\) 的索引。 例如,在 0 和 1 之间遇到了 \(b\),产生了子序列 \((0, b, 1)\)。在 1 和 2 之间遇到了 \(c\) 和 \(d\),产生了子序列 \((1, c, d, 2)\)。 以此类推,当再次回到 0 时,我们就生成了 \(\tau\) 的完整序列。稍后我们将讨论具体的实现方法。

为了从 \(\tau\) 中获得闵可夫斯基和 \(P^+\),还需要处理卷积的自交问题。这是另一个有趣但我们不打算深入探讨的问题(Guibas et al. (1983); Ramkumar (1996))。 虽然细节尚不清楚,但至少可以肯定,存在一个确定的过程,根据 \(P\) 和 \(R\) 构建 \(P^+\)。现在我们直接给出在各种凸性假设下计算 \(P \oplus -R\) 的复杂度结论:

定理 8.4.2. 如果 \(P\) 有 \(n\) 个顶点,且 \(R\) 的顶点数量固定(为常数),那么闵可夫斯基和 \(P \oplus -R\) 可以在以下时间复杂度内完成构建:

\(R\)\(P\)Size of Sum时间复杂度
凸(convex) 凸(convex) \(O(n)\) \(O(n)\)
凸(convex) 非凸(nonconvex)\(O(n)\) \(O(n^2 \log n)\)
非凸(nonconvex)非凸(nonconvex)\(O(n^2)\)\(O(n^2 \log n)\)

这些结果由 Guibas et al.(1983), Toussaint (1983b), Sharir (1987) 以及 Kaul, O'Connor & Srinivasan(1991) 得出。 请注意,我们将机器人的大小视为具有固定数量顶点的形态,并且仅报告关于障碍物多边形的顶点数 \(n\) 的复杂度。习题 8.4.6[4]–[6] 探讨了作为机器人顶点数函数的复杂度。

8.4.4. 闵可夫斯基卷积的实现 (Implementation of Minkowski Convolution)

尽管构建卷积轨迹仅仅是构建其外边界(即闵可夫斯基和)的一半工作,但星形图(star diagram)方法足够优雅,值得我们实现一遍。 所需的大部分精力都在于构建星形图。一旦有了星形图,只需通过重复累加对应的边向量,就可以轻松完成卷积轨迹的追踪。 构建星形图本质上是对向量进行角度排序,我们在 3.5.5 节中已经使用 Graham 凸包算法处理过此类任务。 我们将尽量遵循为该算法使用的惯例。具体而言,我们将把边向量存储在一个结构体数组中,然后使用 qsort 进行排序。 这些结构体以及算法顶层逻辑的 main 函数,如 Code8.1 所示。

        typedef struct tPointStructure tsPoint;
        typedef tsPoint *tPoint;
        
        struct tPointStructure {
            int     vnum;
            tPointi v;
            bool    primary;
        };
        
        #define PMAX 1000                       /* Max # of points */
        typedef tsPoint tPointArray[PMAX];
        static tPointArray P;
        
        int m;      /* Total number of points in both polygons */
        int n;      /* Number of points in primary polygon */
        int s;      /* Number of points in secondary polygon */
        
        main()
        {
            tPointi p0 = {0,0};
            int j0;                               /* index of start point */
        
            j0 = ReadPoints( p0 );
            Vectorize();
            qsort(
                &P[0],                            /* pointer to 1st elem */
                m,                                /* number of elems */
                sizeof( tsPoint ),                /* size of each elem */
                Compare                           /* -1,0,+1 compare function */
            );
            Convolve( j0, p0 );
        }

其中点结构体 tPoint3.5.5 节中使用的结构体仅有一处不同, 它增加了一个布尔字段 primary,用于标识该点属于主(非凸)多边形 \(P\) (TRUE) 还是次(凸)多边形 \(R\)(FALSE)。 该字段由函数 ReadPoints设置(此处未列出),该函数还会利用字段 vnum 分别为两组点分配独立的编号序列。 这样,当 qsort 将所有点混合在数组 P[] 中,我们仍有足够的信息来识别点的身份。 主多边形的顶点数为 \(n\),次多边形的顶点数为 \(s\),总顶点数为 \(m = n + s\)。次多边形 \(R\) 在读取时会进行反射变换,因此后续仅存储和使用 \(-R\)。

虽然我们读入的是多边形顶点,但所有计算都是在边向量上进行的。因此,主过程的第一个实质性操作是通过调用 Vectorize(Code8.2)来计算这些边向量。 这无需像图 8.6(a) 中那样归一化为单位长度。事实上,保持原样的向量更有用。我们选择将这些向量存储在原本存放顶点的同一个数组 P[] 中。 我们需要小心避免误用新计算的向量覆盖了原始点,这也是引入临时变量 last 进行暂存的原因。除此之外,该计算过程还是很直接的。

        void Vectorize( void )
        {
            int i;
            tPointi last;   /* Holds the last vector difference. */
        
            // --- 处理第一个多边形 (索引 0 到 n-1) ---
            SubVec( P[0].v, P[n-1].v, last );
            for( i = 0; i < n-1; i++ )
                SubVec( P[i+1].v, P[i].v, P[i].v );
            P[n-1].v[X] = last[X];
            P[n-1].v[Y] = last[Y];
        
            // --- 处理第二个多边形 (索引 n 到 n+s-1) ---
            SubVec( P[n].v, P[n+s-1].v, last );
            for( i = 0; i < s-1; i++ )
                SubVec( P[n+i+1].v, P[n+i].v, P[n+i].v );
            P[n+s-1].v[X] = last[X];
            P[n+s-1].v[Y] = last[Y];
        }

qsort 的调用与 Code3.6 中的用法类似,不再赘述。 与 Graham 算法一样,编写比较函数 Compare 是一项细致的工作。 我们无法完全照搬之前的模型(Code3.5), 因为在那里的所有向量相对于原点都位于上半平面。而此处的边向量则向各个方向发散。我们采用如下的角度排序约定:沿 \(x\) 轴向左指向的向量(例如 \((-1, 0)\))具有最小角度, 表示从正 \(x\) 轴逆时针旋转 \(-\pi\)。角度值随逆时针旋转而增大,一次经过 \(x\) 轴下方的半平面,经过正 \(x\) 轴,最后穿过上方的半平面。 这与我们之前的约定一致,但将其扩展到了完整的 \(2\pi\) 范围。"位于左侧(left-of)"的判定仍然是决定向量先后顺序的关键。在出现平局(共线)的情况下, 我们优先选择较短的向量。

尽管 left-of 是 Compare 的核心(Code8.3),但我们大部分精力都在处理特殊情况。下半平面上的向量优先于上半平面上的向量。 这可以通过比较每个向量的 v[Y] 来确定。\(x\) 轴上的向量需要特殊处理。否则,我们将进入 left-of 判定和平局裁决, 其处理方式与第 3 章中使用的方法相同。

        int Compare( const void *tpi, const void *tpj )
        {
            int a;              /* AreaSign result */
            int x, y;           /* projections in 1st quadrant */
            tPoint pi, pj;      /* Recasted points */
            tPointi Origin = {0,0};
        
            pi = (tPoint)tpi;
            pj = (tPoint)tpj;
        
            /* A vector in the open upper halfplane is after
               a vector in the closed lower halfplane. */
            if      ( ( pi->v[Y] > 0 ) && (pj->v[Y] <= 0 ) )
                return 1;
            else if ( ( pi->v[Y] <= 0 ) && (pj->v[Y] > 0 ) )
                return -1;
        
            /* A vector on the x axis and one in the lower
               halfplane are handled by the Left computation below. */
        
            /* Both vectors on the x axis require special handling. */
            else if ( ( pi->v[Y] == 0 ) && (pj->v[Y] == 0 ) ) {
                if      ( ( pi->v[X] < 0 ) && ( pj->v[X] > 0 ) )
                    return -1;
                if      ( ( pi->v[X] > 0 ) && ( pj->v[X] < 0 ) )
                    return 1;
                else if ( abs(pi->v[X]) < abs(pj->v[X]) )
                    return -1;
                else if ( abs(pi->v[X]) > abs(pj->v[X]) )
                    return 1;
                else
                    return 0;
            }
        
            /* Otherwise, both in open upper halfplane, or
               both in closed lower halfplane, but not both on x axis. */
            else {
                a = AreaSign( Origin, pi->v, pj->v );
                if      (a > 0)
                    return -1;
                else if (a < 0)
                    return 1;
                else { /* Begin collinear */
                    x = abs( pi->v[X] ) - abs( pj->v[X] );
                    y = abs( pi->v[Y] ) - abs( pj->v[Y] );
                    if      ( (x < 0) || (y < 0) )
                        return -1;
                    else if ( (x > 0) || (y > 0) )
                        return 1;
                    else /* points are coincident */
                        return 0;
                } /* End collinear */
            }
        }

调用 qsort 之后,带有识别标签的边向量会按角度排序,并存储在 P[] 中。 接下来的任务是按照之前描述的方式遍历星形图(star diagram)累加边向量。一个棘手的问题是起始点的选择 —— 初始点 \(p_0\) 应该是什么? 这又取决于我们从哪里开始处理。我们选择从最小角度开始,它对应于向量 \((-1, 0)\),即使多边形边向量中可能不存在这样的向量。 另一种选择是从主多边形的某个特定边向量开始。虽然这并非显而易见的,但这钟选择意味着我们应该从主多边形 \(P\) 的右上角开始,该点还参考次多边形 \(-R\) 的最右上方点进行了偏移。 我们不对此进行论证。起点 \(p_0\) 有 ReadPoints 计算得出,并传递给卷积过程 Convolve, 该过程首先要"移动至(moveto)" 起点,如 Code8.5 所示。

        void Convolve( int j0, tPointi p )
        {
            int i;  /* Index into sorted edge vectors P */
            int j;  /* Primary polygon index */
        
            MoveTo_i( p );
        
            i = 0;      /* Start at angle -pi, rightward vector. */
            j = j0;     /* Start searching for j0. */
        
            do {
                /* Advance around secondary edges until next j reached. */
                while ( !(P[i].primary) && P[i].vnum == j ) {
                    if ( !P[i].primary ) {
                        AddVec( p, P[i].v, p );
                        LineTo_i( p );
                    }
                    i = (i + 1) % m;
                }
        
                /* Advance one primary edge. */
                AddVec( p, P[i].v, p );
                LineTo_i( p );
                j = (j + 1) % n;
        
            } while ( j != j0 );
        }

Convolve 随后通过递增索引 i 来遍历星形图,并通过语句 i = (i+1)%m 在必要时进行回绕。 这是代码中外层的 do-while 循环。在每次迭代中,它都在寻找主多边形的下一条边向量,由索引 j 标识。该索引 j 被初始化为 \(j_0\),即基于 \(p_0\) 的主边向量的索引。 在搜索主向量 \(j\) 的过程中,它会输出遇到的每一个次向量,这对应于图 8.6(a) 中穿插的 \(-R\) 标签。这是在内层 while 循环中完成的。 当内层循环结束时,算法已找到下一个 \(j\),随即输出一条主边向量并将 \(j\) 递增。不断重复该过程,直到 \(j\) 再次回绕到 \(j_0\)。

图 8.7 展示了代码输出的一个示例。相应的星形图显示在图 8.8 中。角度最小的向量是 \((-20, 0)\),对应于 \(R\) 的水平底边,经 \(-R\) 变换之后指向左侧。 从 \(p_0\),即 \(P\) 的最上方点,图中画圈处,出发的第一步,就是这个向左的水平"连线(lineto)"。随后,索引 \(i\) 会遍历 P[] 中所有 \(m = n + s = 22 + 8 = 30\) 个点十次之后,回到 \(j_0\)。这十次循环在最终图形的十个环路中清晰可见。 从该图可以清晰的看到,从卷积到闵可夫斯基(即图8.7的外边界)和还有很多额外工作。

8.4.5. 概念性运动规划算法 (Conceptual Motion Planning Algorithm)

现在我们回到为凸多边形机器人 \(R\) 的规划运动的问题上。同样,这里仅给出算法的粗略概念。设障碍物为 \(P_1, P_2, \ldots, P_m\),总共有 \(n\) 个顶点。该算法包含四个步骤:

  1. 膨胀所有障碍物: \(P_i^+ = P_i \oplus -R\)。
  2. 构造它们的并集 \(P^+ = \bigcup_i P_i^+\)。
  3. 找到包含 \(s\) 和 \(t\) 的连通分量。
  4. 在该连通分量中找到一条连接 \(s\) 和 \(t\) 的路径。

图 8.9 用一个四边形机器人 \(R\) 和八个障碍物(深色阴影)说明了这一过程。

注意,在这个例子中,自由空间有三个连通分量,分别包含点 \(a, b, c\)。从图示的初始位置出发,\(a\) 是可达的,但 \(b, c\) 都不可达。 因此,判断目标位置 \(t\) 是否从 \(s\) 可达,被转化为确定 \(s, t\) 是否位于自由空间的同一个连通分量中。 而为机器人规划路径,则转化为在该分量内为参考点寻找一条路径。

上述四个步骤都涉及有趣的算法问题,但我们在此不深入探讨。鉴于整个方案的复杂性,Kedem & Sharir(1990) 能够实现卓越的 \(O(n \log n)\) 复杂度, 充分彰显了众多致力于该问题各方面研究的学者的贡献。

定理 8.4.3. 寻找一系列平移运动,使凸多边形机器人在给定的起始和终止位置之间移动,同时避开总共有 \(n\) 个顶点的多边形障碍物, 这一过程可以在 \(O(n \log n)\) 时间内完成。更准确地说,如果机器人有 \(k\) 个顶点,则复杂度为 \(O(kn \log(kn))\)。

更令人惊喜的是,对于在 3D 空间中移动的凸多面体机器人,避开多面体障碍物的问题,也已确立了大致相同的复杂度(Aronov & Sharir 1997)。

8.4.6. 练习(Exercises)

  1. [easy] 正方形与三角形之和 Sum of square and triangle。正方形和三角形的闵可夫斯基和(Minkowski sum)最多可以有多少条边?
  2. 卷积循环 Convolution cycles。根据 \(P\) 的结构,开发一种方法来预测 Convolve(代码 8.5)在星形图上循环的次数。在图 8.6(b) 和 8.7 上测试。
  3. 星形图:非凸 Star diagram: nonconvex。探索非凸多边形 \(R\) 的卷积。星形图方法还能找到 \(\tau\) 吗?
  4. [easy] 凸-凸 Convex-Convex。证明分别具有 \(k\) 和 \(n\) 个顶点的两个凸多边形 \(R, P\) 的闵可夫斯基和可以有 \(\Omega(k + n)\) 个顶点。
  5. 凸-非凸 Convex-Nonconvex。证明分别具有 \(k\) 和 \(n\) 个顶点的凸多边形 \(R\) 和可能是非凸的多边形 \(P\) 的闵可夫斯基和可以有 \(\Omega(kn)\) 个顶点。
  6. 非凸-非凸 Nonconvex-Nonconvex。证明分别具有 \(k\) 和 \(n\) 个顶点的两个可能是非凸的多边形 \(R, P\) 的闵可夫斯基和可以有 \(\Omega(k^2n^2)\) 个顶点。
  7. [difficult] 凸区域的并集 Union of convex regions。\(m\) 个凸多边形共有 \(n\) 个顶点,这些凸多边形的并集,最多有多少顶点? 请用 \(n\) 和 \(m\) 的函数表示答案。
    1. 首先猜测一个上界,并用用示例支持。
    2. 证明该上界(Kedem, Livne, Pach & Sharir 1986)。



Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1