Не подтверждена Коммит 6dbe477e создал по автору Ilya V. Portnov's avatar Ilya V. Portnov Зафиксировано автором GitHub
Просмотр файлов

Merge pull request #3755 from nortikin/splprep_node

"Approximate Nurbs Curve" Mk2
владельцы 415efa52 56c91c7a
......@@ -4,9 +4,10 @@ Approximate NURBS Curve
Dependencies
------------
This node requires Geomdl_ library to work.
This node requires either Geomdl_ or SciPy_ library to work.
.. _Geomdl: https://onurraufbingol.com/NURBS-Python/
.. _SciPy: https://scipy.org/
Functionality
-------------
......@@ -17,6 +18,22 @@ points, i.e. goes as close to them as possible while remaining a smooth curve.
In fact, the generated curve always will be a non-rational curve, which means
that all weights will be equal to 1.
This node supports two implementations of curve approximation algorithm.
Different implementation give you different ways of controlling them:
* Geomdl_ implementation can either define the number of control points of
generated curve automatically, or you can provide it with desired number of
control points. This implementation supports only two metrics - euclidean and
centripetal. This implementation can not generate cyclic (closed) curves.
* SciPy_ implementation allows you to define "smoothing" parameter, to define
how smooth you want the curve to be. By default, it selects the smoothing
factor automatically. If you explicitly set the smoothing factor to zero, the
curve will go exactly through all provided points, i.e. the node will perform
interpolation instead of approximation. This implementation supports wider
selection of metrics. This implementation can make cyclic (closed) curves.
Additionally, when smoothing factor is not zero, you can provide different
weights for different points.
.. _NURBS: https://en.wikipedia.org/wiki/Non-uniform_rational_B-spline
Inputs
......@@ -25,21 +42,64 @@ Inputs
This node has the following inputs:
* **Vertices**. The points to be approximated. This input is mandatory.
* **Weights**. This input is available only when **Implementation** parameter
is set to **SciPy**. Weights of points to be approximated. Bigger values of
weight mean that the curve will pass through corresponding point at the
smaller distance. This does not have sense if **Smoothing** input is set to
zero. Optional input. If not connected, the node will consider weights of all
points as equal.
* **Degree**. Degree of the curve to be built. Default value is 3. Most useful values are 3, 5 and 7.
* **PointsCnt**. Number of curve's control points. This input is available only
when **Specify points count** parameter is checked. Default value is 5.
when **Implementation** parameter is set to **Geomdl**, and **Specify points
count** parameter is checked. Default value is 5.
* **Smoothing**. This input is available only when **Implementation** parameter
is set to **SciPy**, and **Scpecify smoothing** parameter is checked.
Smoothing factor. Bigger values will make more smooth curves. Value of 0
(zero) mean that the curve will exactly pass through all points. The default
value is 0.1.
Parameters
----------
This node has the following parameters:
* **Centripetal**. This defines whether the node will use centripetal
approximation method. Unchecked by default.
* **Specify points count**. If checked, then it will be possible to specify the
number of curve's control points in **PointsCnt** input. Otherwise, the node
will determine required number of control points by itself (this number can
be too big for many applications).
* **Implementation**. Approximation algorithm implementation to be used. The available values are:
* **Geomdl**. Use the implementation from Geomdl_ library. This is available only when Geomdl library is installed.
* **SciPy**. Use the implementation from SciPy_ library. This is available only when SciPy library is installed.
By default, the first available implementaiton is used.
* **Centripetal**. This parameter is available only when **Implementation**
parameter is set to **Geomdl**. This defines whether the node will use
centripetal metric. If not checked, the node will use euclidian metric.
Unchecked by default.
* **Specify points count**. This parameter is available only when
**Implementation** parameter is set to **Geomdl**. If checked, then it will
be possible to specify the number of curve's control points in **PointsCnt**
input. Otherwise, the node will determine required number of control points
by itself (this number can be too big for many applications).
* **Cyclic**. This parameter is available only when **Implementation**
parameter is set to **SciPy**. Defines whether the generated curve will be
cyclic (closed). Unchecked by default.
* **Metric**. This parameter is available only when **Implementation**
parameter is set to **SciPy**.Metric to be used for interpolation. The
available values are:
* Manhattan
* Euclidian
* Points (just number of points from the beginning)
* Chebyshev
* Centripetal (square root of Euclidian distance)
* X, Y, Z axis - use distance along one of coordinate axis, ignore others.
The default value is Euclidian.
* **Specify smoothing**. This parameter is available only when
**Implementation** parameter is set to **SciPy**. If checked, the node will
allow you to specify smoothing factor via **Smoothing** input. If not
checked, the node will select the smoothing factor automatically. Unchecked
by default.
Outputs
-------
......@@ -57,3 +117,7 @@ Take points from Greasepencil drawing and approximate them with a smooth curve:
.. image:: https://user-images.githubusercontent.com/284644/74363000-7becef00-4deb-11ea-9963-e864dc3a3599.png
Use SciPy implementation to make a closed curve:
.. image:: https://user-images.githubusercontent.com/284644/101246890-d61ebe00-3737-11eb-942d-c31e02bf3c3d.png
......@@ -71,7 +71,7 @@
## Curves @ NURBS
SvExNurbsCurveNode
SvExApproxNurbsCurveNode
SvApproxNurbsCurveMk2Node
SvExInterpolateNurbsCurveNode
SvDeconstructCurveNode
......
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE
import numpy as np
import bpy
from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty
from sverchok.node_tree import SverchCustomTreeNode
from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_update_node, get_data_nesting_level, ensure_nesting_level
from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_update_node, get_data_nesting_level, ensure_nesting_level, repeat_last_for_length
from sverchok.utils.math import supported_metrics, xyz_metrics
from sverchok.utils.logging import info, exception
from sverchok.utils.curve.nurbs import SvGeomdlCurve
from sverchok.dependencies import geomdl
from sverchok.utils.curve.splprep import scipy_nurbs_approximate
from sverchok.dependencies import geomdl, scipy
from sverchok.utils.dummy_nodes import add_dummy
if geomdl is None:
add_dummy('SvExApproxNurbsCurveNode', "Approximate NURBS Curve", 'geomdl')
else:
if geomdl is None and scipy is None:
add_dummy('SvApproxNurbsCurveMk2Node', "Approximate NURBS Curve", 'geomdl or scipy')
if geomdl is not None:
from geomdl import fitting
class SvExApproxNurbsCurveNode(bpy.types.Node, SverchCustomTreeNode):
"""
Triggers: NURBS Curve
Tooltip: Approximate NURBS Curve
"""
bl_idname = 'SvExApproxNurbsCurveNode'
bl_label = 'Approximate NURBS Curve'
bl_icon = 'CURVE_NCURVE'
degree : IntProperty(
name = "Degree",
min = 2, max = 6,
default = 3,
update = updateNode)
centripetal : BoolProperty(
name = "Centripetal",
default = False,
update = updateNode)
@throttle_and_update_node
def update_sockets(self, context):
self.inputs['PointsCnt'].hide_safe = not self.has_points_cnt
has_points_cnt : BoolProperty(
name = "Specify points count",
default = False,
update = update_sockets)
points_cnt : IntProperty(
name = "Points count",
min = 3, default = 5,
update = updateNode)
def draw_buttons(self, context, layout):
class SvApproxNurbsCurveMk2Node(bpy.types.Node, SverchCustomTreeNode):
"""
Triggers: NURBS Curve
Tooltip: Approximate NURBS Curve
"""
bl_idname = 'SvApproxNurbsCurveMk2Node'
bl_label = 'Approximate NURBS Curve'
bl_icon = 'CURVE_NCURVE'
degree : IntProperty(
name = "Degree",
min = 1, max = 6,
default = 3,
update = updateNode)
centripetal : BoolProperty(
name = "Centripetal",
default = False,
update = updateNode)
metric: EnumProperty(name='Metric',
description = "Knot mode",
default="DISTANCE", items=supported_metrics + xyz_metrics,
update=updateNode)
@throttle_and_update_node
def update_sockets(self, context):
self.inputs['PointsCnt'].hide_safe = not (self.implementation == 'GEOMDL' and self.has_points_cnt)
self.inputs['Smoothing'].hide_safe = not (self.implementation == 'SCIPY' and self.has_smoothing)
self.inputs['Weights'].hide_safe = not (self.implementation == 'SCIPY')
has_points_cnt : BoolProperty(
name = "Specify points count",
default = False,
update = update_sockets)
points_cnt : IntProperty(
name = "Points count",
min = 3, default = 5,
update = updateNode)
def get_implementations(self, context):
implementations = []
if geomdl is not None:
implementations.append(('GEOMDL', "Geomdl", "Geomdl (NURBS-Python) package implementation", 0))
if scipy is not None:
implementations.append(('SCIPY', "SciPy", "SciPy package implementation", 1))
return implementations
implementation : EnumProperty(name = "Implementation",
description = "Approximation algorithm implmenetation",
items = get_implementations,
update = update_sockets)
smoothing : FloatProperty(
name = "Smoothing",
description = "Smoothing factor. Set to 0 to do interpolation",
min = 0.0,
default = 0.1,
update = updateNode)
has_smoothing : BoolProperty(
name = "Specify smoothing",
default = False,
update = update_sockets)
is_cyclic : BoolProperty(
name = "Cyclic",
default = False,
update = updateNode)
def draw_buttons(self, context, layout):
layout.prop(self, 'implementation', text='')
if self.implementation == 'GEOMDL':
layout.prop(self, 'centripetal', toggle=True)
layout.prop(self, 'has_points_cnt', toggle=True)
def sv_init(self, context):
self.inputs.new('SvVerticesSocket', "Vertices")
self.inputs.new('SvStringsSocket', "Degree").prop_name = 'degree'
self.inputs.new('SvStringsSocket', "PointsCnt").prop_name = 'points_cnt'
self.outputs.new('SvCurveSocket', "Curve")
self.outputs.new('SvVerticesSocket', "ControlPoints")
self.outputs.new('SvStringsSocket', "Knots")
self.update_sockets(context)
def process(self):
if not any(socket.is_linked for socket in self.outputs):
return
vertices_s = self.inputs['Vertices'].sv_get()
degree_s = self.inputs['Degree'].sv_get()
points_cnt_s = self.inputs['PointsCnt'].sv_get()
input_level = get_data_nesting_level(vertices_s)
vertices_s = ensure_nesting_level(vertices_s, 4)
degree_s = ensure_nesting_level(degree_s, 2)
points_cnt_s = ensure_nesting_level(points_cnt_s, 2)
nested_output = input_level > 3
curves_out = []
points_out = []
knots_out = []
for params in zip_long_repeat(vertices_s, degree_s, points_cnt_s):
new_curves = []
new_points = []
new_knots = []
for vertices, degree, points_cnt in zip_long_repeat(*params):
else:
layout.prop(self, 'is_cyclic')
layout.prop(self, 'metric')
layout.prop(self, 'has_smoothing')
def sv_init(self, context):
self.inputs.new('SvVerticesSocket', "Vertices")
self.inputs.new('SvStringsSocket', "Weights")
self.inputs.new('SvStringsSocket', "Degree").prop_name = 'degree'
self.inputs.new('SvStringsSocket', "PointsCnt").prop_name = 'points_cnt'
self.inputs.new('SvStringsSocket', "Smoothing").prop_name = 'smoothing'
self.outputs.new('SvCurveSocket', "Curve")
self.outputs.new('SvVerticesSocket', "ControlPoints")
self.outputs.new('SvStringsSocket', "Knots")
self.update_sockets(context)
def process(self):
if not any(socket.is_linked for socket in self.outputs):
return
vertices_s = self.inputs['Vertices'].sv_get()
weights_s = self.inputs['Weights'].sv_get(default=[[[None]]])
degree_s = self.inputs['Degree'].sv_get()
points_cnt_s = self.inputs['PointsCnt'].sv_get()
smoothing_s = self.inputs['Smoothing'].sv_get()
input_level = get_data_nesting_level(vertices_s)
vertices_s = ensure_nesting_level(vertices_s, 4)
degree_s = ensure_nesting_level(degree_s, 2)
points_cnt_s = ensure_nesting_level(points_cnt_s, 2)
smoothing_s = ensure_nesting_level(smoothing_s, 2)
has_weights = self.inputs['Weights'].is_linked
if has_weights:
weights_s = ensure_nesting_level(weights_s, 3)
nested_output = input_level > 3
curves_out = []
points_out = []
knots_out = []
for params in zip_long_repeat(vertices_s, weights_s, degree_s, points_cnt_s, smoothing_s):
new_curves = []
new_points = []
new_knots = []
for vertices, weights, degree, points_cnt, smoothing in zip_long_repeat(*params):
if self.implementation == 'GEOMDL':
kwargs = dict(centripetal = self.centripetal)
if self.has_points_cnt:
kwargs['ctrlpts_size'] = points_cnt
curve = fitting.approximate_curve(vertices, degree, **kwargs)
new_points.append(curve.ctrlpts)
new_knots.append(curve.knotvector)
control_points = curve.ctrlpts
knotvector = curve.knotvector
curve = SvGeomdlCurve(curve)
new_curves.append(curve)
if nested_output:
curves_out.append(new_curves)
points_out.append(new_points)
knots_out.append(new_knots)
else:
curves_out.extend(new_curves)
points_out.extend(new_points)
knots_out.extend(new_knots)
self.outputs['Curve'].sv_set(curves_out)
self.outputs['ControlPoints'].sv_set(points_out)
self.outputs['Knots'].sv_set(knots_out)
else: # SCIPY:
points = np.array(vertices)
if has_weights:
weights = repeat_last_for_length(weights, len(vertices))
else:
weights = None
if not self.has_smoothing:
smoothing = None
curve = scipy_nurbs_approximate(points,
weights = weights,
metric = self.metric,
degree = degree,
smoothing = smoothing,
is_cyclic = self.is_cyclic)
control_points = curve.get_control_points().tolist()
knotvector = curve.get_knotvector().tolist()
new_curves.append(curve)
new_points.append(control_points)
new_knots.append(knotvector)
if nested_output:
curves_out.append(new_curves)
points_out.append(new_points)
knots_out.append(new_knots)
else:
curves_out.extend(new_curves)
points_out.extend(new_points)
knots_out.extend(new_knots)
self.outputs['Curve'].sv_set(curves_out)
self.outputs['ControlPoints'].sv_set(points_out)
self.outputs['Knots'].sv_set(knots_out)
def register():
if geomdl is not None:
bpy.utils.register_class(SvExApproxNurbsCurveNode)
if geomdl is not None or scipy is not None:
bpy.utils.register_class(SvApproxNurbsCurveMk2Node)
def unregister():
if geomdl is not None:
bpy.utils.unregister_class(SvExApproxNurbsCurveNode)
if geomdl is not None or scipy is not None:
bpy.utils.unregister_class(SvApproxNurbsCurveMk2Node)
import bpy
from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty
from sverchok.node_tree import SverchCustomTreeNode
from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_update_node, get_data_nesting_level, ensure_nesting_level
from sverchok.utils.logging import info, exception
from sverchok.utils.curve.nurbs import SvGeomdlCurve
from sverchok.dependencies import geomdl
from sverchok.utils.dummy_nodes import add_dummy
if geomdl is None:
add_dummy('SvExApproxNurbsCurveNode', "Approximate NURBS Curve", 'geomdl')
else:
from geomdl import fitting
class SvExApproxNurbsCurveNode(bpy.types.Node, SverchCustomTreeNode):
"""
Triggers: NURBS Curve
Tooltip: Approximate NURBS Curve
"""
bl_idname = 'SvExApproxNurbsCurveNode'
bl_label = 'Approximate NURBS Curve'
bl_icon = 'CURVE_NCURVE'
degree : IntProperty(
name = "Degree",
min = 2, max = 6,
default = 3,
update = updateNode)
centripetal : BoolProperty(
name = "Centripetal",
default = False,
update = updateNode)
@throttle_and_update_node
def update_sockets(self, context):
self.inputs['PointsCnt'].hide_safe = not self.has_points_cnt
has_points_cnt : BoolProperty(
name = "Specify points count",
default = False,
update = update_sockets)
points_cnt : IntProperty(
name = "Points count",
min = 3, default = 5,
update = updateNode)
def draw_buttons(self, context, layout):
layout.prop(self, 'centripetal', toggle=True)
layout.prop(self, 'has_points_cnt', toggle=True)
def sv_init(self, context):
self.inputs.new('SvVerticesSocket', "Vertices")
self.inputs.new('SvStringsSocket', "Degree").prop_name = 'degree'
self.inputs.new('SvStringsSocket', "PointsCnt").prop_name = 'points_cnt'
self.outputs.new('SvCurveSocket', "Curve")
self.outputs.new('SvVerticesSocket', "ControlPoints")
self.outputs.new('SvStringsSocket', "Knots")
self.update_sockets(context)
def process(self):
if not any(socket.is_linked for socket in self.outputs):
return
vertices_s = self.inputs['Vertices'].sv_get()
degree_s = self.inputs['Degree'].sv_get()
points_cnt_s = self.inputs['PointsCnt'].sv_get()
input_level = get_data_nesting_level(vertices_s)
vertices_s = ensure_nesting_level(vertices_s, 4)
degree_s = ensure_nesting_level(degree_s, 2)
points_cnt_s = ensure_nesting_level(points_cnt_s, 2)
nested_output = input_level > 3
curves_out = []
points_out = []
knots_out = []
for params in zip_long_repeat(vertices_s, degree_s, points_cnt_s):
new_curves = []
new_points = []
new_knots = []
for vertices, degree, points_cnt in zip_long_repeat(*params):
kwargs = dict(centripetal = self.centripetal)
if self.has_points_cnt:
kwargs['ctrlpts_size'] = points_cnt
curve = fitting.approximate_curve(vertices, degree, **kwargs)
new_points.append(curve.ctrlpts)
new_knots.append(curve.knotvector)
curve = SvGeomdlCurve(curve)
new_curves.append(curve)
if nested_output:
curves_out.append(new_curves)
points_out.append(new_points)
knots_out.append(new_knots)
else:
curves_out.extend(new_curves)
points_out.extend(new_points)
knots_out.extend(new_knots)
self.outputs['Curve'].sv_set(curves_out)
self.outputs['ControlPoints'].sv_set(points_out)
self.outputs['Knots'].sv_set(knots_out)
def register():
if geomdl is not None:
bpy.utils.register_class(SvExApproxNurbsCurveNode)
def unregister():
if geomdl is not None:
bpy.utils.unregister_class(SvExApproxNurbsCurveNode)
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE
import numpy as np
from sverchok.utils.curve.nurbs import SvNurbsCurve
from sverchok.utils.geom import Spline
from sverchok.dependencies import scipy
if scipy is not None:
from scipy import interpolate
def scipy_nurbs_approximate(points, weights=None, metric='DISTANCE', degree=3, smoothing=None, is_cyclic=False):
points = np.asarray(points)
if weights is not None and len(points) != len(weights):
raise Exception("Number of weights must be equal to number of points")
if is_cyclic:
points = np.vstack((points, points[0]))
if weights is not None:
weights = np.insert(weights, -1, weights[0])
points_orig = points
points = points.T
kwargs = dict()
if weights is not None:
kwargs['w'] = np.asarray(weights)
if metric is not None:
tknots = Spline.create_knots(points_orig, metric)
if len(tknots) != len(points.T):
raise Exception(f"Number of T knots ({len(tknots)}) is not equal to number of points ({len(points.T)})")
kwargs['u'] = tknots
if degree is not None:
kwargs['k'] = degree
if smoothing is not None:
kwargs['s'] = smoothing
if is_cyclic:
kwargs['per'] = 1
tck, u = interpolate.splprep(points, **kwargs)
knotvector = tck[0]
control_points = np.stack(tck[1]).T
degree = tck[2]
curve = SvNurbsCurve.build(SvNurbsCurve.NATIVE,
degree, knotvector,
control_points)
if is_cyclic:
curve = curve.cut_segment(0.0, 1.0)
#curve.u_bounds = (0.0, 1.0)
return curve
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать