2

Summary
My simple function performing a binary search for a local minimum often returns spurious values corresponding to exactly ¼, ½, and ¾ of the search. Why is this happening?

/**
 * minX  : the smallest input value
 * maxX  : the largest input value
 * ƒ     : a function that returns a value `y` given an `x`
 * ε     : how close in `x` the bounds must be before returning
 * return: the `x` value that produces the smallest `y`
 */
function localMinimum(minX, maxX, ƒ, ε) {
    if (ε===undefined) ε=1e-15;
    let m=minX, n=maxX, k;
    while ((n-m)>ε) {
        k = (n+m)/2;
        if (ƒ(m)<ƒ(n)) n=k;
        else           m=k;
    }
    return k;
}

Live Demo: http://phrogz.net/svg/closest-point-on-bezier_broken.html

The demo lets you adjust a cubic Bézier curve, move the mouse around, and attempts to find a point on the curve closest to the mouse location. In the screenshots below, you can see the curve (black), the location of the mouse (centered in the gray circle), the location found on the curve (red dot), a brute-force graph of the distance from the cursor to each point on the curve (lower right corner), and the t parameter that was calculated as the local minimum (red text).

S-shaped curve stuck at 0.25 C-shaped curve stuck at 0.75 C-shaped curve stuck at 0.5

These screenshots represent the worst case. In them the mouse location is very close to the line, and the "local minimum" function is returning a point that is NOT the local minimum. If you experiment with the demo, you will see that in many cases the local minimum function is working as intended. Just…not near some of the early binary search bounds.

Local minimum correctly identified

Guide to the Code
The localMinimum() function (line 225) is called by the closestPoint() function (line 194):

/**
 * out   : A vmath vector to modify with the point value
 * curve : Array of vectors as control points for a Bézier curve (any degree)
 * pt    : Object with .x/.y to find the point closest to
 * return: A parameter t representing the location of `out`
 */
function closestPoint(out, curve, pt, tmps) {
    let vec=vmath[ 'w' in curve[0] ? 'vec4' : 'z' in curve[0] ? 'vec3' : 'vec2' ];
    return localMinimum(0, 1,
      t => vec.squaredDistance(pt, bézierPoint(out, curve, t)));
}

The bézierPoint() function (line 207) uses De Casteljau's algorithm to calculate a point along the curve parameterized by t. This function works as expected, tested independently of the rest of the problem.

The squaredDistance() function is part of the vmath library, and is as simple as you would imagine.

What am I missing? How is my local minima function failing?

Phrogz
  • 296,393
  • 112
  • 651
  • 745

2 Answers2

1

I had this very same problem using a different set of functions to find closest point on the curve.

The progressive search

function localMinimum(minX, maxX, ƒ, ε) {
    if (ε===undefined) ε=1e-15;
    let m=minX, n=maxX, k;
    while ((n-m)>ε) {
        k = (n+m)/2;
        if (ƒ(m)<ƒ(n)) n=k;
        else           m=k;
    }
    return k;
}

is at fault as it will close in on the wrong local. From memory it is the derivative (or second derivative) of the curve that is the distance from the point that will show an inflection point where you are getting the wrong result. In other words at that part of the curve it moves closer and closer to your point and then starts to move away. The search function assume that this inflection point is the point you are looking for.

The solution is systematic search (at some resolution) then a progressive search for the final result.

Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Thanks for the help. FWIW, it's not that it closed in on the 'wrong' local, but rather that it rejected the portion that included the minimum early on. (See my answer for explanation and fix.) However, your point about a better search function is quite true. As shown in [this answer](https://stackoverflow.com/a/44993719/405017), my 'fixed' `localMinimum()` function suffers from exactly the problem its name is designed to warn of. – Phrogz Jul 09 '17 at 06:51
  • @Phrogz This problem has always bugged me as a simple functional solution would be ideal. A good solution exists requiring that you know the tangent of the curve at the closest point, you align the curve to that tangent and solve for f'(x) = 0 (up to 4 roots). Yet that tangent is as hard to find as the point we are looking for. Your question made me rethink and maybe adding rotation to the bezier function f(x) = f(Rot(x)) then f"'(x) = 0 could find the tangents. But I am have trouble visualising and solving a 4th order poly is hard, need to try some code to see if it would work. – Blindman67 Jul 09 '17 at 07:22
0

As so often happens, the moment you take the time to write up a question, the answer becomes obvious. My localMinimum() function was flawed.

If we look at the graph for the third example above and draw a line from the maximum point, we see that it is on the right side:

enter image description here

The way the algorithm (as shown above) is written, it immediately throws away the right half of the graph and focuses on the left. From that point on, it is doomed. It keeps moving the left bound toward the right at 0.5, but is never allowed to move the right again.

Here is a fixed version of the localMinimum() function, employing a bit of a hack. Given any bounds, it finds the spot in the middle, and then steps to the left and right "slightly" (as determined by the ε parameter) and decides which half to keep based on that.

/**
 * minX  : the smallest input value
 * maxX  : the largest input value
 * ƒ     : a function that returns a value `y` given an `x`
 * ε     : how close in `x` the bounds must be before returning
 * return: the `x` value that produces the smallest `y`
 */
function localMinimum(minX, maxX, ƒ, ε) {
    if (ε===undefined) ε=1e-10;
    let m=minX, n=maxX, k;
    while ((n-m)>ε) {
        k = (n+m)/2;
        if (ƒ(k-ε)<ƒ(k+ε)) n=k;
        else               m=k;
    }
    return k;
}

I've uploaded a fixed version of the demo to:http://phrogz.net/svg/closest-point-on-bezier.html

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Your solution seem to still go considerably wrong for points near the ends of the curve (on either side), as illustrated in http://i.imgur.com/fME6BYj.png – Mike 'Pomax' Kamermans Jul 09 '17 at 18:49
  • Try replacing the binary search by successive parabolic interpolation (see https://en.wikipedia.org/wiki/Successive_parabolic_interpolation). – fang Jul 09 '17 at 19:31
  • @fang That sounds like a great suggestion, but the page you linked to says that it works for unimodal functions. The distance function is definitely not unimodal. – Phrogz Jul 09 '17 at 22:52
  • @Mike'Pomax'Kamermans Agreed. This is the problem of finding a local minimum. I've updated the demo page with additional brute force technique—scan the entire curve at a fixed resolution (25 samples seems pretty good)—and find the shortest distance. Then, use the `localMinimum()` function around just that point to find the true minimum. See also [this answer of mine](https://stackoverflow.com/a/44993719/405017). – Phrogz Jul 09 '17 at 22:54
  • @OP: The successive parabolic interpolation works the best for unimodal functions. But it also works for non-unimodal functions. The only problem is when the function is non-unimodal, there are more than one local minimums and the successive parabolic interpolation will only find one of them. But this is also the case for most minimization algorithms. – fang Jul 10 '17 at 04:04