Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 3D_UNet
  • 3d_watershed
  • conv_zarr_tiff_folders
  • convert_tiff_folders
  • layered_surface_segmentation
  • main
  • memmap_txrm
  • notebook_update
  • notebooks
  • notebooksv1
  • optimize_scaleZYXdask
  • save_files_function
  • scaleZYX_mean
  • test
  • threshold-exploration
  • tr_val_te_splits
  • v0.2.0
  • v0.3.0
  • v0.3.1
  • v0.3.2
  • v0.3.3
  • v0.3.9
  • v0.4.0
  • v0.4.1
24 results

Target

Select target project
  • QIM/tools/qim3d
1 result
Select Git revision
  • 3D_UNet
  • 3d_watershed
  • conv_zarr_tiff_folders
  • convert_tiff_folders
  • layered_surface_segmentation
  • main
  • memmap_txrm
  • notebook_update
  • notebooks
  • notebooksv1
  • optimize_scaleZYXdask
  • save_files_function
  • scaleZYX_mean
  • test
  • threshold-exploration
  • tr_val_te_splits
  • v0.2.0
  • v0.3.0
  • v0.3.1
  • v0.3.2
  • v0.3.3
  • v0.3.9
  • v0.4.0
  • v0.4.1
24 results
Show changes
......@@ -91,6 +91,9 @@ def watershed(bin_vol: np.ndarray, min_distance: int = 5) -> tuple[np.ndarray, i
import skimage
import scipy
if len(np.unique(bin_vol)) > 2:
raise ValueError("bin_vol has to be binary volume - it must contain max 2 unique values.")
# Compute distance transform of binary volume
distance = scipy.ndimage.distance_transform_edt(bin_vol)
......@@ -125,6 +128,7 @@ def fade_mask(
geometry: str = "spherical",
invert: bool = False,
axis: int = 0,
**kwargs,
) -> np.ndarray:
"""
Apply edge fading to a volume.
......@@ -133,8 +137,10 @@ def fade_mask(
vol (np.ndarray): The volume to apply edge fading to.
decay_rate (float, optional): The decay rate of the fading. Defaults to 10.
ratio (float, optional): The ratio of the volume to fade. Defaults to 0.5.
geometric (str, optional): The geometric shape of the fading. Can be 'spherical' or 'cylindrical'. Defaults to 'spherical'.
geometry (str, optional): The geometric shape of the fading. Can be 'spherical' or 'cylindrical'. Defaults to 'spherical'.
invert (bool, optional): Flag for inverting the fading. Defaults to False.
axis (int, optional): The axis along which to apply the fading. Defaults to 0.
**kwargs: Additional keyword arguments for the edge fading.
Returns:
vol_faded (np.ndarray): The volume with edge fading applied.
......@@ -165,6 +171,9 @@ def fade_mask(
shape = vol.shape
z, y, x = np.indices(shape)
# Store the original maximum value of the volume
original_max_value = np.max(vol)
# Calculate the center of the array
center = np.array([(s - 1) / 2 for s in shape])
......@@ -177,10 +186,18 @@ def fade_mask(
distance_list = np.delete(distance_list, axis, axis=0)
distance = np.linalg.norm(distance_list, axis=0)
else:
raise ValueError("geometric must be 'spherical' or 'cylindrical'")
raise ValueError("Geometry must be 'spherical' or 'cylindrical'")
# Normalize the distances so that they go from 0 at the center to 1 at the farthest point
# Compute the maximum distance from the center
max_distance = np.linalg.norm(center)
# Compute ratio to make synthetic blobs exactly cylindrical
# target_max_normalized_distance = 1.4 works well to make the blobs cylindrical
if "target_max_normalized_distance" in kwargs:
target_max_normalized_distance = kwargs["target_max_normalized_distance"]
ratio = np.max(distance) / (target_max_normalized_distance * max_distance)
# Normalize the distances so that they go from 0 at the center to 1 at the farthest point
normalized_distance = distance / (max_distance * ratio)
# Apply the decay rate
......@@ -196,7 +213,10 @@ def fade_mask(
# Apply the fading to the volume
vol_faded = vol * fade_array
return vol_faded
# Normalize the volume to retain the original maximum value
vol_normalized = vol_faded * (original_max_value / np.max(vol_faded))
return vol_normalized
def overlay_rgb_images(
......
......@@ -2,7 +2,7 @@ import argparse
from qim3d.gui import data_explorer, iso3d, annotation_tool, local_thickness, layers2d
def main():
parser = argparse.ArgumentParser(description='Qim3d command-line interface.')
parser = argparse.ArgumentParser(description='qim3d command-line interface.')
subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
# subcommands
......
......@@ -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
......
......@@ -86,11 +86,17 @@ def objects(
```
![colormap objects](assets/screenshots/viz-colormaps-objects.gif)
Tip:
It can be easily used when calling visualization functions as
```python
qim3d.viz.slices(segmented_volume, cmap = 'objects')
```
which automatically detects number of unique classes
and creates the colormap object with defualt arguments.
Tip:
The `min_dist` parameter can be used to control the distance between neighboring colors.
![colormap objects mind_dist](assets/screenshots/viz-colormaps-min_dist.gif)
"""
from skimage import color
......
......@@ -15,11 +15,10 @@ import matplotlib
import numpy as np
import zarr
from qim3d.utils.logger import log
import seaborn as sns
import qim3d
def slices(
vol: np.ndarray,
axis: int = 0,
......@@ -33,9 +32,10 @@ def slices(
img_width: int = 2,
show: bool = False,
show_position: bool = True,
interpolation: Optional[str] = "none",
interpolation: Optional[str] = None,
img_size=None,
cbar: bool = False,
cbar_style: str = "small",
**imshow_kwargs,
) -> plt.Figure:
"""Displays one or several slices from a 3d volume.
......@@ -59,6 +59,7 @@ def slices(
show_position (bool, optional): If True, displays the position of the slices. Defaults to True.
interpolation (str, optional): Specifies the interpolation method for the image. Defaults to None.
cbar (bool, optional): Adds a colorbar positioned in the top-right for the corresponding colormap and data range. Defaults to False.
cbar_style (str, optional): Determines the style of the colorbar. Option 'small' is height of one image row. Option 'large' spans full height of image grid. Defaults to 'small'.
Returns:
fig (matplotlib.figure.Figure): The figure with the slices from the 3d array.
......@@ -68,6 +69,7 @@ def slices(
ValueError: If the axis to slice along is not a valid choice, i.e. not an integer between 0 and the number of dimensions of the volume minus 1.
ValueError: If the file or array is not a volume with at least 3 dimensions.
ValueError: If the `position` keyword argument is not a integer, list of integers or one of the following strings: "start", "mid" or "end".
ValueError: If the cbar_style keyword argument is not one of the following strings: 'small' or 'large'.
Example:
```python
......@@ -82,6 +84,11 @@ def slices(
img_height = img_size
img_width = img_size
# If we pass python None to the imshow function, it will set to
# default value 'antialiased'
if interpolation is None:
interpolation = 'none'
# Numpy array or Torch tensor input
if not isinstance(vol, (np.ndarray, da.core.Array)):
raise ValueError("Data type not supported")
......@@ -91,6 +98,10 @@ def slices(
"The provided object is not a volume as it has less than 3 dimensions."
)
cbar_style_options = ["small", "large"]
if cbar_style not in cbar_style_options:
raise ValueError(f"Value '{cbar_style}' is not valid for colorbar style. Please select from {cbar_style_options}.")
if isinstance(vol, da.core.Array):
vol = vol.compute()
......@@ -100,6 +111,19 @@ def slices(
f"Invalid value for 'axis'. It should be an integer between 0 and {vol.ndim - 1}."
)
if type(cmap) == matplotlib.colors.LinearSegmentedColormap or cmap == 'objects':
num_labels = len(np.unique(vol))
if cmap == 'objects':
cmap = qim3d.viz.colormaps.objects(num_labels)
# If vmin and vmax are not set like this, then in case the
# number of objects changes on new slice, objects might change
# colors. So when using a slider, the same object suddently
# changes color (flickers), which is confusing and annoying.
vmin = 0
vmax = num_labels
# Get total number of slices in the specified dimension
n_total = vol.shape[axis]
......@@ -145,9 +169,10 @@ def slices(
vol = vol.compute()
if cbar:
# In this case, we want the vrange to be constant across the slices, which makes them all comparable to a single cbar.
new_vmin = vmin if vmin else np.min(vol)
new_vmax = vmax if vmax else np.max(vol)
# In this case, we want the vrange to be constant across the
# slices, which makes them all comparable to a single cbar.
new_vmin = vmin if vmin is not None else np.min(vol)
new_vmax = vmax if vmax is not None else np.max(vol)
# Run through each ax of the grid
for i, ax_row in enumerate(axs):
......@@ -157,8 +182,9 @@ def slices(
slice_img = vol.take(slice_idxs[slice_idx], axis=axis)
if not cbar:
# If vmin is higher than the highest value in the image ValueError is raised
# We don't want to override the values because next slices might be okay
# If vmin is higher than the highest value in the
# image ValueError is raised. We don't want to
# override the values because next slices might be okay
new_vmin = (
None
if (isinstance(vmin, (float, int)) and vmin > np.max(slice_img))
......@@ -215,9 +241,11 @@ def slices(
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=UserWarning)
fig.tight_layout()
norm = matplotlib.colors.Normalize(vmin=new_vmin, vmax=new_vmax, clip=True)
mappable = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
if cbar_style =="small":
# Figure coordinates of top-right axis
tr_pos = np.atleast_1d(axs[0])[-1].get_position()
# The width is divided by ncols to make it the same relative size to the images
......@@ -225,6 +253,15 @@ def slices(
[tr_pos.x1 + 0.05 / ncols, tr_pos.y0, 0.05 / ncols, tr_pos.height]
)
fig.colorbar(mappable=mappable, cax=cbar_ax, orientation="vertical")
elif cbar_style == "large":
# Figure coordinates of bottom- and top-right axis
br_pos = np.atleast_1d(axs[-1])[-1].get_position()
tr_pos = np.atleast_1d(axs[0])[-1].get_position()
# The width is divided by ncols to make it the same relative size to the images
cbar_ax = fig.add_axes(
[br_pos.xmax + 0.05 / ncols, br_pos.y0+0.0015, 0.05 / ncols, (tr_pos.y1 - br_pos.y0)-0.0015]
)
fig.colorbar(mappable=mappable, cax=cbar_ax, orientation="vertical")
if show:
plt.show()
......@@ -259,7 +296,7 @@ def slicer(
img_height: int = 3,
img_width: int = 3,
show_position: bool = False,
interpolation: Optional[str] = "none",
interpolation: Optional[str] = None,
img_size=None,
cbar: bool = False,
**imshow_kwargs,
......@@ -775,3 +812,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)
```
![viz histogram](assets/screenshots/viz-histogram-vol.png)
```python
import qim3d
vol = qim3d.examples.bone_128x128x128
qim3d.viz.histogram(vol, bins=32, slice_idx="middle", axis=1, kde=False, log_scale=True)
```
![viz histogram](assets/screenshots/viz-histogram-slice.png)
"""
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
......@@ -83,7 +83,7 @@ def itk_vtk(
Opens a visualization window using the itk-vtk-viewer. Works both for common file types (Tiff, Nifti, etc.) and for **OME-Zarr stores**.
This function starts the itk-vtk-viewer, either using a global
installation or a local installation within the QIM package. It also starts
installation or a local installation within the qim3d package. It also starts
an HTTP server to serve the file to the viewer. Optionally, it can
automatically open a browser window to display the viewer. If the viewer
is not installed, it raises a NotInstalledError.
......
......@@ -8,6 +8,8 @@ Volumetric visualization using K3D
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import Colormap
from qim3d.utils.logger import log
from qim3d.utils.misc import downscale_img, scale_to_float16
......@@ -19,6 +21,7 @@ def vol(
save=False,
grid_visible=False,
cmap=None,
constant_opacity=False,
vmin=None,
vmax=None,
samples="auto",
......@@ -40,7 +43,8 @@ def vol(
If a string is provided, it's interpreted as the file path where the HTML
file will be saved. Defaults to False.
grid_visible (bool, optional): If True, the grid is visible in the plot. Defaults to False.
cmap (list, optional): The color map to be used for the volume rendering. Defaults to None.
cmap (str or matplotlib.colors.Colormap or list, optional): The color map to be used for the volume rendering. If a string is passed, it should be a matplotlib colormap name. Defaults to None.
constant_opacity (bool, float): Set to True if doing an object label visualization with a corresponding cmap; otherwise, the plot may appear poorly. Defaults to False.
vmin (float, optional): Together with vmax defines the data range the colormap covers. By default colormap covers the full range. Defaults to None.
vmax (float, optional): Together with vmin defines the data range the colormap covers. By default colormap covers the full range. Defaults to None
samples (int, optional): The number of samples to be used for the volume rendering in k3d. Defaults to 512.
......@@ -55,6 +59,9 @@ def vol(
Raises:
ValueError: If `aspectmode` is not `'data'` or `'cube'`.
Tip:
The function can be used for object label visualization using a `cmap` created with `qim3d.viz.colormaps.objects` along with setting `objects=True`. The latter ensures appropriate rendering.
Example:
Display a volume inline:
......@@ -122,6 +129,24 @@ def vol(
if vmax:
color_range[1] = vmax
# Handle the different formats that cmap can take
if cmap:
if isinstance(cmap, str):
cmap = plt.get_cmap(cmap) # Convert to Colormap object
if isinstance(cmap, Colormap):
# Convert to the format of cmap required by k3d.volume
attr_vals = np.linspace(0.0, 1.0, num=cmap.N)
RGB_vals = cmap(np.arange(0, cmap.N))[:, :3]
cmap = np.column_stack((attr_vals, RGB_vals)).tolist()
# Default k3d.volume settings
opacity_function = []
interpolation = True
if constant_opacity:
# without these settings, the plot will look bad when cmap is created with qim3d.viz.colormaps.objects
opacity_function = [0.0, float(constant_opacity), 1.0, float(constant_opacity)]
interpolation = False
# Create the volume plot
plt_volume = k3d.volume(
img,
......@@ -133,6 +158,8 @@ def vol(
color_map=cmap,
samples=samples,
color_range=color_range,
opacity_function=opacity_function,
interpolation=interpolation,
)
plot = k3d.plot(grid_visible=grid_visible, **kwargs)
plot += plt_volume
......@@ -202,9 +229,12 @@ def mesh(
raise ValueError("Faces array must have shape (M, 3)")
# Ensure the correct data types and memory layout
verts = np.ascontiguousarray(verts.astype(np.float32)) # Cast and ensure C-contiguous layout
faces = np.ascontiguousarray(faces.astype(np.uint32)) # Cast and ensure C-contiguous layout
verts = np.ascontiguousarray(
verts.astype(np.float32)
) # Cast and ensure C-contiguous layout
faces = np.ascontiguousarray(
faces.astype(np.uint32)
) # Cast and ensure C-contiguous layout
# Create the mesh plot
plt_mesh = k3d.mesh(
......