Convert a Bezier to XY wave

In order to make a nice smooth illustration, I've drawn a bezier* on my graph.

But now my graph has too many lines, and I want to use "markers" to show the bezier as spheres/triangles etc.  For this I think I need to use waves (I do this often with sparse markers).

Is there any way to convert the Bezier to an XY wave, or any other solution here?

Thanks!

(*CTRL+T, right click Polygon tool, draw Bezier)

One (perhaps non-ideal) solution is to trace the curve using IgorThief:

#include <IgorThief>

 

I think you can use DrawAction with the extractOutline keyword to do this but I have never tried using it for this purpose.

Hi,

Since an algorithm is used to create the Bezier curve, can it be exposed so we can extract the values?

Andy

It would be helpful to see an image of what you have or what you're trying to achieve.

Is there a way to tweak the number of points in the waves W_PolyX and W_PolyY that DrawAction extractOuline produces?

Say, I'd like to "wave" the following bezier:

Window Graph0() : Graph
    Make/O w = nan
    Display w
    SetAxis left*,128
    SetDrawLayer UserFront
    SetDrawEnv xcoord= bottom,ycoord= left
    DrawBezier 4.7065034842466,7.14085694275653,0.956151,0.921157,{4.7065034842466,7.14085694275653,4.7065034842466,7.14085694275653,23.73305842356,32.2103830643604,45.4585566789422,48.2641594054833,67.1840549343245,64.3179357466063,90.7319706331314,71.1687650784765,90.7319706331314,71.1687650784765,90.7319706331314,71.1687650784765,95.308229338295,79.5737119191777}
    DrawBezier/A {108.382023375747,95.8329122606684,121.4558174132,112.092112602159,125.384572602189,114.478647825545,125.384572602189,114.478647825545}
EndMacro

I get pretty much get what I want with:

DrawAction extractOutline
Display W_PolyY vs W_PolyX

but the resulting trace does not look as smooth as the curve itself. I wonder if this could be improved if I specified the number of points, which seems to be fixed to 80 in this case.

Interpolation and Smoothing help a bit, but it's not quite there yet.

 

Looking at the source code, it appears that we are drawing a bezier by computing 20 points per bezier segment. There isn't any intelligence or user control for that. If you plot the extracted waves on the graph with your draw-object bezier, and set the fill for the bezier to None, I think you will see that the two versions are the same.

It does seem like we should provide some control over the resolution of the bezier to polygon conversion.

Does the bezier represent something in particular? Where did you get the bezier coordinates from?

In reply to by johnweeks

Thanks John,

johnweeks wrote:

Does the bezier represent something in particular? Where did you get the bezier coordinates from?

The scaling is arbitrary, but the shape is typical for a melting curve of minerals/rocks in temperature vs. pressure space. The discontinuity is a result of a phase change within the solid and must not be smoothed out. I'm compiling literature data, some of which are only shown as drawn curves in papers. I use an IgorThief approach to replicate a given curve with a bezier to extract the numeric values. That works like a charm!

I could not check yet if the apparent non-smoothness of the bezier-to-wave conversion would even show up on a print-out. 

johnweeks wrote:

It does seem like we should provide some control over the resolution of the bezier to polygon conversion.

That would be awesome :-)

 

Igor's BezierToPolygon operation has a /NSEG=(numberOfSegments) parameter:

BezierToPolygon [ /DSTX=destXWave /DSTY=dstYWave /FREE /NSEG=nseg ] bezXWave, bezYWave

The BezierToPolygon operation creates an XY pair of waves approximating the Bezier curves described by bezXWave and bezYWave.

The BezierToPolygon operation was added in Igor Pro 9.00.

Flags

/DSTX=destX    Specifies the X destination wave to be created or overwritten. If you omit /DSTX, destX defaults to W_PolyX.

/DSTY=destY    Specifies the Y destination wave to be created or overwritten. If you omit /DSTY, destY defaults to W_PolyY.

/FREE    Creates output waves as free waves (see Free Waves). /FREE is allowed only in functions. If you use /DSTX or /DSTY then the specified parameter must be either a simple name or a valid wave reference.

/NSEG=nseg    The number of segments used to render each Bezier segment from 1 and 500. The default of 20 is usually sufficient.

/NSEG looks interesting, and it does make a difference in the example of the associated help file.

I might be missing something obvious but where do I get bezXWave and bezYWave from? 

In reply to by ChrLie

Those contain the Bezier control points. You can either compute the control points or draw them and extract the values from the DrawBezier recreation command with something like MakePolyFromDrawnBezier

 

Window Panel0() : Panel
    PauseUpdate; Silent 1       // building window...
    NewPanel /W=(150,77,450,277)
    SetDrawLayer UserBack
    SetDrawEnv linefgc= (65535,0,0)
    DrawBezier 42,85,1,1,{42,85,42,44.8876577597368,54.9092779656255,45,77,45,99.0907220343745,45,104,74.8132267551044,104,99,104,123.186773244896,42,85,42,85}
EndMacro

Function MakePolyFromDrawnBezier()
    // from DrawBezier command: DrawBezier 42,85,1,1,{42,85,42,44.8876577597368,54.9092779656255,45,77,45,99.0907220343745,45,104,74.8132267551044,104,99,104,123.186773244896,42,85,42,85}
    Make/O xy = {42,85,42,44.8876577597368,54.9092779656255,45,77,45,99.0907220343745,45,104,74.8132267551044,104,99,104,123.186773244896,42,85,42,85}
    Variable n= numpnts(xy)/2
    Make/O/N=(n) wx = xy[0+p*2]
    Make/O/N=(n) wy = xy[1+p*2]
    Variable xorg = wx[0]
    Variable yorg = wy[0]
    BezierToPolygon wx,wy // Add /NSEG=num to control precision
    WAVE W_PolyX, W_PolyY
    DrawPoly /W=Panel0 xorg, yorg, 1, 1, W_PolyX,W_PolyY
End

DisplayHelpTopic "Drawing Polygons and Bezier Curves"

Thanks for example! It would be much nicer though if I could programatically extract the information from the window macro.

In reply to by ChrLie

This wasn't as easy as I thought it would be.

This code creates a wave-based DrawPoly object from the "first" drawn Bezier object. "First" means "first in the recreation macro".

Fortunately, you can reorder drawing objects position in the recreation macro by selecting the object and choosing "Send to Back", etc.

"Send To Back" will make the object the first drawn object; just what we need.

The choose MakePolyFromFirstDrawnBezier from the Macros menu. Accept or change the segmentsPerBezier = 20 (experiment with small numbers to see why it matters).

A polygon version of the first Bezier drawn in the top graph, panel, or layout is added to the same window.

None of the Bezier's attributes such as fill or line color are copied to the polygon. You may have to correct the coordinate system if the bezier wasn't drawn with the default coordinate system.

You can comment out the code that adds the DrawPoly object if all you want are the polyX and polyY waves created by MakePolyFromBezierCoordinates().

 

#pragma IgorVersion=9.0

Macro MakePolyFromFirstDrawnBezier(segmentsPerBezier)
    Variable segmentsPerBezier=20
   
    String win = WinName(0,1+4+64) // Graphs, Layouts, Panels
    PolyWavesFromFirstDrawnBezier(win,segmentsPerBezier)
End

Function PolyWavesFromFirstDrawnBezier(String win, Variable segmentsPerBezier)

    Variable xorg, yorg, hscaling, vscaling, isAbsolute
    String coordinates = FirstBezierCoordinates(win, xorg, yorg, hscaling, vscaling, isAbsolute)
    if( strlen(coordinates) )
        [WAVE polyX, WAVE polyY] = MakePolyFromBezierCoordinates(coordinates, segmentsPerBezier)
        String cmd
        if( isAbsolute )
            DrawPoly/ABS /W=$win xorg, yorg, hscaling, vscaling, polyX, polyY
        else
            DrawPoly /W=$win xorg, yorg, hscaling, vscaling, polyX, polyY
        endif
    endif
End

Function [WAVE polyX, WAVE polyY] MakePolyFromBezierCoordinates(String coordinates, Variable segmentsPerBezier)
   
    Variable numItems= ItemsInList(coordinates,",")
    Variable n= numItems/2
    Make/O/D/N=(n)/FREE wx = str2num(StringFromList(0+p*2,coordinates,","))
    Make/O/D/N=(n)/FREE wy = str2num(StringFromList(1+p*2,coordinates,","))
    BezierToPolygon/NSEG=(segmentsPerBezier) wx,wy
    WAVE W_PolyX, W_PolyY
    // BezierToPolygon always creates W_PolyX, W_PolyY, we need waves that won't get overwritten.
    String xname=UniqueName("Poly4BezX",1,0)
    String suffix = xname[strlen("Poly4BezX"),inf]
    String yname= "Poly4BezY"+suffix
    Duplicate/O W_PolyX, $xname; WAVE polyX=$xname
    Duplicate/O W_PolyY, $yname; WAVE polyY=$yname
End


Function/S FirstBezierCoordinates(String win, Variable &xorg, Variable &yorg, Variable &hscaling, Variable &vscaling, Variable &isAbsolute)

    String list = WinRecreation(win,4) // lines end with \r
    // look for first DrawBezier or DrawBezier/ABS command,
    // and accumulate coordinates from immediately following DrawBezier/A commands.
    // Stop accumulating when the next command is NOT DrawBezier/A.
    String separator = "\r"
    Variable separatorLen = strlen(separator)
    Variable numItems = ItemsInList(list, separator)
    Variable i, offset = 0
    Variable foundBezier= 0
    String bezierkey="\tDrawBezier "
    String absbezierkey="\tDrawBezier/ABS "
    String appendKey="\tDrawBezier/A "
    String coordinates=""
    for(i=0; i<numItems; i+=1)
        String item = StringFromList(0, list, separator, offset) // When using offset, the index parameter is always 0
        Variable isDrawBezier = CmpStr(bezierkey,    item[0,strlen(bezierkey)-1]) == 0
        Variable isAbsbezier  = CmpStr(absbezierkey, item[0,strlen(absbezierkey)-1]) == 0
        Variable isAppend     = CmpStr(appendKey,    item[0,strlen(appendKey)-1]) == 0

        if( !foundBezier && (isDrawBezier || isAbsbezier) )
            // we have "\tDrawBezier 42,85,1,1,{42,85,...}"
            // or "\tDrawBezier/ABS 0,0,1,1,{48,104,...}"
            isAbsolute = isAbsbezier
            Variable prefixLen = isAbsbezier ? strlen(absbezierkey) : strlen(bezierkey)
            sscanf item[prefixLen,strlen(item)-1], "%g,%g,%g,%g,{", xorg, yorg, hscaling, vscaling
            SplitString/E=".*\{(.*)\}" item, coordinates
   
            foundBezier= 1
        elseif( foundBezier )
            if( !isAppend ) // must be a command AFTER DrawBezier and optional DrawBezier/A
                break       // this prevents appending coordinates from additional bezier objects.
            endif
            // we have "\tDrawBezier/A {48,104,48,104}"
            String more
            SplitString/E=".*\{(.*)\}" item, more
            coordinates += ","+more
            // keep going, multiple DrawBezier/A commands are allowed.
        endif
        offset += strlen(item) + separatorLen
    endfor
    return coordinates
End

 

Hello Jim,

that's fantastic, another great example of the outstanding support from WM!

I was not aware of WinRecreation (although I wondered if something like this exists) but admittedly I may have choked on the DrawBezier/A issue.

I made some adjustments,

Function PolyWavesFromFirstDrawnBezier(String win, Variable segmentsPerBezier)

    Variable xorg, yorg, hscaling, vscaling, isAbsolute
    String coordinates = FirstBezierCoordinates(win, xorg, yorg, hscaling, vscaling, isAbsolute)
    if( strlen(coordinates) )
        [WAVE polyX, WAVE polyY] = MakePolyFromBezierCoordinates(coordinates, segmentsPerBezier)
        Make/O W_bez2Poly
        Interpolate2/T=1/N=(numPnts(PolyY))/Y=W_Bez2Poly Polyx,Polyy
        KillWaves/Z polyX,polyY
    endif
End

Function [WAVE W_polyX, WAVE W_polyY] MakePolyFromBezierCoordinates(String coordinates, Variable segmentsPerBezier)
   
    Variable numItems= ItemsInList(coordinates,",")
    Variable n= numItems/2
    Make/O/D/N=(n)/FREE wx = str2num(StringFromList(0+p*2,coordinates,","))
    Make/O/D/N=(n)/FREE wy = str2num(StringFromList(1+p*2,coordinates,","))
    BezierToPolygon/NSEG=(segmentsPerBezier) wx,wy
    WAVE W_PolyX, W_PolyY
End

which gives me a single scaled wave as output, but this only makes sense with the bezier drawn with: 

SetDrawEnv xcoord= bottom,ycoord= left

Thanks a lot!