2

I'm looking for an algorithm to calculate the bitangent of a curve, ie. the line below a curve that intersects it at exactly two points: Example 1 Example 2

Specifically, I am looking for the two x-values where the bitangent intersects the curve. These are examples of actual curves that I am studying, I have manually drawn the tangent I would like to find.

Referencing this post from the Mathematica Stack Exchange almost verbatim, I understand that I am looking for two distinct x values for which a generic line and the function describing this curve have

  1. The same y value (ie. the line touches the curve)
  2. The same derivative (ie. the line is tangent to the curve)

This works if we know the function f(x) that describes the curve, because we know the equation of a generic line is y = mx + b. We can then set up the following system of equations and solve:

  1. y1 = f(x1),

  2. y2 = f(x2),

  3. f'(x1) = f'(x2),

  4. f'(x1) = (y2 - y1)/(x2 - x1)

The issue I am having is two fold. I don't know the function of the line, as it comes from a measurement, and even if I did, I don't know how to solve a system of equations using C# / MathNet. The only thing I can say definitively about the curve is the slope of the bitangent will be positive, and the x-axis will run from -150 to 700.

Other things I've tried is using a modified convex hull algorithm, and fitting a cubic spline by manually selecting the knot points, but both these attempts were insufficiently accurate.

So my questions are:

  1. How can I find the function of this curve (ideally using MathNet / C#)
  2. How can I solve the above system of equations (also ideally using MathNet / C#)

Any other algorithm ideas / advice is appreciated!

Thanks

Other related posts on the Mathematica Stack Exchange

Spektre
  • 49,595
  • 11
  • 110
  • 380
ekglimmer
  • 27
  • 6
  • *How can I find the function of this curve* ... you can use [polynomial interpolation](https://en.wikipedia.org/wiki/Polynomial_interpolation) – Selvin Apr 18 '20 at 20:13
  • Do you know what category of functions this equation comes from? For example if it will be a polynomial, trigonometirc etc. – ekke Apr 18 '20 at 21:09
  • which bi-tangent to find ? there are more of them on the first image ... do you have sample data input as a code table? or at least a plot with points colored differently than other stuff so it can be extracted from image easily (preferably single pixel dots of different color than axises and bitangent without compression so PNG preferably)? – Spektre Apr 19 '20 at 09:24
  • I think the category is indeed polynomial but it probably would be of quite a high order if I were to get a perfect fit @ekke. – ekglimmer Apr 19 '20 at 23:17
  • @Spektre that's a good point. Just the bitangent I drew, I would prefer to ignore the other ones but if multiple were found I think I could come up with a heuristic to choose the one I want. I'm not sure what you mean by code table, but I do have sample data. – ekglimmer Apr 19 '20 at 23:18
  • @ekglimmer by `(hard)coded table` I mean something like this: `float table[] = { 1.5,1.8,2.3, ... }` you should add the sample data in here so we can test on the same data input ... By heuristics you mean bi-tangent that connects/touches the most distant points (in `x` or in `y` or euclidean)? – Spektre Apr 20 '20 at 06:55
  • @ekglimmer I added answer with extracted data points and bruteforce approach ... looks like its working even if its `O(n^3)` it computes in few `[ms]` well below one second ... the app opens almost instantly. – Spektre Apr 20 '20 at 09:20

2 Answers2

2

Since the function is not known analytically the only viable approach is working numerically. The idea of using convex hull seems promising. Based on your example data it seems it should work as follows:

  • compute the lower convex hull e.g. with Graham Scan which in fact computes separately the upper and lower one (or eventually the left/right one depending on the implementation)
  • take the longer hull segments
  • maybe the longer one is what you are looking for or eventually you may need to take some number of longer ones (e.g. 2 to 5) and create a ranking of them based on some suitable idea e.g.:
    • the max y-distance between the points over a segment and the segment itself
    • take the most central one
    • take the one with the higher slope

You need to experiment with your data (and know its physical meaning) to see what is the most suitable ranking criteria to select the one segment you need among the longer ones.

Diego Mazzaro
  • 2,734
  • 1
  • 20
  • 21
2

Input data

Well as you did not shared the original input data in numerical form I extracted it from the images. This part is meant for others... First I edited them in paint a bit:

  1. fill the background by Black color

    that removes many of the bi tangent points

  2. crop inside

    that removes axises and scales/grid

  3. manually clear the remnant pixels of what has been left from the bitangent

Then I apply some DIP to shrink all vertical and horizontal lines to single point here the results:

plot0 plot1

and then I extracted xy of any non background pixels (first number is x, second y in pixels):

int data0_xy[]=
    {
      33, 447,  41, 451,  49, 462,  56, 470,  64, 473,  72, 477,  80, 481,  87, 489,  95, 492, 103, 495, 111, 496, 118, 500, 126, 504, 134, 507, 142, 506, 149, 506,
     157, 510, 165, 514, 173, 512, 180, 512, 188, 512, 196, 515, 204, 515, 212, 513, 219, 512, 227, 514, 235, 514, 243, 513, 250, 512, 258, 511, 266, 512, 274, 511,
     281, 508, 289, 505, 297, 505, 305, 506, 312, 505, 320, 500, 328, 500, 336, 500, 343, 501, 351, 497, 359, 495, 367, 497, 374, 499, 382, 500, 390, 498, 398, 498,
     406, 501, 413, 502, 421, 501, 429, 501, 437, 502, 444, 504, 452, 506, 460, 504, 468, 504, 475, 504, 483, 505, 491, 505, 499, 503, 506, 500, 514, 501, 522, 503,
     530, 499, 537, 497, 545, 495, 553, 496, 561, 495, 569, 490, 576, 486, 584, 486, 592, 485, 600, 483, 607, 478, 615, 475, 623, 472, 631, 471, 638, 468, 646, 461,
     654, 457, 662, 454, 669, 452, 677, 450, 685, 443, 693, 436, 700, 431, 708, 429, 716, 425, 724, 417, 732, 410, 739, 405, 747, 400, 755, 397, 763, 387, 770, 380,
     778, 375, 786, 369, 794, 363, 801, 355, 809, 346, 817, 342, 825, 336, 832, 326, 840, 318, 848, 310, 856, 306, 863, 300, 871, 291, 879, 281, 887, 274, 894, 269,
     902, 263, 910, 252, 918, 244, 926, 237, 933, 230, 941, 224, 949, 215, 957, 206, 964, 202, 972, 197, 980, 187, 988, 179, 995, 173,1003, 169,1011, 162,1019, 155,
    1026, 148,1034, 141,1042, 136,1050, 133,1057, 126,1065, 119,1073, 115,1081, 113,1089, 108,1096, 102,1104,  97,1112,  97,1120,  94,1127,  87,1135,  84,1143,  82,
    1151,  84,1158,  80,1166,  77,1174,  74,1182,  72,1189,  73,1197,  72,1205,  69,1213,  67,1220,  68,1228,  68,1236,  64,1244,  62,1252,  61,1259,  63,1267,  60,
    1275,  57,1283,  53,1290,  49,1298,  49,1306,  47,1314,  40,1321,  35,1329,  31,1337,  27,1345,  20,1352,  10,
    };
int data1_xy[]=
    {
      33, 533,  41, 533,  49, 532,  56, 531,  64, 533,  72, 532,  80, 530,  87, 533,  95, 530, 103, 533, 111, 531, 118, 532, 126, 531, 134, 531, 142, 531, 149, 529,
     157, 530, 165, 530, 173, 529, 180, 526, 188, 529, 196, 527, 204, 525, 212, 526, 219, 526, 227, 525, 235, 523, 243, 520, 250, 522, 258, 520, 266, 520, 274, 517,
     281, 519, 289, 516, 297, 515, 305, 513, 312, 511, 320, 510, 328, 509, 336, 507, 343, 503, 351, 503, 359, 501, 367, 500, 374, 496, 382, 496, 390, 493, 398, 490,
     406, 488, 413, 485, 421, 482, 429, 480, 437, 476, 444, 469, 452, 467, 460, 466, 468, 461, 475, 456, 483, 451, 491, 449, 499, 442, 506, 438, 514, 432, 522, 426,
     530, 420, 537, 411, 545, 402, 553, 400, 561, 395, 569, 386, 576, 380, 584, 372, 592, 363, 600, 354, 607, 344, 615, 336, 623, 327, 631, 323, 638, 315, 646, 300,
     654, 299, 662, 291, 669, 284, 677, 273, 685, 266, 693, 256, 700, 250, 708, 243, 716, 237, 724, 227, 732, 217, 739, 210, 747, 197, 755, 185, 763, 184, 770, 151,
     778, 165, 786, 158, 794, 145, 801, 136, 809, 111, 817, 119, 825, 112, 832, 105, 840,  94, 848,  76, 856,  80, 863,  78, 871,  66, 879,  60, 887,  40, 894,  28,
     902,  38, 910,  42, 918,  41, 926,  46, 933,  47, 941,  41, 949,  42, 957,  39, 964,  48, 972,  52, 980,  49, 988,  57, 995,  51,1003,  67,1011,  78,1019,  85,
    1026,  90,1034,  96,1042, 106,1050, 114,1057, 123,1065, 132,1073, 126,1081, 137,1089, 157,1096, 169,1104, 175,1112, 169,1120, 189,1127, 191,1135, 204,1143, 209,
    1151, 221,1158, 229,1166, 235,1174, 226,1182, 241,1189, 244,1197, 257,1205, 264,1213, 260,1220, 273,1228, 275,1236, 280,1244, 285,1252, 288,1259, 290,1267, 292,
    1275, 290,1283, 297,1290, 298,1298, 298,1306, 301,1314, 300,1321, 304,1329, 304,1337, 298,1345, 297,1352, 297,
    };

So now we have more or less the same data as you do...

Bitangent

Well as your data is not that big you can try bruteforce approach.

  1. loop through all point pairs

    simple O(n^2) search. Do not test point pairs twice so the second loop will not test points already done in first loop.

  2. detect if valid bitangent

    so if we compute normal vector to tested bitangent (perpendicular vector to it pointing towards data points). Then all the vectors p(i)-p(bitangent) must have non negative (or positive if you want exactly 2 hits) dot product with our normal. This lead to another O(n) loop resulting in O(n^3) approach. If any dot product crosses zero throw away this bitangent and test the next one. If no dot product crosses you found bitangent so add the actaul points from first two loops as a new bitangent to the list.

This will find all the bitangents on the lowwer side of data (or above it if you flip the normal vector or crossing condition). So now you can apply your heuristics to select bitangent you want.

I do not code in C# so here simple C++ example:

const int n2=sizeof(data_xy)/sizeof(data_xy[0]);    // numbers in data
const int n=n2>>1;                                  // points in data
int bitangent[100],bitangents;                      // 2 poit indexes per bitangent, number of indexes in bitangent[]
// O(n^3) bruteforce bitangent search
int nx,ny,i2,j2,k2,dx,dy;
bitangents=0;
for (i2=0;i2<n2;i2+=2)      // loop all points (bitangent start)
 for (j2=i2+2;j2<n2;j2+=2)  // loop all yet untested points (bitangent end)
    {
    // normal
    ny=-(data_xy[j2+0]-data_xy[i2+0]);
    nx=+(data_xy[j2+1]-data_xy[i2+1]);
    // test overlap
    for (k2=0;k2<n2;k2+=2)
     if ((k2!=i2)&&(k2!=j2))
        {
        dx=data_xy[k2+0]-data_xy[i2+0];
        dy=data_xy[k2+1]-data_xy[i2+1];
        if ((dx*nx)+(dy*ny)<0) { k2=-1; break; } // if dot product is negative overlap occurs so throw solution away
        }
    if (k2>=0)
        {
        bitangent[bitangents]=i2; bitangents++;
        bitangent[bitangents]=j2; bitangents++;
        }
    }

I render like this (VCL):

void draw()
    {
    int i2,j2,x,y,r=3;
    bmp->Canvas->Brush->Color=clWhite;
    bmp->Canvas->FillRect(TRect(0,0,xs,ys));

    // render data lines
    bmp->Canvas->Pen->Color=clBlack;
    for (i2=0;i2<n2;i2+=2)
        {
        x=data_xy[i2+0];
        y=data_xy[i2+1];
        if (!i2) bmp->Canvas->MoveTo(x,y);
        else     bmp->Canvas->LineTo(x,y);
        }

    // render bitangents lines
    bmp->Canvas->Pen->Color=clBlue;
    for (i2=0;i2<bitangents;i2+=2)
        {
        j2=bitangent[i2+0];
        x=data_xy[j2+0];
        y=data_xy[j2+1];
        bmp->Canvas->MoveTo(x,y);
        j2=bitangent[i2+1];
        x=data_xy[j2+0];
        y=data_xy[j2+1];
        bmp->Canvas->LineTo(x,y);
        }

    // render data points
    bmp->Canvas->Pen->Color=clBlack;
    bmp->Canvas->Brush->Color=clRed;
    for (i2=0;i2<n2;i2+=2)
        {
        x=data_xy[i2+0];
        y=data_xy[i2+1];
        bmp->Canvas->Ellipse(x-r,y-r,x+r,y+r);
        }
    // render bitangents points
    bmp->Canvas->Pen->Color=clBlack;
    bmp->Canvas->Brush->Color=clAqua;
    for (i2=0;i2<bitangents;i2++)
        {
        j2=bitangent[i2];
        x=data_xy[j2+0];
        y=data_xy[j2+1];
        bmp->Canvas->Ellipse(x-r,y-r,x+r,y+r);
        }

    Form1->Canvas->Draw(0,0,bmp);
    }

And here output for your data:

output0 output1

As you can see I did not need any floating point operation or variable :) To keep this as simple as possible I used static arrays (no dynamic allocation). As you can see there are quite more bitangents than you think at first.

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • Wow this is incredible! That is EXACTLY what I was looking to do. Thank you. – ekglimmer Apr 20 '20 at 20:02
  • @ekglimmer glad to be of help .... btw this could be speed up a lot by dividing your data into ascending and desceding parts (inflex points) but I see no point to do so if the output is computed almost instantly ... – Spektre Apr 20 '20 at 20:10