Sonntag, 17. August 2014

Adding NURBS capabilities to Python

Python is a great programming language for engineers. Especially with the Numpy, Scipy and Matplotlib packages it comes close to what Matlab offers. But when it comes to free form surfaces, you often find yourself locked in a closed software environment system.
This post shows, how to get some NURBS editing capabilities in Python by making use of a mature and sophisticated, yet open source NURBS library called SISL.
Basically, the SISL library lets you do everything you would expect from a CAD system. I was especially interested in:
  • find intersection of curves/surfaces
  • find closest points on curve/surface
  • calculate curvature and derivatives
The full documentation is available here.

The following image shows a B-Spline curve with the control points and curvature. Three closest points to the curve are evaluated. 
B-Spline with curvature and closest points on curve

Roughtly the necessary steps can be described as:
  1. compile SISL library 
  2. since SISL compiles into a .lib (windows file associations are assumed here) static libary, write wrapper to pack needed functions into a .dll dynamic library
  3. load .dll into Python, access it with help of cTypes
  4. write wrapper/class in Python to easily access SISL functions

Get and compile SISL library 

The library can be downloaded from here. There is available the so called GoTools, which is a huge software package, but we only need SISL. As a result you get the sisl.lib file.

Pack needed functions into .dll dynamic library

Since in Python, as far as I know, the standard way to access libraries is via a .dll dynamic library, we need to write one that exposes the needed functions.
This minimal example creates a SISL curve and calculates the closest points to the curve.
A more general description about how to compile a .dll dynamic library to access from Python is given by the Scipy Cookbook. The following code is mostly taken from the Cookbook and adapted accordingly. For more information about how to call functions from the SISL library, see the SISL manual.

SISLlib.cpp:
 #include <stdio.h>  
 #include <iostream>  
 #include <fstream>  
 #include <string>  
 #include <stdexcept>  
 #include <cstdlib>  
 #include "sisl.h"  
 using namespace std;  
 #ifdef SISLlib_DLL  
 #ifdef SISLlib_EXPORTS  
 #define SISLlib_API __declspec(dllexport)  
 #else  
 #define SISLlib_API __declspec(dllimport)  
 #endif /* SISLlib_EXPORTS */  
 #else  
 #define SISLlib_API extern /* XXX confirm this */  
 #endif /* SISLlib_DLL */  
 #ifdef __cplusplus  
 extern "C" {  
 #endif  
 extern SISLlib_API int SISLclosestPointToCurve(int kind, double *knots1, double *coef1, int number1, int order1, int dim1, double *PointsIn, int nPointsIn, double *pointparout) {  
   /* BSpline Kurve erzeugen */  
   SISLCurve* curve1 = newCurve(number1, // number of control points  
       order1, // order of spline curve (degree + 1)  
       knots1, // pointer to knot vector (parametrization)  
       coef1,  // pointer to coefficient vector (control points)  
       kind,   // kind, 1 = polynomial B-spline curve, 2 = Rational B-spline (nurbs) curve  
       dim1,   // dimension  
       1);   // 0=no copying of information, 'borrow' arrays  
   if (!curve1) {  
     throw runtime_error("Error occured while generating curve1.");  
   }  
      /* search closest points to surface */  
   double epsco = 1.0e-8; /*computational resolution*/  
   double epsge = 1.0e-8;  
   double point[2];  
   int numintpt;  
   double *intpar;  
   int numintcu=0;  
   SISLIntcurve **intcurve=NULL;  
   int jstat;  
      for (int i = 0; i < nPointsIn; i++){  
     point[0]=PointsIn[0+i*2];  
     point[1]=PointsIn[1+i*2];  
     s1953(curve1, point, dim1, epsco, epsge, &numintpt, &intpar, &numintcu, &intcurve, &jstat);  
     if (jstat < 0){  
       throw runtime_error("Error occured inside call to SISL routine s1953.");  
     } else if (jstat > 0){  
       cerr << "WARNING: warning occured inside call to SISL routine s1953.\n" << endl;  
     }  
     if(numintpt >= 1){  
     *(pointparout+i) = intpar[0];  
     }  
   } 
 
   // cleaning up  
   freeCurve(curve1);  
   freeIntcrvlist(intcurve,numintcu);  

   return 0;  
 }  

To compile, use e.g. this makefile, adjust the paths (libpath) accordingly:
 CXX = cl.exe  
 LINK = link.exe  
 CPPFLAGS = -D_WIN32 -D_USRDLL -DSISLlib_DLL -DSISLlib_EXPORTS -Iinclude -nologo -GS -W3 -Wp64 -MD -O2 -EHsc  
 CXXFLAGSALL = -EHsc -nologo -GS -W3 -Wp64 $(CPPFLAGS)  
 CXXFLAGSDBG = -MDd -Od -Z7 -RTCcsu  
 CXXFLAGSOPT = -MD -O2  
 #CXXFLAGS = $(CXXFLAGSALL) $(CXXFLAGSDBG)  
 CXXFLAGS = $(CXXFLAGSALL) $(CXXFLAGSOPT)  
 LINKFLAGSALL = /nologo /DLL /libpath:lib sisl.lib  
 LINKFLAGSDBG = /DEBUG  
 LINKFLAGSOPT =   
 #LINKFLAGS = $(LINKFLAGSALL) $(LINKFLAGSDBG)  
 LINKFLAGS = $(LINKFLAGSALL) $(LINKFLAGSOPT)  
 all: SISLlib.dll  
 SISLlib.dll: SISLlib.obj  
   $(LINK) $(LINKFLAGS) SISLlib.obj /OUT:SISLlib.dll  
 svm.obj: svm.cpp svm.h  
   $(CXX) $(CXXFLAGS) -c SISLlib.cpp  
 clean:  
   -erase /Q *.obj *.dll *.exp *.lib  

With Visual Studio this compiles to SISLlib.dll with the nmake command from the Visual Studio Command Prompt. The SISLlib.dll can then be loaded into Python.

Load SISLlib.dll into Python

When the .dll compiles correctly the hardest work is already done. To load the library into Python following command can be used:
 import numpy as np  
 SISLlib_dll = np.ctypeslib.load_library("SISLlib",r"pathToLibrary\SISLlib")  

Python Class to easily access SISL functions

In Python it is convenient to write a Curve class, which is the interface to the SISL functions.
When a curve object is initialized, it takes as arguments the knot vector, the control points, information about the order of the curve and possibly weights of the coefficients (needed if curve should be a NURBS). It must declare, which data types are expected by a function in the SISLlib.dll as input and what is the ouput data type. It also should provide a Pythonic way to call functions to e.g. calculate the closest point to the curve.
 # create SISL Curve Object  
 curve = SISLcurve(knots,coefs,order,weights)  
 # get closest points to curve   
 parOut = curve.SISLclosestPointToCurve(points)  

This is a proposal of how a curve class could look like.
pySISL.py:
 from __future__ import division  
 import ctypes   
 import numpy as np  
 import matplotlib.pyplot as plt  
 class SISLcurve():  
   def __init__(self, knots, coefs, order, weights=[]):  
     if weights == []:  
       self.kind = 1 # polynomial B-Spline  
       self.weights = weights  
     else:  
       self.kind = 2 # rational B-Spline (NURBS)  
       self.weights = weights.astype("double")  
     self.knots = knots.astype("double")  
     self.coefs = coefs.astype("double")  
     self.order = order  
     self.dim = coefs.shape[0]  
     # import SISL libaray  
     self.SISLlib_dll = np.ctypeslib.load_library("SISLlib",r"G:\ProgrammierProjekte\SISL\SISLlib")  
     # declare SISLclosestPointToCurve function  
     self.SISLlib_dll.SISLclosestPointToCurve.restype = ctypes.c_int  
     self.SISLlib_dll.SISLclosestPointToCurve.argtypes = [ctypes.c_int,           #kind 1: polynomial B-Spline, 2: rational B-Spline (NURBS)  
                         ctypes.POINTER(ctypes.c_double),        #knots  
                         ctypes.POINTER(ctypes.c_double),        #coefs  
                         ctypes.c_int, ctypes.c_int, ctypes.c_int,    #nCoefs, order, dim  
                         ctypes.POINTER(ctypes.c_double),        #points  
                         ctypes.c_int,                  #nPoints  
                         ctypes.POINTER(ctypes.c_double)]        #pointsParOut  
   def prepareSISLdata(self):  
     kind = self.kind  
     knots = self.knots  
     number = self.coefs.shape[1]  
     coefs = self.coefs.reshape((self.coefs.size),order="F")  
     order = self.order  
     dim = self.dim  
     # if curve is rational, coefs need to be multiplied by weight  
     if self.weights != []:  
       weights = self.weights  
       tmp_w = np.tile(weights,(dim,1))  
       tmp_c = self.coefs * tmp_w  
       tmp_cw = np.vstack((tmp_c,weights))  
       coefs = tmp_cw.reshape((tmp_cw.size),order="F")  
     return kind, knots, number, coefs, order, dim  
   def SISLclosestPointToCurve(self, points):  
     kind, knots, number, coefs, order, dim = self.prepareSISLdata()  
     nPoints = points.shape[1]  
     points = points.reshape((points.size),order="F")  
     points = points.astype("double")  
     pointsParOut = np.zeros(nPoints)  
     self.SISLlib_dll.SISLclosestPointToCurve(kind,  
                          knots.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),   
                          coefs.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),   
                          number, order, dim,  
                          points.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),  
                          nPoints,  
                          pointsParOut.ctypes.data_as(ctypes.POINTER(ctypes.c_double)))  
     return pointsParOut  
 if __name__ == "__main__":  
   # Data for 2D Spline  
   knots = np.array([0,0,0,1,1,2,2,3,3,4,4,4])  
   coefs_x = np.array([0,1,1,1,0,-1,-1,-1,0])  
   coefs_y = np.array([-1,-1,0,1,1,1,0,-1,-1])  
   coefs_z = np.array([0,0,0,0,0,0,0,0,0])  
   coefs = np.vstack((coefs_x,coefs_y,coefs_z))  
   weights = np.array([1,(2**0.5)/2,1,(2**0.5)/2,1,(2**0.5)/2,1,(2**0.5)/2,1])  
   order = 3  
   # Points  
   points_x = np.array([0.950000000000000,0.948365660353047,0.946681081750349,0.944627123146684,0.942188773156426,0.939399032892644,0.936232939571863,0.932690108317814,0.928770254558779,0.924462521735862,0.919770399050986,0.914760631089697,0.909376263946973,0.903585812191018,0.897468630037332,0.891015499686028,0.884148637040933,0.876982392633972,0.869461355406687,0.861554859384639,0.853377314168268,0.844768886370890,0.835927093263629,0.826659503750130,0.817152570478120,0.807224602039578,0.797065796079186,0.786515110030005,0.775693455728189,0.764537838907564,0.753050219497601,0.741289980627127,0.729171580374359,0.716773946925715,0.704059298956755,0.690978248179852,0.677612415560273,0.663917247813943,0.649864255028625,0.635503203034109,0.620821802447561,0.605786959796453,0.590392566215039,0.574694342989329,0.558630565252025,0.542185206713670,0.525373448459848,0.508204975104239,0.490632813730356,0.472635874021836,0.454236471406481,0.435439193363185,0.416218675261615,0.396570222858047,0.376542216747913,0.356146885282818,0.335382062721527,0.314254523947087,0.292855861004967,0.271191942243154,0.249289199749707,0.227213115084334,0.205041874526635,0.182805870132726,0.160565960001584,0.138438450747921,0.116457821240756,0.0947018141041910,0.0732783327465884,0.0522156119082409,0.0316246164284243,0.0115659850707168,-0.00791202104385392,-0.0267042000226111,-0.0448139470270519,-0.0621263608066405,-0.0786633164090686,-0.0943337919206955,-0.109168987800449,-0.123103860278432,-0.136168875212454,-0.148344178963388,-0.159644690042500,-0.170098972799865,-0.179716486713481,-0.188530723275019,-0.196584499430869,-0.203908474072773,-0.210535813047408,-0.216518891589666,-0.221902400415273,-0.226721422700915,-0.231011507186882,-0.234811457180879,-0.238156960106410,-0.241084055321751,-0.243621812078548,-0.245789804776811,-0.247616108931611,-0.249497141016798])  
   points_y = np.array([-0.0994321452870676,-0.0994226258843406,-0.0994011421318266,-0.0993638635104812,-0.0993061039921876,-0.0992191204394156,-0.0991094962412971,-0.0989758995773281,-0.0988172225349698,-0.0986324951964819,-0.0984017275113659,-0.0981271747430788,-0.0978224338994053,-0.0974815622227530,-0.0970794433470467,-0.0966373768472060,-0.0961540453186260,-0.0956029034674430,-0.0950148959841584,-0.0943597983531194,-0.0936499939404355,-0.0928880723801365,-0.0920550818411189,-0.0911715420258190,-0.0902160232481631,-0.0892055634568082,-0.0881255694188480,-0.0869769598211418,-0.0857658423138637,-0.0844835330960565,-0.0831380138003898,-0.0817249600103063,-0.0802376046712566,-0.0786796175308041,-0.0770530700400894,-0.0753426682406730,-0.0735450754025746,-0.0716739843232092,-0.0697241104204034,-0.0676828864957862,-0.0655604886165625,-0.0633668842861504,-0.0610870449049815,-0.0587123402033290,-0.0562669315097688,-0.0537535218622797,-0.0511523935680138,-0.0484660107488078,-0.0457142971790514,-0.0429012017883088,-0.0400093517599968,-0.0370374806273727,-0.0340077065131031,-0.0309256570274795,-0.0277765350027134,-0.0245578071914544,-0.0212958999960175,-0.0179995362192438,-0.0146458041074342,-0.0112460425963343,-0.00782424211239759,-0.00437524956478380,-0.000893563813683097,0.00260435993597310,0.00610447748869498,0.00962021509300744,0.0131358483933153,0.0166339093545291,0.0201111949029309,0.0235539076034093,0.0269612433102411,0.0303173926105221,0.0336211753342074,0.0368681605591590,0.0400406671704278,0.0431536061729911,0.0461705095054608,0.0491158099116442,0.0519509427343211,0.0547034649982273,0.0573375222960913,0.0598641536018524,0.0622741490265492,0.0645495916327474,0.0667100720625331,0.0687237608042033,0.0705943638721636,0.0723321403710434,0.0739092963536412,0.0753254443305403,0.0765908300800888,0.0776974052822079,0.0786305979877449,0.0793888141372688,0.0799705067334637,0.0803762588498084,0.0806083507407432,0.0806585219718005,0.0805152222178871,0.0800000000000000])  
   points = np.vstack((points_x,points_y))  
   # create SISL Curve Object  
   curve = SISLcurve(knots,coefs,order,weights)  
   # get closest points to curve   
   parOut = curve.SISLclosestPointToCurve(points)  

Now to actually see whats going on we need to calculate the points in euclidean space from the parameters in parOut. This can be done using SISL function s1542 (evaluate points on curve by parameter) but that would already go beyond this rough guide. The result however could look something like that:
Circle described as NURBS curve, red points on the curve are the closest points on the circle to the green points in the middle



Keine Kommentare:

Kommentar veröffentlichen