— or how to fit B-spline onto contour surface
Happy New Year to everyone who reached this post! Today we are going to explore a mixed power of B-spline and neural network!
When plotting contour surface, have you, like me, done things like increasing PlotPoints so to have smooth looking surface and mesh lines, in the cost of bloomed up notebook size and/or less responsive Graphics3D result? If the answer is yes, then please do take a look at the approach of B-spline. As a nice function approximator, we may not need that much control points to have a applause B-spline if it’s just for meeting the eyes. And for RAM efficiency, we are going to do the computing with differentiable programming, through Wolfam Language’s neural network framework.

Helper functions

ClearAll[FindDivisionsExact]​​FindDivisionsExact[range:{_,_},n_Integer?(#>=2&)]:=Rescale[Range[n]//N//Rescale,{0,1},range]​​FindDivisionsExact[n_Integer?(#>=2&)]:=FindDivisionsExact[{0,1},n]
ClearAll[pipe,branch,branchSeq]​​pipe=RightComposition;​​branch=Through@*{##}&;​​branchSeq=pipe[branch@##,Apply@Sequence]&;

The contour surface model — a well-known example

For demonstration I’ll use one of my favourite example, the transcendental periodic implicit surface in ContourPlot3D’s documentation, constrained in a ball region:
ClearAll[,ℛ]​​=Function[{x,y,z},Cos[x]Sin[y]+Cos[y]Sin[z]+Cos[z]Sin[x]];​​ℛ=Ball[{0,0,0},π];
contourGraph=ContourPlot3D[​​[x,y,z]0,{x,y,z}∈ℛ​​,PlotPoints40,MaxRecursion0​​,MeshStyleGrayLevel[.7]​​,RegionBoundaryStyleNone,BoxedFalse,AxesFalse​​,Lighting"Accent",ContourStyleWhite]
We are going to find mesh lines (curves) on  with their terminals being randomly selected from the surface of ball ℛ:
ClearAll[bℛ]​​bℛ=And[​​[x,y,z]0​​,{x,y,z}∈RegionBoundary[ℛ]​​]//pipe[​​ImplicitRegion[#,{x,y,z}]&​​,DiscretizeRegion[#,BoxedTrue,AxesTrue]&​​]

The B-spline curve model

As we have talked about in past posts, a uniform B-spline curve is simply a linear combination of its control points with BSplineBasis as the weights:
C(t):=
n
∑
i=1
P
i
N
i,d
(t),t∈[0,1]
(
1
)
For our purpose here, we discretize the curve
C(t)
by a naïve uniform sample over
t∈[0,1]
, so it becomes:
C
c×s×d
:=
N
s×n
·
P
n×c×d
,
(
2
)
where
N
is the discrete B-spline basis matrix,
P
is the array of control points. The meaning of their dimensions are
Symbol
Meaning
Variable name
Typical value
c
number of curves
$curveNum
11
s
sample size
$sampleNum
100
d
embedded dimension
$embeddedDim
3
n
number of control points
$CPtsNum
5

Prototype for the curve

As we have shown, the curves are represented as
C=N·P
. But we can’t just throw a LinearLayer for it because we want the terminals predefined and fixed while keeping the “internal” control points trainable. So instead we separate them as
C
c×s×d
:=
N
s×n
·Join
P
startingterminal
1×c×d
,
P
middle
(n-2)×c×d
,
P
endingterminal
1×c×d

(
3
)
<<NeuralNetworks`
ClearAll[curvePrototype]​​curvePrototype=​​NetGraph[​​(*s*n*)"basis"PlaceholderLayer[InputPorts{},OutputPorts{MatrixT[]}]​​,"startCpts"ReplicateLayer[1],"endCpts"ReplicateLayer[1]​​,(*(n-2)*c*d*)"midCpts"NetArrayLayer["Output"TensorOfRankT[3]]​​,(*n*c*d*)"cpts"CatenateLayer[]​​,"curve"DotLayer[]​​,(*c*s*d*)"curveSet"TransposeLayer[]​​​​,{​​Thread[(NetPort/@{"start","end"}){"startCpts","endCpts"}]​​,{"startCpts","midCpts","endCpts"}"cpts"​​,{"basis","cpts"}"curve""curveSet"​​}//Flatten​​,(*c*d*)"start"MatrixT[]​​,(*c*d*)"end"MatrixT[]​​]
Replacing “basis” and “midCpts” layers with concrete NetArrayLayer will instantize the prototype:
Module[{$sampleNum=5,$CPtsNum=3,$curveNum=11,$embeddedDim=2,data,net}​​,net=NetReplacePart[curvePrototype​​,{​​"basis"NetArrayLayer["Array"RandomReal[1,{$sampleNum,$CPtsNum}]]​​,{"midCpts","Array"}RandomReal[1,{$CPtsNum-2,$curveNum,$embeddedDim}]​​}]​​;data=RandomReal[1,{2,$curveNum,$embeddedDim}]//AssociationThread[{"start","end"},#]&​​;data//pipe[​​net​​,MapThread[{#2,Line@#1}&,{#,#//Length//FindDivisionsExact//Map[ColorData@"Rainbow"]}]&​​,Graphics​​]​​]

B-spline basis

To specify a set of B-spline basis through BSplineBasis, we need to firstly decide how “smooth” we want the curves to be (the degree of the spline $bsDegree), and how capable of fitting wiggle shapes we want the curves to be (the number of control points for each spline $CPtsNum). Then the uniform knots can be determined unambiguously. To discretize the basis we need to specify the sample size $sampleNum. The basis set will then be generated and stored in an un-trainable NetArrayLayer:
ClearAll[basisNet]​​basisNet[​​$sampleNum_Integer?Positive​​,{$CPtsNum_Integer?Positive,$bsDegree_Integer?Positive,$fullKnot_List}​​]:=​​Table[BSplineBasis[{$bsDegree,$fullKnot},i-1,#],{i,$CPtsNum}]//​​pipe[​​Map@Function,Apply@branch,Map​​,branchSeq[Identity,FindDivisionsExact[$sampleNum]&]​​,Construct​​,NetArrayLayer[​​"Array"#​​,LearningRateMultipliersNone​​]&​​]

Instance of curves

Replacing “basis” layer with basisNet will instantize a group of B-spline curves:

The contour surface as a neural network

Solve the fitting problem with neural network optimizer

Generally, higher spline degree and more control points result in better fitting:
Mean fit-goodness per point:
We can see the result indeed fits well to the contour surface:

It takes two splines to make a ribbon

Last section gives us a convenient way to fit BSpline curve onto contour surface. And one pair of such curves with sufficient close shape will make a nice ribbon as tensor product BSplineSurface. (Please refer to my last community post.)
Then we combine both set into single one suitable for previous neural net:

Visualizations

As we mentioned before, ribbons are visualized as BSplineSurface:
Playing with the different shadings and Lighting etc. gives all kinds of interesting visualizations:
Or finner ribbons for StippleShading or HalftoneShading:

Some left questions

◼
  • Why neural network instead of old fashion FindFit and friends?
  • Traditional way (formula + fit) is theoretically doable and sometimes faster, but neural network provides an alternative way to represent the computing process (i.e. the formula), with much less RAM requirement
    ◼
  • What about geodesic on curved surface? Can we fitting them with similar method?
  • Well, for contour surface it becomes a constrained optimization problem which can be tricky to train. But for parametrized surface it can be done with reasonable performance.