def best_fit(cls, points: array_like, tol: Optional[float] = None, **kwargs) -> Line: """ Return the line of best fit for a set of points. Parameters ---------- points : array_like Input points. tol : float | None, optional Keyword passed to :meth:`Points.are_collinear` (default None). kwargs : dict, optional Additional keywords passed to :func:`numpy.linalg.svd` Returns ------- Line The line of best fit. Raises ------ ValueError If the points are concurrent. Examples -------- >>> from skspatial.objects import Line >>> points = [[0, 0], [1, 2], [2, 1], [2, 3], [3, 2]] >>> line = Line.best_fit(points) The point on the line is the centroid of the points. >>> line.point Point([1.6, 1.6]) The line direction is a unit vector. >>> line.direction.round(3) Vector([0.707, 0.707]) """ points_spatial = Points(points) if points_spatial.are_concurrent(tol=tol): raise ValueError("The points must not be concurrent.") points_centered, centroid = points_spatial.mean_center( return_centroid=True) _, _, vh = np.linalg.svd(points_centered, **kwargs) direction = vh[0, :] return cls(centroid, direction)
def best_fit(cls, points: array_like) -> Line: """ Return the line of best fit for a set of points. Parameters ---------- points : array_like Input points. Returns ------- Line The line of best fit. Raises ------ ValueError If the points are concurrent. Examples -------- >>> from skspatial.objects import Line >>> points = [[0, 0], [1, 2], [2, 1], [2, 3], [3, 2]] >>> line = Line.best_fit(points) The point on the line is the centroid of the points. >>> line.point Point([1.6, 1.6]) The line direction is a unit vector. >>> line.direction.round(3) Vector([0.707, 0.707]) """ points_spatial = Points(points) if points_spatial.are_concurrent(tol=0): raise ValueError("The points must not be concurrent.") points_centered, centroid = points_spatial.mean_center( return_centroid=True) _, _, vh = np.linalg.svd(points_centered) direction = vh[0, :] return cls(centroid, direction)
def best_fit(cls, points: array_like) -> Sphere: """ Return the sphere of best fit for a set of 3D points. Parameters ---------- points : array_like Input 3D points. Returns ------- Sphere The sphere of best fit. Raises ------ ValueError If the points are not 3D. If there are fewer than four points. If the points lie in a plane. Examples -------- >>> import numpy as np >>> from skspatial.objects import Sphere >>> points = [[1, 0, 1], [0, 1, 1], [1, 2, 1], [1, 1, 2]] >>> sphere = Sphere.best_fit(points) >>> sphere.point Point([1., 1., 1.]) >>> np.round(sphere.radius, 2) 1.0 """ points = Points(points) if points.dimension != 3: raise ValueError("The points must be 3D.") if points.shape[0] < 4: raise ValueError("There must be at least 4 points.") if points.affine_rank() != 3: raise ValueError("The points must not be in a plane.") n = points.shape[0] A = np.hstack((2 * points, np.ones((n, 1)))) b = (points**2).sum(axis=1) c, _, _, _ = np.linalg.lstsq(A, b, rcond=None) center = c[:3] radius = float(np.sqrt(np.dot(center, center) + c[3])) return cls(center, radius)
def best_fit(cls, points: array_like) -> Plane: """ Return the plane of best fit for a set of 3D points. Parameters ---------- points : array_like Input 3D points. Returns ------- Plane The plane of best fit. Raises ------ ValueError If the points are collinear or are not 3D. Examples -------- >>> from skspatial.objects import Plane >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]] >>> plane = Plane.best_fit(points) The point on the plane is the centroid of the points. >>> plane.point Point([0.25, 0.25, 0.25]) The plane normal is a unit vector. >>> plane.normal.round(3) Vector([-0.577, -0.577, -0.577]) >>> Plane.best_fit([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]) Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.])) """ points = Points(points) if points.dimension != 3: raise ValueError("The points must be 3D.") if points.are_collinear(tol=0): raise ValueError("The points must not be collinear.") points_centered, centroid = points.mean_center(return_centroid=True) u, _, _ = np.linalg.svd(points_centered.T) normal = Vector(u[:, -1]) return cls(centroid, normal)
def from_points(cls, point_a: array_like, point_b: array_like, point_c: array_like, **kwargs) -> Plane: """ Instantiate a plane from three points. The three points lie on the plane. Parameters ---------- point_a, point_b, point_c: array_like Three points defining the plane. kwargs: dict, optional Additional keywords passed to :meth:`Points.are_collinear`. Returns ------- Plane Plane containing the three input points. Raises ------ ValueError If the points are collinear. Examples -------- >>> from skspatial.objects import Plane >>> Plane.from_points([0, 0], [1, 0], [3, 3]) Plane(point=Point([0, 0, 0]), normal=Vector([0, 0, 3])) The order of the points affects the direction of the normal vector. >>> Plane.from_points([0, 0], [3, 3], [1, 0]) Plane(point=Point([0, 0, 0]), normal=Vector([ 0, 0, -3])) >>> Plane.from_points([0, 0], [0, 1], [0, 3]) Traceback (most recent call last): ... ValueError: The points must not be collinear. """ if Points([point_a, point_b, point_c]).are_collinear(**kwargs): raise ValueError("The points must not be collinear.") vector_ab = Vector.from_points(point_a, point_b) vector_ac = Vector.from_points(point_a, point_c) return Plane.from_vectors(point_a, vector_ab, vector_ac)
def __init__(self, point_a: array_like, point_b: array_like, point_c: array_like): self.point_a = Point(point_a) self.point_b = Point(point_b) self.point_c = Point(point_c) if not (self.point_a.dimension == self.point_b.dimension == self.point_c.dimension): raise ValueError("The points must have the same dimension.") if Points([self.point_a, self.point_b, self.point_c]).are_collinear(): raise ValueError("The points must not be collinear.") self.dimension = self.point_a.dimension
def to_points(self, n_angles: int = 30) -> Points: """ Return points on the surface of the sphere. Parameters ---------- n_angles: int Number of angles used to generate the points. Returns ------- Points Points on the surface of the sphere. Examples -------- >>> from skspatial.objects import Sphere >>> sphere = Sphere([0, 0, 0], 1) >>> sphere.to_points(n_angles=3).round().unique() Points([[ 0., -1., 0.], [ 0., 0., -1.], [ 0., 0., 1.], [ 0., 1., 0.]]) >>> sphere.to_points(n_angles=4).round(3).unique() Points([[-0.75 , -0.433, -0.5 ], [-0.75 , -0.433, 0.5 ], [ 0. , 0. , -1. ], [ 0. , 0. , 1. ], [ 0. , 0.866, -0.5 ], [ 0. , 0.866, 0.5 ], [ 0.75 , -0.433, -0.5 ], [ 0.75 , -0.433, 0.5 ]]) """ X, Y, Z = self.to_mesh(n_angles) points = _mesh_to_points(X, Y, Z) return Points(points)
def to_points(self, **kwargs) -> Points: """ Return points on the surface of the object. Parameters ---------- kwargs: dict, optional Additional keywords passed to the `to_mesh` method of the class. Returns ------- Points Points on the surface of the object. Examples -------- >>> from skspatial.objects import Sphere >>> sphere = Sphere([0, 0, 0], 1) >>> sphere.to_points(n_angles=3).round().unique() Points([[ 0., -1., 0.], [ 0., 0., -1.], [ 0., 0., 1.], [ 0., 1., 0.]]) >>> sphere.to_points(n_angles=4).round(3).unique() Points([[-0.75 , -0.433, -0.5 ], [-0.75 , -0.433, 0.5 ], [ 0. , 0. , -1. ], [ 0. , 0. , 1. ], [ 0. , 0.866, -0.5 ], [ 0. , 0.866, 0.5 ], [ 0.75 , -0.433, -0.5 ], [ 0.75 , -0.433, 0.5 ]]) """ X, Y, Z = self.to_mesh(**kwargs) points = _mesh_to_points(X, Y, Z) return Points(points)
def centroid(self) -> Point: """ Return the centroid of the triangle. Returns ------- Point Centroid of the triangle. Examples -------- >>> from skspatial.objects import Triangle >>> Triangle([0, 0], [0, 1], [1, 0]).centroid().round(3) Point([0.333, 0.333]) >>> Triangle([0, 0, 0], [1, 2, 3], [4, 5, 6]).centroid().round(3) Point([1.667, 2.333, 3. ]) """ return Points([self.point_a, self.point_b, self.point_c]).centroid()
def to_points( self, lims_x: array_like = (-1, 1), lims_y: array_like = (-1, 1) ) -> Points: """ Return four points on the plane. The coordinate matrices used for 3D plotting are converted to points. Parameters ---------- lims_x, lims_y : (2,) tuple x and y limits of the plane. Tuple of form (min, max). The default is (-1, 1). The point on the plane is used as the origin. Returns ------- Points Four 3D points on the plane. Examples -------- >>> from skspatial.objects import Plane >>> Plane([0, 0, 0], [0, 0, 1]).to_points() Points([[-1., -1., 0.], [ 1., -1., 0.], [-1., 1., 0.], [ 1., 1., 0.]]) """ X, Y, Z = self.to_mesh(lims_x, lims_y) points = _mesh_to_points(X, Y, Z) return Points(points)
def best_fit(cls, points: array_like, **kwargs) -> 'Plane': """ Return the plane of best fit for a set of 3D points. Parameters ---------- points : array_like Input 3D points. kwargs : dict, optional Additional keywords passed to :func:`numpy.linalg.svd` Returns ------- Plane The plane of best fit. Raises ------ ValueError If the points are collinear or are not 3D. Examples -------- >>> from skspatial.objects import Plane >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]] >>> plane = Plane.best_fit(points) The point on the plane is the centroid of the points. >>> plane.point Point([0.25, 0.25, 0.25]) The plane normal is a unit vector. >>> plane.normal.round(3) Vector([-0.577, -0.577, -0.577]) >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]] >>> Plane.best_fit(points) Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.])) >>> Plane.best_fit(points, full_matrices=False) Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.])) """ points = Points(points) if points.dimension != 3: raise ValueError("The points must be 3D.") if points.are_collinear(tol=0): raise ValueError("The points must not be collinear.") points_centered, centroid = points.mean_center(return_centroid=True) u, sigma, _ = np.linalg.svd(points_centered.T, **kwargs) error = min(sigma) normal = Vector(u[:, -1]) return cls(centroid, normal, error=error)
def best_fit(cls, points: array_like) -> Circle: """ Return the sphere of best fit for a set of 2D points. Parameters ---------- points : array_like Input 2D points. Returns ------- Circle The circle of best fit. Raises ------ ValueError If the points are not 2D. If there are fewer than three points. If the points are collinear. Reference --------- https://meshlogic.github.io/posts/jupyter/curve-fitting/fitting-a-circle-to-cluster-of-3d-points/ Examples -------- >>> import numpy as np >>> from skspatial.objects import Circle >>> points = [[1, 1], [2, 2], [3, 1]] >>> circle = Circle.best_fit(points) >>> circle.point Point([2., 1.]) >>> np.round(circle.radius, 2) 1.0 """ points = Points(points) if points.dimension != 2: raise ValueError("The points must be 2D.") if points.shape[0] < 3: raise ValueError("There must be at least 3 points.") if points.affine_rank() != 2: raise ValueError("The points must not be collinear.") n = points.shape[0] A = np.hstack((2 * points, np.ones((n, 1)))) b = (points**2).sum(axis=1) c = np.linalg.lstsq(A, b, rcond=None)[0] center = c[:2] radius = np.sqrt(c[2] + c[0] ** 2 + c[1] ** 2) return cls(center, radius)
def is_coplanar(self, other: 'Line', **kwargs: float) -> bool: """ Check if the line is coplanar with another. Parameters ---------- other : Line Other line. kwargs : dict, optional Additional keywords passed to :func:`numpy.linalg.matrix_rank` Returns ------- bool True if the line is coplanar; false otherwise. Raises ------ TypeError If the input is not a line. References ---------- http://mathworld.wolfram.com/Coplanar.html Examples -------- >>> from skspatial.objects import Line >>> line_a = Line(point=[0, 0, 0], direction=[1, 0, 0]) >>> line_b = Line([-5, 3, 0], [7, 1, 0]) >>> line_c = Line([0, 0, 0], [0, 0, 1]) >>> line_a.is_coplanar(line_b) True >>> line_a.is_coplanar(line_c) True >>> line_b.is_coplanar(line_c) False The input must be another line. >>> from skspatial.objects import Plane >>> line_a.is_coplanar(Plane(line_a.point, line_a.vector)) Traceback (most recent call last): ... TypeError: The input must also be a line. """ if not isinstance(other, type(self)): raise TypeError("The input must also be a line.") point_1 = self.point point_2 = self.to_point() point_3 = other.point point_4 = other.to_point() points = Points([point_1, point_2, point_3, point_4]) return points.are_coplanar(**kwargs)
def best_fit(cls, points: array_like, tol: Optional[float] = None, **kwargs) -> Plane: """ Return the plane of best fit for a set of 3D points. Parameters ---------- points : array_like Input 3D points. tol : float | None, optional Keyword passed to :meth:`Points.are_collinear` (default None). kwargs : dict, optional Additional keywords passed to :func:`numpy.linalg.svd` Returns ------- Plane The plane of best fit. Raises ------ ValueError If the points are collinear or are not 3D. References ---------- Using SVD for some fitting problems Inge Söderkvist Algorithm 3.1 https://www.ltu.se/cms_fs/1.51590!/svd-fitting.pdf Examples -------- >>> from skspatial.objects import Plane >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]] >>> plane = Plane.best_fit(points) The point on the plane is the centroid of the points. >>> plane.point Point([0.25, 0.25, 0.25]) The plane normal is a unit vector. >>> plane.normal.round(3) Vector([-0.577, -0.577, -0.577]) >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]] >>> Plane.best_fit(points) Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.])) >>> Plane.best_fit(points, full_matrices=False) Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.])) """ points = Points(points) if points.dimension != 3: raise ValueError("The points must be 3D.") if points.are_collinear(tol=tol): raise ValueError("The points must not be collinear.") points_centered, centroid = points.mean_center(return_centroid=True) u, _, _ = np.linalg.svd(points_centered.T, **kwargs) normal = Vector(u[:, 2]) return cls(centroid, normal)