1

To calculate the shortest distance from a point to the circumference of an ellipse, I have to calculate the normal/angle that passes through this point. See the following illustration:

situation

The math to calculate this normal is explained here. In short:

enter image description here

Where E is the ellipse and L is the line between the point p and the circumference that creates the normal.

In C++, I have the following code fragment, where I need to insert this formula - solving it to variable t:

#include <cmath>

/// Calculate the normal of a line from ellipse circumference to point.
///
/// @param ew The width of the ellipse
/// @param eh The height of the ellipse
/// @param x The relative x position (relative to ellipse center)
/// @param y The relative y position (relative to ellipse center)
/// @return The normal in radians.
///
auto calculateNormal(float ew, float eh, float x, float y) -> double {
   // ???
   return std::atan(...);
}; 

How can I implement the formula in C++ to get the normal as shown?

Flovdis
  • 2,945
  • 26
  • 49
  • 7
    Show us what you've tried, where it didn't work. We're glad to help, but we're not a code writing service. – lorro Nov 09 '22 at 12:39
  • Maths functions - https://en.cppreference.com/w/cpp/header/cmath – Richard Critten Nov 09 '22 at 12:39
  • 1
    Is it in [Numerical Recipes in C++](https://www.amazon.com/dp/0521750334)? – Eljay Nov 09 '22 at 12:41
  • You seem to know the math. Now convert that to code. Make a few tests including edge cases – Thomas Weller Nov 09 '22 at 12:41
  • 2
    There is a mistake in the maths, this is x0 and not y0 in the denominator (for the tan(t) formula). – Fareanor Nov 09 '22 at 13:06
  • 1
    You need to derive equation of 4th power for lambda from `()+()=1` one, then solve it (upto 4 roots possible) – MBo Nov 09 '22 at 13:29
  • Translating the right-hand side of your final equation, `(b*(a*a - lambda)*y0) / (a*(b*b - lambda)*x0)`, into C++ code is fairly straightforward. Perhaps your question would be better directed at how to get `a`, `b`, `lambda`, `x0`, and `y0` from `ew`, `eh`, `x`, and `y`? Well, skip asking about the ones you know how to get. – JaMiT Nov 09 '22 at 14:24
  • @JaMiT There is no final equation at the picture. One must solve previous equation to get lambda – MBo Nov 09 '22 at 15:13
  • As mentioned, those formulae aren't "done", you have to solve them first before you can implement anything. So to shortcut that, you just hit up good ol' websearch, search for "project point ellipse shortest distance" and then find a tutorial or writeup that already did this for you to follow along. You'll even find forum posts that work out all the maths for your, ending in the quartic you'll need to solve. There is no programming problem here (yet =) – Mike 'Pomax' Kamermans Nov 09 '22 at 18:19
  • @MBo *"There is no final equation at the picture."* -- This is incorrect. There are four equations in the picture. The first starts `E(x,y)=`, the second starts with `L`, the third ends with `=1`, and ***the final equation** in the picture* starts with `tan(t)=`. Not necessarily the final equation that is needed, but the final equation that was presented. -- *"One must solve previous equation to get lambda"* -- yes, that is what I was getting at. I was just being indirect so the OP could have some feeling of accomplishment after narrowing down the question. – JaMiT Nov 10 '22 at 06:25
  • why not [search](https://stackoverflow.com/a/36163847/2521214) the ellipse parametric angle `t` so that `dot(ellipse_point(t)-point,ellipse_outward_normal(t))` is max ? see similar [Ellipse with start/end angle issues](https://stackoverflow.com/a/71219436/2521214) – Spektre Nov 10 '22 at 08:21
  • @Spektre This is my current approach, and it is imprecise and slow. I first see in which quarter the point is (signs), then do a binary search on the points to find the closest one. To get acceptable precision (10^-4) using this approach, I would need at least ~64 iterations, each with multiple calls of sin, cos, and tan. I already optimized it by caching points, yet I hope there is a non-algorithmic solution for this problem. – Flovdis Nov 10 '22 at 10:53
  • @Flovdis I added search based solution without goniometrics. 64 iterations for bin search does not sound right and speeding it up by chaching also does not sound right (bin search is not reusing already tested points). Also no need to disect to quadrants its enough to handle just halves `(x=>0)` or `(x<0)` and just a note I do not think binary search is good fit for this unless your minimizing error is monotonic like based on `atan2` which would be slow anyway ... – Spektre Nov 13 '22 at 12:12

1 Answers1

2

What you currently have is not yet solved system of equations which as I originally taught from experience with similar but slightly simpler problem the algebraic solution leads to horrible stuff that would be too slow and inaccurate to compute (math solver throw about 2 pages of equations for roots which I throw away right away) so I would attack this with search approach instead.

If you are worrying about speed due to using goniometrics you can avoid that completely by simply computing normal as numeric derivation of the circumference points and just compare the slopes between normal (nx,ny) and found_point(x,y) - input_point(px,py).

The algo is like this:

  1. search x in range <-a,+a>

  2. compute y=f(x) and y1=f(x+epsilon)

    I simply rewrite the implicit ellipse equation into something like this:

    float ellipse_y(float rx,float ry,float x)   // y = f(x)
         {
         if (fabs(x)>rx) return 0.0
         return sqrt((rx*rx)-(x*x))*ry/rx;
         }
    

    of coarse the result should be +/- depending on the quadrant so if py<0 use negative values...

    The epsilon should be some small value but not too small, I used 0.001*rx where rx,ry are sizes of the ellipse half axises.

  3. compute normal (nx,ny)

    so simply take two consequent points (x,y) and (x+epsilon,y1) substract them and rotate by 90deg by exchanging their coordinates and negate one of them. Once put together I got this:

    void ellipse_n(float rx,float ry,float &nx,float &ny,float x,float &y)   // x',y',y = f(x)
         {
         if (x<=-rx){ y=0.0; nx=-1.0; ny=0.0; return; }
         ny=x+(0.001*rx);                // epsilon
         if (ny>=+rx){ y=0.0; nx=+1.0; ny=0.0; return; }
         y=ellipse_y(rx,ry,x);           // first point
         nx=y-ellipse_y(rx,ry,ny);       // second point
         ny=ny-x;
     /*
         // normalize
         x=divide(1.0,sqrt((nx*nx)+(ny*ny)));
         nx*=x;
         ny*=x;
     */
         }
    

    The normalization is optional (I commented it out for speed as its not needed for the search itself).

  4. compute error e for the search

    Simply the slopes (x-px,y-py) and (nx,ny) should be equal so:

    e=fabs(((y-py)*nx)-((x-px)*ny));
    

    The x search should minimize the e towards zero.

Do not forget to handle py<0 by negating y. Putting all togetherusing my approx search leads to:

//---------------------------------------------------------------------------
float ellipse_y(float rx,float ry,float x)  // y = f(x)
    {
    if (fabs(x)>rx) return 0.0;
    return sqrt((rx*rx)-(x*x))*ry/rx;
    }
//---------------------------------------------------------------------------
void ellipse_pn(float rx,float ry,float &nx,float &ny,float x,float &y) // x',y',y = f(x) if (py>=0)
    {
    if (x<=-rx){ y=0.0; nx=-1.0; ny=0.0; return; }
    ny=x+(0.001*rx);                // epsilon
    if (ny>=+rx){ y=0.0; nx=+1.0; ny=0.0; return; }
    y=ellipse_y(rx,ry,x);           // first point
    nx=y-ellipse_y(rx,ry,ny);       // second point
    ny=ny-x;
    }
//---------------------------------------------------------------------------
void ellipse_nn(float rx,float ry,float &nx,float &ny,float x,float &y) // x',y',y = f(x) if (py<=0)
    {
    if (x<=-rx){ y=0.0; nx=-1.0; ny=0.0; return; }
    ny=x+(0.001*rx);                // epsilon
    if (ny>=+rx){ y=0.0; nx=+1.0; ny=0.0; return; }
    y=-ellipse_y(rx,ry,x);          // first point
    nx=y+ellipse_y(rx,ry,ny);       // second point
    ny=ny-x;
    }
//---------------------------------------------------------------------------
void this_is_main_code()
    {
    float rx=0.95,ry=0.35;  // ellipse
    float px=-0.25,py=0.15; // input point
    float x,y,nx,ny;
    approx ax; double e;
    if (py>=0.0)
        {
        for (ax.init(-rx,+rx,0.25*rx,3,&e);!ax.done;ax.step())
            {
            x=ax.a;
            ellipse_pn(rx,ry,nx,ny,x,y);
            e=fabs(((y-py)*nx)-((x-px)*ny));
            }
        x=ax.aa; y=+ellipse_y(rx,ry,x);
        }
    else{
        for (ax.init(-rx,+rx,0.25*rx,3,&e);!ax.done;ax.step())
            {
            x=ax.a;
            ellipse_nn(rx,ry,nx,ny,x,y);
            e=fabs(((y-py)*nx)-((x-px)*ny));
            }
        x=ax.aa; y=-ellipse_y(rx,ry,x);
        }
    // here (x,y) is found solution and (nx,ny) normal
    }
//---------------------------------------------------------------------------

You still can adjust the search parameters (step and recursions) to have desired precision and speed. Also you can optimize by inlinening and removing heap thrashing and moving the range checks before the iterations...

Also you should adjust the normal computation for range <rx-epsilon,rx> as it truncates it to x=+rx (you could use negative epsilon for that).

[Ed1t1]

If I see it right the point with matching normal is also closest points so we can search directly that (which is even faster):

// search closest point
if (py>=0.0)
    {
    for (ax.init(-rx,+rx,0.25*rx,3,&e);!ax.done;ax.step())
        {
        x=ax.a;
        y=ellipse_y(rx,ry,x);
        x-=px; y-=py; e=(x*x)+(y*y);
        }
    x=ax.aa; y=+ellipse_y(rx,ry,x);
    }
else{
    for (ax.init(-rx,+rx,0.25*rx,3,&e);!ax.done;ax.step())
        {
        x=ax.a;
        y=-ellipse_y(rx,ry,x);
        x-=px; y-=py; e=(x*x)+(y*y);
        }
    x=ax.aa; y=-ellipse_y(rx,ry,x);
    }

Also I feel there still might be some better solution using graphic approach like rescale to circle solve for circle and then scale back to ellipse +/- some corrections however too lazy to try...

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • Your solution is interesting. I will try to implement it and see if it improves the accuracy. – Flovdis Nov 14 '22 at 09:00
  • @Flovdis I used my approx search as the data is not monotonic (that is why binary search is a problem), however there is only 1 or 2 solutions and always in locally global min or max so maybe bisection would be faster as it needs less iterations per recursion and there are no solutions that could be skipped anyway... in the approx search link the other answer describes the bisection ... – Spektre Nov 14 '22 at 09:04