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/
.idea/
.cache/
.pytest_cache/
.ruff_cache/
*.swp
*.swo
*.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
!!! 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"
qim3d gui --data-explorer
......
......@@ -8,5 +8,5 @@
- Downloader
- export_ome_zarr
- import_ome_zarr
- save_mesh
- load_mesh
\ No newline at end of file
- load_mesh
- save_mesh
\ No newline at end of file
......@@ -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.
For details see [here](cli.md#qim3d-gui).
For details see [here](../cli/cli.md#qim3d-gui).
::: qim3d.gui.data_explorer
options:
......
......@@ -11,7 +11,7 @@ hide:
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)
......
......@@ -23,6 +23,8 @@ The `qim3d` library aims to provide easy ways to explore and get insights from v
- plot_cc
- colormaps
- fade_mask
- line_profile
- threshold
::: qim3d.viz.colormaps
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,
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/
"""
__version__ = "1.0.0"
__version__ = '1.1.0'
import importlib as _importlib
class _LazyLoader:
"""Lazy loader to load submodules only when they are accessed"""
def __init__(self, module_name):
......@@ -48,7 +50,7 @@ _submodules = [
'mesh',
'features',
'operations',
'detection'
'detection',
]
# Creating lazy loaders for each submodule
......
import argparse
import webbrowser
import os
import platform
import webbrowser
import outputformat as ouf
import qim3d
import os
QIM_TITLE = ouf.rainbow(
rf"""
......@@ -16,126 +18,123 @@ QIM_TITLE = ouf.rainbow(
""",
return_str=True,
cmap="hot",
cmap='hot',
)
def parse_tuple(arg):
# 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():
parser = argparse.ArgumentParser(description="qim3d command-line interface.")
subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand")
parser = argparse.ArgumentParser(description='qim3d command-line interface.')
subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
# 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(
"--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."
'--data-explorer', action='store_true', help='Run data explorer.'
)
gui_parser.add_argument('--iso3d', action='store_true', help='Run iso3d.')
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(
"--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(
"--platform", action="store_true", help="Use QIM platform address"
'--platform', action='store_true', help='Use QIM platform address'
)
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_parser = subparsers.add_parser("viz", help="Volumetric visualization.")
viz_parser.add_argument("source", help="Path to the image file")
viz_parser = subparsers.add_parser('viz', help='Volumetric visualization.')
viz_parser.add_argument('source', help='Path to the image file')
viz_parser.add_argument(
"-m",
"--method",
'-m',
'--method',
type=str,
metavar="METHOD",
default="itk-vtk",
help="Which method is used to display file.",
metavar='METHOD',
default='itk-vtk',
help='Which method is used to display file.',
)
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(
"--no-browser", action="store_true", help="Do not launch browser."
'--no-browser', action='store_true', help='Do not launch browser.'
)
# Preview
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(
"filename",
'filename',
type=str,
metavar="FILENAME",
help="Path to image that will be displayed",
metavar='FILENAME',
help='Path to image that will be displayed',
)
preview_parser.add_argument(
"--slice",
'--slice',
type=int,
metavar="S",
metavar='S',
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(
"--axis",
'--axis',
type=int,
metavar="AX",
metavar='AX',
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(
"--resolution",
'--resolution',
type=int,
metavar="RES",
metavar='RES',
default=80,
help="Resolution of displayed image. Defaults to 80.",
help='Resolution of displayed image. Defaults to 80.',
)
preview_parser.add_argument(
"--absolute_values",
action="store_false",
help="By default set the maximum value to be 255 so the contrast is strong. This turns it off.",
'--absolute_values',
action='store_false',
help='By default set the maximum value to be 255 so the contrast is strong. This turns it off.',
)
# File Convert
convert_parser = subparsers.add_parser(
"convert",
help="Convert files to different formats without loading the entire file into memory",
'convert',
help='Convert files to different formats without loading the entire file into memory',
)
convert_parser.add_argument(
"input_path",
'input_path',
type=str,
metavar="Input path",
help="Path to image that will be converted",
metavar='Input path',
help='Path to image that will be converted',
)
convert_parser.add_argument(
"output_path",
'output_path',
type=str,
metavar="Output path",
help="Path to save converted image",
metavar='Output path',
help='Path to save converted image',
)
convert_parser.add_argument(
"--chunks",
'--chunks',
type=parse_tuple,
metavar="Chunk shape",
metavar='Chunk shape',
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()
if args.subcommand == "gui":
if args.subcommand == 'gui':
arghost = args.host
inbrowser = not args.no_browser # Should automatically open in browser
......@@ -152,7 +151,7 @@ def main():
interface_class = qim3d.gui.layers2d.Interface
else:
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
interface = (
......@@ -164,31 +163,27 @@ def main():
else:
interface.launch(inbrowser=inbrowser, force_light_mode=False)
elif args.subcommand == "viz":
if args.method == "itk-vtk":
elif args.subcommand == 'viz':
if args.method == 'itk-vtk':
# We need the full path to the file for the viewer
current_dir = os.getcwd()
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))
print("\nGenerating k3d plot...")
print('\nGenerating k3d plot...')
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:
print("Opening in default browser...")
print('Opening in default browser...')
webbrowser.open_new_tab(args.destination)
else:
raise NotImplementedError(
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)
qim3d.viz.image_preview(
......@@ -199,22 +194,21 @@ def main():
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)
elif args.subcommand is None:
print(QIM_TITLE)
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"
" \n"
' \n'
"For more information on each subcommand, type 'qim3d <subcommand> --help'.\n"
)
print(welcome_text)
parser.print_help()
print("\n")
print('\n')
if __name__ == "__main__":
if __name__ == '__main__':
main()
from qim3d.detection._common_detection_methods import *
\ No newline at end of file
from qim3d.detection._common_detection_methods import *
""" Blob detection using Difference of Gaussian (DoG) method """
"""Blob detection using Difference of Gaussian (DoG) method"""
import numpy as np
from qim3d.utils._logger import log
__all__ = ["blobs"]
__all__ = ['blobs']
def blobs(
vol: np.ndarray,
background: str = "dark",
background: str = 'dark',
min_sigma: float = 1,
max_sigma: float = 50,
sigma_ratio: float = 1.6,
......@@ -56,18 +58,19 @@ def blobs(
# Visualize detected blobs
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
# Visualize binary binary_volume
qim3d.viz.slicer(binary_volume)
```
![blob detection](../../assets/screenshots/blob_get_mask.gif)
"""
from skimage.feature import blob_dog
if background == "bright":
log.info("Bright background selected, volume will be inverted.")
if background == 'bright':
log.info('Bright background selected, volume will be inverted.')
vol = np.invert(vol)
blobs = blob_dog(
......@@ -109,8 +112,8 @@ def blobs(
(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][
dist <= radius
] = True
binary_volume[z_start:z_end, y_start:y_end, x_start:x_end][dist <= radius] = (
True
)
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 qim3d.utils._logger import log as _log
from qim3d.io import load as _load
from qim3d.utils._logger import log as _log
# Save the original log level and set to ERROR
# to suppress the log messages during loading
_original_log_level = _log.level
_log.setLevel("ERROR")
_log.setLevel('ERROR')
# 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)})
# 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 qim3d.processing
import qim3d
from qim3d.utils._logger import log
import trimesh
import qim3d
from pygel3d import hmesh
def volume(obj: np.ndarray|trimesh.Trimesh,
**mesh_kwargs
) -> float:
def volume(obj: np.ndarray|hmesh.Manifold) -> float:
"""
Compute the volume of a 3D volume or mesh.
Compute the volume of a 3D mesh using the Pygel3D library.
Args:
obj (np.ndarray or trimesh.Trimesh): Either a np.ndarray volume or a mesh object of type trimesh.Trimesh.
**mesh_kwargs (Any): Additional arguments for mesh creation if the input is a volume.
obj (numpy.ndarray or pygel3d.hmesh.Manifold): Either a np.ndarray volume or a mesh object of type pygel3d.hmesh.Manifold.
Returns:
volume (float): The volume of the object.
Example:
Compute volume from a mesh:
```python
......@@ -27,8 +23,8 @@ def volume(obj: np.ndarray|trimesh.Trimesh,
mesh = qim3d.io.load_mesh('path/to/mesh.obj')
# Compute the volume of the mesh
vol = qim3d.features.volume(mesh)
print('Volume:', vol)
volume = qim3d.features.volume(mesh)
print(f'Volume: {volume}')
```
Compute volume from a np.ndarray:
......@@ -37,35 +33,30 @@ def volume(obj: np.ndarray|trimesh.Trimesh,
# Generate a 3D blob
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
volume = qim3d.features.volume(synthetic_blob, level=0.5)
volume = qim3d.features.volume(synthetic_blob, level=0.5)
print('Volume:', volume)
volume = qim3d.features.volume(synthetic_blob)
print(f'Volume: {volume}')
```
"""
if isinstance(obj, np.ndarray):
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|trimesh.Trimesh,
**mesh_kwargs
) -> float:
def area(obj: np.ndarray|hmesh.Manifold) -> float:
"""
Compute the surface area of a 3D volume or mesh.
Compute the surface area of a 3D mesh using the Pygel3D library.
Args:
obj (np.ndarray or trimesh.Trimesh): Either a np.ndarray volume or a mesh object of type trimesh.Trimesh.
**mesh_kwargs (Any): Additional arguments for mesh creation if the input is a volume.
obj (numpy.ndarray or pygel3d.hmesh.Manifold): Either a np.ndarray volume or a mesh object of type pygel3d.hmesh.Manifold.
Returns:
area (float): The surface area of the object.
area (float): The surface area of the object.
Example:
Compute area from a mesh:
```python
......@@ -76,8 +67,7 @@ def area(obj: np.ndarray|trimesh.Trimesh,
# Compute the surface area of the 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:
......@@ -86,39 +76,30 @@ def area(obj: np.ndarray|trimesh.Trimesh,
# Generate a 3D blob
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
volume = qim3d.features.area(synthetic_blob, level=0.5)
volume = qim3d.features.area(synthetic_blob, level=0.5)
print('Area:', volume)
area = qim3d.features.area(synthetic_blob)
print(f'Area: {area}')
```
"""
if isinstance(obj, np.ndarray):
log.info("Converting volume to mesh.")
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
return obj.area
obj = qim3d.mesh.from_volume(obj)
return hmesh.area(obj)
def sphericity(obj: np.ndarray|trimesh.Trimesh,
**mesh_kwargs
) -> float:
def sphericity(obj: np.ndarray|hmesh.Manifold) -> float:
"""
Compute the sphericity of a 3D volume or mesh.
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.
Compute the sphericity of a 3D mesh using the Pygel3D library.
Args:
obj (np.ndarray or trimesh.Trimesh): Either a np.ndarray volume or a mesh object of type trimesh.Trimesh.
**mesh_kwargs (Any): Additional arguments for mesh creation if the input is a volume.
obj (numpy.ndarray or pygel3d.hmesh.Manifold): Either a np.ndarray volume or a mesh object of type pygel3d.hmesh.Manifold.
Returns:
sphericity (float): A float value representing the sphericity of the object.
sphericity (float): The sphericity of the object.
Example:
Compute sphericity from a mesh:
```python
......@@ -129,7 +110,7 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh,
# Compute the sphericity of the mesh
sphericity = qim3d.features.sphericity(mesh)
sphericity = qim3d.features.sphericity(mesh)
print(f'Sphericity: {sphericity}')
```
Compute sphericity from a np.ndarray:
......@@ -138,32 +119,25 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh,
# Generate a 3D blob
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
sphericity = qim3d.features.sphericity(synthetic_blob, level=0.5)
sphericity = qim3d.features.sphericity(synthetic_blob, level=0.5)
sphericity = qim3d.features.sphericity(synthetic_blob)
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):
log.info("Converting volume to mesh.")
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
obj = qim3d.mesh.from_volume(obj)
volume = qim3d.features.volume(obj)
area = qim3d.features.area(obj)
volume = qim3d.features.volume(obj)
area = qim3d.features.area(obj)
if area == 0:
log.warning("Surface area is zero, sphericity is undefined.")
log.warning('Surface area is zero, sphericity is undefined.')
return np.nan
sphericity = (np.pi ** (1 / 3) * (6 * volume) ** (2 / 3)) / area
log.info(f"Sphericity: {sphericity}")
return sphericity
# log.info(f"Sphericity: {sphericity}")
return sphericity
\ No newline at end of file
from ._common_filter_methods import *
\ No newline at end of file
from ._common_filter_methods import *
"""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
from scipy import ndimage
from skimage import morphology
import dask.array as da
import dask_image.ndfilters as dask_ndfilters
from qim3d.utils import log
__all__ = [
"FilterBase",
"Gaussian",
"Median",
"Maximum",
"Minimum",
"Pipeline",
"Tophat",
"gaussian",
"median",
"maximum",
"minimum",
"tophat",
'FilterBase',
'Gaussian',
'Median',
'Maximum',
'Minimum',
'Pipeline',
'Tophat',
'gaussian',
'median',
'maximum',
'minimum',
'tophat',
]
class FilterBase:
def __init__(self,
*args,
dask: bool = False,
chunks: str = "auto",
**kwargs):
def __init__(self, *args, dask: bool = False, chunks: str = 'auto', **kwargs):
"""
Base class for image filters.
Args:
*args: Additional positional arguments for filter initialization.
**kwargs: Additional keyword arguments for filter initialization.
"""
self.args = args
self.dask = dask
......@@ -54,6 +51,7 @@ class Gaussian(FilterBase):
sigma (float): Standard deviation for Gaussian kernel.
*args: Additional arguments.
**kwargs: Additional keyword arguments.
"""
super().__init__(*args, **kwargs)
self.sigma = sigma
......@@ -67,14 +65,22 @@ class Gaussian(FilterBase):
Returns:
The filtered image or volume.
"""
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):
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.
......@@ -83,6 +89,7 @@ class Median(FilterBase):
footprint (np.ndarray, optional): The structuring element for filtering.
*args: Additional arguments.
**kwargs: Additional keyword arguments.
"""
if size is None and footprint is None:
raise ValueError("Either 'size' or 'footprint' must be provided.")
......@@ -99,12 +106,22 @@ class Median(FilterBase):
Returns:
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):
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.
......@@ -113,6 +130,7 @@ class Maximum(FilterBase):
footprint (np.ndarray, optional): The structuring element for filtering.
*args: Additional arguments.
**kwargs: Additional keyword arguments.
"""
if size is None and footprint is None:
raise ValueError("Either 'size' or 'footprint' must be provided.")
......@@ -129,12 +147,22 @@ class Maximum(FilterBase):
Returns:
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):
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.
......@@ -143,6 +171,7 @@ class Minimum(FilterBase):
footprint (np.ndarray, optional): The structuring element for filtering.
*args: Additional arguments.
**kwargs: Additional keyword arguments.
"""
if size is None and footprint is None:
raise ValueError("Either 'size' or 'footprint' must be provided.")
......@@ -159,8 +188,16 @@ class Minimum(FilterBase):
Returns:
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):
......@@ -173,11 +210,13 @@ class Tophat(FilterBase):
Returns:
The filtered image or volume.
"""
return tophat(input, dask=self.dask, **self.kwargs)
class Pipeline:
"""
Example:
```python
......@@ -216,6 +255,7 @@ class Pipeline:
Args:
*args: Variable number of filter instances to be applied sequentially.
"""
self.filters = {}
......@@ -232,13 +272,14 @@ class Pipeline:
Raises:
AssertionError: If `fn` is not an instance of the FilterBase class.
"""
if not isinstance(fn, FilterBase):
filter_names = [
subclass.__name__ for subclass in FilterBase.__subclasses__()
]
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
......@@ -248,7 +289,7 @@ class Pipeline:
Args:
fn (FilterBase): An instance of a FilterBase subclass to be appended.
Example:
```python
import qim3d
......@@ -262,6 +303,7 @@ class Pipeline:
# Append a second filter to the pipeline
pipeline.append(Median(size=5))
```
"""
self._add_filter(str(len(self.filters)), fn)
......@@ -274,6 +316,7 @@ class Pipeline:
Returns:
The filtered image or volume after applying all sequential filters.
"""
for fn in self.filters.values():
input = fn(input)
......@@ -281,12 +324,8 @@ class Pipeline:
def gaussian(
vol: np.ndarray,
sigma: float,
dask: bool = False,
chunks: str = "auto",
**kwargs
) -> np.ndarray:
vol: np.ndarray, sigma: float, 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.
......@@ -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.
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.
*args (Any): Additional positional arguments for the Gaussian filter.
**kwargs (Any): Additional keyword arguments for the Gaussian filter.
Returns:
filtered_vol (np.ndarray): The filtered image or volume.
"""
if dask:
......@@ -314,12 +353,12 @@ def gaussian(
def median(
vol: np.ndarray,
vol: np.ndarray,
size: float = None,
footprint: np.ndarray = None,
dask: bool = False,
chunks: str = "auto",
**kwargs
dask: bool = False,
chunks: str = 'auto',
**kwargs,
) -> np.ndarray:
"""
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(
Raises:
RuntimeError: If neither size nor footprint is defined
"""
if size 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 not isinstance(vol, da.Array):
vol = da.from_array(vol, chunks=chunks)
......@@ -357,9 +397,9 @@ def maximum(
vol: np.ndarray,
size: float = None,
footprint: np.ndarray = None,
dask: bool = False,
chunks: str = "auto",
**kwargs
dask: bool = False,
chunks: str = 'auto',
**kwargs,
) -> np.ndarray:
"""
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(
Returns:
filtered_vol (np.ndarray): The filtered image or volume.
Raises:
RuntimeError: If neither size nor footprint is defined
"""
if size 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 not isinstance(vol, da.Array):
vol = da.from_array(vol, chunks=chunks)
......@@ -394,12 +435,12 @@ def maximum(
def minimum(
vol: np.ndarray,
vol: np.ndarray,
size: float = None,
footprint: np.ndarray = None,
dask: bool = False,
chunks: str = "auto",
**kwargs
dask: bool = False,
chunks: str = 'auto',
**kwargs,
) -> np.ndarray:
"""
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(
Raises:
RuntimeError: If neither size nor footprint is defined
"""
if size 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 not isinstance(vol, da.Array):
vol = da.from_array(vol, chunks=chunks)
......@@ -432,10 +474,8 @@ def minimum(
res = ndimage.minimum_filter(vol, size, footprint, **kwargs)
return res
def tophat(vol: np.ndarray,
dask: bool = False,
**kwargs
):
def tophat(vol: np.ndarray, dask: bool = False, **kwargs):
"""
Remove background from the volume.
......@@ -448,24 +488,25 @@ def tophat(vol: np.ndarray,
Returns:
filtered_vol (np.ndarray): The volume with background removed.
"""
radius = kwargs["radius"] if "radius" in kwargs else 3
background = kwargs["background"] if "background" in kwargs else "dark"
radius = kwargs['radius'] if 'radius' in kwargs else 3
background = kwargs['background'] if 'background' in kwargs else 'dark'
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(
"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)
selem = morphology.ball(radius)
vol = vol - morphology.white_tophat(vol, selem)
if background == "bright":
if background == 'bright':
vol = np.invert(vol)
return vol
\ No newline at end of file
return vol