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

Target

Select target project
  • QIM/tools/qim3d
1 result
Show changes
Commits on Source (11)
Showing
with 360 additions and 228 deletions
...@@ -13,6 +13,7 @@ build/ ...@@ -13,6 +13,7 @@ build/
.idea/ .idea/
.cache/ .cache/
.pytest_cache/ .pytest_cache/
.ruff_cache/
*.swp *.swp
*.swo *.swo
*.pyc *.pyc
......
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: detect-private-key
- id: check-added-large-files
- id: check-docstring-first
- id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.7
hooks:
# Run the formatter and fix code styling
- id: ruff-format
# Run the linter and fix what is possible
- id: ruff
args: ['--fix']
\ No newline at end of file
docs/assets/screenshots/interactive_thresholding.gif

2.57 MiB

docs/assets/screenshots/pygel3d_visualization.png

197 KiB

docs/assets/screenshots/viz-line_profile.gif

4.74 MiB

...@@ -44,7 +44,7 @@ The command line interface allows you to run graphical user interfaces directly ...@@ -44,7 +44,7 @@ The command line interface allows you to run graphical user interfaces directly
!!! Example !!! Example
Here's an example of how to open the [Data Explorer](gui.md#qim3d.gui.data_explorer) Here's an example of how to open the [Data Explorer](../gui/gui.md#qim3d.gui.data_explorer)
``` title="Command" ``` title="Command"
qim3d gui --data-explorer qim3d gui --data-explorer
......
...@@ -8,5 +8,5 @@ ...@@ -8,5 +8,5 @@
- Downloader - Downloader
- export_ome_zarr - export_ome_zarr
- import_ome_zarr - import_ome_zarr
- save_mesh - load_mesh
- load_mesh - save_mesh
\ No newline at end of file \ No newline at end of file
...@@ -21,7 +21,7 @@ The `qim3d` library provides a set of custom made GUIs that ease the interaction ...@@ -21,7 +21,7 @@ The `qim3d` library provides a set of custom made GUIs that ease the interaction
``` ```
In general, the GUIs can be launched directly from the command line. In general, the GUIs can be launched directly from the command line.
For details see [here](cli.md#qim3d-gui). For details see [here](../cli/cli.md#qim3d-gui).
::: qim3d.gui.data_explorer ::: qim3d.gui.data_explorer
options: options:
......
...@@ -11,7 +11,7 @@ hide: ...@@ -11,7 +11,7 @@ hide:
Below, you'll find details about the version history of `qim3d`. Below, you'll find details about the version history of `qim3d`.
Remember to keep your pip installation [up to date](index.md/#get-the-latest-version) so that you have the latest features! Remember to keep your pip installation [up to date](../../index.md/#get-the-latest-version) so that you have the latest features!
### v1.0.0 (21/01/2025) ### v1.0.0 (21/01/2025)
......
...@@ -23,6 +23,8 @@ The `qim3d` library aims to provide easy ways to explore and get insights from v ...@@ -23,6 +23,8 @@ The `qim3d` library aims to provide easy ways to explore and get insights from v
- plot_cc - plot_cc
- colormaps - colormaps
- fade_mask - fade_mask
- line_profile
- threshold
::: qim3d.viz.colormaps ::: qim3d.viz.colormaps
options: options:
......
# See list of rules here: https://docs.astral.sh/ruff/rules/
[tool.ruff]
line-length = 88
indent-width = 4
[tool.ruff.lint]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
select = [
"F",
"E", # Errors
"W", # Warnings
"I", # Imports
"N", # Naming
"D", # Documentation
"UP", # Upgrades
"YTT",
"ANN",
"ASYNC",
"S",
"BLE",
"B",
"A",
"COM",
"C4",
"T10",
"DJ",
"EM",
"EXE",
"ISC",
"LOG",
"PIE",
"PYI",
"PT",
"RSE",
"SLF",
"SLOT",
"SIM",
"TID",
"TCH",
"INT",
"ERA",
"PGH",
]
ignore = [
"F821",
"F841",
"E501",
"E731",
"D100",
"D101",
"D107",
"D201",
"D202",
"D205",
"D211",
"D212",
"D401",
"D407",
"ANN002",
"ANN003",
"ANN101",
"ANN201",
"ANN204",
"S101",
"S301",
"S311",
"S507",
"S603",
"S605",
"S607",
"B008",
"B026",
"B028",
"B905",
"W291",
"W293",
"COM812",
"ISC001",
"SIM113",
]
[tool.ruff.format]
# Use single quotes for strings
quote-style = "single"
\ No newline at end of file
"""qim3d: A Python package for 3D image processing and visualization. """
qim3d: A Python package for 3D image processing and visualization.
The qim3d library is designed to make it easier to work with 3D imaging data in Python. The qim3d 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, It offers a range of features, including data loading and manipulation,
image processing and filtering, visualization of 3D data, and analysis of imaging results. image processing and filtering, visualization of 3D data, and analysis of imaging results.
...@@ -8,13 +9,14 @@ Documentation available at https://platform.qim.dk/qim3d/ ...@@ -8,13 +9,14 @@ Documentation available at https://platform.qim.dk/qim3d/
""" """
__version__ = "1.0.0" __version__ = '1.1.0'
import importlib as _importlib import importlib as _importlib
class _LazyLoader: class _LazyLoader:
"""Lazy loader to load submodules only when they are accessed""" """Lazy loader to load submodules only when they are accessed"""
def __init__(self, module_name): def __init__(self, module_name):
...@@ -48,7 +50,7 @@ _submodules = [ ...@@ -48,7 +50,7 @@ _submodules = [
'mesh', 'mesh',
'features', 'features',
'operations', 'operations',
'detection' 'detection',
] ]
# Creating lazy loaders for each submodule # Creating lazy loaders for each submodule
......
import argparse import argparse
import webbrowser import os
import platform import platform
import webbrowser
import outputformat as ouf import outputformat as ouf
import qim3d import qim3d
import os
QIM_TITLE = ouf.rainbow( QIM_TITLE = ouf.rainbow(
rf""" rf"""
...@@ -16,126 +18,123 @@ QIM_TITLE = ouf.rainbow( ...@@ -16,126 +18,123 @@ QIM_TITLE = ouf.rainbow(
""", """,
return_str=True, return_str=True,
cmap="hot", cmap='hot',
) )
def parse_tuple(arg): def parse_tuple(arg):
# Remove parentheses if they are included and split by comma # Remove parentheses if they are included and split by comma
return tuple(map(int, arg.strip("()").split(","))) return tuple(map(int, arg.strip('()').split(',')))
def main(): 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") subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
# GUIs # GUIs
gui_parser = subparsers.add_parser("gui", help="Graphical User Interfaces.") gui_parser = subparsers.add_parser('gui', help='Graphical User Interfaces.')
gui_parser.add_argument( gui_parser.add_argument(
"--data-explorer", action="store_true", help="Run data explorer." '--data-explorer', action='store_true', help='Run data explorer.'
)
gui_parser.add_argument("--iso3d", action="store_true", help="Run iso3d.")
gui_parser.add_argument(
"--annotation-tool", action="store_true", help="Run annotation tool."
) )
gui_parser.add_argument('--iso3d', action='store_true', help='Run iso3d.')
gui_parser.add_argument( gui_parser.add_argument(
"--local-thickness", action="store_true", help="Run local thickness tool." '--annotation-tool', action='store_true', help='Run annotation tool.'
) )
gui_parser.add_argument( gui_parser.add_argument(
"--layers", action="store_true", help="Run Layers." '--local-thickness', action='store_true', help='Run local thickness tool.'
) )
gui_parser.add_argument("--host", default="0.0.0.0", help="Desired host.") gui_parser.add_argument('--layers', action='store_true', help='Run Layers.')
gui_parser.add_argument('--host', default='0.0.0.0', help='Desired host.')
gui_parser.add_argument( gui_parser.add_argument(
"--platform", action="store_true", help="Use QIM platform address" '--platform', action='store_true', help='Use QIM platform address'
) )
gui_parser.add_argument( gui_parser.add_argument(
"--no-browser", action="store_true", help="Do not launch browser." '--no-browser', action='store_true', help='Do not launch browser.'
) )
# Viz # Viz
viz_parser = subparsers.add_parser("viz", help="Volumetric visualization.") viz_parser = subparsers.add_parser('viz', help='Volumetric visualization.')
viz_parser.add_argument("source", help="Path to the image file") viz_parser.add_argument('source', help='Path to the image file')
viz_parser.add_argument( viz_parser.add_argument(
"-m", '-m',
"--method", '--method',
type=str, type=str,
metavar="METHOD", metavar='METHOD',
default="itk-vtk", default='itk-vtk',
help="Which method is used to display file.", help='Which method is used to display file.',
) )
viz_parser.add_argument( viz_parser.add_argument(
"--destination", default="k3d.html", help="Path to save html file." '--destination', default='k3d.html', help='Path to save html file.'
) )
viz_parser.add_argument( viz_parser.add_argument(
"--no-browser", action="store_true", help="Do not launch browser." '--no-browser', action='store_true', help='Do not launch browser.'
) )
# Preview # Preview
preview_parser = subparsers.add_parser( preview_parser = subparsers.add_parser(
"preview", help="Preview of the image in CLI" 'preview', help='Preview of the image in CLI'
) )
preview_parser.add_argument( preview_parser.add_argument(
"filename", 'filename',
type=str, type=str,
metavar="FILENAME", metavar='FILENAME',
help="Path to image that will be displayed", help='Path to image that will be displayed',
) )
preview_parser.add_argument( preview_parser.add_argument(
"--slice", '--slice',
type=int, type=int,
metavar="S", metavar='S',
default=None, default=None,
help="Specifies which slice of the image will be displayed.\nDefaults to middle slice. If number exceeds number of slices, last slice will be displayed.", help='Specifies which slice of the image will be displayed.\nDefaults to middle slice. If number exceeds number of slices, last slice will be displayed.',
) )
preview_parser.add_argument( preview_parser.add_argument(
"--axis", '--axis',
type=int, type=int,
metavar="AX", metavar='AX',
default=0, default=0,
help="Specifies from which axis will be the slice taken. Defaults to 0.", help='Specifies from which axis will be the slice taken. Defaults to 0.',
) )
preview_parser.add_argument( preview_parser.add_argument(
"--resolution", '--resolution',
type=int, type=int,
metavar="RES", metavar='RES',
default=80, default=80,
help="Resolution of displayed image. Defaults to 80.", help='Resolution of displayed image. Defaults to 80.',
) )
preview_parser.add_argument( preview_parser.add_argument(
"--absolute_values", '--absolute_values',
action="store_false", action='store_false',
help="By default set the maximum value to be 255 so the contrast is strong. This turns it off.", help='By default set the maximum value to be 255 so the contrast is strong. This turns it off.',
) )
# File Convert # File Convert
convert_parser = subparsers.add_parser( convert_parser = subparsers.add_parser(
"convert", 'convert',
help="Convert files to different formats without loading the entire file into memory", help='Convert files to different formats without loading the entire file into memory',
) )
convert_parser.add_argument( convert_parser.add_argument(
"input_path", 'input_path',
type=str, type=str,
metavar="Input path", metavar='Input path',
help="Path to image that will be converted", help='Path to image that will be converted',
) )
convert_parser.add_argument( convert_parser.add_argument(
"output_path", 'output_path',
type=str, type=str,
metavar="Output path", metavar='Output path',
help="Path to save converted image", help='Path to save converted image',
) )
convert_parser.add_argument( convert_parser.add_argument(
"--chunks", '--chunks',
type=parse_tuple, type=parse_tuple,
metavar="Chunk shape", metavar='Chunk shape',
default=(64, 64, 64), default=(64, 64, 64),
help="Chunk size for the zarr file. Defaults to (64, 64, 64).", help='Chunk size for the zarr file. Defaults to (64, 64, 64).',
) )
args = parser.parse_args() args = parser.parse_args()
if args.subcommand == "gui": if args.subcommand == 'gui':
arghost = args.host arghost = args.host
inbrowser = not args.no_browser # Should automatically open in browser inbrowser = not args.no_browser # Should automatically open in browser
...@@ -152,7 +151,7 @@ def main(): ...@@ -152,7 +151,7 @@ def main():
interface_class = qim3d.gui.layers2d.Interface interface_class = qim3d.gui.layers2d.Interface
else: else:
print( print(
"Please select a tool by choosing one of the following flags:\n\t--data-explorer\n\t--iso3d\n\t--annotation-tool\n\t--local-thickness" 'Please select a tool by choosing one of the following flags:\n\t--data-explorer\n\t--iso3d\n\t--annotation-tool\n\t--local-thickness'
) )
return return
interface = ( interface = (
...@@ -164,31 +163,27 @@ def main(): ...@@ -164,31 +163,27 @@ def main():
else: else:
interface.launch(inbrowser=inbrowser, force_light_mode=False) interface.launch(inbrowser=inbrowser, force_light_mode=False)
elif args.subcommand == "viz": elif args.subcommand == 'viz':
if args.method == 'itk-vtk':
if args.method == "itk-vtk":
# We need the full path to the file for the viewer # We need the full path to the file for the viewer
current_dir = os.getcwd() current_dir = os.getcwd()
full_path = os.path.normpath(os.path.join(current_dir, args.source)) full_path = os.path.normpath(os.path.join(current_dir, args.source))
qim3d.viz.itk_vtk(full_path, open_browser = not args.no_browser) qim3d.viz.itk_vtk(full_path, open_browser=not args.no_browser)
elif args.method == 'k3d':
elif args.method == "k3d":
volume = qim3d.io.load(str(args.source)) volume = qim3d.io.load(str(args.source))
print("\nGenerating k3d plot...") print('\nGenerating k3d plot...')
qim3d.viz.volumetric(volume, show=False, save=str(args.destination)) qim3d.viz.volumetric(volume, show=False, save=str(args.destination))
print(f"Done, plot available at <{args.destination}>") print(f'Done, plot available at <{args.destination}>')
if not args.no_browser: if not args.no_browser:
print("Opening in default browser...") print('Opening in default browser...')
webbrowser.open_new_tab(args.destination) webbrowser.open_new_tab(args.destination)
else: else:
raise NotImplementedError( raise NotImplementedError(
f"Method '{args.method}' is not valid. Try 'k3d' or default 'itk-vtk-viewer'" f"Method '{args.method}' is not valid. Try 'k3d' or default 'itk-vtk-viewer'"
) )
elif args.subcommand == "preview": elif args.subcommand == 'preview':
image = qim3d.io.load(args.filename) image = qim3d.io.load(args.filename)
qim3d.viz.image_preview( qim3d.viz.image_preview(
...@@ -199,22 +194,21 @@ def main(): ...@@ -199,22 +194,21 @@ def main():
relative_intensity=args.absolute_values, relative_intensity=args.absolute_values,
) )
elif args.subcommand == "convert": elif args.subcommand == 'convert':
qim3d.io.convert(args.input_path, args.output_path, chunk_shape=args.chunks) qim3d.io.convert(args.input_path, args.output_path, chunk_shape=args.chunks)
elif args.subcommand is None: elif args.subcommand is None:
print(QIM_TITLE) print(QIM_TITLE)
welcome_text = ( welcome_text = (
"\nqim3d is a Python package for 3D image processing and visualization.\n" '\nqim3d is a Python package for 3D image processing and visualization.\n'
f"For more information, please visit {ouf.c('https://platform.qim.dk/qim3d/', color='orange', return_str=True)}\n" f"For more information, please visit {ouf.c('https://platform.qim.dk/qim3d/', color='orange', return_str=True)}\n"
" \n" ' \n'
"For more information on each subcommand, type 'qim3d <subcommand> --help'.\n" "For more information on each subcommand, type 'qim3d <subcommand> --help'.\n"
) )
print(welcome_text) print(welcome_text)
parser.print_help() parser.print_help()
print("\n") print('\n')
if __name__ == "__main__": if __name__ == '__main__':
main() main()
from qim3d.detection._common_detection_methods import * from qim3d.detection._common_detection_methods import *
\ No newline at end of file
""" Blob detection using Difference of Gaussian (DoG) method """ """Blob detection using Difference of Gaussian (DoG) method"""
import numpy as np import numpy as np
from qim3d.utils._logger import log from qim3d.utils._logger import log
__all__ = ["blobs"] __all__ = ['blobs']
def blobs( def blobs(
vol: np.ndarray, vol: np.ndarray,
background: str = "dark", background: str = 'dark',
min_sigma: float = 1, min_sigma: float = 1,
max_sigma: float = 50, max_sigma: float = 50,
sigma_ratio: float = 1.6, sigma_ratio: float = 1.6,
...@@ -56,18 +58,19 @@ def blobs( ...@@ -56,18 +58,19 @@ def blobs(
# Visualize detected blobs # Visualize detected blobs
qim3d.viz.circles(blobs, vol, alpha=0.8, color='blue') qim3d.viz.circles(blobs, vol, alpha=0.8, color='blue')
``` ```
![blob detection](../../assets/screenshots/blob_detection.gif) ![blob detection](../../assets/screenshots/blob_detection.gif)
```python ```python
# Visualize binary binary_volume # Visualize binary binary_volume
qim3d.viz.slicer(binary_volume) qim3d.viz.slicer(binary_volume)
``` ```
![blob detection](../../assets/screenshots/blob_get_mask.gif) ![blob detection](../../assets/screenshots/blob_get_mask.gif)
""" """
from skimage.feature import blob_dog from skimage.feature import blob_dog
if background == "bright": if background == 'bright':
log.info("Bright background selected, volume will be inverted.") log.info('Bright background selected, volume will be inverted.')
vol = np.invert(vol) vol = np.invert(vol)
blobs = blob_dog( blobs = blob_dog(
...@@ -109,8 +112,8 @@ def blobs( ...@@ -109,8 +112,8 @@ def blobs(
(x_indices - x) ** 2 + (y_indices - y) ** 2 + (z_indices - z) ** 2 (x_indices - x) ** 2 + (y_indices - y) ** 2 + (z_indices - z) ** 2
) )
binary_volume[z_start:z_end, y_start:y_end, x_start:x_end][ binary_volume[z_start:z_end, y_start:y_end, x_start:x_end][dist <= radius] = (
dist <= radius True
] = True )
return blobs, binary_volume return blobs, binary_volume
""" Example images for testing and demonstration purposes. """ """Example images for testing and demonstration purposes."""
from pathlib import Path as _Path from pathlib import Path as _Path
from qim3d.utils._logger import log as _log
from qim3d.io import load as _load from qim3d.io import load as _load
from qim3d.utils._logger import log as _log
# Save the original log level and set to ERROR # Save the original log level and set to ERROR
# to suppress the log messages during loading # to suppress the log messages during loading
_original_log_level = _log.level _original_log_level = _log.level
_log.setLevel("ERROR") _log.setLevel('ERROR')
# Load image examples # Load image examples
for _file_path in _Path(__file__).resolve().parent.glob("*.tif"): for _file_path in _Path(__file__).resolve().parent.glob('*.tif'):
globals().update({_file_path.stem: _load(_file_path, progress_bar=False)}) globals().update({_file_path.stem: _load(_file_path, progress_bar=False)})
# Restore the original log level # Restore the original log level
......
from ._common_features_methods import volume, area, sphericity from ._common_features_methods import area, sphericity, volume
import numpy as np import numpy as np
import qim3d.processing import qim3d
from qim3d.utils._logger import log from qim3d.utils._logger import log
import trimesh
import qim3d import qim3d
from pygel3d import hmesh
def volume(obj: np.ndarray|hmesh.Manifold) -> float:
def volume(obj: np.ndarray|trimesh.Trimesh,
**mesh_kwargs
) -> float:
""" """
Compute the volume of a 3D volume or mesh. Compute the volume of a 3D mesh using the Pygel3D library.
Args: Args:
obj (np.ndarray or trimesh.Trimesh): Either a np.ndarray volume or a mesh object of type trimesh.Trimesh. obj (numpy.ndarray or pygel3d.hmesh.Manifold): Either a np.ndarray volume or a mesh object of type pygel3d.hmesh.Manifold.
**mesh_kwargs (Any): Additional arguments for mesh creation if the input is a volume.
Returns: Returns:
volume (float): The volume of the object. volume (float): The volume of the object.
Example: Example:
Compute volume from a mesh: Compute volume from a mesh:
```python ```python
...@@ -27,8 +23,8 @@ def volume(obj: np.ndarray|trimesh.Trimesh, ...@@ -27,8 +23,8 @@ def volume(obj: np.ndarray|trimesh.Trimesh,
mesh = qim3d.io.load_mesh('path/to/mesh.obj') mesh = qim3d.io.load_mesh('path/to/mesh.obj')
# Compute the volume of the mesh # Compute the volume of the mesh
vol = qim3d.features.volume(mesh) volume = qim3d.features.volume(mesh)
print('Volume:', vol) print(f'Volume: {volume}')
``` ```
Compute volume from a np.ndarray: Compute volume from a np.ndarray:
...@@ -37,35 +33,30 @@ def volume(obj: np.ndarray|trimesh.Trimesh, ...@@ -37,35 +33,30 @@ def volume(obj: np.ndarray|trimesh.Trimesh,
# Generate a 3D blob # Generate a 3D blob
synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015) synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015)
synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015)
# Compute the volume of the blob # Compute the volume of the blob
volume = qim3d.features.volume(synthetic_blob, level=0.5) volume = qim3d.features.volume(synthetic_blob)
volume = qim3d.features.volume(synthetic_blob, level=0.5) print(f'Volume: {volume}')
print('Volume:', volume)
``` ```
""" """
if isinstance(obj, np.ndarray): if isinstance(obj, np.ndarray):
log.info("Converting volume to mesh.") log.info("Converting volume to mesh.")
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs) obj = qim3d.mesh.from_volume(obj)
return obj.volume return hmesh.volume(obj)
def area(obj: np.ndarray|hmesh.Manifold) -> float:
def area(obj: np.ndarray|trimesh.Trimesh,
**mesh_kwargs
) -> float:
""" """
Compute the surface area of a 3D volume or mesh. Compute the surface area of a 3D mesh using the Pygel3D library.
Args: Args:
obj (np.ndarray or trimesh.Trimesh): Either a np.ndarray volume or a mesh object of type trimesh.Trimesh. obj (numpy.ndarray or pygel3d.hmesh.Manifold): Either a np.ndarray volume or a mesh object of type pygel3d.hmesh.Manifold.
**mesh_kwargs (Any): Additional arguments for mesh creation if the input is a volume.
Returns: Returns:
area (float): The surface area of the object. area (float): The surface area of the object.
Example: Example:
Compute area from a mesh: Compute area from a mesh:
```python ```python
...@@ -76,8 +67,7 @@ def area(obj: np.ndarray|trimesh.Trimesh, ...@@ -76,8 +67,7 @@ def area(obj: np.ndarray|trimesh.Trimesh,
# Compute the surface area of the mesh # Compute the surface area of the mesh
area = qim3d.features.area(mesh) area = qim3d.features.area(mesh)
area = qim3d.features.area(mesh) print(f'Area: {area}')
print(f"Area: {area}")
``` ```
Compute area from a np.ndarray: Compute area from a np.ndarray:
...@@ -86,39 +76,30 @@ def area(obj: np.ndarray|trimesh.Trimesh, ...@@ -86,39 +76,30 @@ def area(obj: np.ndarray|trimesh.Trimesh,
# Generate a 3D blob # Generate a 3D blob
synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015) synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015)
synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015)
# Compute the surface area of the blob # Compute the surface area of the blob
volume = qim3d.features.area(synthetic_blob, level=0.5) area = qim3d.features.area(synthetic_blob)
volume = qim3d.features.area(synthetic_blob, level=0.5) print(f'Area: {area}')
print('Area:', volume)
``` ```
""" """
if isinstance(obj, np.ndarray): if isinstance(obj, np.ndarray):
log.info("Converting volume to mesh.") log.info("Converting volume to mesh.")
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs) obj = qim3d.mesh.from_volume(obj)
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
return obj.area
return hmesh.area(obj)
def sphericity(obj: np.ndarray|trimesh.Trimesh, def sphericity(obj: np.ndarray|hmesh.Manifold) -> float:
**mesh_kwargs
) -> float:
""" """
Compute the sphericity of a 3D volume or mesh. Compute the sphericity of a 3D mesh using the Pygel3D library.
Sphericity is a measure of how spherical an object is. It is defined as the ratio
of the surface area of a sphere with the same volume as the object to the object's
actual surface area.
Args: Args:
obj (np.ndarray or trimesh.Trimesh): Either a np.ndarray volume or a mesh object of type trimesh.Trimesh. obj (numpy.ndarray or pygel3d.hmesh.Manifold): Either a np.ndarray volume or a mesh object of type pygel3d.hmesh.Manifold.
**mesh_kwargs (Any): Additional arguments for mesh creation if the input is a volume.
Returns: Returns:
sphericity (float): A float value representing the sphericity of the object. sphericity (float): The sphericity of the object.
Example: Example:
Compute sphericity from a mesh: Compute sphericity from a mesh:
```python ```python
...@@ -129,7 +110,7 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh, ...@@ -129,7 +110,7 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh,
# Compute the sphericity of the mesh # Compute the sphericity of the mesh
sphericity = qim3d.features.sphericity(mesh) sphericity = qim3d.features.sphericity(mesh)
sphericity = qim3d.features.sphericity(mesh) print(f'Sphericity: {sphericity}')
``` ```
Compute sphericity from a np.ndarray: Compute sphericity from a np.ndarray:
...@@ -138,32 +119,25 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh, ...@@ -138,32 +119,25 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh,
# Generate a 3D blob # Generate a 3D blob
synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015) synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015)
synthetic_blob = qim3d.generate.noise_object(noise_scale = 0.015)
# Compute the sphericity of the blob # Compute the sphericity of the blob
sphericity = qim3d.features.sphericity(synthetic_blob, level=0.5) sphericity = qim3d.features.sphericity(synthetic_blob)
sphericity = qim3d.features.sphericity(synthetic_blob, level=0.5) print(f'Sphericity: {sphericity}')
``` ```
!!! info "Limitations due to pixelation"
Sphericity is particularly sensitive to the resolution of the mesh, as it directly impacts the accuracy of surface area and volume calculations.
Since the mesh is generated from voxel-based 3D volume data, the discrete nature of the voxels leads to pixelation effects that reduce the precision of sphericity measurements.
Higher resolution meshes may mitigate these errors but often at the cost of increased computational demands.
""" """
if isinstance(obj, np.ndarray): if isinstance(obj, np.ndarray):
log.info("Converting volume to mesh.") log.info("Converting volume to mesh.")
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs) obj = qim3d.mesh.from_volume(obj)
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
volume = qim3d.features.volume(obj) volume = qim3d.features.volume(obj)
area = qim3d.features.area(obj) area = qim3d.features.area(obj)
volume = qim3d.features.volume(obj)
area = qim3d.features.area(obj)
if area == 0: if area == 0:
log.warning("Surface area is zero, sphericity is undefined.") log.warning('Surface area is zero, sphericity is undefined.')
return np.nan return np.nan
sphericity = (np.pi ** (1 / 3) * (6 * volume) ** (2 / 3)) / area sphericity = (np.pi ** (1 / 3) * (6 * volume) ** (2 / 3)) / area
log.info(f"Sphericity: {sphericity}") # log.info(f"Sphericity: {sphericity}")
return sphericity return sphericity
\ No newline at end of file
from ._common_filter_methods import * from ._common_filter_methods import *
\ No newline at end of file
"""Provides filter functions and classes for image processing""" """Provides filter functions and classes for image processing"""
from typing import Type, Union from typing import Type
import dask.array as da
import dask_image.ndfilters as dask_ndfilters
import numpy as np import numpy as np
from scipy import ndimage from scipy import ndimage
from skimage import morphology from skimage import morphology
import dask.array as da
import dask_image.ndfilters as dask_ndfilters
from qim3d.utils import log from qim3d.utils import log
__all__ = [ __all__ = [
"FilterBase", 'FilterBase',
"Gaussian", 'Gaussian',
"Median", 'Median',
"Maximum", 'Maximum',
"Minimum", 'Minimum',
"Pipeline", 'Pipeline',
"Tophat", 'Tophat',
"gaussian", 'gaussian',
"median", 'median',
"maximum", 'maximum',
"minimum", 'minimum',
"tophat", 'tophat',
] ]
class FilterBase: class FilterBase:
def __init__(self, def __init__(self, *args, dask: bool = False, chunks: str = 'auto', **kwargs):
*args,
dask: bool = False,
chunks: str = "auto",
**kwargs):
""" """
Base class for image filters. Base class for image filters.
Args: Args:
*args: Additional positional arguments for filter initialization. *args: Additional positional arguments for filter initialization.
**kwargs: Additional keyword arguments for filter initialization. **kwargs: Additional keyword arguments for filter initialization.
""" """
self.args = args self.args = args
self.dask = dask self.dask = dask
...@@ -54,6 +51,7 @@ class Gaussian(FilterBase): ...@@ -54,6 +51,7 @@ class Gaussian(FilterBase):
sigma (float): Standard deviation for Gaussian kernel. sigma (float): Standard deviation for Gaussian kernel.
*args: Additional arguments. *args: Additional arguments.
**kwargs: Additional keyword arguments. **kwargs: Additional keyword arguments.
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.sigma = sigma self.sigma = sigma
...@@ -67,14 +65,22 @@ class Gaussian(FilterBase): ...@@ -67,14 +65,22 @@ class Gaussian(FilterBase):
Returns: Returns:
The filtered image or volume. The filtered image or volume.
""" """
return gaussian( return gaussian(
input, sigma=self.sigma, dask=self.dask, chunks=self.chunks, *self.args, **self.kwargs input,
sigma=self.sigma,
dask=self.dask,
chunks=self.chunks,
*self.args,
**self.kwargs,
) )
class Median(FilterBase): class Median(FilterBase):
def __init__(self, size: float = None, footprint: np.ndarray = None, *args, **kwargs): def __init__(
self, size: float = None, footprint: np.ndarray = None, *args, **kwargs
):
""" """
Median filter initialization. Median filter initialization.
...@@ -83,6 +89,7 @@ class Median(FilterBase): ...@@ -83,6 +89,7 @@ class Median(FilterBase):
footprint (np.ndarray, optional): The structuring element for filtering. footprint (np.ndarray, optional): The structuring element for filtering.
*args: Additional arguments. *args: Additional arguments.
**kwargs: Additional keyword arguments. **kwargs: Additional keyword arguments.
""" """
if size is None and footprint is None: if size is None and footprint is None:
raise ValueError("Either 'size' or 'footprint' must be provided.") raise ValueError("Either 'size' or 'footprint' must be provided.")
...@@ -99,12 +106,22 @@ class Median(FilterBase): ...@@ -99,12 +106,22 @@ class Median(FilterBase):
Returns: Returns:
The filtered image or volume. The filtered image or volume.
""" """
return median(vol=input, size=self.size, footprint=self.footprint, dask=self.dask, chunks=self.chunks, **self.kwargs) return median(
vol=input,
size=self.size,
footprint=self.footprint,
dask=self.dask,
chunks=self.chunks,
**self.kwargs,
)
class Maximum(FilterBase): class Maximum(FilterBase):
def __init__(self, size: float = None, footprint: np.ndarray = None, *args, **kwargs): def __init__(
self, size: float = None, footprint: np.ndarray = None, *args, **kwargs
):
""" """
Maximum filter initialization. Maximum filter initialization.
...@@ -113,6 +130,7 @@ class Maximum(FilterBase): ...@@ -113,6 +130,7 @@ class Maximum(FilterBase):
footprint (np.ndarray, optional): The structuring element for filtering. footprint (np.ndarray, optional): The structuring element for filtering.
*args: Additional arguments. *args: Additional arguments.
**kwargs: Additional keyword arguments. **kwargs: Additional keyword arguments.
""" """
if size is None and footprint is None: if size is None and footprint is None:
raise ValueError("Either 'size' or 'footprint' must be provided.") raise ValueError("Either 'size' or 'footprint' must be provided.")
...@@ -129,12 +147,22 @@ class Maximum(FilterBase): ...@@ -129,12 +147,22 @@ class Maximum(FilterBase):
Returns: Returns:
The filtered image or volume. The filtered image or volume.
""" """
return maximum(vol=input, size=self.size, footprint=self.footprint, dask=self.dask, chunks=self.chunks, **self.kwargs) return maximum(
vol=input,
size=self.size,
footprint=self.footprint,
dask=self.dask,
chunks=self.chunks,
**self.kwargs,
)
class Minimum(FilterBase): class Minimum(FilterBase):
def __init__(self, size: float = None, footprint: np.ndarray = None, *args, **kwargs): def __init__(
self, size: float = None, footprint: np.ndarray = None, *args, **kwargs
):
""" """
Minimum filter initialization. Minimum filter initialization.
...@@ -143,6 +171,7 @@ class Minimum(FilterBase): ...@@ -143,6 +171,7 @@ class Minimum(FilterBase):
footprint (np.ndarray, optional): The structuring element for filtering. footprint (np.ndarray, optional): The structuring element for filtering.
*args: Additional arguments. *args: Additional arguments.
**kwargs: Additional keyword arguments. **kwargs: Additional keyword arguments.
""" """
if size is None and footprint is None: if size is None and footprint is None:
raise ValueError("Either 'size' or 'footprint' must be provided.") raise ValueError("Either 'size' or 'footprint' must be provided.")
...@@ -159,8 +188,16 @@ class Minimum(FilterBase): ...@@ -159,8 +188,16 @@ class Minimum(FilterBase):
Returns: Returns:
The filtered image or volume. The filtered image or volume.
""" """
return minimum(vol=input, size=self.size, footprint=self.footprint, dask=self.dask, chunks=self.chunks, **self.kwargs) return minimum(
vol=input,
size=self.size,
footprint=self.footprint,
dask=self.dask,
chunks=self.chunks,
**self.kwargs,
)
class Tophat(FilterBase): class Tophat(FilterBase):
...@@ -173,11 +210,13 @@ class Tophat(FilterBase): ...@@ -173,11 +210,13 @@ class Tophat(FilterBase):
Returns: Returns:
The filtered image or volume. The filtered image or volume.
""" """
return tophat(input, dask=self.dask, **self.kwargs) return tophat(input, dask=self.dask, **self.kwargs)
class Pipeline: class Pipeline:
""" """
Example: Example:
```python ```python
...@@ -216,6 +255,7 @@ class Pipeline: ...@@ -216,6 +255,7 @@ class Pipeline:
Args: Args:
*args: Variable number of filter instances to be applied sequentially. *args: Variable number of filter instances to be applied sequentially.
""" """
self.filters = {} self.filters = {}
...@@ -232,13 +272,14 @@ class Pipeline: ...@@ -232,13 +272,14 @@ class Pipeline:
Raises: Raises:
AssertionError: If `fn` is not an instance of the FilterBase class. AssertionError: If `fn` is not an instance of the FilterBase class.
""" """
if not isinstance(fn, FilterBase): if not isinstance(fn, FilterBase):
filter_names = [ filter_names = [
subclass.__name__ for subclass in FilterBase.__subclasses__() subclass.__name__ for subclass in FilterBase.__subclasses__()
] ]
raise AssertionError( raise AssertionError(
f"filters should be instances of one of the following classes: {filter_names}" f'filters should be instances of one of the following classes: {filter_names}'
) )
self.filters[name] = fn self.filters[name] = fn
...@@ -248,7 +289,7 @@ class Pipeline: ...@@ -248,7 +289,7 @@ class Pipeline:
Args: Args:
fn (FilterBase): An instance of a FilterBase subclass to be appended. fn (FilterBase): An instance of a FilterBase subclass to be appended.
Example: Example:
```python ```python
import qim3d import qim3d
...@@ -262,6 +303,7 @@ class Pipeline: ...@@ -262,6 +303,7 @@ class Pipeline:
# Append a second filter to the pipeline # Append a second filter to the pipeline
pipeline.append(Median(size=5)) pipeline.append(Median(size=5))
``` ```
""" """
self._add_filter(str(len(self.filters)), fn) self._add_filter(str(len(self.filters)), fn)
...@@ -274,6 +316,7 @@ class Pipeline: ...@@ -274,6 +316,7 @@ class Pipeline:
Returns: Returns:
The filtered image or volume after applying all sequential filters. The filtered image or volume after applying all sequential filters.
""" """
for fn in self.filters.values(): for fn in self.filters.values():
input = fn(input) input = fn(input)
...@@ -281,12 +324,8 @@ class Pipeline: ...@@ -281,12 +324,8 @@ class Pipeline:
def gaussian( def gaussian(
vol: np.ndarray, vol: np.ndarray, sigma: float, dask: bool = False, chunks: str = 'auto', **kwargs
sigma: float, ) -> np.ndarray:
dask: bool = False,
chunks: str = "auto",
**kwargs
) -> np.ndarray:
""" """
Applies a Gaussian filter to the input volume using scipy.ndimage.gaussian_filter or dask_image.ndfilters.gaussian_filter. Applies a Gaussian filter to the input volume using scipy.ndimage.gaussian_filter or dask_image.ndfilters.gaussian_filter.
...@@ -295,11 +334,11 @@ def gaussian( ...@@ -295,11 +334,11 @@ def gaussian(
sigma (float or sequence of floats): The standard deviations of the Gaussian filter are given for each axis as a sequence, or as a single number, in which case it is equal for all axes. sigma (float or sequence of floats): The standard deviations of the Gaussian filter are given for each axis as a sequence, or as a single number, in which case it is equal for all axes.
dask (bool, optional): Whether to use Dask for the Gaussian filter. dask (bool, optional): Whether to use Dask for the Gaussian filter.
chunks (int or tuple or "'auto'", optional): Defines how to divide the array into blocks when using Dask. Can be an integer, tuple, size in bytes, or "auto" for automatic sizing. chunks (int or tuple or "'auto'", optional): Defines how to divide the array into blocks when using Dask. Can be an integer, tuple, size in bytes, or "auto" for automatic sizing.
*args (Any): Additional positional arguments for the Gaussian filter.
**kwargs (Any): Additional keyword arguments for the Gaussian filter. **kwargs (Any): Additional keyword arguments for the Gaussian filter.
Returns: Returns:
filtered_vol (np.ndarray): The filtered image or volume. filtered_vol (np.ndarray): The filtered image or volume.
""" """
if dask: if dask:
...@@ -314,12 +353,12 @@ def gaussian( ...@@ -314,12 +353,12 @@ def gaussian(
def median( def median(
vol: np.ndarray, vol: np.ndarray,
size: float = None, size: float = None,
footprint: np.ndarray = None, footprint: np.ndarray = None,
dask: bool = False, dask: bool = False,
chunks: str = "auto", chunks: str = 'auto',
**kwargs **kwargs,
) -> np.ndarray: ) -> np.ndarray:
""" """
Applies a median filter to the input volume using scipy.ndimage.median_filter or dask_image.ndfilters.median_filter. Applies a median filter to the input volume using scipy.ndimage.median_filter or dask_image.ndfilters.median_filter.
...@@ -337,11 +376,12 @@ def median( ...@@ -337,11 +376,12 @@ def median(
Raises: Raises:
RuntimeError: If neither size nor footprint is defined RuntimeError: If neither size nor footprint is defined
""" """
if size is None: if size is None:
if footprint is None: if footprint is None:
raise RuntimeError("no footprint or filter size provided") raise RuntimeError('no footprint or filter size provided')
if dask: if dask:
if not isinstance(vol, da.Array): if not isinstance(vol, da.Array):
vol = da.from_array(vol, chunks=chunks) vol = da.from_array(vol, chunks=chunks)
...@@ -357,9 +397,9 @@ def maximum( ...@@ -357,9 +397,9 @@ def maximum(
vol: np.ndarray, vol: np.ndarray,
size: float = None, size: float = None,
footprint: np.ndarray = None, footprint: np.ndarray = None,
dask: bool = False, dask: bool = False,
chunks: str = "auto", chunks: str = 'auto',
**kwargs **kwargs,
) -> np.ndarray: ) -> np.ndarray:
""" """
Applies a maximum filter to the input volume using scipy.ndimage.maximum_filter or dask_image.ndfilters.maximum_filter. Applies a maximum filter to the input volume using scipy.ndimage.maximum_filter or dask_image.ndfilters.maximum_filter.
...@@ -374,14 +414,15 @@ def maximum( ...@@ -374,14 +414,15 @@ def maximum(
Returns: Returns:
filtered_vol (np.ndarray): The filtered image or volume. filtered_vol (np.ndarray): The filtered image or volume.
Raises: Raises:
RuntimeError: If neither size nor footprint is defined RuntimeError: If neither size nor footprint is defined
""" """
if size is None: if size is None:
if footprint is None: if footprint is None:
raise RuntimeError("no footprint or filter size provided") raise RuntimeError('no footprint or filter size provided')
if dask: if dask:
if not isinstance(vol, da.Array): if not isinstance(vol, da.Array):
vol = da.from_array(vol, chunks=chunks) vol = da.from_array(vol, chunks=chunks)
...@@ -394,12 +435,12 @@ def maximum( ...@@ -394,12 +435,12 @@ def maximum(
def minimum( def minimum(
vol: np.ndarray, vol: np.ndarray,
size: float = None, size: float = None,
footprint: np.ndarray = None, footprint: np.ndarray = None,
dask: bool = False, dask: bool = False,
chunks: str = "auto", chunks: str = 'auto',
**kwargs **kwargs,
) -> np.ndarray: ) -> np.ndarray:
""" """
Applies a minimum filter to the input volume using scipy.ndimage.minimum_filter or dask_image.ndfilters.minimum_filter. Applies a minimum filter to the input volume using scipy.ndimage.minimum_filter or dask_image.ndfilters.minimum_filter.
...@@ -417,11 +458,12 @@ def minimum( ...@@ -417,11 +458,12 @@ def minimum(
Raises: Raises:
RuntimeError: If neither size nor footprint is defined RuntimeError: If neither size nor footprint is defined
""" """
if size is None: if size is None:
if footprint is None: if footprint is None:
raise RuntimeError("no footprint or filter size provided") raise RuntimeError('no footprint or filter size provided')
if dask: if dask:
if not isinstance(vol, da.Array): if not isinstance(vol, da.Array):
vol = da.from_array(vol, chunks=chunks) vol = da.from_array(vol, chunks=chunks)
...@@ -432,10 +474,8 @@ def minimum( ...@@ -432,10 +474,8 @@ def minimum(
res = ndimage.minimum_filter(vol, size, footprint, **kwargs) res = ndimage.minimum_filter(vol, size, footprint, **kwargs)
return res return res
def tophat(vol: np.ndarray,
dask: bool = False, def tophat(vol: np.ndarray, dask: bool = False, **kwargs):
**kwargs
):
""" """
Remove background from the volume. Remove background from the volume.
...@@ -448,24 +488,25 @@ def tophat(vol: np.ndarray, ...@@ -448,24 +488,25 @@ def tophat(vol: np.ndarray,
Returns: Returns:
filtered_vol (np.ndarray): The volume with background removed. filtered_vol (np.ndarray): The volume with background removed.
""" """
radius = kwargs["radius"] if "radius" in kwargs else 3 radius = kwargs['radius'] if 'radius' in kwargs else 3
background = kwargs["background"] if "background" in kwargs else "dark" background = kwargs['background'] if 'background' in kwargs else 'dark'
if dask: if dask:
log.info("Dask not supported for tophat filter, switching to scipy.") log.info('Dask not supported for tophat filter, switching to scipy.')
if background == "bright": if background == 'bright':
log.info( log.info(
"Bright background selected, volume will be temporarily inverted when applying white_tophat" 'Bright background selected, volume will be temporarily inverted when applying white_tophat'
) )
vol = np.invert(vol) vol = np.invert(vol)
selem = morphology.ball(radius) selem = morphology.ball(radius)
vol = vol - morphology.white_tophat(vol, selem) vol = vol - morphology.white_tophat(vol, selem)
if background == "bright": if background == 'bright':
vol = np.invert(vol) vol = np.invert(vol)
return vol return vol
\ No newline at end of file