diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0068784 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,79 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +# -- Path Info --------------------------------------------------------------- +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('../')) +# sys.path.insert(0, os.path.abspath('../../')) +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'DPM_Tools' +copyright = '2024, Digital Porous Media Team' +author = 'Digital Porous Media Team' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.mathjax', + 'sphinx_copybutton', + 'sphinx_design', + 'myst_nb', +] + +add_module_names = False # dpm_tools.visualization -> visualization +autosummary_generate = True +globaltoc_maxdepth = 2 + +templates_path = ['_templates'] +master_doc = 'index' +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False +# A list of ignored prefixes for module index sorting. +modindex_common_prefix = ['dpm_tools'] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'karma_sphinx_theme' +html_static_path = ['_static'] +html_domain_indices = True +html_use_index = True +html_split_index = False +# +# html_theme_options = { +# "icon_links": [ +# { +# "name": "GitHub", +# "url": "https://github.com/digital-porous-media/dpm_tools", +# "icon": "fab fa-github-square", +# }, +# ], +# "external_links": [ +# { +# "name": "Issue Tracker", "url": "https://github.com/digital-porous-media/dpm_tools/issues" +# }, +# { +# "name": "Get Help", "url": "https://github.com/digital-porous-media/dpm_tools/discussions" +# }, +# ], +# "navigation_with_keys": False, +# "show_prev_next": False, +# "icon_links_label": "Quick Links", +# "use_edit_page_button": False, +# "search_bar_position": "sidebar", +# "navbar_align": "left", +# } diff --git a/docs/dpm_tools.io.rst b/docs/dpm_tools.io.rst new file mode 100644 index 0000000..652d098 --- /dev/null +++ b/docs/dpm_tools.io.rst @@ -0,0 +1,37 @@ +dpm\_tools.io package +===================== + +Submodules +---------- + +dpm\_tools.io.io\_utils module +------------------------------ + +.. automodule:: dpm_tools.io.io_utils + :members: + :undoc-members: + :show-inheritance: + +dpm\_tools.io.read\_data module +------------------------------- + +.. automodule:: dpm_tools.io.read_data + :members: + :undoc-members: + :show-inheritance: + +dpm\_tools.io.write\_data module +-------------------------------- + +.. automodule:: dpm_tools.io.write_data + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: dpm_tools.io + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/dpm_tools.morphology.rst b/docs/dpm_tools.morphology.rst new file mode 100644 index 0000000..e525796 --- /dev/null +++ b/docs/dpm_tools.morphology.rst @@ -0,0 +1,21 @@ +dpm\_tools.morphology package +============================= + +Submodules +---------- + +dpm\_tools.morphology.features module +------------------------------------- + +.. automodule:: dpm_tools.morphology.features + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: dpm_tools.morphology + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/dpm_tools.rst b/docs/dpm_tools.rst new file mode 100644 index 0000000..3908310 --- /dev/null +++ b/docs/dpm_tools.rst @@ -0,0 +1,20 @@ +dpm\_tools package +================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + dpm_tools.io + dpm_tools.morphology + dpm_tools.visualization + +Module contents +--------------- + +.. automodule:: dpm_tools + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/dpm_tools.visualization.rst b/docs/dpm_tools.visualization.rst new file mode 100644 index 0000000..ffdfcb7 --- /dev/null +++ b/docs/dpm_tools.visualization.rst @@ -0,0 +1,29 @@ +dpm\_tools.visualization package +================================ + +Submodules +---------- + +dpm\_tools.visualization.plot\_2d module +---------------------------------------- + +.. automodule:: dpm_tools.visualization.plot_2d + :members: + :undoc-members: + :show-inheritance: + +dpm\_tools.visualization.plot\_3d module +---------------------------------------- + +.. automodule:: dpm_tools.visualization.plot_3d + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: dpm_tools.visualization + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..19915d3 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +.. DPM_Tools documentation master file, created by + sphinx-quickstart on Mon Feb 19 10:35:17 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to DPM_Tools's documentation! +===================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..0f8bbe4 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +dpm_tools +========= + +.. toctree:: + :maxdepth: 4 + + dpm_tools diff --git a/dpm_tools/__init__.py b/dpm_tools/__init__.py index c664b7f..c6469cc 100644 --- a/dpm_tools/__init__.py +++ b/dpm_tools/__init__.py @@ -1,7 +1,9 @@ -# import io -# import visualization from time import perf_counter_ns +from . import morphology +from . import io +from . import visualization + def _get_version(): from .__version__ import __version__ suffix = "dev" diff --git a/dpm_tools/io/__init__.py b/dpm_tools/io/__init__.py index d4dbd9f..5b4c413 100644 --- a/dpm_tools/io/__init__.py +++ b/dpm_tools/io/__init__.py @@ -1,13 +1,5 @@ -# from .io_utils import _find_files -# from .io_utils import _find_tiff_files -# from .io_utils import _evaluate_dimensions -# from .io_utils import _sort_files -# from .io_utils import _combine_slices -from .io_utils import convert_filetype +from .io_utils import * -from .read_data import read_image -from .read_data import Image -from .read_data import ImageFromFile -from .read_data import Vector +from .read_data import * -from .write_data import write_image +from .write_data import * diff --git a/dpm_tools/io/read_data.py b/dpm_tools/io/read_data.py index 372c148..79f2619 100644 --- a/dpm_tools/io/read_data.py +++ b/dpm_tools/io/read_data.py @@ -7,6 +7,12 @@ from dataclasses import dataclass, field from collections.abc import Iterable +# __all__ = [ +# 'read_image', +# 'ImageFromFile', +# 'Image', +# 'Vector' +# ] def _read_tiff(filepath: str, full_path: bool = True, **kwargs) -> np.ndarray: """ A utility function to read in TIFF files diff --git a/dpm_tools/morphology/__init__.py b/dpm_tools/morphology/__init__.py index 91f3f6a..98d3fec 100644 --- a/dpm_tools/morphology/__init__.py +++ b/dpm_tools/morphology/__init__.py @@ -1,9 +1,4 @@ -from .features import edt -from .features import slicewise_edt -from .features import slicewise_mis -from .features import chords -from .features import constriction_factor -from .features import tof +from .features import * -from ._feature_utils import _get_trend +from ._feature_utils import * diff --git a/dpm_tools/morphology/_feature_utils.py b/dpm_tools/morphology/_feature_utils.py index d067988..6cbb0f8 100644 --- a/dpm_tools/morphology/_feature_utils.py +++ b/dpm_tools/morphology/_feature_utils.py @@ -1,21 +1,28 @@ import numpy as np +__all__ = ['_set_linear_trend'] -def _get_trend(data, inlet_value: float = 2, outlet_value: float = 1, grid_shift: bool = True) -> np.ndarray: + +def _set_linear_trend(data, inlet_value: float = 2, outlet_value: float = 1, grid_shift: bool = True) -> np.ndarray: """ - Get a linear trend through the domain - Assumes grain voxels are 0 + Set a linear trend through the foreground of a 3D image. Phases labeled 0 will be masked out. + :param data: Image dataclass + :param inlet_value: Value to set at the inlet + :param outlet_value: Value to set at the outlet + :param grid_shift: If True, shift the inlet and outlet values by 1/nz + :return: Linear trend through foreground of the image + :rtype: numpy.ndarray """ linear = np.zeros_like(data.image, dtype=np.float32) if grid_shift: - inlet_value = 2 - 1/data.nz - outlet_value = 1 - 1/data.nz + inlet_value = 2 - 1 / data.nz + outlet_value = 1 - 1 / data.nz tmp = np.linspace(inlet_value, outlet_value, data.nz) linear = np.broadcast_to(tmp, data.image.shape) - #for tmp_slice, i in enumerate(tmp): + # for tmp_slice, i in enumerate(tmp): # linear[:, :, tmp_slice] = np.ones((data.nx, data.ny)) * i mask = (data.img != 0) @@ -23,4 +30,3 @@ def _get_trend(data, inlet_value: float = 2, outlet_value: float = 1, grid_shift linear = linear * mask return linear - diff --git a/dpm_tools/morphology/features.py b/dpm_tools/morphology/features.py index 3dffb20..693b058 100644 --- a/dpm_tools/morphology/features.py +++ b/dpm_tools/morphology/features.py @@ -1,13 +1,14 @@ import numpy as np import porespy as ps -from scipy.ndimage.morphology import distance_transform_edt as edist -from typing import Tuple -from ..__init__ import timer +from edt import edt as edist +from typing import Tuple, Literal -@timer def slicewise_edt(data) -> np.ndarray: """ - Returns Euclidean distance transform map of each slice individually + Compute the Euclidean distance transform map of each slice individually and stacks them into a single 3D array. + :param data: Image dataclass + :return: Euclidean distance transform map of each slice + :rtype: numpy.ndarray """ edt = np.zeros_like(data.image, dtype=np.float32) for s in range(data.image.shape[2]): @@ -15,38 +16,46 @@ def slicewise_edt(data) -> np.ndarray: return edt -@timer + def edt(data) -> np.ndarray: """ - Returns Euclidean distance transform map of the entire 3D image + Compute the 3D Euclidean distance transform map of the entire image. + :param data: Image dataclass + :return: Euclidean distance transform map of the entire image + :rtype: numpy.ndarray """ return edist(data.image) -@timer + def slicewise_mis(data, **kwargs) -> np.ndarray: """ - A function that calculates the slice-wise maximum inscribed sphere (maximum inscribed disk) + Compute the slice-wise maximum inscribed sphere (maximum inscribed disk) using PoreSpy. + :param data: Image dataclass + :return: Maximum inscribed sphere computed on each slice (maximum inscribed disk) + :rtype: numpy.ndarray """ - # TODO why do we pad this? + # ? why do we pad this? input_image = np.pad(array=data.image.copy(), pad_width=((0, 0), (0, 0), (0, 1)), constant_values=1) # Calculate slice-wise local thickness from PoreSpy thickness = np.zeros_like(input_image) for img_slice in range(data.nz + 1): - thickness[:, :, img_slice] = ps.filters.local_thickness(input_image[:, :, img_slice], sizes=40, mode='hybrid', - divs=4) + thickness[:, :, img_slice] = ps.filters.local_thickness(input_image[:, :, img_slice], **kwargs) return thickness -@timer -def chords(data, **kwargs) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + +def chords(data) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ - A function that calculates the ellipse area based on the chord lengths in slices orthogonal to direction of flow + Compute the ellipse area based on the chord lengths in slices orthogonal to direction of flow Assumes img = 1 for pore space, 0 for grain space - Returns length of chords in x, y, and ellipse areas + :param data: Image dataclass + :return: length of chords in x, y, and ellipse areas + :rtype: Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray] """ ellipse_area = np.zeros_like(data.image, dtype=np.float32) - + sz_x = np.zeros_like(ellipse_area) + sz_y = np.zeros_like(ellipse_area) for i in range(data.nz): # Calculate the chords in x and y for each slice in z chords_x = ps.filters.apply_chords(im=data.image[:, :, i], spacing=0, trim_edges=False, axis=0) @@ -57,16 +66,22 @@ def chords(data, **kwargs) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: sz_y = ps.filters.region_size(chords_y) # Calculate ellipse area from chords - ellipse_area[:, :, i] = np.pi/4 * sz_x * sz_y + ellipse_area[:, :, i] = np.pi / 4 * sz_x * sz_y return sz_x, sz_y, ellipse_area -@timer -def tof(data, boundary: str = 'l', detrend: bool = True) -> np.ndarray: + +def tof(data, boundary: Literal['l', 'r'] = 'l', detrend: bool = True) -> np.ndarray: """ - Get time of flight map (solution to Eikonal equation) from specified boundary (inlet or outlet) + Compute time of flight map (solution to Eikonal equation) from specified boundary (inlet or outlet) Assumes img = 1 in pore space, 0 for grain space Assumes flow is in z direction (orthogonal to axis 2) + :param data: Image dataclass + :param boundary: Left ('l') or right ('r') boundaries corresponding to the inlet or outlet + :param detrend: If ``detrend`` is True, then subtract the solution assuming no solid matrix is present in the image. + The solid matrix is still masked. + :return: Time of flight map from specified boundary + :rtype: numpy.ndarray """ inlet = np.zeros_like(data.image) if boundary[0].lower() == 'l': @@ -87,17 +102,20 @@ def tof(data, boundary: str = 'l', detrend: bool = True) -> np.ndarray: return tof_map -@timer -def constriction_factor(thickness_map: np.ndarray, power: float = None) -> np.ndarray: + +def constriction_factor(thickness_map: np.ndarray, power: float = 1.0) -> np.ndarray: """ - A function that calculates the slice-wise constriction factor from the input thickness map. - Constriction factor defined as thickness[x, y, z] / thickness[x, y, z+1] + Compute the slice-wise constriction factor from the input thickness map. + Constriction factor is defined as thickness[x, y, z] / thickness[x, y, z+1] Padded with reflected values at outlet + :param thickness_map: Any 3D thickness map. Examples could be slice-wise EDT, MIS, etc. + :param power: Power to raise the thickness map + :return: Slice-wise constriction factor + :rtype: numpy.ndarray """ thickness_map = np.pad(thickness_map.copy(), ((0, 0), (0, 0), (0, 1)), 'reflect') - if power is not None: - thickness_map = np.power(thickness_map, power) + thickness_map = np.power(thickness_map, power) constriction_map = np.divide(thickness_map[:, :, :-1], thickness_map[:, :, 1:]) @@ -108,10 +126,3 @@ def constriction_factor(thickness_map: np.ndarray, power: float = None) -> np.nd constriction_map[np.isnan(constriction_map)] = 0 return constriction_map - -if __name__ == '__main__': - sim = np.fromfile('D:/pore_features/sp_micromodel/sim/elecpot.raw') - plt.imshow(sim[2]) - plt.show() - # bin = np.fromfile('D:/pore_features/sp_micromodel/sim/segmented.raw') - diff --git a/dpm_tools/visualization/_3d_vis_utils.py b/dpm_tools/visualization/_3d_vis_utils.py index 530e329..5eff50d 100644 --- a/dpm_tools/visualization/_3d_vis_utils.py +++ b/dpm_tools/visualization/_3d_vis_utils.py @@ -2,12 +2,16 @@ import pyvista as pv from matplotlib import cm from matplotlib.colors import ListedColormap +from typing import Tuple +import pathlib -def _initialize_plotter(bg: str = 'w', *args, **kwargs): + +def _initialize_plotter(bg: str = 'w', *args, **kwargs) -> pv.Plotter: """ A helper function to initialize a PyVista Plotter object - Default background set to white - Returns a plotter object + :param bg: The background color to set the plotter to. Defaults to white. + :returns: A PyVista Plotter object with specified bg and kwargs. + :rtype: pyvista.Plotter """ # Initialize PV object plotter_obj = pv.Plotter(*args, **kwargs) @@ -24,10 +28,23 @@ def _initialize_plotter(bg: str = 'w', *args, **kwargs): def _wrap_array(img: np.ndarray) -> pv.DataSet: + """ + A helper function to wrap a NumPy array to a PyVista object. + :param img: The NumPy array to wrap. + :returns: A PyVista DataSet object with the wrapped array. + :rtype: pyvista.DataSet + """ return pv.wrap(img) -def _custom_cmap(vector, color_map: str = 'turbo'): +def _custom_cmap(vector, color_map: str = 'turbo') -> Tuple[ListedColormap, float, float]: + """ + Compute a custom color map based on the vector magnitude. + :param vector: A NumPy array containing the vector data. + :param color_map: The color map to use. Defaults to 'turbo'. + :returns: A PyVista colormap object and the minimum and maximum vector magnitude. + :rtype: (pyvista.Colormap, float, float) + """ vector_magnitude = np.sqrt(np.einsum('ij,ij->i', vector, vector)) log_mag = np.log10(vector_magnitude[vector_magnitude != 0]) @@ -42,20 +59,34 @@ def _custom_cmap(vector, color_map: str = 'turbo'): return new_cmap, 10 ** min_magnitude, 10 ** max_magnitude -def _show_3d(plotter_obj, filepath="", take_screenshot=False, interactive=False, **kwargs): - +def _show_3d(plotter_obj: pv.Plotter, filepath: pathlib.Path, take_screenshot: bool = False, interactive: bool = False, + **kwargs) -> None: + """ + A helper function to show a 3D plot with the option to take a screenshot + :param plotter_obj: The PyVista Plotter object to show. + :param filepath: The filepath to save the gif to. + :param take_screenshot: If ``take_screenshot`` is ``True``, this function will take a screenshot of the plot. + :param interactive: The function shows the plot interactively by default. This can lead to issues when taking a + screenshot. So we can set it to ``False`` if you don't want to show the plot interactively when taking a screenshot. + :returns: None + """ if take_screenshot: cpos = plotter_obj.show(interactive=interactive, return_cpos=True, - screenshot=filepath, **kwargs) + screenshot=filepath, **kwargs) print(cpos) else: cpos = plotter_obj.show(interactive=True, return_cpos=True) print(cpos) -def _initialize_kwargs(plotter_kwargs: dict = None, mesh_kwargs: dict = None): + +def _initialize_kwargs(plotter_kwargs: dict = None, mesh_kwargs: dict = None) -> Tuple[dict, dict]: """ - Utility function to initialize kwargs for PyVista plotting + Utility function to initialize default kwargs for PyVista plotting + :param plotter_kwargs: A dictionary of kwargs to pass to the PyVista Plotter object. + :param mesh_kwargs: A dictionary of kwargs to pass to the PyVista Mesh object. + :returns: Dictionaries of default kwargs to pass to the PyVista Plotter object and PyVista + :rtype: Tuple[dict, dict] """ if plotter_kwargs is None: plotter_kwargs = {} @@ -68,6 +99,3 @@ def _initialize_kwargs(plotter_kwargs: dict = None, mesh_kwargs: dict = None): 'ambient': 0.15} return plotter_kwargs, mesh_kwargs - - - diff --git a/dpm_tools/visualization/__init__.py b/dpm_tools/visualization/__init__.py index fe9fd7b..4e042a1 100644 --- a/dpm_tools/visualization/__init__.py +++ b/dpm_tools/visualization/__init__.py @@ -1,19 +1,7 @@ -from .plot_2d import hist -from .plot_2d import plot_slice -from .plot_2d import make_thumbnail -from .plot_2d import make_gif +from .plot_2d import * -from .plot_3d import orthogonal_slices -from .plot_3d import plot_isosurface -from .plot_3d import bounding_box -from .plot_3d import plot_glyph -from .plot_3d import plot_streamlines -from .plot_3d import plot_scalar_volume +from .plot_3d import * -from ._vis_utils import _make_dir -from ._vis_utils import _write_hist_csv +from ._vis_utils import * -from ._3d_vis_utils import _initialize_plotter -from ._3d_vis_utils import _wrap_array - -from ..__init__ import timer \ No newline at end of file +from ._3d_vis_utils import * diff --git a/dpm_tools/visualization/_vis_utils.py b/dpm_tools/visualization/_vis_utils.py index b7699a6..31dcd7b 100644 --- a/dpm_tools/visualization/_vis_utils.py +++ b/dpm_tools/visualization/_vis_utils.py @@ -1,13 +1,15 @@ import os import csv import numpy as np -from matplotlib import pyplot as plt -import matplotlib.animation as anim +import pathlib -def _make_dir(dir_name: str) -> str: +def _make_dir(dir_name: pathlib.Path) -> pathlib.Path: """ A utility function to create a directory of name if it does not already exist + :param dir_name: The name of the directory to be created + :return: The path to the directory + :rtype: pathlib.Path """ if not os.path.isdir(dir_name): print('Creating new directory...') @@ -18,9 +20,13 @@ def _make_dir(dir_name: str) -> str: return dir_name -def _write_hist_csv(freq: list, bins: np.ndarray, filename: str) -> None: +def _write_hist_csv(freq: list, bins: np.ndarray, filename: pathlib.Path) -> None: """ A utility function to write out the histogram to a csv file + :param freq: List of frequencies to write to the csv file. + :param bins: Bin edges of the histogram + :param filename: The path to the csv file + :return: None """ with open(filename + '.csv', 'w', newline='') as csvfile: histwriter = csv.writer(csvfile, delimiter=',') @@ -36,7 +42,12 @@ def _scale_image(image_data: np.ndarray, scale_to: type = np.uint8) -> np.ndarra A utility function to scale the data to a different datatype range Default converts to uint8 Allows thumbnails and animated gif to show properly + :param image_data: The 3D image to be scaled + :param scale_to: The datatype to scale the image to + :return: A copy of the image scaled to the specified datatype + :rtype: np.ndarray """ + # assert image_data.dtype.type is not scale_to, f"Image data is already of type {scale_to.__name__}" if 'int' in scale_to.__name__: diff --git a/dpm_tools/visualization/plot_2d.py b/dpm_tools/visualization/plot_2d.py index 5c9a053..eaf6601 100644 --- a/dpm_tools/visualization/plot_2d.py +++ b/dpm_tools/visualization/plot_2d.py @@ -5,12 +5,10 @@ from tqdm import tqdm from ._vis_utils import _make_dir, _write_hist_csv, _scale_image -from ..__init__ import timer # TODO Add fig save decorator -@timer def hist(data, data2: np.array = None, nbins: int = 256, @@ -18,6 +16,13 @@ def hist(data, **kwargs): """ Generate a histogram + + Parameters: + ___ + :data: The data to plot histogram for. + :param data2: The data to plot histogram for. + :param nbins: The number of bins for the histogram. + If save_fig is True, a save path should be supplied to kwargs under key "filepath" Use data2 for adding a second distribution and plotting them together """ @@ -61,7 +66,6 @@ def hist(data, return fig -@timer def plot_slice(data, slice_num: int = None, slice_axis: int = 0, **kwargs): if 'origin' not in kwargs: @@ -87,7 +91,6 @@ def plot_slice(data, slice_num: int = None, slice_axis: int = 0, **kwargs): return fig -@timer def make_thumbnail(data, thumb_slice: int = None, fig_size: tuple = (1, 1), slice_axis: int = 0, **kwargs): if thumb_slice is None: thumb_slice = int(np.floor(data.image.shape[slice_axis] / 2)) @@ -105,7 +108,6 @@ def make_thumbnail(data, thumb_slice: int = None, fig_size: tuple = (1, 1), slic return fig -@timer def make_gif(data, dpi: int = 96, **kwargs): """ Function to make and save a gif diff --git a/dpm_tools/visualization/plot_3d.py b/dpm_tools/visualization/plot_3d.py index b8f5061..025de28 100644 --- a/dpm_tools/visualization/plot_3d.py +++ b/dpm_tools/visualization/plot_3d.py @@ -1,30 +1,27 @@ import numpy as np -import matplotlib.pyplot as plt import pyvista as pv -from ._3d_vis_utils import _initialize_plotter, _wrap_array, _custom_cmap, _initialize_kwargs -from ..__init__ import timer +from ._3d_vis_utils import _initialize_plotter, _wrap_array, _custom_cmap import warnings -@timer def orthogonal_slices(data, fig: pv.DataSet = None, show_slices: list = None, plotter_kwargs: dict = None, mesh_kwargs: dict = None, slider = False) -> pv.Plotter: """ Plots 3 orthogonal slices of a 3D image. + Parameters: data: A dataclass containing 3D image data fig: Pyvista plotter object show_slices: List of slices in x, y, z to show. Default is middle slice in each direction. plotter_kwargs: Additional keyword arguments to pass to the plotter. mesh_kwargs: Pyvista mesh keyword arguments to pass to the plotter. + Returns: fig: PyVista plotter object with added orthogonal slice mesh. """ if show_slices is None: show_slices = [data.nx // 2, data.ny // 2, data.nz // 2] - - #plotter_kwargs, mesh_kwargs = _initialize_kwargs(plotter_kwargs, mesh_kwargs) # Overriding the above line because it prevents orthogonal slices from showing for some reason. if plotter_kwargs is None: @@ -132,11 +129,11 @@ def update(self): return fig -@timer def plot_isosurface(data, fig: pv.Plotter = None, show_isosurface: list = None, mesh_kwargs: dict = None, plotter_kwargs: dict = None) -> pv.Plotter: """ Plots 3D isosurfaces + Parameters: data: A dataclass containing 3D labeled image data fig: Pyvista plotter object @@ -179,7 +176,6 @@ def plot_isosurface(data, fig: pv.Plotter = None, show_isosurface: list = None, return fig -@timer def bounding_box(data, fig: pv.Plotter = None, mesh_kwargs: dict = None, plotter_kwargs: dict = None) -> pv.Plotter: """ Add a bounding box mesh to the Plotter. Assumes the isosurface is at 255. @@ -212,7 +208,6 @@ def bounding_box(data, fig: pv.Plotter = None, mesh_kwargs: dict = None, plotter return fig -@timer def plot_glyph(vector_data, fig: pv.Plotter = None, glyph: pv.PolyData = None, glyph_space: int = 1, glyph_kwargs: dict = None, mesh_kwargs: dict = None, plotter_kwargs: dict = None) -> pv.Plotter: """ @@ -296,7 +291,6 @@ def plot_glyph(vector_data, fig: pv.Plotter = None, glyph: pv.PolyData = None, g return fig -@timer def plot_streamlines(vector_data, fig: pv.Plotter = None, tube_radius: float = None, streamline_kwargs: dict = None, mesh_kwargs: dict = None, plotter_kwargs: dict = None) -> pv.Plotter: """ @@ -353,7 +347,6 @@ def plot_streamlines(vector_data, fig: pv.Plotter = None, tube_radius: float = N return fig -@timer def plot_scalar_volume(data, fig: pv.Plotter = None, mesh_kwargs: dict = None, plotter_kwargs: dict = None) -> pv.Plotter: """ diff --git a/Tutorial_3d.py b/examples/Tutorial_3d.py similarity index 97% rename from Tutorial_3d.py rename to examples/Tutorial_3d.py index 2af9ddc..c00036c 100644 --- a/Tutorial_3d.py +++ b/examples/Tutorial_3d.py @@ -18,7 +18,7 @@ # *************************************************************************** # ## Reading an image by passing the directory to the function read_image: -image = read_image('./data/35_1.tiff') +image = read_image('../data/35_1.tiff') ## Converting the image to "Image" class img_sample = Image(image) @@ -75,9 +75,9 @@ # *************************************************************************** # ## Importing the velocity data -vx = np.load('data/3d_spherepack_project175/vx_rsz_100.npy') -vy = np.load('data/3d_spherepack_project175/vy_rsz_100.npy') -vz = np.load('data/3d_spherepack_project175/vz_rsz_100.npy') +vx = np.load('../data/3d_spherepack_project175/vx_rsz_100.npy') +vy = np.load('../data/3d_spherepack_project175/vy_rsz_100.npy') +vz = np.load('../data/3d_spherepack_project175/vz_rsz_100.npy') ## Getting the magnitude v = np.sqrt(vx**2 + vy**2 + vz**2) diff --git a/requirements.txt b/requirements.txt index d1336d9..f25e756 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy >= 1.21.5 matplotlib >= 3.5.0 -pyvista >= 0.42.3 +pyvista[all] >= 0.42.3 pandas~=1.4.2 exifread == 3.0.0 @@ -8,5 +8,6 @@ tifffile >= 2021.7.2 netcdf4 >= 1.6.0 hdf5storage == 0.1.18 -pillow~=9.0.1 tqdm~=4.64.0 +porespy +edt