Skip to content
Snippets Groups Projects
Commit 14bed6a3 authored by fima's avatar fima :beers:
Browse files

Merge branch 'pre_commits' into 'main'

Pre commits

See merge request !153
parents 3e1468bd 52b613b5
No related branches found
No related tags found
1 merge request!153Pre commits
Showing
with 826 additions and 609 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
# 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.
It offers a range of features, including data loading and manipulation,
......@@ -8,13 +9,14 @@ Documentation available at https://platform.qim.dk/qim3d/
"""
__version__ = "1.0.0"
__version__ = '1.0.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)
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()
"""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,
......@@ -63,11 +65,12 @@ def blobs(
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."""
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
from qim3d.utils._logger import log
import trimesh
import qim3d
import qim3d.processing
from qim3d.utils._logger import log
def volume(obj: np.ndarray|trimesh.Trimesh,
**mesh_kwargs
) -> float:
def volume(obj: np.ndarray | trimesh.Trimesh, **mesh_kwargs) -> float:
"""
Compute the volume of a 3D volume or mesh.
......@@ -47,15 +46,13 @@ def volume(obj: np.ndarray|trimesh.Trimesh,
"""
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)
return obj.volume
def area(obj: np.ndarray|trimesh.Trimesh,
**mesh_kwargs
) -> float:
def area(obj: np.ndarray | trimesh.Trimesh, **mesh_kwargs) -> float:
"""
Compute the surface area of a 3D volume or mesh.
......@@ -93,18 +90,17 @@ def area(obj: np.ndarray|trimesh.Trimesh,
volume = qim3d.features.area(synthetic_blob, level=0.5)
print('Area:', volume)
```
"""
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, **mesh_kwargs)
return obj.area
def sphericity(obj: np.ndarray|trimesh.Trimesh,
**mesh_kwargs
) -> float:
def sphericity(obj: np.ndarray | trimesh.Trimesh, **mesh_kwargs) -> float:
"""
Compute the sphericity of a 3D volume or mesh.
......@@ -149,9 +145,10 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh,
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.")
log.info('Converting volume to mesh.')
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
obj = qim3d.mesh.from_volume(obj, **mesh_kwargs)
......@@ -161,9 +158,9 @@ def sphericity(obj: np.ndarray|trimesh.Trimesh,
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}")
log.info(f'Sphericity: {sphericity}')
return sphericity
"""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
......@@ -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,11 +324,7 @@ class Pipeline:
def gaussian(
vol: np.ndarray,
sigma: float,
dask: bool = False,
chunks: str = "auto",
**kwargs
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.
......@@ -300,6 +339,7 @@ def gaussian(
Returns:
filtered_vol (np.ndarray): The filtered image or volume.
"""
if dask:
......@@ -318,8 +358,8 @@ def median(
size: float = None,
footprint: np.ndarray = None,
dask: bool = False,
chunks: str = "auto",
**kwargs
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,10 +377,11 @@ 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):
......@@ -358,8 +399,8 @@ def maximum(
size: float = None,
footprint: np.ndarray = None,
dask: bool = False,
chunks: str = "auto",
**kwargs
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.
......@@ -377,10 +418,11 @@ def maximum(
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):
......@@ -398,8 +440,8 @@ def minimum(
size: float = None,
footprint: np.ndarray = None,
dask: bool = False,
chunks: str = "auto",
**kwargs
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,10 +459,11 @@ 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):
......@@ -432,10 +475,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 +489,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
from ._generators import noise_object
from ._aggregators import noise_object_collection
from ._generators import noise_object
......@@ -22,6 +22,7 @@ def random_placement(
Returns:
collection (numpy.ndarray): 3D volume of the collection with the blob placed.
placed (bool): Flag for placement success.
"""
# Find available (zero) elements in collection
available_z, available_y, available_x = np.where(collection == 0)
......@@ -44,14 +45,12 @@ def random_placement(
if np.all(
collection[start[0] : end[0], start[1] : end[1], start[2] : end[2]] == 0
):
# Check if placement is within bounds (bool)
within_bounds = np.all(start >= 0) and np.all(
end <= np.array(collection.shape)
)
if within_bounds:
# Place blob
collection[start[0] : end[0], start[1] : end[1], start[2] : end[2]] = (
blob
......@@ -81,6 +80,7 @@ def specific_placement(
collection (numpy.ndarray): 3D volume of the collection with the blob placed.
placed (bool): Flag for placement success.
positions (list[tuple]): List of remaining positions to place blobs.
"""
# Flag for placement success
placed = False
......@@ -99,14 +99,12 @@ def specific_placement(
if np.all(
collection[start[0] : end[0], start[1] : end[1], start[2] : end[2]] == 0
):
# Check if placement is within bounds (bool)
within_bounds = np.all(start >= 0) and np.all(
end <= np.array(collection.shape)
)
if within_bounds:
# Place blob
collection[start[0] : end[0], start[1] : end[1], start[2] : end[2]] = (
blob
......@@ -288,23 +286,24 @@ def noise_object_collection(
qim3d.viz.slices_grid(vol, num_slices=15, slice_axis=1)
```
![synthetic_collection_tube](../../assets/screenshots/synthetic_collection_tube_slices.png)
"""
if verbose:
original_log_level = log.getEffectiveLevel()
log.setLevel("DEBUG")
log.setLevel('DEBUG')
# Check valid input types
if not isinstance(collection_shape, tuple) or len(collection_shape) != 3:
raise TypeError(
"Shape of collection must be a tuple with three dimensions (z, y, x)"
'Shape of collection must be a tuple with three dimensions (z, y, x)'
)
if len(min_shape) != len(max_shape):
raise ValueError("Object shapes must be tuples of the same length")
raise ValueError('Object shapes must be tuples of the same length')
if (positions is not None) and (len(positions) != num_objects):
raise ValueError(
"Number of objects must match number of positions, otherwise set positions = None"
'Number of objects must match number of positions, otherwise set positions = None'
)
# Set seed for random number generator
......@@ -317,8 +316,8 @@ def noise_object_collection(
labels = np.zeros_like(collection_array)
# Fill the 3D array with synthetic blobs
for i in tqdm(range(num_objects), desc="Objects placed"):
log.debug(f"\nObject #{i+1}")
for i in tqdm(range(num_objects), desc='Objects placed'):
log.debug(f'\nObject #{i+1}')
# Sample from blob parameter ranges
if min_shape == max_shape:
......@@ -327,7 +326,7 @@ def noise_object_collection(
blob_shape = tuple(
rng.integers(low=min_shape[i], high=max_shape[i]) for i in range(3)
)
log.debug(f"- Blob shape: {blob_shape}")
log.debug(f'- Blob shape: {blob_shape}')
# Scale object shape
final_shape = tuple(l * r for l, r in zip(blob_shape, object_shape_zoom))
......@@ -335,19 +334,19 @@ def noise_object_collection(
# Sample noise scale
noise_scale = rng.uniform(low=min_object_noise, high=max_object_noise)
log.debug(f"- Object noise scale: {noise_scale:.4f}")
log.debug(f'- Object noise scale: {noise_scale:.4f}')
gamma = rng.uniform(low=min_gamma, high=max_gamma)
log.debug(f"- Gamma correction: {gamma:.3f}")
log.debug(f'- Gamma correction: {gamma:.3f}')
if max_high_value > min_high_value:
max_value = rng.integers(low=min_high_value, high=max_high_value)
else:
max_value = min_high_value
log.debug(f"- Max value: {max_value}")
log.debug(f'- Max value: {max_value}')
threshold = rng.uniform(low=min_threshold, high=max_threshold)
log.debug(f"- Threshold: {threshold:.3f}")
log.debug(f'- Threshold: {threshold:.3f}')
# Generate synthetic object
blob = qim3d.generate.noise_object(
......@@ -367,7 +366,7 @@ def noise_object_collection(
low=min_rotation_degrees, high=max_rotation_degrees
) # Sample rotation angle
axes = rng.choice(rotation_axes) # Sample the two axes to rotate around
log.debug(f"- Rotation angle: {angle:.2f} at axes: {axes}")
log.debug(f'- Rotation angle: {angle:.2f} at axes: {axes}')
blob = scipy.ndimage.rotate(blob, angle, axes, order=1)
......@@ -396,7 +395,7 @@ def noise_object_collection(
if not placed:
# Log error if not all num_objects could be placed (this line of code has to be here, otherwise it will interfere with tqdm progress bar)
log.error(
f"Object #{i+1} could not be placed in the collection, no space found. Collection contains {i}/{num_objects} objects."
f'Object #{i+1} could not be placed in the collection, no space found. Collection contains {i}/{num_objects} objects.'
)
if verbose:
......
......@@ -4,6 +4,7 @@ from noise import pnoise3
import qim3d.processing
def noise_object(
base_shape: tuple = (128, 128, 128),
final_shape: tuple = (128, 128, 128),
......@@ -14,7 +15,7 @@ def noise_object(
threshold: float = 0.5,
smooth_borders: bool = False,
object_shape: str = None,
dtype: str = "uint8",
dtype: str = 'uint8',
) -> np.ndarray:
"""
Generate a 3D volume with Perlin noise, spherical gradient, and optional scaling and gamma correction.
......@@ -103,12 +104,13 @@ def noise_object(
qim3d.viz.slices_grid(vol, num_slices=15)
```
![synthetic_blob_tube_slice](../../assets/screenshots/synthetic_blob_tube_slice.png)
"""
if not isinstance(final_shape, tuple) or len(final_shape) != 3:
raise TypeError("Size must be a tuple of 3 dimensions")
raise TypeError('Size must be a tuple of 3 dimensions')
if not np.issubdtype(dtype, np.number):
raise ValueError("Invalid data type")
raise ValueError('Invalid data type')
# Initialize the 3D array for the shape
volume = np.empty((base_shape[0], base_shape[1], base_shape[2]), dtype=np.float32)
......@@ -119,18 +121,17 @@ def noise_object(
# Calculate the distance from the center of the shape
center = np.array(base_shape) / 2
dist = np.sqrt((z - center[0])**2 +
(y - center[1])**2 +
(x - center[2])**2)
dist = np.sqrt((z - center[0]) ** 2 + (y - center[1]) ** 2 + (x - center[2]) ** 2)
dist /= np.sqrt(3 * (center[0] ** 2))
# Generate Perlin noise and adjust the values based on the distance from the center
vectorized_pnoise3 = np.vectorize(pnoise3) # Vectorize pnoise3, since it only takes scalar input
vectorized_pnoise3 = np.vectorize(
pnoise3
) # Vectorize pnoise3, since it only takes scalar input
noise = vectorized_pnoise3(z.flatten() * noise_scale,
y.flatten() * noise_scale,
x.flatten() * noise_scale
noise = vectorized_pnoise3(
z.flatten() * noise_scale, y.flatten() * noise_scale, x.flatten() * noise_scale
).reshape(base_shape)
volume = (1 + noise) * (1 - dist)
......@@ -150,11 +151,16 @@ def noise_object(
if smooth_borders:
# Maximum value among the six sides of the 3D volume
max_border_value = np.max([
np.max(volume[0, :, :]), np.max(volume[-1, :, :]),
np.max(volume[:, 0, :]), np.max(volume[:, -1, :]),
np.max(volume[:, :, 0]), np.max(volume[:, :, -1])
])
max_border_value = np.max(
[
np.max(volume[0, :, :]),
np.max(volume[-1, :, :]),
np.max(volume[:, 0, :]),
np.max(volume[:, -1, :]),
np.max(volume[:, :, 0]),
np.max(volume[:, :, -1]),
]
)
# Compute threshold such that there will be no straight cuts in the blob
threshold = max_border_value / max_value
......@@ -171,42 +177,47 @@ def noise_object(
)
# Fade into a shape if specified
if object_shape == "cylinder":
if object_shape == 'cylinder':
# Arguments for the fade_mask function
geometry = "cylindrical" # Fade in cylindrical geometry
axis = np.argmax(volume.shape) # Fade along the dimension where the object is the largest
target_max_normalized_distance = 1.4 # This value ensures that the object will become cylindrical
geometry = 'cylindrical' # Fade in cylindrical geometry
axis = np.argmax(
volume.shape
) # Fade along the dimension where the object is the largest
target_max_normalized_distance = (
1.4 # This value ensures that the object will become cylindrical
)
volume = qim3d.operations.fade_mask(volume,
volume = qim3d.operations.fade_mask(
volume,
geometry=geometry,
axis=axis,
target_max_normalized_distance = target_max_normalized_distance
target_max_normalized_distance=target_max_normalized_distance,
)
elif object_shape == "tube":
elif object_shape == 'tube':
# Arguments for the fade_mask function
geometry = "cylindrical" # Fade in cylindrical geometry
axis = np.argmax(volume.shape) # Fade along the dimension where the object is the largest
geometry = 'cylindrical' # Fade in cylindrical geometry
axis = np.argmax(
volume.shape
) # Fade along the dimension where the object is the largest
decay_rate = 5 # Decay rate for the fade operation
target_max_normalized_distance = 1.4 # This value ensures that the object will become cylindrical
target_max_normalized_distance = (
1.4 # This value ensures that the object will become cylindrical
)
# Fade once for making the object cylindrical
volume = qim3d.operations.fade_mask(volume,
volume = qim3d.operations.fade_mask(
volume,
geometry=geometry,
axis=axis,
decay_rate=decay_rate,
target_max_normalized_distance=target_max_normalized_distance,
invert = False
invert=False,
)
# Fade again with invert = True for making the object a tube (i.e. with a hole in the middle)
volume = qim3d.operations.fade_mask(volume,
geometry = geometry,
axis = axis,
decay_rate = decay_rate,
invert = True
volume = qim3d.operations.fade_mask(
volume, geometry=geometry, axis=axis, decay_rate=decay_rate, invert=True
)
# Convert to desired data type
......
from fastapi import FastAPI
import qim3d.utils
from . import data_explorer
from . import iso3d
from . import local_thickness
from . import annotation_tool
from . import layers2d
from . import annotation_tool, data_explorer, iso3d, layers2d, local_thickness
from .qim_theme import QimTheme
def run_gradio_app(gradio_interface, host="0.0.0.0"):
def run_gradio_app(gradio_interface, host='0.0.0.0'):
import gradio as gr
import uvicorn
# Get port using the QIM API
port_dict = qim3d.utils.get_port_dict()
if "gradio_port" in port_dict:
port = port_dict["gradio_port"]
elif "port" in port_dict:
port = port_dict["port"]
if 'gradio_port' in port_dict:
port = port_dict['gradio_port']
elif 'port' in port_dict:
port = port_dict['port']
else:
raise Exception("Port not specified from QIM API")
raise Exception('Port not specified from QIM API')
qim3d.utils.gradio_header(gradio_interface.title, port)
......@@ -30,7 +28,7 @@ def run_gradio_app(gradio_interface, host="0.0.0.0"):
app = gr.mount_gradio_app(app, gradio_interface, path=path)
# Full path
print(f"http://{host}:{port}{path}")
print(f'http://{host}:{port}{path}')
# Run the FastAPI server usign uvicorn
uvicorn.run(app, host=host, port=int(port))
......@@ -27,6 +27,7 @@ import tempfile
import gradio as gr
import numpy as np
from PIL import Image
import qim3d
from qim3d.gui.interface import BaseInterface
......@@ -34,17 +35,19 @@ from qim3d.gui.interface import BaseInterface
class Interface(BaseInterface):
def __init__(self, name_suffix: str = "", verbose: bool = False, img: np.ndarray = None):
def __init__(
self, name_suffix: str = '', verbose: bool = False, img: np.ndarray = None
):
super().__init__(
title="Annotation Tool",
title='Annotation Tool',
height=768,
width="100%",
width='100%',
verbose=verbose,
custom_css="annotation_tool.css",
custom_css='annotation_tool.css',
)
self.username = getpass.getuser()
self.temp_dir = os.path.join(tempfile.gettempdir(), f"qim-{self.username}")
self.temp_dir = os.path.join(tempfile.gettempdir(), f'qim-{self.username}')
self.name_suffix = name_suffix
self.img = img
......@@ -57,7 +60,7 @@ class Interface(BaseInterface):
# Get the temporary files from gradio
temp_path_list = []
for filename in os.listdir(self.temp_dir):
if "mask" and self.name_suffix in str(filename):
if 'mask' and self.name_suffix in str(filename):
# Get the list of the temporary files
temp_path_list.append(os.path.join(self.temp_dir, filename))
......@@ -76,9 +79,9 @@ class Interface(BaseInterface):
this is safer and backwards compatible (should be)
"""
self.mask_names = [
f"red{self.name_suffix}",
f"green{self.name_suffix}",
f"blue{self.name_suffix}",
f'red{self.name_suffix}',
f'green{self.name_suffix}',
f'blue{self.name_suffix}',
]
# Clean up old files
......@@ -86,7 +89,7 @@ class Interface(BaseInterface):
files = os.listdir(self.temp_dir)
for filename in files:
# Check if "mask" is in the filename
if ("mask" in filename) and (self.name_suffix in filename):
if ('mask' in filename) and (self.name_suffix in filename):
file_path = os.path.join(self.temp_dir, filename)
os.remove(file_path)
......@@ -94,13 +97,13 @@ class Interface(BaseInterface):
files = None
def create_preview(self, img_editor: gr.ImageEditor) -> np.ndarray:
background = img_editor["background"]
masks = img_editor["layers"][0]
background = img_editor['background']
masks = img_editor['layers'][0]
overlay_image = qim3d.operations.overlay_rgb_images(background, masks)
return overlay_image
def cerate_download_list(self, img_editor: gr.ImageEditor) -> list[str]:
masks_rgb = img_editor["layers"][0]
masks_rgb = img_editor['layers'][0]
mask_threshold = 200 # This value is based
mask_list = []
......@@ -114,7 +117,7 @@ class Interface(BaseInterface):
# Save only if we have a mask
if np.sum(mask) > 0:
mask_list.append(mask)
filename = f"mask_{self.mask_names[idx]}.tif"
filename = f'mask_{self.mask_names[idx]}.tif'
if not os.path.exists(self.temp_dir):
os.makedirs(self.temp_dir)
filepath = os.path.join(self.temp_dir, filename)
......@@ -128,11 +131,11 @@ class Interface(BaseInterface):
def define_interface(self, **kwargs):
brush = gr.Brush(
colors=[
"rgb(255,50,100)",
"rgb(50,250,100)",
"rgb(50,100,255)",
'rgb(255,50,100)',
'rgb(50,250,100)',
'rgb(50,100,255)',
],
color_mode="fixed",
color_mode='fixed',
default_size=10,
)
with gr.Row():
......@@ -142,26 +145,25 @@ class Interface(BaseInterface):
img_editor = gr.ImageEditor(
value=(
{
"background": self.img,
"layers": [Image.new("RGBA", self.img.shape, (0, 0, 0, 0))],
"composite": None,
'background': self.img,
'layers': [Image.new('RGBA', self.img.shape, (0, 0, 0, 0))],
'composite': None,
}
if self.img is not None
else None
),
type="numpy",
image_mode="RGB",
type='numpy',
image_mode='RGB',
brush=brush,
sources="upload",
sources='upload',
interactive=True,
show_download_button=True,
container=False,
transforms=["crop"],
transforms=['crop'],
layers=False,
)
with gr.Column(scale=1, min_width=256):
with gr.Row():
overlay_img = gr.Image(
show_download_button=False,
......@@ -169,7 +171,7 @@ class Interface(BaseInterface):
visible=False,
)
with gr.Row():
masks_download = gr.File(label="Download masks", visible=False)
masks_download = gr.File(label='Download masks', visible=False)
# fmt: off
img_editor.change(
......
This diff is collapsed.
from abc import ABC, abstractmethod
from os import listdir, path
from pathlib import Path
from abc import abstractmethod, ABC
from os import path, listdir
import gradio as gr
import numpy as np
from .qim_theme import QimTheme
import qim3d.gui
import numpy as np
# TODO: when offline it throws an error in cli
class BaseInterface(ABC):
"""
Annotation tool and Data explorer as those don't need any examples.
"""
......@@ -19,7 +19,7 @@ class BaseInterface(ABC):
self,
title: str,
height: int,
width: int = "100%",
width: int = '100%',
verbose: bool = False,
custom_css: str = None,
):
......@@ -38,7 +38,7 @@ class BaseInterface(ABC):
self.qim_dir = Path(qim3d.__file__).parents[0]
self.custom_css = (
path.join(self.qim_dir, "css", custom_css)
path.join(self.qim_dir, 'css', custom_css)
if custom_css is not None
else None
)
......@@ -72,8 +72,7 @@ class BaseInterface(ABC):
quiet=not self.verbose,
height=self.height,
width=self.width,
favicon_path=Path(qim3d.__file__).parents[0]
/ "gui/assets/qim3d-icon.svg",
favicon_path=Path(qim3d.__file__).parents[0] / 'gui/assets/qim3d-icon.svg',
**kwargs,
)
......@@ -88,7 +87,7 @@ class BaseInterface(ABC):
title=self.title,
css=self.custom_css,
) as gradio_interface:
gr.Markdown(f"# {self.title}")
gr.Markdown(f'# {self.title}')
self.define_interface(**kwargs)
return gradio_interface
......@@ -96,11 +95,12 @@ class BaseInterface(ABC):
def define_interface(self, **kwargs):
pass
def run_interface(self, host: str = "0.0.0.0"):
def run_interface(self, host: str = '0.0.0.0'):
qim3d.gui.run_gradio_app(self.create_interface(), host)
class InterfaceWithExamples(BaseInterface):
"""
For Iso3D and Local Thickness
"""
......@@ -117,7 +117,23 @@ class InterfaceWithExamples(BaseInterface):
self._set_examples_list()
def _set_examples_list(self):
valid_sufixes = (".tif", ".tiff", ".h5", ".nii", ".gz", ".dcm", ".DCM", ".vol", ".vgi", ".txrm", ".txm", ".xrm")
valid_sufixes = (
'.tif',
'.tiff',
'.h5',
'.nii',
'.gz',
'.dcm',
'.DCM',
'.vol',
'.vgi',
'.txrm',
'.txm',
'.xrm',
)
examples_folder = path.join(self.qim_dir, 'examples')
self.img_examples = [path.join(examples_folder, example) for example in listdir(examples_folder) if example.endswith(valid_sufixes)]
self.img_examples = [
path.join(examples_folder, example)
for example in listdir(examples_folder)
if example.endswith(valid_sufixes)
]
......@@ -15,6 +15,7 @@ app.launch()
```
"""
import os
import gradio as gr
......@@ -23,21 +24,19 @@ import plotly.graph_objects as go
from scipy import ndimage
import qim3d
from qim3d.utils._logger import log
from qim3d.gui.interface import InterfaceWithExamples
from qim3d.utils._logger import log
# TODO img in launch should be self.img
class Interface(InterfaceWithExamples):
def __init__(self,
verbose:bool = False,
plot_height:int = 768,
img = None):
super().__init__(title = "Isosurfaces for 3D visualization",
def __init__(self, verbose: bool = False, plot_height: int = 768, img=None):
super().__init__(
title='Isosurfaces for 3D visualization',
height=1024,
width=960,
verbose = verbose)
verbose=verbose,
)
self.interface = None
self.img = img
......@@ -48,11 +47,13 @@ class Interface(InterfaceWithExamples):
self.vol = qim3d.io.load(gradiofile.name)
assert self.vol.ndim == 3
except AttributeError:
raise gr.Error("You have to select a file")
raise gr.Error('You have to select a file')
except ValueError:
raise gr.Error("Unsupported file format")
raise gr.Error('Unsupported file format')
except AssertionError:
raise gr.Error(F"File has to be 3D structure. Your structure has {self.vol.ndim} dimension{'' if self.vol.ndim == 1 else 's'}")
raise gr.Error(
f"File has to be 3D structure. Your structure has {self.vol.ndim} dimension{'' if self.vol.ndim == 1 else 's'}"
)
def resize_vol(self, display_size: int):
"""Resizes the loaded volume to the display size"""
......@@ -61,7 +62,7 @@ class Interface(InterfaceWithExamples):
original_Z, original_Y, original_X = np.shape(self.vol)
max_size = np.max([original_Z, original_Y, original_X])
if self.verbose:
log.info(f"\nOriginal volume: {original_Z, original_Y, original_X}")
log.info(f'\nOriginal volume: {original_Z, original_Y, original_X}')
# Resize for display
self.vol = ndimage.zoom(
......@@ -76,14 +77,15 @@ class Interface(InterfaceWithExamples):
)
if self.verbose:
log.info(
f"Resized volume: {self.display_size_z, self.display_size_y, self.display_size_x}"
f'Resized volume: {self.display_size_z, self.display_size_y, self.display_size_x}'
)
def save_fig(self, fig: go.Figure, filename: str):
# Write Plotly figure to disk
fig.write_html(filename)
def create_fig(self,
def create_fig(
self,
gradio_file: gr.File,
display_size: int,
opacity: float,
......@@ -106,7 +108,6 @@ class Interface(InterfaceWithExamples):
show_x_slice: bool,
slice_x_location: int,
) -> tuple[go.Figure, str]:
# Load volume
self.load_data(gradio_file)
......@@ -161,10 +162,10 @@ class Interface(InterfaceWithExamples):
),
showscale=show_colorbar,
colorbar=dict(
thickness=8, outlinecolor="#fff", len=0.5, orientation="h"
thickness=8, outlinecolor='#fff', len=0.5, orientation='h'
),
reversescale=reversescale,
hoverinfo = "skip",
hoverinfo='skip',
)
)
......@@ -175,13 +176,13 @@ class Interface(InterfaceWithExamples):
scene_xaxis_visible=show_axis,
scene_yaxis_visible=show_axis,
scene_zaxis_visible=show_axis,
scene_aspectmode="data",
scene_aspectmode='data',
height=self.plot_height,
hovermode=False,
scene_camera_eye=dict(x=2.0, y=-2.0, z=1.5),
)
filename = "iso3d.html"
filename = 'iso3d.html'
self.save_fig(fig, filename)
return fig, filename
......@@ -189,10 +190,9 @@ class Interface(InterfaceWithExamples):
def remove_unused_file(self):
# Remove localthickness.tif file from working directory
# as it otherwise is not deleted
os.remove("iso3d.html")
os.remove('iso3d.html')
def define_interface(self, **kwargs):
gr.Markdown(
"""
This tool uses Plotly Volume (https://plotly.com/python/3d-volume-plots/) to create iso surfaces from voxels based on their intensity levels.
......@@ -203,117 +203,111 @@ class Interface(InterfaceWithExamples):
with gr.Row():
# Input and parameters column
with gr.Column(scale=1, min_width=320):
with gr.Tab("Input"):
with gr.Tab('Input'):
# File loader
gradio_file = gr.File(
show_label=False
)
with gr.Tab("Examples"):
gradio_file = gr.File(show_label=False)
with gr.Tab('Examples'):
gr.Examples(examples=self.img_examples, inputs=gradio_file)
# Run button
with gr.Row():
with gr.Column(scale=3, min_width=64):
btn_run = gr.Button(
value="Run 3D visualization", variant = "primary"
value='Run 3D visualization', variant='primary'
)
with gr.Column(scale=1, min_width=64):
btn_clear = gr.Button(
value="Clear", variant = "stop"
)
btn_clear = gr.Button(value='Clear', variant='stop')
with gr.Tab("Display"):
with gr.Tab('Display'):
# Display options
display_size = gr.Slider(
32,
128,
step=4,
label="Display resolution",
info="Number of voxels for the largest dimension",
label='Display resolution',
info='Number of voxels for the largest dimension',
value=64,
)
surface_count = gr.Slider(
2, 16, step=1, label="Total iso-surfaces", value=6
2, 16, step=1, label='Total iso-surfaces', value=6
)
show_caps = gr.Checkbox(value=False, label="Show surface caps")
show_caps = gr.Checkbox(value=False, label='Show surface caps')
with gr.Row():
opacityscale = gr.Dropdown(
choices=["uniform", "extremes", "min", "max"],
value="uniform",
label="Opacity scale",
info="Handles opacity acording to voxel value",
choices=['uniform', 'extremes', 'min', 'max'],
value='uniform',
label='Opacity scale',
info='Handles opacity acording to voxel value',
)
opacity = gr.Slider(
0.0, 1.0, step=0.1, label="Max opacity", value=0.4
0.0, 1.0, step=0.1, label='Max opacity', value=0.4
)
with gr.Row():
min_value = gr.Slider(
0.0, 1.0, step=0.05, label="Min value", value=0.1
0.0, 1.0, step=0.05, label='Min value', value=0.1
)
max_value = gr.Slider(
0.0, 1.0, step=0.05, label="Max value", value=1
0.0, 1.0, step=0.05, label='Max value', value=1
)
with gr.Tab("Slices") as slices:
show_z_slice = gr.Checkbox(value=False, label="Show Z slice")
with gr.Tab('Slices') as slices:
show_z_slice = gr.Checkbox(value=False, label='Show Z slice')
slice_z_location = gr.Slider(
0.0, 1.0, step=0.05, value=0.5, label="Position"
0.0, 1.0, step=0.05, value=0.5, label='Position'
)
show_y_slice = gr.Checkbox(value=False, label="Show Y slice")
show_y_slice = gr.Checkbox(value=False, label='Show Y slice')
slice_y_location = gr.Slider(
0.0, 1.0, step=0.05, value=0.5, label="Position"
0.0, 1.0, step=0.05, value=0.5, label='Position'
)
show_x_slice = gr.Checkbox(value=False, label="Show X slice")
show_x_slice = gr.Checkbox(value=False, label='Show X slice')
slice_x_location = gr.Slider(
0.0, 1.0, step=0.05, value=0.5, label="Position"
0.0, 1.0, step=0.05, value=0.5, label='Position'
)
with gr.Tab("Misc"):
with gr.Tab('Misc'):
with gr.Row():
colormap = gr.Dropdown(
choices=[
"Blackbody",
"Bluered",
"Blues",
"Cividis",
"Earth",
"Electric",
"Greens",
"Greys",
"Hot",
"Jet",
"Magma",
"Picnic",
"Portland",
"Rainbow",
"RdBu",
"Reds",
"Viridis",
"YlGnBu",
"YlOrRd",
'Blackbody',
'Bluered',
'Blues',
'Cividis',
'Earth',
'Electric',
'Greens',
'Greys',
'Hot',
'Jet',
'Magma',
'Picnic',
'Portland',
'Rainbow',
'RdBu',
'Reds',
'Viridis',
'YlGnBu',
'YlOrRd',
],
value="Magma",
label="Colormap",
value='Magma',
label='Colormap',
)
show_colorbar = gr.Checkbox(
value=False, label="Show color scale"
value=False, label='Show color scale'
)
reversescale = gr.Checkbox(
value=False, label="Reverse color scale"
)
flip_z = gr.Checkbox(value=True, label="Flip Z axis")
show_axis = gr.Checkbox(value=True, label="Show axis")
show_ticks = gr.Checkbox(value=False, label="Show ticks")
only_wireframe = gr.Checkbox(
value=False, label="Only wireframe"
value=False, label='Reverse color scale'
)
flip_z = gr.Checkbox(value=True, label='Flip Z axis')
show_axis = gr.Checkbox(value=True, label='Show axis')
show_ticks = gr.Checkbox(value=False, label='Show ticks')
only_wireframe = gr.Checkbox(value=False, label='Only wireframe')
# Inputs for gradio
inputs = [
......@@ -346,7 +340,7 @@ class Interface(InterfaceWithExamples):
plot_download = gr.File(
interactive=False,
label="Download interactive plot",
label='Download interactive plot',
show_label=True,
visible=False,
)
......@@ -367,5 +361,6 @@ class Interface(InterfaceWithExamples):
fn=self.remove_unused_file).success(
fn=self.set_visible, inputs=None, outputs=plot_download)
if __name__ == "__main__":
if __name__ == '__main__':
Interface().run_interface()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment