diff --git a/docs/assets/screenshots/viz-histogram-slice.png b/docs/assets/screenshots/viz-histogram-slice.png new file mode 100644 index 0000000000000000000000000000000000000000..0ddf7b495be7f5782e913cdd7a03ed113d972604 Binary files /dev/null and b/docs/assets/screenshots/viz-histogram-slice.png differ diff --git a/docs/assets/screenshots/viz-histogram-vol.png b/docs/assets/screenshots/viz-histogram-vol.png new file mode 100644 index 0000000000000000000000000000000000000000..332a4d24d1d85075e58eb898ee80236f0e670c69 Binary files /dev/null and b/docs/assets/screenshots/viz-histogram-vol.png differ diff --git a/docs/assets/screenshots/viz-histogram.png b/docs/assets/screenshots/viz-histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..d27cc4575424b1cdf0c5e5c9cef2d820c89a8012 Binary files /dev/null and b/docs/assets/screenshots/viz-histogram.png differ diff --git a/docs/viz.md b/docs/viz.md index 3efe4276bbb332c3b31e01dfb3c9d0a0dabf9e8c..8b6788b60610d4eee757505ca28bdac5b8691572 100644 --- a/docs/viz.md +++ b/docs/viz.md @@ -4,6 +4,7 @@ The `qim3d` library aims to provide easy ways to explore and get insights from v ::: qim3d.viz options: members: + - histogram - slices - slicer - orthogonal diff --git a/qim3d/viz/__init__.py b/qim3d/viz/__init__.py index 84b2e8356e5f9dd082ef3df17c8960c14f352b9e..33d94416d98cdf1af5d2c9fca548416eee65a359 100644 --- a/qim3d/viz/__init__.py +++ b/qim3d/viz/__init__.py @@ -7,6 +7,7 @@ from .explore import ( slicer, slices, chunks, + histogram, ) from .itk_vtk_viewer import itk_vtk, Installer, NotInstalledError from .k3d import vol, mesh diff --git a/qim3d/viz/explore.py b/qim3d/viz/explore.py index d7392cbdb92ca249c872f97f45ff138216c66940..23425112365a2b4d87e7f6db84ebe810e4158d67 100644 --- a/qim3d/viz/explore.py +++ b/qim3d/viz/explore.py @@ -15,7 +15,7 @@ import matplotlib import numpy as np import zarr from qim3d.utils.logger import log - +import seaborn as sns import qim3d @@ -775,3 +775,133 @@ def chunks(zarr_path: str, **kwargs): # Display the VBox display(final_layout) + + +def histogram( + vol: np.ndarray, + bins: Union[int, str] = "auto", + slice_idx: Union[int, str] = None, + axis: int = 0, + kde: bool = True, + log_scale: bool = False, + despine: bool = True, + show_title: bool = True, + color="qim3d", + edgecolor=None, + figsize=(8, 4.5), + element="step", + return_fig=False, + show=True, + **sns_kwargs, +): + """ + Plots a histogram of voxel intensities from a 3D volume, with options to show a specific slice or the entire volume. + + Utilizes [seaborn.histplot](https://seaborn.pydata.org/generated/seaborn.histplot.html) for visualization. + + Args: + vol (np.ndarray): A 3D NumPy array representing the volume to be visualized. + bins (Union[int, str], optional): Number of histogram bins or a binning strategy (e.g., "auto"). Default is "auto". + axis (int, optional): Axis along which to take a slice. Default is 0. + slice_idx (Union[int, str], optional): Specifies the slice to visualize. If an integer, it represents the slice index along the selected axis. + If "middle", the function uses the middle slice. If None, the entire volume is visualized. Default is None. + kde (bool, optional): Whether to overlay a kernel density estimate. Default is True. + log_scale (bool, optional): Whether to use a logarithmic scale on the y-axis. Default is False. + despine (bool, optional): If True, removes the top and right spines from the plot for cleaner appearance. Default is True. + show_title (bool, optional): If True, displays a title with slice information. Default is True. + color (str, optional): Color for the histogram bars. If "qim3d", defaults to the qim3d color. Default is "qim3d". + edgecolor (str, optional): Color for the edges of the histogram bars. Default is None. + figsize (tuple, optional): Size of the figure (width, height). Default is (8, 4.5). + element (str, optional): Type of histogram to draw ('bars', 'step', or 'poly'). Default is "step". + return_fig (bool, optional): If True, returns the figure object instead of showing it directly. Default is False. + show (bool, optional): If True, displays the plot. If False, suppresses display. Default is True. + **sns_kwargs: Additional keyword arguments for `seaborn.histplot`. + + Returns: + Optional[matplotlib.figure.Figure]: If `return_fig` is True, returns the generated figure object. Otherwise, returns None. + + Raises: + ValueError: If `axis` is not a valid axis index (0, 1, or 2). + ValueError: If `slice_idx` is an integer and is out of range for the specified axis. + + Example: + ```python + import qim3d + + vol = qim3d.examples.bone_128x128x128 + qim3d.viz.histogram(vol) + ``` +  + + ```python + import qim3d + + vol = qim3d.examples.bone_128x128x128 + qim3d.viz.histogram(vol, bins=32, slice_idx="middle", axis=1, kde=False, log_scale=True) + ``` +  + """ + + if not (0 <= axis < vol.ndim): + raise ValueError(f"Axis must be an integer between 0 and {vol.ndim - 1}.") + + if slice_idx == "middle": + slice_idx = vol.shape[axis] // 2 + + if slice_idx: + if 0 <= slice_idx < vol.shape[axis]: + img_slice = np.take(vol, indices=slice_idx, axis=axis) + data = img_slice.ravel() + title = f"Intensity histogram of slice #{slice_idx} {img_slice.shape} along axis {axis}" + else: + raise ValueError( + f"Slice index out of range. Must be between 0 and {vol.shape[axis] - 1}." + ) + else: + data = vol.ravel() + title = f"Intensity histogram for whole volume {vol.shape}" + + fig, ax = plt.subplots(figsize=figsize) + + if log_scale: + plt.yscale("log") + + if color == "qim3d": + color = qim3d.viz.colormaps.qim(1.0) + + sns.histplot( + data, + bins=bins, + kde=kde, + color=color, + element=element, + edgecolor=edgecolor, + **sns_kwargs, + ) + + if despine: + sns.despine( + fig=None, + ax=None, + top=True, + right=True, + left=False, + bottom=False, + offset={"left": 0, "bottom": 18}, + trim=True, + ) + + plt.xlabel("Voxel Intensity") + plt.ylabel("Frequency") + + if show_title: + plt.title(title, fontsize=10) + + # Handle show and return + if show: + plt.show() + else: + plt.close(fig) + + if return_fig: + return fig