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
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
Show changes

Commits on Source 22

27 files
+ 476
92
Compare changes
  • Side-by-side
  • Inline

Files

+3 −2
Original line number Diff line number Diff line
# Qim3D (Quantitative Imaging in 3D)
# qim3D (Quantitative Imaging in 3D)

The `qim3d` (kɪm θriː diː) library is designed to make it easier to work with 3D imaging data in Python. It offers a range of features, including data loading and manipulation, image processing and filtering, visualization of 3D data, and analysis of imaging results.

@@ -8,6 +8,7 @@ Whether you are working with medical imaging data, materials science data, or an

Documentation available at https://platform.qim.dk/qim3d/

For more information on the QIM center visit https://qim.dk/

# License
This project is licensed under the MIT License.
+1 −1
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ This offers quick interactions, making it ideal for tasks that require efficienc
| `--anotation-tool` | Starts the annotation tool |
| `--layers` | Starts the tool for segmenting layers |
| `--host` | Desired host for the server. By default runs on `0.0.0.0`  |
| `--platform` | Uses the Qim platform API for a unique path and port depending on the username |
| `--platform` | Uses the QIM platform API for a unique path and port depending on the username |


!!! Example
+21 −9
Original line number Diff line number Diff line
@@ -137,6 +137,16 @@ The latest stable version can be simply installed using `pip`. Open your termina
!!! note
    The base installation of `qim3d` does not include deep-learning dependencies, keeping the library lighter for scenarios where they are unnecessary. If you need to use deep-learning features, you can install the additional dependencies by running: **`pip install qim3d['deep-learning']`**

After completing the installation, you can verify its success by running one or both of the following commands:

    qim3d

or:

    pip show qim3d

If either command displays information about the qim3d library, the installation was successful.

### Troubleshooting

Here are some solutions for commonly found issues during installation and usage of `qim3d`.
@@ -204,14 +214,16 @@ This project is licensed under the [MIT License](https://lab.compute.dtu.dk/QIM/
Below is a list of contributors to the project, arranged in chronological order of their first commit to the repository:

| Author                      |   Commits | First commit |
|:--------------------------|----------:|-------------:|
| Felipe Delestro           |       195 | 2023-05-12   |
|:----------------------------|----------:|-------------:|
| Felipe Delestro             |       231 | 2023-05-12   |
| Stefan Engelmann Jensen     |        29 | 2023-06-29   |
| Oskar Kristoffersen         |        15 | 2023-07-05   |
| Christian Kento Rasmussen   |        22 | 2024-02-01   |
| Alessia Saccardo          |         7 | 2024-02-19   |
| David Grundfest           |         8 | 2024-04-12   |
| Anna Bøgevang Ekner       |         5 | 2024-04-18   |
| Alessia Saccardo            |        13 | 2024-02-19   |
| David Grundfest             |        16 | 2024-04-12   |
| Anna Bøgevang Ekner         |         6 | 2024-04-18   |
| David Diamond Wang Johansen |         1 | 2024-10-31   |


## Support

Original line number Diff line number Diff line
@@ -9,6 +9,14 @@ As the library is still in its early development stages, **there may be breaking

And remember to keep your pip installation [up to date](/qim3d/#get-the-latest-version) so that you have the latest features!

### v0.4.5 (21/11/2024)

- Updated Layer surface segmentation GUI
- Sphericity as feature from volumes
- Colorbar for visualization functions
- Chunk visualization tool
- Histogram visualization

### v0.4.4 (11/10/2024)

- Introduction of `itk-vtk-viewer` for OME-Zarr data visualization 🎉 ![itk-vtk-viewer](assets/screenshots/itk-vtk-viewer.gif)
+1 −0
Original line number Diff line number Diff line
@@ -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
+2 −2
Original line number Diff line number Diff line
site_name: qim3d documentation
site_url: https://platform.qim.dk/qim3d/
site_author: Qim3d contributors
site_description: Documentation for the Qim3d python library
site_author: qim3d contributors
site_description: Documentation for the qim3d python library
repo_url: https://lab.compute.dtu.dk/QIM/tools/qim3d
repo_name: Gitlab

Original line number Diff line number Diff line
@@ -8,7 +8,7 @@ Documentation available at https://platform.qim.dk/qim3d/

"""

__version__ = "0.4.4"
__version__ = "0.4.5"


import importlib as _importlib
+2 −2
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@ def parse_tuple(arg):


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")

    # GUIs
@@ -167,7 +167,7 @@ def main():

            except qim3d.viz.NotInstalledError as err:
                print(err)
                message = "Itk-vtk-viewer is not installed or qim3d can not find it.\nYou can either:\n\to  Use 'qim3d viz SOURCE -m k3d' to display data using different method\n\to  Install itk-vtk-viewer yourself following https://kitware.github.io/itk-vtk-viewer/docs/cli.html#Installation\n\to  Let QIM3D install itk-vtk-viewer now (it will also install node.js in qim3d library)\nDo you want QIM3D to install itk-vtk-viewer now?"
                message = "Itk-vtk-viewer is not installed or qim3d can not find it.\nYou can either:\n\to  Use 'qim3d viz SOURCE -m k3d' to display data using different method\n\to  Install itk-vtk-viewer yourself following https://kitware.github.io/itk-vtk-viewer/docs/cli.html#Installation\n\to  Let qim3D install itk-vtk-viewer now (it will also install node.js in qim3d library)\nDo you want qim3D to install itk-vtk-viewer now?"
                print(message)
                answer = input("[Y/n]:")
                if answer in "Yy":
Original line number Diff line number Diff line
@@ -2,6 +2,8 @@ import numpy as np
import scipy.ndimage
from noise import pnoise3

import qim3d.processing

def blob(
    base_shape: tuple = (128, 128, 128),
    final_shape: tuple = (128, 128, 128),
@@ -11,6 +13,7 @@ def blob(
    max_value: int = 255,
    threshold: float = 0.5,
    smooth_borders: bool = False,
    object_shape: str = None,
    dtype: str = "uint8",
    ) -> np.ndarray:
    """
@@ -25,6 +28,7 @@ def blob(
        max_value (int, optional): Maximum value for the volume intensity. Defaults to 255.
        threshold (float, optional): Threshold value for clipping low intensity values. Defaults to 0.5.
        smooth_borders (bool, optional): Flag for automatic computation of the threshold value to ensure a blob with no straight edges. If True, the `threshold` parameter is ignored. Defaults to False.
        object_shape (str, optional): Shape of the object to generate, either "cylinder", or "tube". Defaults to None.
        dtype (str, optional): Desired data type of the output volume. Defaults to "uint8".

    Returns:
@@ -41,17 +45,64 @@ def blob(
        # Generate synthetic blob
        synthetic_blob = qim3d.generate.blob(noise_scale = 0.015)

        # Visualize 3D volume
        qim3d.viz.vol(synthetic_blob)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_blob.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Visualize slices
        qim3d.viz.slices(synthetic_blob, vmin = 0, vmax = 255, n_slices = 15)
        ```
        ![synthetic_blob](assets/screenshots/synthetic_blob_slices.png)

    Example:
        ```python
        # Visualize 3D volume
        qim3d.viz.vol(synthetic_blob)
        import qim3d

        # Generate tubular synthetic blob
        vol = qim3d.generate.blob(base_shape = (10, 300, 300),
                                final_shape = (100, 100, 100),
                                noise_scale = 0.3,
                                gamma = 2,
                                threshold = 0.0,
                                object_shape = "cylinder"
                                )

        # Visualize synthetic blob
        qim3d.viz.vol(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_blob.html" width="100%" height="500" frameborder="0"></iframe>
        <iframe src="https://platform.qim.dk/k3d/synthetic_blob_cylinder.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Visualize slices
        qim3d.viz.slices(vol, n_slices=15, axis=1)
        ```
        ![synthetic_blob_cylinder_slice](assets/screenshots/synthetic_blob_cylinder_slice.png)

    Example:
        ```python
        import qim3d

        # Generate tubular synthetic blob
        vol = qim3d.generate.blob(base_shape = (200, 100, 100),
                                final_shape = (400, 100, 100),
                                noise_scale = 0.03,
                                gamma = 0.12,
                                threshold = 0.85,
                                object_shape = "tube"
                                )

        # Visualize synthetic blob
        qim3d.viz.vol(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_blob_tube.html" width="100%" height="500" frameborder="0"></iframe>
        
        ```python
        # Visualize
        qim3d.viz.slices(vol, n_slices=15)
        ```
        ![synthetic_blob_tube_slice](assets/screenshots/synthetic_blob_tube_slice.png)    
    """

    if not isinstance(final_shape, tuple) or len(final_shape) != 3:
@@ -93,6 +144,10 @@ def blob(
    # Scale the volume to the maximum value
    volume = volume * max_value

    # If object shape is specified, smooth borders are disabled
    if object_shape:
        smooth_borders = False

    if smooth_borders: 
        # Maximum value among the six sides of the 3D volume
        max_border_value = np.max([
@@ -115,4 +170,46 @@ def blob(
        volume, np.array(final_shape) / np.array(base_shape), order=order
    )

    return volume.astype(dtype)
    # Fade into a shape if specified
    if object_shape == "cylinder":

        # Arguments for the fade_mask function
        geometry = "cylindrical"        # Fade in cylindrical geometry
        axis = np.argmax(volume.shape)  # Fade along the dimension where the object is the largest
        target_max_normalized_distance = 1.4   # This value ensures that the object will become cylindrical

        volume = qim3d.processing.operations.fade_mask(volume, 
                                                       geometry = geometry, 
                                                       axis = axis, 
                                                       target_max_normalized_distance = target_max_normalized_distance
                                                       )

    elif object_shape == "tube":

        # Arguments for the fade_mask function
        geometry = "cylindrical"        # Fade in cylindrical geometry
        axis = np.argmax(volume.shape)  # Fade along the dimension where the object is the largest
        decay_rate = 5                  # Decay rate for the fade operation
        target_max_normalized_distance = 1.4   # This value ensures that the object will become cylindrical

        # Fade once for making the object cylindrical
        volume = qim3d.processing.operations.fade_mask(volume, 
                                                       geometry = geometry, 
                                                       axis = axis,
                                                       decay_rate = decay_rate,
                                                       target_max_normalized_distance = target_max_normalized_distance,
                                                       invert = False
                                                       )

        # Fade again with invert = True for making the object a tube (i.e. with a hole in the middle)
        volume = qim3d.processing.operations.fade_mask(volume, 
                                                       geometry = geometry, 
                                                       axis = axis, 
                                                       decay_rate = decay_rate,
                                                       invert = True
                                                       )
        
    # Convert to desired data type
    volume = volume.astype(dtype)

    return volume
 No newline at end of file
Original line number Diff line number Diff line
@@ -139,6 +139,7 @@ def collection(
    min_threshold: float = 0.5,
    max_threshold: float = 0.6,
    smooth_borders: bool = False,
    object_shape: str = None,
    seed: int = 0,
    verbose: bool = False,
) -> tuple[np.ndarray, object]:
@@ -163,30 +164,30 @@ def collection(
        max_high_value (int, optional): Maximum maximum value for the volume intensity. Defaults to 255.
        min_threshold (float, optional): Minimum threshold value for clipping low intensity values. Defaults to 0.5.
        max_threshold (float, optional): Maximum threshold value for clipping low intensity values. Defaults to 0.6.
        smooth_borders (bool, optional): Flag for smoothing blob borders to avoid straight edges in the objects. If True, the `min_threshold` and `max_threshold` parameters are ignored. Defaults to False.
        smooth_borders (bool, optional): Flag for smoothing object borders to avoid straight edges in the objects. If True, the `min_threshold` and `max_threshold` parameters are ignored. Defaults to False.
        object_shape (str, optional): Shape of the object to generate, either "cylinder", or "tube". Defaults to None.
        seed (int, optional): Seed for reproducibility. Defaults to 0.
        verbose (bool, optional): Flag to enable verbose logging. Defaults to False.


    Returns:
        synthetic_collection (numpy.ndarray): 3D volume of the generated collection of synthetic objects with specified parameters.
        labels (numpy.ndarray): Array with labels for each voxel, same shape as synthetic_collection.

    Raises:
        TypeError: If `collection_shape` is not 3D.
        ValueError: If blob parameters are invalid.
        ValueError: If object parameters are invalid.

    Note:
        - The function places objects without overlap.
        - The function can either place objects at random positions in the collection (if `positions = None`) or at specific positions provided in the `positions` argument. If specific positions are provided, the number of blobs must match the number of positions (e.g. `num_objects = 2` with `positions = [(12, 8, 10), (24, 20, 18)]`).
        - If not all `num_objects` can be placed, the function returns the `synthetic_collection` volume with as many blobs as possible in it, and logs an error.
        - Labels for all objects are returned, even if they are not a sigle connected component.
        - If not all `num_objects` can be placed, the function returns the `synthetic_collection` volume with as many objects as possible in it, and logs an error.
        - Labels for all objects are returned, even if they are not a single connected component.

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of blobs
        # Generate synthetic collection of objects
        num_objects = 15
        synthetic_collection, labels = qim3d.generate.collection(num_objects = num_objects)

@@ -207,12 +208,11 @@ def collection(
        ```
        ![synthetic_collection](assets/screenshots/synthetic_collection_default_labels.gif)


    Example:
        ```python
        import qim3d

        # Generate synthetic collection of dense blobs
        # Generate synthetic collection of dense objects
        synthetic_collection, labels = qim3d.generate.collection(
                                    min_high_value = 255,
                                    max_high_value = 255,
@@ -228,34 +228,66 @@ def collection(
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_dense.html" width="100%" height="500" frameborder="0"></iframe>

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of cylindrical structures
        vol, labels = qim3d.generate.collection(num_objects = 40,
                                                collection_shape = (300, 150, 150),
                                                min_shape = (280, 10, 10),
                                                max_shape = (290, 15, 15),
                                                min_object_noise = 0.08,
                                                max_object_noise = 0.09,
                                                max_rotation_degrees = 5,
                                                min_threshold = 0.7,
                                                max_threshold = 0.9,
                                                min_gamma = 0.10,
                                                max_gamma = 0.11,
                                                object_shape = "cylinder"
                                                )

        # Visualize synthetic collection
        qim3d.viz.vol(vol)

        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_cylinder.html" width="100%" height="500" frameborder="0"></iframe>
        
        ```python
        # Visualize slices
        qim3d.viz.slices(vol, n_slices=15)
        ```
        ![synthetic_collection_cylinder](assets/screenshots/synthetic_collection_cylinder_slices.png)    
        
    Example:
        ```python
        import qim3d

        # Generate synthetic collection of tubular structures
        synthetic_collection, labels = qim3d.generate.collection(
                                    num_objects=10,
                                    collection_shape=(200,100,100),
                                    min_shape = (190, 50, 50),
                                    max_shape = (200, 60, 60),
                                    object_shape_zoom = (1, 0.2, 0.2),
                                    min_object_noise = 0.01,
                                    max_object_noise = 0.02,
                                    max_rotation_degrees=10,
                                    min_threshold = 0.95,
                                    max_threshold = 0.98,
                                    min_gamma = 0.02,
                                    max_gamma = 0.03
        # Generate synthetic collection of tubular (hollow) structures
        vol, labels = qim3d.generate.collection(num_objects = 10,
                                                collection_shape = (200, 200, 200),
                                                min_shape = (180, 25, 25),
                                                max_shape = (190, 35, 35),
                                                min_object_noise = 0.02,
                                                max_object_noise = 0.03,
                                                max_rotation_degrees = 5,
                                                min_threshold = 0.7,
                                                max_threshold = 0.9,
                                                min_gamma = 0.10,
                                                max_gamma = 0.11,
                                                object_shape = "tube"
                                                )

        # Visualize synthetic collection
        qim3d.viz.vol(synthetic_collection)
        qim3d.viz.vol(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_tubular.html" width="100%" height="500" frameborder="0"></iframe>

        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_tube.html" width="100%" height="500" frameborder="0"></iframe>
        
        ```python
        # Visualize slices
        qim3d.viz.slices(vol, n_slices=15, axis=1)
        ```
        ![synthetic_collection_tube](assets/screenshots/synthetic_collection_tube_slices.png)
    """
    if verbose:
        original_log_level = log.getEffectiveLevel()
@@ -270,10 +302,6 @@ def collection(
    if len(min_shape) != len(max_shape):
        raise ValueError("Object shapes must be tuples of the same length")

    # if not isinstance(blob_shapes, list) or \
    #     len(blob_shapes) != 2 or len(blob_shapes[0]) != 3 or len(blob_shapes[1]) != 3:
    #     raise TypeError("Blob shapes must be a list of two tuples with three dimensions (z, y, x)")

    if (positions is not None) and (len(positions) != num_objects):
        raise ValueError(
            "Number of objects must match number of positions, otherwise set positions = None"
@@ -301,6 +329,10 @@ def collection(
            )
        log.debug(f"- Blob shape: {blob_shape}")

        # Scale object shape
        final_shape = tuple(l * r for l, r in zip(blob_shape, object_shape_zoom))
        final_shape = tuple(int(x) for x in final_shape) # NOTE: Added this 

        # Sample noise scale
        noise_scale = rng.uniform(low=min_object_noise, high=max_object_noise)
        log.debug(f"- Object noise scale: {noise_scale:.4f}")
@@ -317,15 +349,16 @@ def collection(
        threshold = rng.uniform(low=min_threshold, high=max_threshold)
        log.debug(f"- Threshold: {threshold:.3f}")

        # Generate synthetic blob
        # Generate synthetic object
        blob = qim3d.generate.blob(
            base_shape=blob_shape,
            final_shape=tuple(l * r for l, r in zip(blob_shape, object_shape_zoom)),
            final_shape=final_shape,
            noise_scale=noise_scale,
            gamma=gamma,
            max_value=max_value,
            threshold=threshold,
            smooth_borders=smooth_borders,
            object_shape=object_shape,
        )

        # Rotate object
@@ -336,21 +369,21 @@ def collection(
            axes = rng.choice(rotation_axes)  # Sample the two axes to rotate around
            log.debug(f"- Rotation angle: {angle:.2f} at axes: {axes}")

            blob = scipy.ndimage.rotate(blob, angle, axes, order=0)
            blob = scipy.ndimage.rotate(blob, angle, axes, order=1)

        # Place synthetic object into the collection
        # If positions are specified, place blob at one of the specified positions
        # If positions are specified, place object at one of the specified positions
        collection_before = collection_array.copy()
        if positions:
            collection_array, placed, positions = specific_placement(
                collection_array, blob, positions
            )

        # Otherwise, place blob at a random available position
        # Otherwise, place object at a random available position
        else:
            collection_array, placed = random_placement(collection_array, blob, rng)

        # Break if blob could not be placed
        # Break if object could not be placed
        if not placed:
            break

Original line number Diff line number Diff line
@@ -57,7 +57,7 @@ class BaseInterface(ABC):
            If defined, the interface will be launched with the image already there
            This argument is used especially in jupyter notebooks, where you can launch
            interface in loop with different picture every step
        force_light_mode: The qim platform doesn't have night mode. The qim_theme thus
        force_light_mode: The QIM platform doesn't have night mode. The QimTheme thus
            has option to display only light mode so it corresponds with the website. Preferably
            will be removed as we add night mode to the website.
        """
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@ import gradio as gr

class QimTheme(gr.themes.Default):
    """
    QIM3D Theme for gradio interface
    Theme for qim3d gradio interfaces.
    The theming options are quite broad. However if there is something you can not achieve with this theme
    there is a possibility to add some more css if you override _get_css_theme function as shown at the bottom
    in comments.
@@ -12,7 +12,7 @@ class QimTheme(gr.themes.Default):
        Parameters:
        -----------
        - force_light_mode (bool, optional): Gradio themes have dark mode by default. 
                Qim platform is not ready for dark mode yet, thus the tools should also be in light mode.
                QIM platform is not ready for dark mode yet, thus the tools should also be in light mode.
                This sets the darkmode values to be the same as light mode values.
        """
        super().__init__()
Original line number Diff line number Diff line
@@ -413,7 +413,8 @@ def save(
    """Save data to a specified file path.

    Args:
        path (str): The path to save file to
        path (str): The path to save file to. File format is chosen based on the extension. 
            Supported extensions are: <em>'.tif', '.tiff', '.nii', '.nii.gz', '.h5', '.vol', '.vgi', '.dcm', '.DCM', '.zarr', '.jpeg', '.jpg', '.png'</em>
        data (numpy.ndarray): The data to be saved
        replace (bool, optional): Specifies if an existing file with identical path should be replaced.
            Default is False.
@@ -425,6 +426,13 @@ def save(
            as several files (only relevant for TIFF stacks). Default is 0, i.e., the first dimension.
        **kwargs: Additional keyword arguments to be passed to the DataSaver constructor

    Raises:
        ValueError: If the provided path is an existing directory and self.basename is not provided <strong>OR</strong>
         If the file format is not supported <strong>OR</strong>
         If the provided path does not exist and self.basename is not provided <strong>OR</strong>
         If a file extension is not provided <strong>OR</strong>
         if a file with the specified path already exists and replace=False.

    Example:
        ```python
        import qim3d
Original line number Diff line number Diff line
@@ -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(
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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

Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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.
+36 −6
Original line number Diff line number Diff line
@@ -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(