Skip to content

Commit

Permalink
Add methods for Line/Plane of best fit
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Hynes committed Mar 12, 2019
1 parent 0ba8e22 commit 75fce37
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 3 deletions.
43 changes: 42 additions & 1 deletion skspatial/objects/line.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
from dpcontracts import require, ensure
from dpcontracts import require, ensure, types

from skspatial.transformation import mean_center
from .base_line_plane import _BaseLinePlane
from .point import Point
from .vector import Vector
Expand Down Expand Up @@ -293,3 +294,43 @@ def intersect_line(self, other):
vector_a_scaled = num / denom * self.direction

return self.point.add(vector_a_scaled)

@classmethod
@types(points=np.ndarray)
@require("There must be at least two points.", lambda args: args.points.shape[0] >= 2)
@ensure("The output must be a line.", lambda _, result: isinstance(result, Line))
def best_fit(cls, points):
"""
Return the line of best fit for a set of points.
Parameters
----------
points : ndarray
Input points.
Returns
-------
Line
The line of best fit.
Examples
--------
>>> import numpy as np
>>> from skspatial.objects import Line
>>> points = np.array([[1, 0], [2, 0], [3, 0]])
>>> line = Line.best_fit(points)
>>> line.point
Point([2., 0., 0.])
>>> line.direction
Vector([1., 0., 0.])
"""
points_centered, centroid = mean_center(points)

_, _, vh = np.linalg.svd(points_centered)
direction = Vector(vh[0, :])

return cls(centroid, direction)
42 changes: 41 additions & 1 deletion skspatial/objects/plane.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
from dpcontracts import require, ensure
from dpcontracts import require, ensure, types

from skspatial.transformation import mean_center
from .base_line_plane import _BaseLinePlane
from .line import Line
from .point import Point
Expand Down Expand Up @@ -369,3 +370,42 @@ def intersect_plane(self, other):
direction_line = self.normal.cross(other.normal)

return Line(point_line, direction_line)

@classmethod
@types(points=np.ndarray)
@require("There must be at least three points.", lambda args: args.points.shape[0] >= 3)
@ensure("The output must be a plane.", lambda _, result: isinstance(result, Plane))
def best_fit(cls, points):
"""
Return the plane of best fit for a set of points.
Parameters
----------
points : ndarray
Input points.
Returns
-------
Plane
The plane of best fit.
Examples
--------
>>> from skspatial.objects import Plane
>>> points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]])
>>> plane = Plane.best_fit(points)
>>> plane.point
Point([0.5, 0.5, 0. ])
>>> plane.normal
Vector([0., 0., 1.])
"""
points_centered, centroid = mean_center(points)

u, s, vh = np.linalg.svd(points_centered.T)
normal = Vector(u[:, -1])

return cls(centroid, normal)
63 changes: 62 additions & 1 deletion skspatial/transformation.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,81 @@
"""Transformations of spatial entities."""

import numpy as np
from dpcontracts import require, types
from dpcontracts import require, ensure, types


@types(points=np.ndarray)
@require("The input must be a 2D array of points", lambda args: args.points.ndim == 2)
@ensure("The output length must be the input width.", lambda args, result: result.size == args.points.shape[1])
@ensure("The output must be a 1D array of points", lambda _, result: result.ndim == 1)
def get_centroid(points):
"""
Return the centroid of a set of points.
Parameters
----------
points : ndarray
(n, d) array of n points.
Returns
-------
ndarray
Centroid of the points.
Examples
--------
>>> import numpy as np
>>> from skspatial.transformation import get_centroid
>>> points = np.array([[1, 2, 3], [2, 2, 3]])
>>> get_centroid(points)
array([1.5, 2. , 3. ])
"""
return points.mean(axis=0)


@types(points=np.ndarray)
@require("The input must be a 2D array of points", lambda args: args.points.ndim == 2)
@ensure("The centered points must have the input shape", lambda args, result: result[0].shape == args.points.shape)
@ensure(
"The centroid length must be the dimension of the points.", lambda _, result: result[1].size == result[0].shape[1]
)
@ensure("The centroid must be a 1D array.", lambda _, result: result[1].ndim == 1)
def mean_center(points):
"""
Mean-center a set of points.
The centroid of the points is subtracted from the points.
Parameters
----------
points : ndarray
(n, d) array of n points.
Returns
-------
points_centered : ndarray
(n, d) array of mean-centered points.
centroid : ndarray
(d,) array.
Examples
--------
>>> import numpy as np
>>> from skspatial.transformation import mean_center
>>> points = np.array([[4, 4, 4], [2, 2, 2]])
>>> points_centered, centroid = mean_center(points)
>>> points_centered
array([[ 1., 1., 1.],
[-1., -1., -1.]])
>>> centroid
array([3., 3., 3.])
"""
centroid = get_centroid(points)

points_centered = points - centroid
Expand Down

0 comments on commit 75fce37

Please sign in to comment.