Source code for crispy.visualize

import numpy as np
from astropy.io import fits
from astropy.stats import median_absolute_deviation as mads
from plotly.subplots import make_subplots
import plotly.graph_objects as go

[docs] def render_point_cloud(cube, savename=None, showfig=False, bins=15, vmin=None, vmax=None, cmap="magma_r", z_stretch=1, size=2, fig=None, cbar_label="", min_opacity=0.01, max_opacity=0.3): """ Render a 3D scatter plot from a 3D data cube with efficient visualization. The function generates a 3D point cloud visualization from a 3D numpy array, coloring the points based on their values and assigning opacity based on percentile bins. A single colorscale spans all bins. Parameters ---------- cube : numpy.ndarray A 3D numpy array of float values to visualize. NaN values are ignored. savename : str, optional Path to save the interactive HTML file. If None, the figure is not saved. Default is None. showfig : bool, optional Whether to display the figure interactively. Default is False. bins : int, optional Number of percentile bins for dividing the data. Default is 15. vmin : float, optional Minimum value for normalization. If None, the 10th percentile of the data is used. Default is None. vmax : float, optional Maximum value for normalization. If None, the 99th percentile of the data is used. Default is None. cmap : str, optional Colormap for the data points. Uses Plotly-compatible colormap names. Default is "magma_r". z_stretch : float, optional Scaling factor for the Z-axis to modify aspect ratio. Default is 1. size : int, optional The marker (i.e., point) size used to render the point cloud. Default is 2 fig : plotly.graph_objects.Figure, optional Existing Plotly figure to add the scatter plot to. If None, a new figure is created. Default is None. cbar_label : str, optional Label for the colorbar. Default is an empty string. min_opacity : float, optional Minimum value of the marker opacity. Default is 0.01 max_opacity : float, optional Maximum value of the marker opacity. Default is 0.3 Returns ------- plotly.graph_objects.Figure The Plotly figure object containing the 3D scatter plot. Notes ----- - Points outside the range [vmin, vmax] are excluded. - Percentile bins determine point opacity for better depth visualization. - A shared color axis is used for consistency across bins. Examples -------- >>> import numpy as np >>> X, Y, Z = np.mgrid[-1:1:30j, -1:1:30j, -1:1:30j] >>> cube = np.sin(np.pi * X) * np.cos(np.pi * Z) * np.sin(np.pi * Y) >>> render_point_cloud(cube, showfig=True, bins=4) """ # ensures plotly-compatible endianness cube = _ensure_endianness(cube) # Flatten the array and remove NaN values valid_values = cube[np.isfinite(cube)].flatten() # Determine vmin and vmax if not provided if vmin is None: vmin = np.percentile(valid_values, 10) if vmax is None: # filter outliers and add a small padding vpad = 0.5 / bins if bins < 5 else 0.1 vmax = np.percentile(valid_values, 99.5) * (1 + vpad) # Mask values outside the range [vmin, vmax] mask = np.isfinite(cube) & (cube >= vmin) & (cube <= vmax) masked_values = cube[mask] z, y, x = np.where(mask) # ensures plotly-compatible endianness x = _ensure_endianness(x) y = _ensure_endianness(y) z = _ensure_endianness(z) # Compute percentiles and opacity levels percentiles = np.linspace(0, 100, bins + 1) cutoffs = np.percentile(masked_values, percentiles) opacity_levels = np.linspace(min_opacity, max_opacity, bins) # Use provided figure or create a new one if fig is None: fig = go.Figure() # Add a scatter trace for each bin for i in range(bins): bin_mask = (masked_values >= cutoffs[i]) & (masked_values < cutoffs[i + 1]) sub_x = x[bin_mask] sub_y = y[bin_mask] sub_z = z[bin_mask] sub_values = masked_values[bin_mask] fig.add_trace(go.Scatter3d( x=sub_x, y=sub_y, z=sub_z, mode='markers', marker=dict( size=size, color=sub_values, colorscale=cmap, cmin=vmin, cmax=vmax, opacity=opacity_levels[i], coloraxis="coloraxis", # Link to the shared color axis showscale = False # don't show colorbar for individual bins ), name=f'Bin {i + 1} ({cutoffs[i]:.2f}-{cutoffs[i + 1]:.2f})', showlegend=False )) # Adjust aspect ratio based on array dimensions shape = cube.shape max_dim = max(shape) fig.update_layout( scene=dict( xaxis=dict(range=[0, shape[2]], title='X'), yaxis=dict(range=[0, shape[1]], title='Y'), zaxis=dict(range=[0, shape[0]], title='Z'), aspectratio=dict( x=shape[2] / max_dim, y=shape[1] / max_dim, z=shape[0] / max_dim * z_stretch ) ), coloraxis=dict( colorscale=cmap, cmin=vmin, cmax=vmax, colorbar=dict(title=cbar_label) ), # Define a shared color axis title="", showlegend=True ) # Save the figure if savename is provided if savename is not None: fig.write_html(savename) # Show the figure if showfig is True if showfig: fig.show() return fig
[docs] def ridge_trace_3D(x, y, z, size=2.5, color='black', opacity=0.5, name='ridges'): """ Create a 3D scatter trace for visualizing ridge points. Parameters ---------- x, y, z : array-like Coordinates of the ridge points in 3D space. size : int, optional Marker size. Default is 2. color : str, optional Marker color. Default is "black". opacity : float, optional Opacity of the markers. Default is 0.5. name : str, optional Name for the trace. Default is "ridge". Returns ------- plotly.graph_objects.Scatter3d A Plotly 3D scatter trace. """ # create trace, with plotly-compatible endianness ensured trace = go.Scatter3d( x=_ensure_endianness(x), y=_ensure_endianness(y), z=_ensure_endianness(z), mode='markers', marker=dict( size=size, # Marker size color=color, # Static color opacity=opacity ), name=name, showlegend = False ) return trace
[docs] def mask_trace_3D(mask3D, size=2.5, color='black', opacity=0.9, name='spines'): """ Generate a 3D scatter plot of True-valued positions in a 3D boolean array. Parameters ---------- mask3D : numpy.ndarray A 3D boolean numpy array where True values indicate points to be plotted. size : int, optional Size of the scatter plot markers. Default is 2. color : str, optional Color of the markers. Default is "black". opacity : float, optional Opacity of the markers, ranging from 0 (transparent) to 1 (opaque). Default is 0.9. name : str, optional Label for the trace in the Plotly legend. Default is "spines". Returns ------- plotly.graph_objects.Scatter3d A Plotly 3D scatter trace representing the masked points. """ # Find the indices of True values in the 3D array z, y, x = np.where(mask3D) # Create a scatter plot, ensures plotly-compatible endianness trace = go.Scatter3d( x=_ensure_endianness(x), # x-coordinates y=_ensure_endianness(y), # y-coordinates z=_ensure_endianness(z), # z-coordinates mode='markers', marker=dict( size=size, # Size of the markers color=color, # Color of the markers symbol='circle', opacity=opacity ), name=name, showlegend = False ) return trace
def _get_xyz(cube): """ Generate 3D coordinate grids for a data cube. """ im = cube nx, ny, nz = im.shape[2], im.shape[1], im.shape[0] z, y, x = np.meshgrid(np.arange(0, nz, 1), np.arange(0, ny, 1), np.arange(0, nx, 1), indexing='ij') # ensures plotly-compatible endianness x = _ensure_endianness(x) y = _ensure_endianness(y) z = _ensure_endianness(z) return x, y, z
[docs] def skel_volume(image, savename=None, showfig=True, opacity=0.75, colorscale='inferno', fig=None, z_stretch=1, cbar_label=''): """ Render a 3D skeleton volume using isosurface visualization. This function provides a specialized version of `render_volume()` for visualizing skeleton-like structures or spines in 3D datasets. It simplifies the parameterization for this specific use case. Parameters ---------- image : numpy.ndarray A 3D binary or float array representing the skeleton structure. If the data type is boolean, it will be converted to an integer array for visualization. savename : str, optional Path to save the interactive HTML file. If None, the figure is not saved. Default is None. showfig : bool, optional Whether to display the figure interactively. Default is True. opacity : float, optional Opacity of the skeleton isosurfaces. Default is 0.75. colorscale : str, optional Colormap for the isosurfaces. Uses Plotly-compatible colormap names. Default is "inferno". fig : plotly.graph_objects.Figure, optional Existing Plotly figure to add the volume rendering to. If None, a new figure is created. Default is None. z_stretch : float, optional Scaling factor for the Z-axis to modify aspect ratio. Default is 1. cbar_label : str, optional Label for the colorbar. Default is an empty string. Returns ------- plotly.graph_objects.Figure The Plotly figure object containing the 3D skeleton volume rendering. Notes ----- - This function is a wrapper around `render_volume()` with preset parameters tailored for skeleton visualization. - If `image` contains NaN values, they will be handled by `render_volume()`. Examples -------- >>> import numpy as np >>> image = np.random.random((50, 50, 50)) > 0.95 # Generate a random binary skeleton >>> skel_volume(image, showfig=True, opacity=0.5) """ if isinstance(image, bool): image = image.astype(np.uint8) # ensures plotly-compatible endianness image = _ensure_endianness(image) return render_volume(image, savename=savename, showfig=showfig, vmin=1e-3, vmax=1e-2, surface_count=1, opacity=opacity, colorscale=colorscale, showscale=False, fig=fig, z_stretch=z_stretch, cbar_label=cbar_label)
[docs] def render_volume(cube, savename=None, showfig=False, vmin=None, vmax=None, surface_count=12, opacity=None, colorscale='YlGnBu', z_stretch=1, showscale=True, fig=None, val_fill=0.0, cbar_label=''): """ Render a 3D volume using layers of isosurfaces. This function creates a 3D interactive volume rendering visualization of a 3D data cube by plotting multiple isosurfaces at specified value intervals. Parameters ---------- cube : numpy.ndarray or str A 3D numpy array of float values or a file path to a FITS file. If the input is a file path, the data will be loaded and used. NaN values are replaced with `val_fill` before rendering. savename : str, optional Path to save the interactive HTML file. If None, the figure is not saved. Default is None. showfig : bool, optional Whether to display the figure interactively. Default is False. vmin : float, optional Minimum isosurface value. If None, the 10-sigma level above the estimated RMS is used. Default is None. vmax : float, optional Maximum isosurface value. If None, the 99.99th percentile of the data is used. Default is None. surface_count : int, optional Number of isosurfaces to plot. Higher values create finer visualization but increase rendering cost. Default is 12. opacity : float, optional Opacity of the isosurfaces. If None, it is set to `2 / surface_count` for semi-transparency. Default is None. colorscale : str, optional Colormap for the isosurfaces. Uses Plotly-compatible colormap names. Default is "YlGnBu". z_stretch : float, optional Scaling factor for the Z-axis to modify aspect ratio. Default is 1. showscale : bool, optional Whether to display the color scale bar. Default is True. fig : plotly.graph_objects.Figure, optional Existing Plotly figure to add the volume rendering to. If None, a new figure is created. Default is None. val_fill : float, optional Value to replace NaN voxels in the cube. Default is 0.0. cbar_label : str, optional Label for the colorbar. Default is an empty string. Returns ------- plotly.graph_objects.Figure The Plotly figure object containing the 3D volume rendering. Notes ----- - NaN values in the cube are replaced with `val_fill` before visualization. - Isosurface values range between `vmin` and `vmax`, divided into `surface_count` levels. - The visualization requires non-NaN input data for accurate results. Examples -------- >>> import numpy as np >>> X, Y, Z = np.mgrid[-1:1:30j, -1:1:30j, -1:1:30j] >>> cube = np.sin(np.pi * X) * np.cos(np.pi * Z) * np.sin(np.pi * Y) >>> render_volume(cube, showfig=True, surface_count=10) """ if isinstance(cube, str): cube, hdr = fits.getdata(cube, header=True) cube = cube.copy() # note: the visualization may not work if NaN values are present. Try replacing NaN with values like zeros. cube[np.isnan(cube)] = val_fill X, Y, Z = _get_xyz(cube) # x,y,z endianess already checked # ensures plotly-compatible endianness cube = _ensure_endianness(cube) if fig is None: fig = make_subplots(rows=1, cols=1) if vmax is None: # use 99.99 percentile vmax = np.percentile(cube, 99.99) print("no vamx provided, using the 99.99 percentile value: {}".format(np.round(vmax,2))) if vmin is None: # use the estimated 10-sigma vmin = mads(cube, ignore_nan=True)*10.0 if vmin > vmax: vmin = vmax/2.0 print("no vmin provided, using the 10 sigma above the rms value: {}".format(np.round(vmin,2))) if opacity is None: if surface_count > 2: opacity = 2.0/surface_count else: opacity = 1.0 fig.add_volume(x=X.flatten(), y=Y.flatten(), z=Z.flatten(), value=cube.flatten(), colorscale=colorscale, isomin=vmin, isomax=vmax, opacity=opacity, surface_count=surface_count, showscale = showscale ) # Adjust aspect ratio based on cube dimensions shape = cube.shape max_dim = max(shape) fig.update_layout( scene=dict( xaxis=dict(range=[0, shape[2]], title='X'), yaxis=dict(range=[0, shape[1]], title='Y'), zaxis=dict(range=[0, shape[0]], title='Z'), aspectratio=dict( x=shape[2] / max_dim, y=shape[1] / max_dim, z=shape[0] / max_dim * z_stretch ) ), coloraxis=dict( colorscale=colorscale, cmin=vmin, cmax=vmax, colorbar=dict(title=cbar_label) ), # Define a shared color axis title="", showlegend=True ) fig.update_scenes(zaxis_autorange="reversed") if savename is not None: fig.write_html(savename) if showfig: fig.show() return fig
def _ensure_endianness(data): return np.ascontiguousarray(data, dtype=data.dtype.newbyteorder('='))