Point cloud slices are planar point clouds which can be used to create 2D curves.
These slices can be generated simply from images with edge detection algorithms. An other way is to slice 3D Point clouds.
A point cloud holds typically thousends of points. And the points are not located on exact positions. Scanner and Cameras generate some rounding errors and noise.
It's interesting to have a way from such a point cloud back to an near exact CAD model.
This is an introduction how to achieve this with FreeCAD.
It's for the special case where the curve is closed and has it's center of mass inside on a position where for any point its connecting line to the center is completly inside the “figure”.
These are the steps to proceed.
All methods are stored in the nurbs workbench but can be used standalone.
The dataset of 1000 points are points around the origin with mean distance rm and To have more irregularitiy we overlay all with two sine waves.
count=1000 ri=100 rm=400 # random ankle kaps=np.random.random(count)*2*np.pi #random radius rads=np.random.random(count)*ri*np.cos(kaps*5)*np.cos(kaps*1.3) + rm y= np.cos(kaps) * rads x=np.sin(kaps) * rads z=np.zeros(count) pps=np.array([x,y,z]).swapaxes(0,1) goods=[FreeCAD.Vector(tuple(p)) for p in pps] Points.show(Points.Points(goods))
To run the script we import the nurbswb and the script and call its run function.
import nurbswb; import nurbswb.gen_random_dat; nurbswb.gen_random_dat.run()
There are different ways to get an approximation of a curve. The simplest is to create a circle. The center is the center of mass of the point cloud.
Then there are at least 3 main approches:
If we have the point cloid as a volume inner and outer circle can be used to make something like a tube or a worm.
More complex is to order the points in a useful way and to connect them to a polygon. Because there can be a local noise of points some local average processing is needed.
For a outer and inner polygon the extrema of the distances to the center can be used. The strategy is: Jump from one top to the next.
First the center of the cloud and the distances of all points from this center is calculated.
npts=np.array(pts).swapaxes(0,1) mp=(npts.mean(),npts.mean(),npts.mean()) # center vm=FreeCAD.Vector(mp) # distances of all points lengths=np.array([(FreeCAD.Vector(p)-vm).Length for p in pts])
Later the directions of each point viewd from center is used to order the points.Note that this is essential. The calcualted curve cannot change its direction forward and backward as when writing a S-curve. For such cases an other method is required.
for v in pts: vn=v-vm aps[np.arctan2(vn.x,vn.y)]=vn rads[np.arctan2(vn.x,vn.y)]=vn.Length kaps=aps.keys() kaps.sort() #the poits ordered and its distances ptss=[aps[k] for k in kaps] radss=[rads[k] for k in kaps]
kaps is a list of all directions. If there are gaps in this list an extra interpolation step which fills these gaps makes sence.
If the median approximation is calculated the radia have to be smoothed. This is done with the scipy.signal.medfilt method. The size of the window is configurable.
l=np.array(radss) f=medianfilterwindow path=np.concatenate([[l] * f,l,[l[-1]]*f]) l2 = sp.signal.medfilt(path,f) mmaa=l2[f:-f]
For the outer curves a jumping sequence form one top to the next is needed. This is done with the scipy.signal.argrelextrema method.
if inner: z1=argrelextrema(radss, np.less) else: z1=argrelextrema(radss, np.greater)
And at last the gaps are filled with numpy.interp.
So the method generates 3 circles and 3 wires: inner green, outer red and intermediate blue.
How to use:
import nurbswb; import nurbswb.orderpoints; nurbswb.orderpoints.run()
In the next step a Bspline is approximated. The Part.BSpline approximate method is the first step. But its result has too many poles for later editing by hand.
There is an extra reduce process: The curvature of the first approximated curve is computed and then only points where the curvature has extrem values are used as places for poles
bc=Part.BSplineCurve() bc.approximate(pts,DegMax=3,Tolerance=1) bc.setPeriodic()
Once again the scipy.signal.argrelextrema method is a good friend.
vps=np.array([bc.value(1.0/ct*i)+off for i in x]) y=np.array([sc.curvatureAt(1.0/ct*i) for i in x]) z=argrelextrema(y, np.greater) z2=argrelextrema(y, np.less) zc=np.concatenate([z,z2,[0,ct]]) zc.sort() exps=vps[zc] ps=[tuple(p) for p in exps] Draft.makeBSpline(ps,closed=True,face=False)
There is a parameter how fine the starting curve is inspected.
How to use:
import nurbswb; import nurbswb.simplecurve; nurbswb.simplecurve.run()
From the Draft BSpline to the Sketcher BSpline its not a big step. Take the poles over and create the helper stuff.
sk.addGeometry(Part.BSplineCurve(l,True),False) conList =  for i,p in enumerate(pts): conList.append(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint',i,3,k,i)) sk.addConstraint(conList)
How to use:
import nurbswb; import nurbswb.createsketchspline; nurbswb.createsketchspline.run()