Source code for spine.vis.trace.box

"""Defines functions used to draw finite-sized boxes.

These tools are typically used to represent the extent of a voxel or
a voxel neighborhood in an image. In the context of the Point-Proposal Network,
this helps represent the region proposed by the network at layers
deeper than the original resolution of the image.

The :func:`box_trace` function is also used to represent the extent of the
active volume of the modules that make up a detector.
"""

from __future__ import annotations

import time
from typing import Any

import numpy as np
import plotly.graph_objs as go

from .utils import (
    ColorInput,
    HoverTextInput,
    IntensityInput,
    is_scalar_sequence,
    require_matching_length,
)

__all__ = ["box_trace", "box_traces", "scatter_boxes"]


[docs] def box_trace( lower: np.ndarray, upper: np.ndarray, draw_faces: bool = False, line: dict[str, Any] | None = None, linewidth: float | None = None, color: ColorInput = None, cmin: float | None = None, cmax: float | None = None, colorscale: str | dict | None = None, intensity: IntensityInput = None, hovertext: HoverTextInput = None, showscale: bool = False, **kwargs: Any, ) -> go.Scatter3d | go.Mesh3d: """Function which produces a plotly trace of a box given its lower bounds and upper bounds in x, y and z. Parameters ---------- lower : np.ndarray (3) Vector of lower boundaries in x, z and z upper : np.ndarray (3) Vector of upper boundaries in x, z and z draw_faces : bool, default False Weather or not to draw the box faces, or only the edges line : dict, optional Dictionary which specifies box line properties linewidth : int, optional Width of the box edge lines color : Union[str, int, float, Sequence], optional Color of the box. Can be a single Plotly color, a single numeric value, or a per-vertex sequence of scalar values. cmin : float, optional Minimum value of the color range cmax : float, optional Maximum value of the color range colorscale : Union[str, dict] Colorscale intensity : Union[int, float, Sequence], optional Color intensity of the box along the colorscale axis. Can be a single numeric value or a per-vertex sequence. hovertext : Union[int, float, str, Sequence], optional Text associated with the box. Can be a scalar label or a per-vertex sequence of labels. showscale : bool, default False If True, show the colorscale of the :class:`plotly.graph_objs.Mesh3d` **kwargs : dict, optional List of additional arguments to pass to :class:`plotly.graph_objs.Scatter3D` or :class:`plotly.graph_objs.Mesh3D`, depending on what the `draw_faces` parameter is set to. Returns ------- Union[plotly.graph_objs.Scatter3D, plotly.graph_objs.Mesh3D] Box trace """ # Check the parameters if len(lower) != 3 or len(upper) != 3: raise ValueError("Must specify 3 values for both lower and upper boundaries.") if not np.all(np.asarray(upper) > np.asarray(lower)): raise ValueError( "Each upper boundary should be greater than its lower counterpart." ) # List of box vertices in the edges that join them in the box mesh box_vertices = np.array( [[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 1, 1, 0, 0, 1, 1], [0, 1, 0, 1, 0, 1, 0, 1]] ).T box_edge_index = np.array( [[0, 0, 0, 1, 1, 2, 2, 3, 4, 4, 5, 6], [1, 2, 4, 3, 5, 3, 6, 7, 5, 6, 7, 7]] ) box_tri_index = np.array( [ [0, 6, 3, 2, 7, 6, 1, 1, 5, 5, 6, 7], [6, 0, 0, 0, 4, 4, 7, 7, 4, 0, 2, 3], [2, 4, 1, 3, 5, 7, 5, 3, 0, 1, 7, 2], ] ) # List of scaled vertices vertices = lower + box_vertices * (upper - lower) # 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 # Create the box trace if not draw_faces: # Build a list of box edges to draw (padded with None values to break # them from each other) edges = np.full((3 * box_edge_index.shape[1], 3), None) edges[np.arange(0, edges.shape[0], 3)] = vertices[box_edge_index[0]] edges[np.arange(1, edges.shape[0], 3)] = vertices[box_edge_index[1]] # Build a line property, if needed if ( color is not None or linewidth is not None or cmin is not None or cmax is not None or colorscale is not None ): if line is not None: raise ValueError( "Must not specify `line` when providing `color`, " "`linewidth`, `cmin` or `cmax` independently." ) if color is not None and not isinstance(color, str): color = np.full(len(edges), color) line = { "color": color, "width": linewidth, "cmin": cmin, "cmax": cmax, "colorscale": colorscale, } # Return trace trace = go.Scatter3d( x=edges[:, 0], y=edges[:, 1], z=edges[:, 2], mode="lines", line=line, hovertext=hovertext, hovertemplate=hovertemplate, **kwargs, ) else: # If the color is a number, must be specified as an intensity if color is not None and not isinstance(color, str): if intensity is not None: raise ValueError("Must not provide both `color` and `intensity`.") intensity = np.full(len(vertices), color) color = None trace = go.Mesh3d( x=vertices[:, 0], y=vertices[:, 1], z=vertices[:, 2], i=box_tri_index[0], j=box_tri_index[1], k=box_tri_index[2], color=color, intensity=intensity, showscale=showscale, cmin=cmin, cmax=cmax, colorscale=colorscale, hovertext=hovertext, hovertemplate=hovertemplate, **kwargs, ) # Return trace return trace
[docs] def box_traces( lowers: np.ndarray, uppers: np.ndarray, draw_faces: bool = False, color: ColorInput = None, linewidth: float | None = None, hovertext: HoverTextInput = None, cmin: float | None = None, cmax: float | None = None, shared_legend: bool = True, legendgroup: str | None = None, showlegend: bool = True, name: str | None = None, **kwargs: Any, ) -> list[go.Scatter3d | go.Mesh3d]: """Function which produces a list of plotly traces of boxes given a list of lower bounds and upper bounds in x, y and z. Parameters ---------- lowers : np.ndarray (N, 3) List of vector of lower boundaries in x, z and z uppers : np.ndarray (N, 3) List of vector of upper boundaries in x, z and z draw_faces : bool, default False Weather or not to draw the box faces, or only the edges color : Union[str, int, float, Sequence], optional Color of the boxes, either as one shared value or one value per box. linewidth : int, default 2 Width of the box edge lines hovertext : Union[int, float, str, Sequence], optional Text associated with the boxes, either as one shared label or one label per box. cmin : float, optional Minimum value along the color scale cmax : float, optional Maximum value along the color scale shared_legend : bool, default True If True, the plotly legend of all boxes is shared as one legendgroup : str, optional Legend group to be shared between all boxes showlegend : bool, default `True` Whether to show legends on not name : str, optional Name of the trace(s) **kwargs : dict, optional List of additional arguments to pass to :class:`plotly.graph_objs.Scatter3D` or :class:`plotly.graph_objs.Mesh3D`, depending on what the `draw_faces` parameter is set to. Returns ------- Union[List[plotly.graph_objs.Scatter3D], List[plotly.graph_objs.Mesh3D]] Box traces """ # Check the parameters if len(lowers) != len(uppers): raise ValueError( "Provide as many upper boundary vector as their lower counterpart." ) require_matching_length( color, len(lowers), "Specify one color for all boxes, or one color per box.", ) require_matching_length( hovertext, len(lowers), "Specify one hovertext for all boxes, or one hovertext per box.", ) # If one color is provided per box, give an associated hovertext if hovertext is None and is_scalar_sequence(color): hovertext = [f"Value: {v:0.3f}" for v in color] # If cmin/cmax are not provided, must build them so that all boxes # share the same colorscale range (not guaranteed otherwise) if is_scalar_sequence(color) and len(color) > 0: if cmin is None: cmin = np.min(np.asarray(color)) if cmax is None: cmax = np.max(np.asarray(color)) # If the legend is to be shared, make sure there is a common legend group if shared_legend and legendgroup is None: legendgroup = "group_" + str(time.time()) # Loop over the list of box boundaries traces = [] for i, (lower, upper) in enumerate(zip(lowers, uppers)): # Fetch the right color/hovertext combination col, hov = color, hovertext if is_scalar_sequence(color): col = color[i] if is_scalar_sequence(hovertext): hov = hovertext[i] # If the legend is shared, only draw the legend of the first trace if shared_legend: showlegend = showlegend and i == 0 name_i = name else: name_i = f"{name} {i}" # Append list of traces traces.append( box_trace( lower, upper, draw_faces, linewidth=linewidth, color=col, hovertext=hov, cmin=cmin, cmax=cmax, legendgroup=legendgroup, showlegend=showlegend, name=name_i, **kwargs, ) ) return traces
[docs] def scatter_boxes( coords: np.ndarray, dimension: float | np.ndarray, draw_faces: bool = True, color: str | float | np.ndarray = "orange", hovertext: int | str | np.ndarray | None = None, linewidth: float = 2, shared_legend: bool = True, **kwargs: Any, ) -> list[go.Scatter3d | go.Mesh3d]: """Function which produces a list of plotly traces of boxes given a list of coordinates and a box dimension. This function assumes that the coordinates represent the lower bounds of the voxels they point at. This follows the `MinkowskiEngine` convention, which is the package used for space convolutions. This can be used to represent the PPN regions of interest in a space compressed by a factor (b_x, b_y, b_z) from the original image resolution. Parameters ---------- coords : np.ndarray (N, 3) Coordinates of in multiples of box lengths in each dimension dimension : Union[float, np.ndarray] Dimensions of the boxes. Specify it as either a single number (for cubes) or an array of values in each dimension, i.e. (b_x, b_y, b_z) draw_faces : bool, default True Weather or not to draw the box faces, or only the edges color : Union[str, np.ndarray], default 'orange' Color of boxes or list of color of boxes hovertext : Union[int, str, np.ndarray], optional Text associated with every box or each box linewidth : int, default 2 Width of the box edge lines shared_legend : bool, default True If True, the plotly legend of all boxes is shared as one **kwargs : dict, optional List of additional arguments to pass to :class:`plotly.graph_objs.Scatter3D` or :class:`plotly.graph_objs.Mesh3D`, depending on what the `draw_faces` parameter is set to. Returns ------- Union[List[plotly.graph_objs.Scatter3D], List[plotly.graph_objs.Mesh3D]] Box traces """ # Check the input if isinstance(dimension, (list, tuple, np.ndarray)): if len(dimension) != 3: raise ValueError("Must specify three dimensions for the box size.") dimension = np.asarray(dimension) # Compute the lower and upper boundaries, return box traces lowers = coords uppers = coords + dimension return box_traces( lowers, uppers, draw_faces=draw_faces, color=color, linewidth=linewidth, hovertext=hovertext, shared_legend=shared_legend, **kwargs, )