Ellipse's farthest point to another point

258 Views Asked by At

I am trying to find the farthest and closest points of a ellipse without using any brute force type of coding. The processing power is limited so it should be as pinpoint as possible. I have tried a binary search method but it should still be faster than that.

All values of ellipse is known (Center Point(C), Rotation(r), focal points, long side's distance(a), short side's distance(b)) as well as the point outside of the ellipse(P).

Thanks!

1

There are 1 best solutions below

1
On BEST ANSWER

Rewritten on 2016-04-29:

Let $\vec{c} = (x_c, y_c)$ be the center of the ellipse, $\vec{a} = (x_a, y_a)$ its semi-major axis, and $\vec{b} = (x_b, y_b)$ its semi-minor axis, $\lVert\vec{a}\rVert\ge\lVert\vec{b}\rVert$. Note that the axes are orthogonal, $\vec{a}\cdot\vec{b} = 0$.

To simplify the situation, let's switch to a coordinate system where the center of the ellipse is at origin, semi-major axis is parallel to the $x$ axis, and semi-minor axis to the $y$ axis. In this coordinate system, the point $\vec{p} = (x_p, y_p)$ is at $$\begin{cases} x = (\vec{p} - \vec{c}) \cdot \frac{\vec{a}}{\lVert\vec{a}\rVert} \\ y = (\vec{p} - \vec{c}) \cdot \frac{\vec{b}}{\lVert\vec{b}\rVert} \end{cases}$$ We only need to consider the positive quadrant of the ellipse, because we can negate both $x$ and $a$ if $x \lt 0$; and negate $y$ and $b$ if $y \lt 0$.)

Using $r_x = \lVert\vec{a}\rVert$ and $r_y = \lVert\vec{b}\rVert$, we can parametrize the positive quadrant of the ellipse $t = x / r_x$, i.e. $$\begin{cases} x_{+}(t) = r_x \; t \\ y_{+}(t) = r_y \; \sqrt{1 - t^2} \end{cases}$$

Point $t$ in the positive quadrant is closest to our point $(x, y)$ when the distance squared $s_N(t)$ reaches a minimum: $$s_N(t) = \left ( (x - x_{+}(t))^2 + (y - y_{+}(t))^2 \right ) = (x - t r_x)^2 + (y - r_y \sqrt{1 - t^2})^2$$ It reaches a minimum when $t = 0$, $t = 1$, or $ds_N(t)/dt = 0$. The roots of the last one are the same as the roots of the quartic equation $$f(t) = T_4 t^4 + T_3 t^3 + T_2 t^2 + T_1 t + T_0 = 0$$ where $$\begin{align} T_4 &= (r_x^2 - r_y^2)^2 \\ T_3 &= 2 x r_x ( r_y^2 - r_x^2 ) \\ T_2 &= x^2 r_x^2 + y^2 r_y^2 - (r_x^2 - r_y^2)^2 \\ T_1 &= 2 x r_x ( r_x^2 - r_y^2 ) \\ T_0 &= - x^2 r_x^2 \end{align}$$ Note that you can evaluate it as $f(t) = T_0 + t (T_1 + t(T_2 + t(T_3 + t T_4)))$, and that $0 \le t \le 1$. It typically has one or three roots within that range, so you probably want to use a method that also examines its derivative (which is, of course, $T_1 + t(2 T_2 + t(3 T_3 + t 4 T_4))$). Note that this is a simple polynomial, and thus very fast to evaluate. Compute all $s_N(t)$ for $t = 0$, $t = 1$, and for $f(t) = 0, 0 \lt t \lt 1$, and pick the $t$ that yields the smallest $s_N(t)$. That point $t$ on the positive quadrant of the ellipse is the nearest to our point $(x, y)$: $$\vec{p}_{nearest} = \vec{c} + \vec{a} t + \vec{b} \sqrt{1 - t^2} = \begin{cases} x_{nearest} = x_c + x_a t + x_b \sqrt{1 - t^2} \\ y_{nearest} = y_c + y_a t + y_b \sqrt{1 - t^2} \end{cases}$$

The point furthest on the ellipse must be in the opposite quadrant. The point $t$ in the quadrant opposite to point $(x,y)$ is $$\begin{cases} x_{-}(t) = - r_x \; t \\ y_{-}(t) = - r_y \; \sqrt{1 - t^2} \end{cases}$$ and the distance squared is $$s_F(t) = (x - x_{-}(t))^2 + (y - y_{-}(t))^2 = (x + t r_x)^2 + (y + r_y \sqrt{1 - t^2})^2$$ If $s_F(t)$ is a global maximum $0 \le t \le 1$, then the point on the ellipse furthest from point $(x,y)$ is $$\vec{p}_{furthest} = \vec{c} - \vec{a} t - \vec{b} \sqrt{1 - t^2} = \begin{cases} x_{furthest} = x_c - x_a t - x_b \sqrt{1 - t^2} \\ y_{furthest} = y_c - y_a t - y_b \sqrt{1 - t^2} \end{cases}$$ $s_F(t)$ reaches a maximum when $ds_F(t)/dt = 0$, i.e. when $$g(t) = T_4 t^4 + T_3 t^3 + T_2 t^2 + T_1 t + T_0 = 0$$ where $$\begin{align} T_4 &= (r_x^2 - r_y^2)^2 \\ T_3 &= 2 x r_x ( r_x^2 - r_y^2 ) \\ T_2 &= x^2 r_x^2 + y^2 r_y^2 - (r_x^2 - r_y^2)^2 \\ T_1 &= 2 x r_x ( r_y^2 - r_x^2 ) \\ T_0 &= - x^2 r_x^2 \end{align}$$ Note that the coefficients are the same as for the nearest case, except with $T_3$ and $T_1$ negated. Again, of all the values of $t$ where $g(t) = 0, 0 \lt t \lt 1$, and of $t = 0$ and $t = 1$, pick the $t$ where $s_F(t)$ is the largest, to find the point on the ellipse furthest from point $(x, y)$.

Implementation-wise, the only open question is how to solve the quartic equations. For these coefficients, there do exist analytical solutions that can be found using e.g. Maple, but they are computationally intensive (tested 3,500 to 10,000 clock cycles on Intel Core i5-4200U using double precision floating-point arithmetic).

Here is a test program, that takes a Xorshift* pseudorandom number generator seed, generates a test ellipse, and outputs an SVG image for easy verification:

#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <stdio.h>
#include <complex.h>
#include <math.h>

#ifndef  M_2PI
#define  M_2PI  6.283185307179586476925286766559005768394
#endif

typedef struct {
    double  x;
    double  y;
} vec2d;

/* Maximum eccentricity that is equated to zero. */
#define ECCEPS 0.0

/* Maximum imaginary part assumed zero.
 * It is okay for this to be large for the points-on-ellipse case,
 * as the quartic roots are explicitly tested.
*/
#define IEPS 0.5

/* quartic_roots() - Find real roots for quartic functions
 *      f1(x) = c0 + x*c1 + x*x*c2 - x*x*x*c1 + x*x*x*x
 *      f2(x) = c0 - x*c1 + x*x*c2 + x*x*x*c1 + x*x*x*x
 * and return the number of real roots found. Note that
 *      f1(x) = 0    <=>   f2(-x) = 0
 * IEPS is the magnitude of imaginary part still considered zero.
 * The function returns the number of real roots.
*/
static int quartic_roots(const double c0, const double c1, const double c2, double *const roots)
{
    int          nroots = 0;

    const double real01 = c2 * c2;
    const double real02 = c2 * real01;
    const double real03 = c1 * c1;
    const double real04 = c1 * real03;
    const double real05 = real03 * real03;
    const double real06 = c0 * c0;
    const double real07 = 12.0 * real02;
    const double real08 = 54.0 * c2;
    const double real09 = 108.0 * real03;
    const double real10 = -432.0 * c2;
    const double real11 = 0.25 * c1;
    const double real12 = 384*real06*real01 - 12*real04*real04 + real03*(real07 + real06*(real10 - 576.0)) + real05*(real08 - 3*real01 + 81*real06 + 81) + c0*(-768*real06 - 48*real01*real01 + real05*(real08+18) + real03*(real10 + real07 - 240*real01));

    if (real12 >= 0.0) {
        const double real21 = cbrt( c0*real09 + 12 * sqrt(real12) + 8*real02 + real09 + c2*(36*real03 - 288*c0)) / 6.0;
        const double real22 = real21 + (3*real03 + 12*c0 + real01) / (9.0 * real21);
        const double real23 = 0.25 * real03 - c2 / 1.5 + real22;

        if (real23 > 0.0) {
            const double real24 = 0.5 * sqrt(real23);
            const double real25 = (0.125 * real04 - 0.5 * c1 * (c2 + 2)) / real24;
            const double real26 = 0.5 * real03 - c2/0.75 - real22;
            const double real27 = real26 + real25;
            const double real28 = real26 - real25;

            if (real27 >= 0.0) {
                const double real29 = real11 + real24;
                const double real30 = 0.5 * sqrt(real27);
                roots[nroots++] = real29 + real30;
                roots[nroots++] = real29 - real30;
            }

            if (real28 >= 0.0) {
                const double real29 = real11 - real24;
                const double real30 = 0.5 * sqrt(real28);
                roots[nroots++] = real29 + real30;
                roots[nroots++] = real29 - real30;
            }
            return nroots;

        } else {
            const double complex complex4 = 0.5 * cpow(real23, 0.5);
            const double complex complex5 = (0.125 * real04 - 0.5 * c1 * (c2 + 2)) / complex4;
            const double complex complex6 = 0.5 * real03 - c2/0.75 - real22;
            const double complex complex7 = 0.5 * cpow(complex6 - complex5, 0.5);
            const double complex complex8 = 0.5 * cpow(complex6 + complex5, 0.5);
            const double complex root1 = complex4 + complex8;
            const double complex root2 = complex4 - complex8;
            const double complex root3 = -complex4 + complex7;
            const double complex root4 = -complex4 - complex7;

            if (cimag(root1) >= -IEPS && cimag(root1) <= IEPS)
                roots[nroots++] = real11 + creal(root1);
            if (cimag(root2) >= -IEPS && cimag(root2) <= IEPS)
                roots[nroots++] = real11 + creal(root2);
            if (cimag(root3) >= -IEPS && cimag(root3) <= IEPS)
                roots[nroots++] = real11 + creal(root3);
            if (cimag(root4) >= -IEPS && cimag(root4) <= IEPS)
                roots[nroots++] = real11 + creal(root4);
            return nroots;
        }

    } else {
        const double complex complex1 = cpow( c0*real09 + 12 * cpow(real12, 0.5) + 8*real02 + real09 + c2*(36*real03 - 288*c0), 1.0/3.0) / 6.0;
        const double complex complex2 = complex1 + (3*real03 + 12*c0 + real01) / (9.0 * complex1);
        const double complex complex3 = 0.25 * real03 - c2 / 1.5 + complex2;
        const double complex complex4 = 0.5 * cpow(complex3, 0.5);
        const double complex complex5 = (0.125 * real04 - 0.5 * c1 * (c2 + 2)) / complex4;
        const double complex complex6 = 0.5 * real03 - c2/0.75 - complex2;
        const double complex complex7 = 0.5 * cpow(complex6 - complex5, 0.5);
        const double complex complex8 = 0.5 * cpow(complex6 + complex5, 0.5);
        const double complex root1 = complex4 + complex8;
        const double complex root2 = complex4 - complex8;
        const double complex root3 = -complex4 + complex7;
        const double complex root4 = -complex4 - complex7;

        if (cimag(root1) >= -IEPS && cimag(root1) <= IEPS)
            roots[nroots++] = real11 + creal(root1);
        if (cimag(root2) >= -IEPS && cimag(root2) <= IEPS)
            roots[nroots++] = real11 + creal(root2);
        if (cimag(root3) >= -IEPS && cimag(root3) <= IEPS)
            roots[nroots++] = real11 + creal(root3);
        if (cimag(root4) >= -IEPS && cimag(root4) <= IEPS)
            roots[nroots++] = real11 + creal(root4);
        return nroots;
    }
}

/** on_ellipse() - Find the points on ellipse nearest and furthest to 'point'.
 * @point: Target point
 * @center: Center of the ellipse
 * @semimajor: Semi-major axis vector
 * @fraction: Ratio of semi-minor radius to semi-major radius
 * @nearest: If non-NULL, is used to store the closest point on the ellipse
 * @furthest: If non-NULL, is used to store the furthest point on the ellipse
 * Note that fraction = sqrt(1 - eccentricity*eccentricity).
*/
void on_ellipse(const vec2d point, const vec2d center, const vec2d semimajor, const double fraction,
                vec2d *const nearest, vec2d *const furthest)
{
    vec2d  xaxis, yaxis;
    double x, y, xsqr, ysqr, dsqr, xradius, yradius;

    if (fraction > 1.0) {
        yaxis = semimajor;
        xaxis.x =  yaxis.y * fraction;
        xaxis.y = -yaxis.x * fraction;
    } else {
        xaxis = semimajor;
        yaxis.x =  xaxis.y * fraction;
        yaxis.y = -xaxis.x * fraction;
    }

    xsqr = xaxis.x * xaxis.x + xaxis.y * xaxis.y;
    ysqr = yaxis.x * yaxis.x + yaxis.y * yaxis.y;
    dsqr = xsqr - ysqr;

    xradius = sqrt(xsqr);
    yradius = sqrt(ysqr);

    x = ((point.x - center.x) * xaxis.x + (point.y - center.y) * xaxis.y) / xradius;
    y = ((point.x - center.x) * yaxis.x + (point.y - center.y) * yaxis.y) / yradius;

    if (dsqr > ECCEPS) {
        double  t[4], nx, ny, fx, fy, nd, fd;
        int     i, n;

        if (signbit(x)) {
            x = -x;
            xaxis.x = -xaxis.x;
            xaxis.y = -xaxis.y;
        }
        if (signbit(y)) {
            y = -y;
            yaxis.x = -yaxis.x;
            yaxis.y = -yaxis.y;
        }

        /* Check whether t=0 or t=1 is nearer. */
        {   const double td = (x - xradius)*(x - xradius) + y*y;
            nd = x*x + (y - yradius)*(y - yradius);
            if (nd < td) {
                nx = 0.0;
                ny = 1.0;
            } else {
                nx = 1.0;
                ny = 0.0;
                nd = td;
            }
        }

        /* Check whether t=0 or t=1 is further. */
        {
            const double td = (x + xradius)*(x + xradius) + y*y;
            fd = x*x + (y + yradius)*(y + yradius);
            if (fd > td) {
                fx = 0.0;
                fy = 1.0;
            } else {
                fx = 1.0;
                fy = 0.0;
                fd = td;
            }
        }

        /* Solve the quartic equation. */
        n = quartic_roots(-(x*x*xsqr) / (dsqr*dsqr), 2*x*xradius / dsqr, (x*x*xsqr + y*y*ysqr) / (dsqr*dsqr) - 1.0, t);
        for (i = 0; i < n; i++)
            if (t[i] > 0.0 && t[i] < 1.0) {
                const double tx = t[i];
                const double ty = sqrt(1.0 - tx*tx);
                const double td = (x - tx*xradius)*(x - tx*xradius) + (y - ty*yradius)*(y - ty*yradius);
                if (td < nd) {
                    nx = tx;
                    ny = ty;
                    nd = td;
                }
            } else
            if (t[i] > -1.0 && t[i] < 0.0) {
                const double tx = -t[i];
                const double ty = sqrt(1.0 - tx*tx);
                const double td = (x + tx*xradius)*(x + tx*xradius) + (y + ty*yradius)*(y + ty*yradius);
                if (td > fd) {
                    fx = tx;
                    fy = ty;
                    fd = td;
                }
            }

        /* Save. */
        if (nearest) {
            nearest->x = center.x + nx * xaxis.x + ny * yaxis.x;
            nearest->y = center.y + nx * xaxis.y + ny * yaxis.y;
        }
        if (furthest) {
            furthest->x = center.x - fx * xaxis.x - fy * yaxis.x;
            furthest->y = center.y - fx * xaxis.y - fy * yaxis.y;
        }
    } else {
        /* This is a circle. */
        const double d = sqrt(x*x + y*y);
        const double tx = x / d;
        const double ty = y / d;
        if (nearest) {
            nearest->x = center.x + tx * xaxis.x + ty * yaxis.x;
            nearest->y = center.y + tx * xaxis.y + ty * yaxis.y;
        }
        if (furthest) {
            furthest->x = center.x - tx * xaxis.x - ty * yaxis.x;
            furthest->y = center.y - tx * xaxis.y - ty * yaxis.y;
        }
    }
}

static uint64_t seed = 1;

static uint64_t perturb(int n)
{
    while (n-->0) {
        seed ^= seed >> 12;
        seed ^= seed << 25;
        seed ^= seed >> 27;
    }
    return seed;
}

static double range(const double min, const double max)
{
    seed ^= seed >> 12;
    seed ^= seed << 25;
    seed ^= seed >> 27;
    return (double)(uint64_t)(seed * UINT64_C(2685821657736338717)) * (max - min) / 18446744073709551616.0 + min;
}

int main(int argc, char *argv[])
{
    vec2d   center, major, minor, target, near, far;
    double  rmajor, rminor, alpha;
    char    dummy;

    if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s SEED > image.svg\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    if (sscanf(argv[1], " %" SCNu64 " %c", &seed, &dummy) != 1 || seed == 0) {
        fprintf(stderr, "%s: Invalid Xorshift64* seed.\n", argv[1]);
        return EXIT_FAILURE;
    }

    /* Perturb the state, just in case... */
    perturb(1031);

    target.x = range(0.0,  1024.0);
    target.y = range(0.0,   512.0);
    center.x = range(256.0, 768.0);
    center.y = range(128.0, 384.0);
    alpha    = range(0.0, M_2PI);
    rmajor   = range(4.0, 128.0);
    rminor   = range(4.0, 64.0);

    major.x =  rmajor * cos(alpha);
    major.y =  rmajor * sin(alpha);
    minor.x = -rminor * sin(alpha);   /* rminor * cos(alpha + 90 degrees) */
    minor.y =  rminor * cos(alpha);   /* rminor * sin(alpha + 90 degrees) */

    on_ellipse(target, center, major, rminor/rmajor, &near, &far);

    printf("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 1024 512\">");
    printf("<rect x=\"0\" y=\"0\" width=\"1024\" height=\"512\" stroke=\"none\" fill=\"#ffffff\" />\n");
    printf("<circle cx=\"%.3f\" cy=\"%.3f\" r=\"3\" stroke=\"#990000\" stroke-width=\"1\" fill=\"none\" />\n", near.x, near.y);
    printf("<circle cx=\"%.3f\" cy=\"%.3f\" r=\"3\" stroke=\"#990000\" stroke-width=\"1\" fill=\"none\" />\n", far.x, far.y);
    printf("<circle cx=\"%.3f\" cy=\"%.3f\" r=\"5\" stroke=\"#000000\" stroke-width=\"1\" fill=\"none\" />\n", target.x, target.y);
    printf("<path d=\"M%.3f%+.3fL%.3f%+.3f\" stroke=\"#999999\" stroke-width=\"1\" fill=\"none\" />\n", center.x, center.y, center.x + major.x, center.y + major.y);
    printf("<path d=\"M%.3f%+.3fL%.3f%+.3f\" stroke=\"#999999\" stroke-width=\"1\" fill=\"none\" />\n", center.x, center.y, center.x + minor.x, center.y + minor.y);
    printf("<ellipse cx=\"%.3f\" cy=\"%.3f\" rx=\"%.3f\" ry=\"%.3f\" transform=\"rotate(%.3f, %.3f, %.3f)\" stroke=\"#000000\" stroke-width=\"1\" fill=\"none\" />\n",
           center.x, center.y, rmajor, rminor, alpha * 360.0 / M_2PI, center.x, center.y);
    printf("<path d=\"M%.3f%+.3fL%.3f%+.3fL%.3f%+.3f\" stroke=\"#990000\" stroke-width=\"1\" fill=\"none\" />\n", near.x, near.y, target.x, target.y, far.x, far.y);
    printf("</svg>\n");

    return EXIT_SUCCESS;
}

Note that complex arithmetic is required for the intermediate values. To make it a bit faster, I split the real-only case out, so that complex intermediates are only used when really necessary. (The intermediates are not purely imaginary either at any point other than immediately after taking the square root of a negative real.)

To avoid the cases where the root has a small imaginary part due to numerical inaccuracy, I introduced the IEPS macro. In this particular case, it can be rather large, as the roots are tested anyway; as long as it is at least as large as the largest expected numerical inaccuracy in the imaginary part, the code should find the points correctly.

Evaluating the quartic function to be solved, as well as its derivative, should only take a few clock cycles as they are simple polynoms. If limited accuracy is also desired, it may well be better to solve the quartic functions ($C_0 + C_1 t + C_2 t^2 - C_1 t^3 + t^4 = 0$ and $C_0 - C_1 t + C_2 t^2 + C_1 t^3 + t^4 = 0$) numerically.

I suspect Newton's method is not suitable, as it tends to overshoot. Personally, I think I'd split the range into a small number of spans, and use a binary search to find the root $t$ within each span, if the span spans zero, or bulges towards zero. (Since $t = x/\sqrt{x^2 + y^2}$ is the solution for the circle case, it is also an interesting point in the general case here.)