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_vectors(cls, point: array_like, vector_a: array_like, vector_b: array_like, **kwargs) -> Plane: """ Instantiate a plane from a point and two vectors. The two vectors span the plane. Parameters ---------- point : array_like Point on the plane. vector_a, vector_b : array_like Input vectors. kwargs : dict, optional Additional keywords passed to :meth:`Vector.is_parallel`. Returns ------- Plane Plane containing input point and spanned by the two input vectors. Raises ------ ValueError If the vectors are parallel. Examples -------- >>> from skspatial.objects import Plane >>> Plane.from_vectors([0, 0], [1, 0], [0, 1]) Plane(point=Point([0, 0, 0]), normal=Vector([0, 0, 1])) >>> Plane.from_vectors([0, 0], [1, 0], [2, 0]) Traceback (most recent call last): ... ValueError: The vectors must not be parallel. """ vector_a = Vector(vector_a) if vector_a.is_parallel(vector_b, **kwargs): raise ValueError("The vectors must not be parallel.") # The cross product returns a 3D vector. vector_normal = vector_a.cross(vector_b) # Convert the point to 3D so that it matches the vector dimension. point = Point(point).set_dimension(3) return cls(point, vector_normal)
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, **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 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)