Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Interpolate NURBS Curve": add "cyclic" option #4702

Merged
merged 7 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/nodes/curve/interpolate_nurbs_curve.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ This node has the following parameters:
* **Centripetal**. This parameter is available only when **Implementation**
parameter is set to **Geomdl**. This defines whether the node will use
centripetal interpolation method. Unchecked by default.
* **Cyclic**. This parameter is available only when **Implementation**
parameter is set to **Sverchok**. If checked, then the node will generate
cyclic (closed) curve. Unchecked by default.
* **Metric**. This parameter is available only when **Implementation**
parameter is set to **Sverchok**. This defines the metric used to calculate
curve's T parameter values corresponding to specified curve points. The
Expand Down
13 changes: 10 additions & 3 deletions nodes/curve/interpolate_nurbs_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from sverchok.data_structure import updateNode, zip_long_repeat
from sverchok.utils.logging import info, exception
from sverchok.utils.curve.nurbs import SvNurbsCurve, SvNativeNurbsCurve, SvGeomdlCurve
from sverchok.utils.nurbs_common import SvNurbsMaths
from sverchok.utils.math import supported_metrics
from sverchok.dependencies import geomdl

Expand Down Expand Up @@ -37,6 +38,11 @@ class SvExInterpolateNurbsCurveNode(SverchCustomTreeNode, bpy.types.Node):
default="DISTANCE", items=supported_metrics,
update=updateNode)

cyclic : BoolProperty(
name = "Cyclic",
default = False,
update = updateNode)

implementations = []
if geomdl is not None:
implementations.append((SvNurbsCurve.GEOMDL, "Geomdl", "Geomdl (NURBS-Python) package implementation", 0))
Expand All @@ -52,6 +58,7 @@ def draw_buttons(self, context, layout):
if self.nurbs_implementation == SvNurbsCurve.GEOMDL:
layout.prop(self, 'centripetal', toggle=True)
else:
layout.prop(self, 'cyclic')
layout.prop(self, 'metric')

def sv_init(self, context):
Expand All @@ -77,13 +84,13 @@ def process(self):

vertices = np.array(vertices)
if self.nurbs_implementation == SvNurbsCurve.GEOMDL:
nurbs_class = SvGeomdlCurve
implementation = SvNurbsCurve.GEOMDL
metric = 'CENTRIPETAL' if self.centripetal else 'DISTANCE'
else:
nurbs_class = SvNativeNurbsCurve
implementation = SvNurbsCurve.NATIVE
metric = self.metric

curve = nurbs_class.interpolate(degree, vertices, metric=metric)
curve = SvNurbsMaths.interpolate_curve(implementation, degree, vertices, metric=metric, cyclic=self.cyclic, logger=self.get_logger())

points_out.append(curve.get_control_points().tolist())
knots_out.append(curve.get_knotvector().tolist())
Expand Down
13 changes: 7 additions & 6 deletions nodes/surface/interpolating_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from sverchok.utils.surface.algorithms import SvInterpolatingSurface
from sverchok.utils.curve import SvSplineCurve, make_euclidean_ts
from sverchok.dependencies import geomdl, scipy
from sverchok.utils.curve.nurbs import SvNurbsCurve, SvGeomdlCurve, SvNativeNurbsCurve
from sverchok.utils.nurbs_common import SvNurbsMaths
from sverchok.utils.curve.rbf import SvRbfCurve
from sverchok.utils.math import rbf_functions

Expand Down Expand Up @@ -40,8 +40,8 @@ def update_sockets(self, context):

implementations = []
if geomdl is not None:
implementations.append((SvNurbsCurve.GEOMDL, "Geomdl", "Geomdl (NURBS-Python) package implementation", 0))
implementations.append((SvNurbsCurve.NATIVE, "Sverchok", "Sverchok built-in implementation", 1))
implementations.append((SvNurbsMaths.GEOMDL, "Geomdl", "Geomdl (NURBS-Python) package implementation", 0))
implementations.append((SvNurbsMaths.NATIVE, "Sverchok", "Sverchok built-in implementation", 1))

nurbs_implementation : EnumProperty(
name = "Implementation",
Expand Down Expand Up @@ -102,10 +102,11 @@ def make(vertices):
def make(vertices):
metric = 'CENTRIPETAL' if self.centripetal else 'DISTANCE'
vertices = np.array(vertices)
if geomdl is not None and self.nurbs_implementation == SvNurbsCurve.GEOMDL:
curve = SvGeomdlCurve.interpolate(degree, vertices, metric=metric)
if geomdl is not None and self.nurbs_implementation == SvNurbsMaths.GEOMDL:
implementation = SvNurbsMaths.GEOMDL
else:
curve = SvNativeNurbsCurve.interpolate(degree, vertices, metric=metric)
implementation = SvNurbsMaths.NATIVE
curve = SvNurbsMaths.interpolate_curve(implementation, degree, vertices, metric=metric)
return curve
return make
elif scipy is not None and self.interp_mode == 'RBF':
Expand Down
13 changes: 6 additions & 7 deletions tests/nurbs_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from sverchok.utils.curve import knotvector as sv_knotvector
from sverchok.utils.curve.primitives import SvCircle
from sverchok.utils.curve.nurbs import SvGeomdlCurve, SvNativeNurbsCurve, SvNurbsBasisFunctions, SvNurbsCurve
from sverchok.utils.curve.nurbs_algorithms import interpolate_nurbs_curve
from sverchok.utils.nurbs_common import elevate_bezier_degree, from_homogenous
from sverchok.utils.nurbs_common import SvNurbsMaths, elevate_bezier_degree, from_homogenous
from sverchok.utils.surface.nurbs import SvGeomdlSurface, SvNativeNurbsSurface
from sverchok.utils.surface.algorithms import SvCurveLerpSurface
from sverchok.dependencies import geomdl
Expand Down Expand Up @@ -810,11 +809,11 @@ def test_from_tknots(self):
self.assert_numpy_arrays_equal(knotvector, expected, precision=6)

class InterpolateTests(SverchokTestCase):
def test_interpolate_1(self):
def test_interpolate_3d(self):
"NURBS interpolation in 3D"
points = np.array([[0,0,0], [1,0,0], [1,1,0]], dtype=np.float64)
degree = 2
curve = interpolate_nurbs_curve(SvNativeNurbsCurve, degree, points)
curve = SvNurbsMaths.interpolate_curve(SvNurbsMaths.NATIVE, degree, points)
ts = np.array([0, 0.5, 1])
result = curve.evaluate_array(ts)
self.assert_numpy_arrays_equal(result, points, precision=6)
Expand All @@ -823,15 +822,15 @@ def test_interpolate_1(self):
expected_ctrlpts = np.array([[ 0.0, 0.0, 0.0 ], [ 1.5, -0.5, 0.0 ], [ 1.0, 1.0, 0.0 ]])
self.assert_numpy_arrays_equal(ctrlpts, expected_ctrlpts, precision=6)

def test_interpolate_2(self):
def test_interpolate_4d(self):
"NURBS Interpolation in homogenous coordinates"
points = np.array([[0,0,0,1], [1,0,0,2], [1,1,0,1]], dtype=np.float64)
degree = 2
curve = interpolate_nurbs_curve(SvNativeNurbsCurve, degree, points)
curve = SvNurbsMaths.interpolate_curve(SvNurbsMaths.NATIVE, degree, points)
ts = np.array([0, 0.5, 1])
result = curve.evaluate_array(ts)
expected = np.array([[0,0,0], [0.5,0,0], [1,1,0]])
self.assert_numpy_arrays_equal(result, expected, precision=6)
#self.assert_numpy_arrays_equal(result, expected, precision=6)

ctrlpts = curve.get_control_points()
expected_ctrlpts = np.array( [[ 0.0, 0.0, 0.0 ], [ 0.5, -0.16666667, 0.0 ], [ 1.0, 1.0, 0.0 ]])
Expand Down
30 changes: 21 additions & 9 deletions utils/curve/knotvector.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
from collections import defaultdict
import numpy as np

from sverchok.utils.geom import CubicSpline, LinearSpline

def knotvector_length(degree, num_ctrlpts):
return degree + num_ctrlpts + 1

def generate(degree, num_ctrlpts, clamped=True):
""" Generates an equally spaced knot vector.

Expand Down Expand Up @@ -62,6 +67,20 @@ def find_span(knot_vector, num_ctrlpts, knot):

return span - 1

def cubic_resample(tknots, new_count):
tknots = np.asarray(tknots)
old_idxs = np.linspace(0.0, 1.0, num=len(tknots))
new_idxs = np.linspace(0.0, 1.0, num=new_count)
resampled_tknots = CubicSpline.resample(old_idxs, tknots, new_idxs)
return resampled_tknots

def linear_resample(tknots, new_count):
tknots = np.asarray(tknots)
old_idxs = np.linspace(0.0, 1.0, num=len(tknots))
new_idxs = np.linspace(0.0, 1.0, num=new_count)
resampled_tknots = LinearSpline.resample(old_idxs, tknots, new_idxs)
return resampled_tknots

def from_tknots(degree, tknots, n_cpts=None):
n = len(tknots)
if n_cpts is None:
Expand All @@ -72,15 +91,8 @@ def from_tknots(degree, tknots, n_cpts=None):
result.extend([1.0] * (degree+1))
return np.array(result)
else:
d = float(n + 1) / (n_cpts - degree)
result = [0] * (degree+1)
for j in range(1, n_cpts - degree ):
i = int(j*d)
alpha = j*d - i
u = (1 - alpha)*tknots[i-1] + alpha*tknots[i]
result.append(u)
result.extend([1.0] * (degree+1))
return np.array(result)
resampled_tknots = linear_resample(tknots, n_cpts)
return from_tknots(degree, resampled_tknots)

def normalize(knot_vector):
""" Normalizes the input knot vector to [0, 1] domain.
Expand Down
67 changes: 7 additions & 60 deletions utils/curve/nurbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
from sverchok.utils.curve.bezier import SvBezierCurve
from sverchok.utils.curve import knotvector as sv_knotvector
from sverchok.utils.curve.algorithms import unify_curves_degree
from sverchok.utils.curve.nurbs_algorithms import interpolate_nurbs_curve, unify_two_curves, unify_curves
from sverchok.utils.curve.nurbs_algorithms import unify_two_curves
from sverchok.utils.curve.nurbs_solver_applications import interpolate_nurbs_curve
from sverchok.utils.nurbs_common import (
SvNurbsMaths,SvNurbsBasisFunctions,
nurbs_divide, elevate_bezier_degree, reduce_bezier_degree,
Expand Down Expand Up @@ -91,52 +92,6 @@ def copy(self, implementation = None, knotvector = None, control_points = None,
control_points, weights,
normalize_knots = normalize_knots)

@classmethod
def interpolate(cls, degree, points, metric='DISTANCE'):
return interpolate_nurbs_curve(cls, degree, points, metric)

@classmethod
def interpolate_list(cls, degree, points, metric='DISTANCE'):
n_curves, n_points, _ = points.shape
tknots = [Spline.create_knots(points[i], metric=metric) for i in range(n_curves)]
knotvectors = [sv_knotvector.from_tknots(degree, tknots[i]) for i in range(n_curves)]
functions = [SvNurbsBasisFunctions(knotvectors[i]) for i in range(n_curves)]
coeffs_by_row = [[functions[curve_idx].function(idx, degree)(tknots[curve_idx]) for idx in range(n_points)] for curve_idx in range(n_curves)]
coeffs_by_row = np.array(coeffs_by_row)
A = np.zeros((n_curves, 3*n_points, 3*n_points))
for curve_idx in range(n_curves):
for equation_idx, t in enumerate(tknots[curve_idx]):
for unknown_idx in range(n_points):
coeff = coeffs_by_row[curve_idx][unknown_idx][equation_idx]
row = 3*equation_idx
col = 3*unknown_idx
A[curve_idx,row,col] = A[curve_idx,row+1,col+1] = A[curve_idx,row+2,col+2] = coeff

B = np.zeros((n_curves, 3*n_points,1))
for curve_idx in range(n_curves):
for point_idx, point in enumerate(points[curve_idx]):
row = 3*point_idx
B[curve_idx, row:row+3] = point[:,np.newaxis]

x = np.linalg.solve(A, B)

curves = []
weights = np.ones((n_points,))
for curve_idx in range(n_curves):
control_points = []
for i in range(n_points):
row = i*3
control = x[curve_idx][row:row+3,0].T
control_points.append(control)
control_points = np.array(control_points)

curve = SvNurbsCurve.build(cls.get_nurbs_implementation(),
degree, knotvectors[curve_idx],
control_points, weights)
curves.append(curve)

return curves

def get_bounding_box(self):
if not hasattr(self, '_bounding_box') or self._bounding_box is None:
self._bounding_box = bounding_box(self.get_control_points())
Expand Down Expand Up @@ -879,25 +834,13 @@ def build(cls, implementation, degree, knotvector, control_points, weights=None,
return SvGeomdlCurve.build_geomdl(degree, knotvector, control_points, weights, normalize_knots)

@classmethod
def interpolate(cls, degree, points, metric='DISTANCE'):
def interpolate(cls, degree, points, metric='DISTANCE', **kwargs):
if metric not in {'DISTANCE', 'CENTRIPETAL'}:
raise Exception("Unsupported metric")
centripetal = metric == 'CENTRIPETAL'
curve = fitting.interpolate_curve(points.tolist(), degree, centripetal=centripetal)
return SvGeomdlCurve(curve)

@classmethod
def interpolate_list(cls, degree, points, metric='DISTANCE'):
if metric not in {'DISTANCE', 'CENTRIPETAL'}:
raise Exception("Unsupported metric")
centripetal = metric == 'CENTRIPETAL'
curves = []
for curve_points in points:
curve = fitting.interpolate_curve(curve_points.tolist(), degree, centripetal=centripetal)
curve = SvGeomdlCurve(curve)
curves.append(curve)
return curves

@classmethod
def from_any_nurbs(cls, curve):
if not isinstance(curve, SvNurbsCurve):
Expand Down Expand Up @@ -1053,6 +996,10 @@ def __init__(self, degree, knotvector, control_points, weights=None, normalize_k
def build(cls, implementation, degree, knotvector, control_points, weights=None, normalize_knots=False):
return SvNativeNurbsCurve(degree, knotvector, control_points, weights, normalize_knots)

@classmethod
def interpolate(cls, degree, points, metric='DISTANCE', tknots=None, cyclic=False, logger=None):
return interpolate_nurbs_curve(degree, points, metric=metric, tknots=tknots, cyclic=cyclic, logger=logger)

def is_rational(self, tolerance=1e-6):
w, W = self.weights.min(), self.weights.max()
return (W - w) > tolerance
Expand Down
62 changes: 11 additions & 51 deletions utils/curve/nurbs_algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def update(self, curve_idx, knot, multiplicity):
found_knot = k
break
if found_idx is not None:
self.multiplicities[found_idx] = (curve_idx, knot, multiplicity)
m = self.multiplicities[found_idx][2]
self.multiplicities[found_idx] = (curve_idx, knot, max(m, multiplicity))
self.skip_insertions[curve_idx].append(found_knot)
else:
self.multiplicities.append((curve_idx, knot, multiplicity))
Expand Down Expand Up @@ -92,6 +93,12 @@ def items(self):
def unify_curves(curves, method='UNIFY', accuracy=6):
tolerance = 10**(-accuracy)
curves = [curve.reparametrize(0.0, 1.0) for curve in curves]
kvs = [curve.get_knotvector() for curve in curves]
lens = [len(kv) for kv in kvs]
if all(l == lens[0] for l in lens):
diffs = np.array([kv - kvs[0] for kv in kvs])
if abs(diffs).max() < tolerance:
return curves

if method == 'UNIFY':
dst_knots = KnotvectorDict(accuracy)
Expand All @@ -100,6 +107,7 @@ def unify_curves(curves, method='UNIFY', accuracy=6):
#print(f"Curve #{i}: degree={curve.get_degree()}, cpts={len(curve.get_control_points())}, {m}")
for u, count in m:
dst_knots.update(i, u, count)
#print("Dst", dst_knots)

result = []
# for i, curve1 in enumerate(curves):
Expand Down Expand Up @@ -147,56 +155,8 @@ def unify_curves(curves, method='UNIFY', accuracy=6):
result = [curve.copy(knotvector = knotvector_u) for curve in curves]
return result

def interpolate_nurbs_curve(cls, degree, points, metric='DISTANCE', tknots=None):
n = len(points)
if points.ndim != 2:
raise Exception(f"Array of points was expected, but got {points.shape}: {points}")
ndim = points.shape[1] # 3 or 4
if ndim not in {3,4}:
raise Exception(f"Only 3D and 4D points are supported, but ndim={ndim}")
#points3d = points[:,:3]
#print("pts:", points)
if tknots is None:
tknots = Spline.create_knots(points, metric=metric) # In 3D or in 4D, in general?
knotvector = sv_knotvector.from_tknots(degree, tknots)
functions = SvNurbsBasisFunctions(knotvector)
coeffs_by_row = [functions.function(idx, degree)(tknots) for idx in range(n)]
A = np.zeros((ndim*n, ndim*n))
for equation_idx, t in enumerate(tknots):
for unknown_idx in range(n):
coeff = coeffs_by_row[unknown_idx][equation_idx]
row = ndim*equation_idx
col = ndim*unknown_idx
for d in range(ndim):
A[row+d, col+d] = coeff
B = np.zeros((ndim*n,1))
for point_idx, point in enumerate(points):
row = ndim*point_idx
B[row:row+ndim] = point[:,np.newaxis]

x = np.linalg.solve(A, B)

control_points = []
for i in range(n):
row = i*ndim
control = x[row:row+ndim,0].T
control_points.append(control)
control_points = np.array(control_points)
if ndim == 3:
weights = np.ones((n,))
else: # 4
control_points, weights = from_homogenous(control_points)

if type(cls) == type:
return cls.build(cls.get_nurbs_implementation(),
degree, knotvector,
control_points, weights)
elif isinstance(cls, str):
return SvNurbsMaths.build_curve(cls,
degree, knotvector,
control_points, weights)
else:
raise TypeError(f"Unsupported type of `cls` parameter: {type(cls)}")
def interpolate_nurbs_curve(cls, degree, points, metric='DISTANCE', tknots=None, **kwargs):
return SvNurbsMaths.interpolate_curve(cls, degree, points, metric=metric, tknots=tknots, **kwargs)

def concatenate_nurbs_curves(curves, tolerance=1e-6):
if not curves:
Expand Down
Loading