Source code for spine.vis.trace.cone

"""Module to convert a point cloud into a cone envelope."""

from __future__ import annotations

from typing import Any

import numpy as np
from plotly import graph_objs as go

from spine.math.decomposition import principal_components

from .utils import ColorInput, HoverTextInput, IntensityInput, is_scalar_sequence

__all__ = ["cone_trace"]


[docs] def cone_trace( points: np.ndarray, fraction: float = 0.5, num_samples: int = 10, color: ColorInput = None, hovertext: HoverTextInput = None, intensity: IntensityInput = None, showscale: bool = False, **kwargs: Any, ) -> go.Mesh3d: """Converts a cloud of points into a 3D cone. This function uses the PCA and the average angle w.r.t. to the point of maximum curvature as a basis to construct a cone. Parameters ---------- points : np.ndarray (N, 3) Array of point coordinates fraction : float, default 0.5 Fraction of the points contained in the cone (angle quantile) num_samples : int, default 10 Number of points sampled along h and phi in the conical coordinate system of the cone. A larger number increases the resolution. showscale : bool, default False If True, show the colorscale of the :class:`plotly.graph_objs.Mesh3d` color : Union[str, int, float, Sequence], optional Color of the cone. Must be provided as one scalar value. hovertext : Union[int, float, str, Sequence], optional Text associated with the cone. Can be a scalar label or a per-vertex sequence of labels. intensity : Union[int, float, Sequence], optional Intensity of the cone colors. Can be a single numeric value or a per-vertex sequence. showscale : bool, default False Whether to show the color scale for the mesh **kwargs : dict, optional Additional parameters to pass to the :class:`plotly.graph_objs.Mesh3d` object """ # Get the centroid and the principal components centroid = np.mean(points, axis=0) pcomp = principal_components(points) rotmat = np.flip(pcomp, axis=0) paxis = pcomp[0] # Collapse point cloud onto principal axis, find end points points_pa = np.dot(points, paxis) end_ids = np.argmin(points_pa), np.argmax(points_pa) # Find the directions w.r.t. each end point centroid = np.mean(points, axis=0) dirs = np.vstack([centroid - points[end_id] for end_id in end_ids]) dirs = dirs / np.linalg.norm(dirs, axis=1)[:, None] # Find a quantile angle w.r.t. to each direction dots = np.zeros((2, len(points)), dtype=points.dtype) for i, end_id in enumerate(end_ids): for j, point in enumerate(points): if j != end_id: diff = point - points[end_id] dots[i, j] = np.dot(diff / np.linalg.norm(diff), dirs[i]) if not 0.0 < fraction < 1.0: raise ValueError("The `fraction` parameter should be a probability.") angles = np.arccos(dots) means = np.mean(angles, axis=1) quantiles = np.quantile(angles, fraction, axis=1) # The point with the lowest mean angle is the start, select it start_id = np.argmin(means) start_pos = points[end_ids[start_id]] # Define the cone main axis length and its opening angle length = abs(points_pa[end_ids[1]] - points_pa[end_ids[0]]) theta = quantiles[start_id] # Compute the points on a cone with half-opening angle theta r = np.linspace(0, 1, num=num_samples) phi = np.linspace(0, 2 * np.pi, num=num_samples) r, phi = np.meshgrid(r, phi) x = r * np.tan(theta) * np.cos(phi) y = r * np.tan(theta) * np.sin(phi) z = r unit_points = np.vstack((x.flatten(), y.flatten(), z.flatten())).T # Rotate and offset the cone cone_points = start_pos + length * np.dot(unit_points, rotmat) # Convert the color provided to a set of intensities mesh_color = None if color is not None: if intensity is not None: raise ValueError("Provide either `color` or `intensity`, not both.") if is_scalar_sequence(color): raise ValueError("Should provide a single color for the cone.") if isinstance(color, str): mesh_color = color else: intensity = np.full(len(cone_points), color) # Update hovertemplate style hovertemplate = "x: %{x}<br>y: %{y}<br>z: %{z}" if hovertext is not None: if is_scalar_sequence(hovertext): hovertemplate += "<br>%{text}" else: hovertemplate += f"<br>{hovertext}" hovertext = None # Append Mesh3d object return go.Mesh3d( x=cone_points[:, 0], y=cone_points[:, 1], z=cone_points[:, 2], color=mesh_color, intensity=intensity, alphahull=0, showscale=showscale, hovertext=hovertext, hovertemplate=hovertemplate, **kwargs, )