Convert a Bezier to XY wave
jcor
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>
October 29, 2020 at 10:56 am - Permalink
I think you can use DrawAction with the extractOutline keyword to do this but I have never tried using it for this purpose.
October 30, 2020 at 06:38 am - Permalink
Hi,
Since an algorithm is used to create the Bezier curve, can it be exposed so we can extract the values?
Andy
October 30, 2020 at 07:15 am - Permalink
In reply to Hi, Since an algorithm is… by hegedus
In the meantime, here's the "Cubic Bezier Curve Flattening using Forward Differencing" algorithm Igor's is based on:
https://gist.github.com/rlindsay/c55be560ec41144f521f
October 31, 2020 at 05:37 pm - Permalink
It would be helpful to see an image of what you have or what you're trying to achieve.
November 3, 2020 at 12:20 am - Permalink
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:
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:
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.
March 25, 2022 at 04:05 am - Permalink
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?
March 25, 2022 at 10:05 am - Permalink
In reply to Looking at the source code,… by johnweeks
Thanks John,
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.
That would be awesome :-)
March 25, 2022 at 10:46 am - Permalink
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.
March 25, 2022 at 03:34 pm - Permalink
/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?
March 26, 2022 at 01:34 am - Permalink
In reply to /NSEG looks interesting, and… 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
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"
March 26, 2022 at 12:22 pm - Permalink
Thanks for example! It would be much nicer though if I could programatically extract the information from the window macro.
March 28, 2022 at 03:27 am - Permalink
In reply to Thanks for example! It would… 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().
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
March 29, 2022 at 05:23 pm - Permalink
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,
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!
March 30, 2022 at 02:55 am - Permalink