Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
Loading items

Target

Select target project
  • QIM/tools/qim3d
1 result
Select Git revision
Loading items
Show changes
Showing
with 2007 additions and 1226 deletions
# Data visualization
The `qim3d` library aims to provide easy ways to explore and get insights from volumetric data.
::: qim3d.viz
options:
members:
- slices
- slicer
- orthogonal
- vol
- local_thickness
- vectors
- plot_cc
- colormaps
- interactive_fade_mask
::: qim3d.viz.colormaps
options:
members:
- objects
- qim
site_name: qim3d documentation
site_url: https://platform.qim.dk/qim3d/
site_author: Qim3d contributors
site_description: Documentation for the Qim3d python library
repo_url: https://lab.compute.dtu.dk/QIM/tools/qim3d
repo_name: Gitlab
nav:
- qim3d: index.md
- Input & Output: io.md
- Data Generation: generate.md
- Processing: processing.md
- Visualization: viz.md
- GUIs: gui.md
- ML Models: models.md
- CLI: cli.md
- Release history: releases.md
theme:
language: en
name: material
nav_position: top
logo: assets/qim3d-icon.svg
favicon: assets/qim3d-icon.svg
features:
- navigation.tabs
- navigation.tabs.sticky
- navigation.sections
- navigation.expand
- navigation.instant
# - navigation.tracking
- toc.integrate
# - toc.follow
- search.suggest
# - content.tabs.link
# - content.code.annotation
- content.code.copy
- content.code.annotate
- header.autohide
palette:
primary: white
accent: orange
extra_css:
- stylesheets/extra.css
extra:
social:
- icon: fontawesome/brands/x-twitter
link: https://twitter.com/QIMCenter
markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- admonition
- attr_list
- md_in_html
- tables
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.details
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
plugins:
- tags
- search
- mkdocstrings:
handlers:
python:
options:
show_root_heading: true
show_source: true
show_root_full_path: true
show_object_full_path: true
show_symbol_type_heading: true
show_symbol_type_toc: false
separate_signature: true
\ No newline at end of file
import qim3d.io as io
import qim3d.gui as gui
import qim3d.viz as viz
import qim3d.utils as utils
import qim3d.models as models
import logging
examples = io.ImgExamples()
downloader = io.Downloader()
"""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,
image processing and filtering, visualization of 3D data, and analysis of imaging results.
Documentation available at https://platform.qim.dk/qim3d/
"""
__version__ = "0.4.1"
import importlib as _importlib
class _LazyLoader:
"""Lazy loader to load submodules only when they are accessed"""
def __init__(self, module_name):
self.module_name = module_name
self.module = None
def _load(self):
if self.module is None:
self.module = _importlib.import_module(self.module_name)
return self.module
def __getattr__(self, item):
module = self._load()
return getattr(module, item)
# List of submodules
_submodules = [
"examples",
"generate",
"gui",
"io",
"models",
"processing",
"tests",
"utils",
"viz",
"cli",
]
# Creating lazy loaders for each submodule
for submodule in _submodules:
globals()[submodule] = _LazyLoader(f"qim3d.{submodule}")
import argparse
import webbrowser
import outputformat as ouf
import qim3d
QIM_TITLE = ouf.rainbow(
f"\n _ _____ __ \n ____ _(_)___ ___ |__ /____/ / \n / __ `/ / __ `__ \ /_ </ __ / \n/ /_/ / / / / / / /__/ / /_/ / \n\__, /_/_/ /_/ /_/____/\__,_/ \n /_/ v{qim3d.__version__}\n\n",
return_str=True,
cmap="hot",
)
def parse_tuple(arg):
# Remove parentheses if they are included and split by comma
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")
# GUIs
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."
)
gui_parser.add_argument(
"--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(
"--platform", action="store_true", help="Use QIM platform address"
)
gui_parser.add_argument(
"--no-browser", action="store_true", help="Do not launch browser."
)
# Viz
viz_parser = subparsers.add_parser("viz", help="Volumetric visualization.")
viz_parser.add_argument("filename", default=False, help="Path to the image file")
viz_parser.add_argument(
"--destination", default="k3d.html", help="Path to save html file."
)
viz_parser.add_argument(
"--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_parser.add_argument(
"filename",
type=str,
metavar="FILENAME",
help="Path to image that will be displayed",
)
preview_parser.add_argument(
"--slice",
type=int,
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.",
)
preview_parser.add_argument(
"--axis",
type=int,
metavar="AX",
default=0,
help="Specifies from which axis will be the slice taken. Defaults to 0.",
)
preview_parser.add_argument(
"--resolution",
type=int,
metavar="RES",
default=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.",
)
# File Convert
convert_parser = subparsers.add_parser(
"convert",
help="Convert files to different formats without loading the entire file into memory",
)
convert_parser.add_argument(
"input_path",
type=str,
metavar="Input path",
help="Path to image that will be converted",
)
convert_parser.add_argument(
"output_path",
type=str,
metavar="Output path",
help="Path to save converted image",
)
convert_parser.add_argument(
"--chunks",
type=parse_tuple,
metavar="Chunk shape",
default=(64, 64, 64),
help="Chunk size for the zarr file. Defaults to (64, 64, 64).",
)
args = parser.parse_args()
if args.subcommand == "gui":
arghost = args.host
inbrowser = not args.no_browser # Should automatically open in browser
interface = None
if args.data_explorer:
interface_class = qim3d.gui.data_explorer.Interface
elif args.iso3d:
interface_class = qim3d.gui.iso3d.Interface
elif args.annotation_tool:
interface_class = qim3d.gui.annotation_tool.Interface
elif args.local_thickness:
interface_class = qim3d.gui.local_thickness.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"
)
return
interface = (
interface_class()
) # called here if we add another arguments to initialize
if args.platform:
interface.run_interface(host=arghost)
else:
interface.launch(inbrowser=inbrowser, force_light_mode=False)
elif args.subcommand == "viz":
# Load the data
print(f"Loading data from {args.filename}")
volume = qim3d.io.load(str(args.filename))
print(f"Done, volume shape: {volume.shape}")
# Make k3d plot
print("\nGenerating k3d plot...")
qim3d.viz.vol(volume, show=False, save=str(args.destination))
print(f"Done, plot available at <{args.destination}>")
if not args.no_browser:
print("Opening in default browser...")
webbrowser.open_new_tab(args.destination)
elif args.subcommand == "preview":
image = qim3d.io.load(args.filename)
qim3d.viz.image_preview(
image,
image_width=args.resolution,
axis=args.axis,
slice=args.slice,
relative_intensity=args.absolute_values,
)
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"
f"For more information, please visit {ouf.c('https://platform.qim.dk/qim3d/', color='orange', return_str=True)}\n"
" \n"
"For more information on each subcommand, type 'qim3d <subcommand> --help'.\n"
)
print(welcome_text)
parser.print_help()
print("\n")
if __name__ == "__main__":
main()
/* When we created the annotation tool, gradio.ImageEditor did not work as it was supposed to. There was an argument height, width which
applied to the wrapper around it. Argument canvas_size worked only if you did not upload any image. If you did the canvas would resize
itself to the size of the image. This way we set the width. If you only set the parameter 'width' insted of 'min-width', it doesn'ŧ work.
Canceling set max-heigth (the important is necessary) will allow the canvas to be as high as needed according to the picture.*/
canvas{
min-width: 600px;
max-height: none !important; /* The '!important' has to be here*/
}
......@@ -58,14 +58,17 @@
--font: 'Source Sans Pro', 'ui-sans-serif', 'system-ui', sans-serif;
--font-mono: 'IBM Plex Mono', 'ui-monospace', 'Consolas', monospace;
--body-background-fill: var(--background-fill-primary);
--body-text-color: var(--neutral-800);
--body-text-color: var(--neutral-300);
/*Headers*/
--body-text-size: var(--text-md);
--body-text-weight: 400;
--embed-radius: var(--radius-lg);
--color-accent: var(--primary-500);
--color-accent-soft: var(--primary-50);
--background-fill-primary: white;
--background-fill-secondary: var(--neutral-50);
--background-fill-primary: var(--neutral-900);
/*File explorer background*/
--background-fill-secondary: var(--neutral-500);
/*Dropdown hover*/
--border-color-accent: var(--primary-300);
--border-color-primary: var(--neutral-200);
--link-text-color: var(--secondary-600);
......@@ -87,7 +90,8 @@
--block-label-border-color: var(--border-color-primary);
--block-label-border-width: 1px;
--block-label-shadow: var(--block-shadow);
--block-label-text-color: var(--neutral-500);
--block-label-text-color: var(--neutral-50);
/*the small labels in corners*/
--block-label-margin: 0;
--block-label-padding: var(--spacing-sm) var(--spacing-lg);
--block-label-radius: calc(var(--radius-lg) - 1px) 0 calc(var(--radius-lg) - 1px) 0;
......@@ -100,7 +104,7 @@
--block-title-background-fill: none;
--block-title-border-color: none;
--block-title-border-width: 0px;
--block-title-text-color: var(--neutral-500);
--block-title-text-color: var(--neutral-50);
--block-title-padding: 0;
--block-title-radius: none;
--block-title-text-size: var(--text-md);
......@@ -115,7 +119,7 @@
--section-header-text-weight: 400;
--border-color-accent-subdued: var(--primary-200);
--chatbot-code-background-color: var(--neutral-100);
--checkbox-background-color: var(--background-fill-primary);
--checkbox-background-color: white;
--checkbox-background-color-focus: var(--checkbox-background-color);
--checkbox-background-color-hover: var(--checkbox-background-color);
--checkbox-background-color-selected: var(--secondary-600);
......@@ -125,8 +129,10 @@
--checkbox-border-color-selected: var(--secondary-600);
--checkbox-border-radius: var(--radius-sm);
--checkbox-border-width: var(--input-border-width);
--checkbox-label-background-fill: linear-gradient(to top, var(--neutral-50), white);
--checkbox-label-background-fill-hover: linear-gradient(to top, var(--neutral-100), white);
--checkbox-label-background-fill: var(--background-fill-primary);
/*linear-gradient(to top, var(--neutral-50), white);*/
--checkbox-label-background-fill-hover: var(--neutral-500);
/*linear-gradient(to top, var(--neutral-100), white);*/
--checkbox-label-background-fill-selected: var(--checkbox-label-background-fill);
--checkbox-label-border-color: var(--border-color-primary);
--checkbox-label-border-color-hover: var(--checkbox-label-border-color);
......@@ -146,7 +152,7 @@
--error-border-width: 1px;
--error-text-color: #b91c1c;
--error-icon-color: #b91c1c;
--input-background-fill: white;
--input-background-fill: var(--background-fill-primary);
--input-background-fill-focus: var(--secondary-500);
--input-background-fill-hover: var(--input-background-fill);
--input-border-color: var(--border-color-primary);
......@@ -158,7 +164,7 @@
--input-radius: var(--radius-lg);
--input-shadow: 0 0 0 var(--shadow-spread) transparent, var(--shadow-inset);
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--secondary-50), var(--shadow-inset);
--input-text-size: var(--text-md);
--input-text-size: var(--text-lg);
--input-text-weight: 400;
--loader-color: var(--color-accent);
--prose-text-size: var(--text-md);
......@@ -203,6 +209,7 @@
--button-small-text-weight: 400;
--button-transition: none;
}
/* Up here is the override for dark mode */
/* Now comes our custom CSS */
......@@ -231,6 +238,7 @@ h2 {
.no-border {
border-width: 0px !important;
}
/* Hides Gradio footer */
footer {
visibility: hidden;
......@@ -343,7 +351,7 @@ input[type="range"]::-webkit-slider-thumb {
/* Buttons */
.btn-html {
position: sticky;
display:flex;
/* display: flex; */
justify-content: center;
background: white !important;
border-color: #6c757d !important;
......@@ -620,3 +628,60 @@ div.svelte-1frtwj3 {
.wrap.default {
visibility: hidden !important;
}
/* Annotation tool CSS */
.no-interpolation {
image-rendering: pixelated !important;
}
.annotation-tool {
image-rendering: pixelated !important;
}
.annotation-tool:not(.no-img) .source-wrap {
display: none !important;
}
.annotation-tool button[title="Clear canvas"] {
display: none !important;
}
/* For backward and forward buttons in Annotation tool CSS */
.annotation-tool .row-wrap {
justify-content: center !important;
}
.annotation-tool .controls-wrap {
justify-content: center !important;
top: -8px !important;
left: 0px !important;
}
.annotation-tool .controls-wrap .small {
width: 20px !important;
height: 20px !important;
}
.annotation-tool .controls-wrap .padded {
padding: 8px !important;
border: 0px !important;
background-color: rgba(255, 255, 255, 0.8);
}
/*controls gr.File in Annotation tool CSS */
.annotation-tool .download {
min-width: 4rem !important;
width: 10%;
white-space: nowrap;
text-align: right;
}
.annotation-tool .stage-wrap canvas {
min-width: 512px !important;
max-height: 100% !important;
}
\ No newline at end of file
""" 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
# Save the original log level and set to ERROR
# to suppress the log messages during loading
_original_log_level = _log.level
_log.setLevel("ERROR")
# Load image examples
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
_log.setLevel(_original_log_level)
from .blob_ import blob
from .collection_ import collection
\ No newline at end of file
import numpy as np
import scipy.ndimage
from noise import pnoise3
def blob(
base_shape: tuple = (128, 128, 128),
final_shape: tuple = (128, 128, 128),
noise_scale: float = 0.05,
order: int = 1,
gamma: int = 1.0,
max_value: int = 255,
threshold: float = 0.5,
smooth_borders: bool = False,
dtype: str = "uint8",
) -> np.ndarray:
"""
Generate a 3D volume with Perlin noise, spherical gradient, and optional scaling and gamma correction.
Args:
base_shape (tuple, optional): Shape of the initial volume to generate. Defaults to (128, 128, 128).
final_shape (tuple, optional): Desired shape of the final volume. Defaults to (128, 128, 128).
noise_scale (float, optional): Scale factor for Perlin noise. Defaults to 0.05.
order (int, optional): Order of the spline interpolation used in resizing. Defaults to 1.
gamma (float, optional): Gamma correction factor. Defaults to 1.0.
max_value (int, optional): Maximum value for the volume intensity. Defaults to 255.
threshold (float, optional): Threshold value for clipping low intensity values. Defaults to 0.5.
smooth_borders (bool, optional): Flag for automatic computation of the threshold value to ensure a blob with no straight edges. If True, the `threshold` parameter is ignored. Defaults to False.
dtype (str, optional): Desired data type of the output volume. Defaults to "uint8".
Returns:
synthetic_blob (numpy.ndarray): Generated 3D volume with specified parameters.
Raises:
TypeError: If `final_shape` is not a tuple or does not have three elements.
ValueError: If `dtype` is not a valid numpy number type.
Example:
```python
import qim3d
# Generate synthetic blob
synthetic_blob = qim3d.generate.blob(noise_scale = 0.05)
# Visualize slices
qim3d.viz.slices(synthetic_blob, vmin = 0, vmax = 255, n_slices = 15)
```
![synthetic_blob](assets/screenshots/synthetic_blob_slices.png)
```python
# Visualize 3D volume
qim3d.viz.vol(synthetic_blob)
```
<iframe src="https://platform.qim.dk/k3d/synthetic_blob.html" width="100%" height="500" frameborder="0"></iframe>
"""
if not isinstance(final_shape, tuple) or len(final_shape) != 3:
raise TypeError("Size must be a tuple of 3 dimensions")
if not np.issubdtype(dtype, np.number):
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)
# Generate grid of coordinates
z, y, x = np.indices(base_shape)
# 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(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
noise = vectorized_pnoise3(z.flatten() * noise_scale,
y.flatten() * noise_scale,
x.flatten() * noise_scale
).reshape(base_shape)
volume = (1 + noise) * (1 - dist)
# Normalize
volume = (volume - np.min(volume)) / (np.max(volume) - np.min(volume))
# Gamma correction
volume = np.power(volume, gamma)
# Scale the volume to the maximum value
volume = volume * max_value
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])
])
# Compute threshold such that there will be no straight cuts in the blob
threshold = max_border_value / max_value
# Clip the low values of the volume to create a coherent volume
volume[volume < threshold * max_value] = 0
# Clip high values
volume[volume > max_value] = max_value
# Scale up the volume of volume to size
volume = scipy.ndimage.zoom(
volume, np.array(final_shape) / np.array(base_shape), order=order
)
return volume.astype(dtype)
import numpy as np
import scipy.ndimage
from tqdm.notebook import tqdm
import qim3d.generate
from qim3d.utils.logger import log
def random_placement(
collection: np.ndarray,
blob: np.ndarray,
rng: np.random.Generator,
) -> tuple[np.ndarray, bool]:
"""
Place blob at random available position in collection.
Args:
collection (numpy.ndarray): 3D volume of the collection.
blob (numpy.ndarray): 3D volume of the blob.
rng (numpy.random.Generator): Random number generator.
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)
# Flag for placement success
placed = False
# Attempt counter
j = 1
while (not placed) and (j < 200_000):
# Select a random available position in collection
idx = rng.choice(len(available_z))
z, y, x = available_z[idx], available_y[idx], available_x[idx]
start = np.array([z, y, x]) # Start position of blob placement
end = start + np.array(blob.shape) # End position of blob placement
# Check if the blob fits in the selected region (without overlap)
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
)
placed = True
# Increment attempt counter
j += 1
return collection, placed
def specific_placement(
collection: np.ndarray,
blob: np.ndarray,
positions: list[tuple],
) -> tuple[np.ndarray, bool]:
"""
Place blob at one of the specified positions in the collection.
Args:
collection (numpy.ndarray): 3D volume of the collection.
blob (numpy.ndarray): 3D volume of the blob.
positions (list[tuple]): List of specified positions as (z, y, x) coordinates for the blobs.
Returns:
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
for position in positions:
# Get coordinates of next position
z, y, x = position
# Place blob with center at specified position
start = (
np.array([z, y, x]) - np.array(blob.shape) // 2
) # Start position of blob placement
end = start + np.array(blob.shape) # End position of blob placement
# Check if the blob fits in the selected region (without overlap)
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
)
placed = True
# Remove position from list
positions.remove(position)
break
return collection, placed, positions
def collection(
collection_shape: tuple = (200, 200, 200),
num_objects: int = 15,
positions: list[tuple] = None,
min_shape: tuple = (40, 40, 40),
max_shape: tuple = (60, 60, 60),
object_shape_zoom: tuple = (1.0, 1.0, 1.0),
min_object_noise: float = 0.02,
max_object_noise: float = 0.05,
min_rotation_degrees: int = 0,
max_rotation_degrees: int = 360,
rotation_axes: list[tuple] = [(0, 1), (0, 2), (1, 2)],
min_gamma: float = 0.8,
max_gamma: float = 1.2,
min_high_value: int = 128,
max_high_value: int = 255,
min_threshold: float = 0.5,
max_threshold: float = 0.6,
smooth_borders: bool = False,
seed: int = 0,
verbose: bool = False,
) -> tuple[np.ndarray, object]:
"""
Generate a 3D volume of multiple synthetic objects using Perlin noise.
Args:
collection_shape (tuple, optional): Shape of the final collection volume to generate. Defaults to (200, 200, 200).
num_objects (int, optional): Number of synthetic objects to include in the collection. Defaults to 15.
positions (list[tuple], optional): List of specific positions as (z, y, x) coordinates for the objects. If not provided, they are placed randomly into the collection. Defaults to None.
min_shape (tuple, optional): Minimum shape of the objects. Defaults to (40, 40, 40).
max_shape (tuple, optional): Maximum shape of the objects. Defaults to (60, 60, 60).
object_shape_zoom (tuple, optional): Scaling factors for each dimension of each object. Defaults to (1.0, 1.0, 1.0).
min_object_noise (float, optional): Minimum scale factor for Perlin noise. Defaults to 0.02.
max_object_noise (float, optional): Maximum scale factor for Perlin noise. Defaults to 0.05.
min_rotation_degrees (int, optional): Minimum rotation angle in degrees. Defaults to 0.
max_rotation_degrees (int, optional): Maximum rotation angle in degrees. Defaults to 360.
rotation_axes (list[tuple], optional): List of axis pairs that will be randomly chosen to rotate around. Defaults to [(0, 1), (0, 2), (1, 2)].
min_gamma (float, optional): Minimum gamma correction factor. Defaults to 0.8.
max_gamma (float, optional): Maximum gamma correction factor. Defaults to 1.2.
min_high_value (int, optional): Minimum maximum value for the volume intensity. Defaults to 128.
max_high_value (int, optional): Maximum maximum value for the volume intensity. Defaults to 255.
min_threshold (float, optional): Minimum threshold value for clipping low intensity values. Defaults to 0.5.
max_threshold (float, optional): Maximum threshold value for clipping low intensity values. Defaults to 0.6.
smooth_borders (bool, optional): Flag for smoothing blob borders to avoid straight edges in the objects. If True, the `min_threshold` and `max_threshold` parameters are ignored. Defaults to False.
seed (int, optional): Seed for reproducibility. Defaults to 0.
verbose (bool, optional): Flag to enable verbose logging. Defaults to False.
Returns:
synthetic_collection (numpy.ndarray): 3D volume of the generated collection of synthetic objects with specified parameters.
labels (numpy.ndarray): Array with labels for each voxel, same shape as synthetic_collection.
Raises:
TypeError: If `collection_shape` is not 3D.
ValueError: If blob parameters are invalid.
Note:
- The function places objects without overlap.
- The function can either place objects at random positions in the collection (if `positions = None`) or at specific positions provided in the `positions` argument. If specific positions are provided, the number of blobs must match the number of positions (e.g. `num_objects = 2` with `positions = [(12, 8, 10), (24, 20, 18)]`).
- If not all `num_objects` can be placed, the function returns the `synthetic_collection` volume with as many blobs as possible in it, and logs an error.
- Labels for all objects are returned, even if they are not a sigle connected component.
Example:
```python
import qim3d
# Generate synthetic collection of blobs
num_objects = 15
synthetic_collection, labels = qim3d.generate.collection(num_objects = num_objects)
# Visualize synthetic collection
qim3d.viz.vol(synthetic_collection)
```
<iframe src="https://platform.qim.dk/k3d/synthetic_collection_default.html" width="100%" height="500" frameborder="0"></iframe>
```python
qim3d.viz.slicer(synthetic_collection)
```
![synthetic_collection](assets/screenshots/synthetic_collection_default.gif)
```python
# Visualize labels
cmap = qim3d.viz.colormaps.objects(nlabels=num_objects)
qim3d.viz.slicer(labels, cmap=cmap, vmax=num_objects)
```
![synthetic_collection](assets/screenshots/synthetic_collection_default_labels.gif)
Example:
```python
import qim3d
# Generate synthetic collection of dense blobs
synthetic_collection, labels = qim3d.generate.collection(
min_high_value = 255,
max_high_value = 255,
min_object_noise = 0.05,
max_object_noise = 0.05,
min_threshold = 0.99,
max_threshold = 0.99,
min_gamma = 0.02,
max_gamma = 0.02)
# Visualize synthetic collection
qim3d.viz.vol(synthetic_collection)
```
<iframe src="https://platform.qim.dk/k3d/synthetic_collection_dense.html" width="100%" height="500" frameborder="0"></iframe>
Example:
```python
import qim3d
# Generate synthetic collection of tubular structures
synthetic_collection, labels = qim3d.generate.collection(
num_objects=10,
collection_shape=(200,100,100),
min_shape = (190, 50, 50),
max_shape = (200, 60, 60),
object_shape_zoom = (1, 0.2, 0.2),
min_object_noise = 0.01,
max_object_noise = 0.02,
max_rotation_degrees=10,
min_threshold = 0.95,
max_threshold = 0.98,
min_gamma = 0.02,
max_gamma = 0.03
)
# Visualize synthetic collection
qim3d.viz.vol(synthetic_collection)
```
<iframe src="https://platform.qim.dk/k3d/synthetic_collection_tubular.html" width="100%" height="500" frameborder="0"></iframe>
"""
if verbose:
original_log_level = log.getEffectiveLevel()
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)"
)
if len(min_shape) != len(max_shape):
raise ValueError("Object shapes must be tuples of the same length")
# if not isinstance(blob_shapes, list) or \
# len(blob_shapes) != 2 or len(blob_shapes[0]) != 3 or len(blob_shapes[1]) != 3:
# raise TypeError("Blob shapes must be a list of two tuples with three dimensions (z, y, x)")
if (positions is not None) and (len(positions) != num_objects):
raise ValueError(
"Number of objects must match number of positions, otherwise set positions = None"
)
# Set seed for random number generator
rng = np.random.default_rng(seed)
# Initialize the 3D array for the shape
collection_array = np.zeros(
(collection_shape[0], collection_shape[1], collection_shape[2]), dtype=np.uint8
)
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}")
# Sample from blob parameter ranges
if min_shape == max_shape:
blob_shape = min_shape
else:
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}")
# Sample noise scale
noise_scale = rng.uniform(low=min_object_noise, high=max_object_noise)
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}")
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}")
threshold = rng.uniform(low=min_threshold, high=max_threshold)
log.debug(f"- Threshold: {threshold:.3f}")
# Generate synthetic blob
blob = qim3d.generate.blob(
base_shape=blob_shape,
final_shape=tuple(l * r for l, r in zip(blob_shape, object_shape_zoom)),
noise_scale=noise_scale,
gamma=gamma,
max_value=max_value,
threshold=threshold,
smooth_borders=smooth_borders,
)
# Rotate object
if max_rotation_degrees > 0:
angle = rng.uniform(
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}")
blob = scipy.ndimage.rotate(blob, angle, axes, order=0)
# Place synthetic object into the collection
# If positions are specified, place blob at one of the specified positions
collection_before = collection_array.copy()
if positions:
collection_array, placed, positions = specific_placement(
collection_array, blob, positions
)
# Otherwise, place blob at a random available position
else:
collection_array, placed = random_placement(collection_array, blob, rng)
# Break if blob could not be placed
if not placed:
break
# Update labels
new_labels = np.where(collection_array != collection_before, i + 1, 0).astype(
labels.dtype
)
labels += new_labels
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."
)
if verbose:
log.setLevel(original_log_level)
return collection_array, labels
from fastapi import FastAPI
import qim3d.utils
from . import data_explorer
from . import iso3d
from . import local_thickness
from . import annotation_tool
from .qim_theme import QimTheme
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"]
else:
raise Exception("Port not specified from QIM API")
qim3d.utils.gradio_header(gradio_interface.title, port)
# Create FastAPI with mounted gradio interface
app = FastAPI()
path = f"/gui/{port_dict['username']}/{port}/"
app = gr.mount_gradio_app(app, gradio_interface, path=path)
# Full path
print(f"http://{host}:{port}{path}")
# Run the FastAPI server usign uvicorn
uvicorn.run(app, host=host, port=int(port))
import tifffile
import os
import numpy as np
import gradio as gr
from qim3d.io import load # load or DataLoader?
from qim3d.utils import internal_tools
class Interface:
def __init__(self):
self.verbose = False
self.title = "Annotation tool"
# self.plot_height = 768
self.height = 1024
# self.width = 960
self.max_masks = 3
self.mask_opacity = 0.5
self.cmy_hex = ["#00ffff", "#ff00ff", "#ffff00"] # Colors for max_masks>3?
"""
The GUI can be launched directly from the command line:
# CSS path
current_dir = os.path.dirname(os.path.abspath(__file__))
self.css_path = os.path.join(current_dir, "..", "css", "gradio.css")
```bash
qim3d gui --annotation-tool
```
def launch(self, img=None, **kwargs):
# Create gradio interfaces
self.interface = self.create_interface(img=img)
Or launched from a python script
# Set gradio verbose level
if self.verbose:
quiet = False
else:
quiet = True
```python
import qim3d
self.interface.launch(
quiet=quiet,
height=self.height,
# width=self.width,
show_tips=False,
**kwargs,
)
return
def get_result(self):
# Get the temporary files from gradio
temp_sets = self.interface.temp_file_sets
for temp_set in temp_sets:
if "mask" in str(temp_set):
# Get the list of the temporary files
temp_path_list = list(temp_set)
# Files are not in creation order,
# so we need to get find the latest
creation_time_list = []
for path in temp_path_list:
creation_time_list.append(os.path.getctime(path))
# Get index for the latest file
file_idx = np.argmax(creation_time_list)
# Load the temporary file
mask = load(temp_path_list[file_idx])
return mask
def create_interface(self, img=None):
with gr.Blocks(css=self.css_path) as gradio_interface:
masks_state = gr.State(value={})
counts = gr.Number(value=1, visible=False)
with gr.Row():
with gr.Column(scale=1, min_width=320):
upload_img_btn = gr.UploadButton(
label="Upload image",
file_types=["image"],
interactive=True if img is None else False,
)
clear_img_btn = gr.Button(
value="Clear image", interactive=False if img is None else True
)
vol = qim3d.examples.NT_128x128x128
annotation_tool = qim3d.gui.annotation_tool.Interface()
with gr.Row():
with gr.Column(scale=2, min_width=32):
selected_mask = gr.Radio(
choices=["Mask 1"],
value="Mask 1",
label="Choose which mask to draw",
scale=1,
)
with gr.Column(scale=1, min_width=64):
add_mask_btn = gr.Button(
value="Add mask",
scale=2,
)
with gr.Row():
prep_dl_btn = gr.Button(
value="Prepare mask for download",
visible=False if img is None else True,
)
with gr.Row():
save_output = gr.File(
show_label=True,
label="Output file",
visible=False,
)
# We can directly pass the image we loaded to the interface
app = annotation_tool.launch(vol[0])
```
![gui-annotation_tool](assets/screenshots/gui-annotation_tool.gif)
with gr.Column(scale=4):
with gr.Row():
input_img = gr.Image(
label="Input",
tool="sketch",
value=img,
height=600,
width=600,
brush_color="#00ffff",
mask_opacity=self.mask_opacity,
interactive=False if img is None else True,
)
"""
output_masks = []
for mask_idx in range(self.max_masks):
with gr.Row(): # make a new row for every mask
output_mask = gr.Image(
label=f"Mask {mask_idx+1}",
visible=True if mask_idx == 0 else False,
image_mode="L",
height=600,
width=600,
interactive=False
if img is None
else True, # If statement added bc of bug after Gradio 3.44.x
show_download_button=False,
)
output_masks.append(output_mask)
import getpass
import os
import tempfile
# Operations
operations = Operations(max_masks=self.max_masks, cmy_hex=self.cmy_hex)
import gradio as gr
import numpy as np
from PIL import Image
# Update component configuration when image is uploaded
upload_img_btn.upload(
fn=operations.upload_img_update,
inputs=upload_img_btn,
outputs=[input_img, clear_img_btn, upload_img_btn, prep_dl_btn]
+ output_masks,
)
from qim3d.io import load, save
from qim3d.processing.operations import overlay_rgb_images
from qim3d.gui.interface import BaseInterface
# Add mask below when 'add mask' button is clicked
add_mask_btn.click(
fn=operations.increment_mask,
inputs=counts,
outputs=[counts, selected_mask] + output_masks,
)
# TODO: img in launch should be self.img
# Draw mask when input image is edited
input_img.edit(
fn=operations.update_masks,
inputs=[input_img, selected_mask, masks_state, upload_img_btn],
outputs=output_masks,
)
# Update brush color according to radio setting
selected_mask.change(
fn=operations.update_brush_color,
inputs=selected_mask,
outputs=input_img,
class Interface(BaseInterface):
def __init__(self, name_suffix: str = "", verbose: bool = False, img=None):
super().__init__(
title="Annotation Tool",
height=768,
width="100%",
verbose=verbose,
custom_css="annotation_tool.css",
)
# Make file download visible
prep_dl_btn.click(
fn=operations.save_mask,
inputs=output_masks,
outputs=[save_output, save_output],
).success(
fn=lambda: os.remove('mask.tif')
) # Remove mask file from working directory immediately after sending it to /tmp/gradio
self.username = getpass.getuser()
self.temp_dir = os.path.join(tempfile.gettempdir(), f"qim-{self.username}")
self.name_suffix = name_suffix
self.img = img
# Update 'Add mask' button interactivit according to the current count
counts.change(
fn=operations.set_add_mask_btn_interactivity,
inputs=counts,
outputs=add_mask_btn,
)
self.n_masks = 3
self.img_editor = None
self.masks_rgb = None
self.temp_files = []
# Reset component configuration when image is cleared
clear_img_btn.click(
fn=operations.clear_img_update,
inputs=None,
outputs=[
selected_mask,
prep_dl_btn,
save_output,
counts,
input_img,
upload_img_btn,
clear_img_btn,
def get_result(self):
# 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):
# Get the list of the temporary files
temp_path_list.append(os.path.join(self.temp_dir, filename))
# Make dictionary of maks
masks = {}
for temp_file in temp_path_list:
mask_file = os.path.basename(temp_file)
mask_name = os.path.splitext(mask_file)[0]
masks[mask_name] = load(temp_file)
return masks
def clear_files(self):
"""
Should be moved up to __init__ function, but given how is this interface implemented in some files
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}",
]
+ output_masks,
)
return gradio_interface
class Operations:
def __init__(self, max_masks, cmy_hex):
self.max_masks = max_masks
self.cmy_hex = cmy_hex
def update_masks(self, input_img, selected_mask, masks_state, file):
# Binarize mask (it is not per default due to anti-aliasing)
input_mask = input_img["mask"]
input_mask[input_mask > 0] = 255
# Clean up old files
try:
file_name = file.name
except AttributeError:
file_name = "nb_img"
# Add new file to state dictionary when this function sees it first time
if file_name not in masks_state.keys():
masks_state[file_name] = [[] for _ in range(self.max_masks)]
# Get index of currently selected and non-selected masks
sel_mask_idx = int(selected_mask[-1]) - 1
nonsel_mask_idxs = [
mask_idx
for mask_idx in list(range(self.max_masks))
if mask_idx != sel_mask_idx
]
# Add background to state first time function is invoked in current session
if len(masks_state[file_name][0]) == 0:
for i in range(len(masks_state[file_name])):
masks_state[file_name][i].append(input_mask)
# Check for discrepancy between what is drawn and what is shown as output masks
masks_state_combined = 0
for i in range(len(masks_state[file_name])):
masks_state_combined += masks_state[file_name][i][-1]
discrepancy = masks_state_combined != input_mask
if np.any(discrepancy): # Correct discrepancy in output masks
for i in range(self.max_masks):
masks_state[file_name][i][-1][discrepancy] = 0
# Add most recent change in input to currently selected mask
mask2append = input_mask
for mask_idx in nonsel_mask_idxs:
mask2append -= masks_state[file_name][mask_idx][-1]
masks_state[file_name][sel_mask_idx].append(mask2append)
return [masks_state[file_name][i][-1] for i in range(self.max_masks)]
def save_mask(self, *masks):
# Go from multi-channel to single-channel mask
stacked_masks = np.stack(masks, axis=-1)
final_mask = np.zeros_like(masks[0])
final_mask[np.where(stacked_masks == 255)[:2]] = (
np.where(stacked_masks == 255)[-1] + 1
)
# Save output image in a temp space (and to current directory which is a bug)
filename = "mask.tif"
tifffile.imwrite(filename, final_mask)
save_output_update = gr.File(visible=True)
return save_output_update, filename
def increment_mask(self, counts):
# increment count by 1
counts += 1
counts = int(counts)
counts_update = gr.Number(value=counts)
selected_mask_update = gr.Radio(
value=f"Mask {counts}", choices=[f"Mask {i+1}" for i in range(counts)]
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):
file_path = os.path.join(self.temp_dir, filename)
os.remove(file_path)
except FileNotFoundError:
files = None
def create_preview(self, img_editor):
background = img_editor["background"]
masks = img_editor["layers"][0]
overlay_image = overlay_rgb_images(background, masks)
return overlay_image
def cerate_download_list(self, img_editor):
masks_rgb = img_editor["layers"][0]
mask_threshold = 200 # This value is based
mask_list = []
files_list = []
# Go through each channel
for idx in range(self.n_masks):
mask_grayscale = masks_rgb[:, :, idx]
mask = mask_grayscale > mask_threshold
# Save only if we have a mask
if np.sum(mask) > 0:
mask_list.append(mask)
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)
files_list.append(filepath)
save(filepath, mask, replace=True)
self.temp_files.append(filepath)
return files_list
def define_interface(self, **kwargs):
brush = gr.Brush(
colors=[
"rgb(255,50,100)",
"rgb(50,250,100)",
"rgb(50,100,255)",
],
color_mode="fixed",
default_size=10,
)
output_masks_update = [gr.Image(visible=True)] * counts + [
gr.Image(visible=False)
] * (self.max_masks - counts)
return [counts_update, selected_mask_update] + output_masks_update
def update_brush_color(self, selected_mask):
sel_mask_idx = int(selected_mask[-1]) - 1
if sel_mask_idx < len(self.cmy_hex):
input_img_update = gr.Image(brush_color=self.cmy_hex[sel_mask_idx])
else:
input_img_update = gr.Image(brush_color="#000000") # Return black brush
return input_img_update
with gr.Row():
with gr.Column(
scale=6,
):
img_editor = gr.ImageEditor(
value=(
{
"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",
brush=brush,
sources="upload",
interactive=True,
show_download_button=True,
container=False,
transforms=["crop"],
layers=False,
)
with gr.Column(scale=1, min_width=256):
def set_add_mask_btn_interactivity(self, counts):
add_mask_btn_update = (
gr.Button(interactive=True)
if counts < self.max_masks
else gr.Button(interactive=False)
with gr.Row():
overlay_img = gr.Image(
show_download_button=False,
show_label=False,
visible=False,
)
return add_mask_btn_update
def clear_img_update(self):
selected_mask_update = gr.Radio(
choices=["Mask 1"], value="Mask 1"
) # Reset radio component to only show 'Mask 1'
prep_dl_btn_update = gr.Button(
visible=False
) # Make 'Prepare mask for download' button invisible
save_output_update = gr.File(visible=False) # Make File save box invisible
counts_update = gr.Number(value=1) # Reset invisible counter to 1
input_img_update = gr.Image(
value=None, interactive=False
) # Set input image component to non-interactive (so a new image cannot be uploaded directly in the component)
upload_img_btn_update = gr.Button(
interactive=True
) # Make 'Upload image' button interactive
clear_img_btn_update = gr.Button(
interactive=False
) # Make 'Clear image' button non-interactive
output_masks_update = [
gr.Image(value=None, visible=True if i == 0 else False, interactive=False)
for i in range(self.max_masks)
] # Remove drawn masks and set as invisible except mask 1. 'interactive=False' added bc of bug after Gradio 3.44.x
return [
selected_mask_update,
prep_dl_btn_update,
save_output_update,
counts_update,
input_img_update,
upload_img_btn_update,
clear_img_btn_update,
] + output_masks_update
def upload_img_update(self, file):
input_img_update = gr.Image(
value=load(file.name), interactive=True
) # Upload image from button to Image components
clear_img_btn_update = gr.Button(
interactive=True
) # Make 'Clear image' button interactive
upload_img_btn_update = gr.Button(
interactive=False
) # Make 'Upload image' button non-interactive
prep_dl_btn_update = gr.Button(
visible=True
) # Make 'Prepare mask for download' button visible
output_masks_update = [
gr.Image(interactive=True)
] * self.max_masks # This line is added bc of bug in Gradio after 3.44.x
return [
input_img_update,
clear_img_btn_update,
upload_img_btn_update,
prep_dl_btn_update,
] + output_masks_update
def run_interface(host = "0.0.0.0"):
gradio_interface = Interface().create_interface()
internal_tools.run_gradio_app(gradio_interface,host)
if __name__ == "__main__":
# Creates interface
run_interface()
\ No newline at end of file
with gr.Row():
masks_download = gr.File(label="Download masks", visible=False)
# fmt: off
img_editor.change(
fn = self.clear_files, inputs = None , outputs = None).then( # Prepares for handling the new update
fn = self.create_preview, inputs = img_editor, outputs = overlay_img).then( # Create the preview in top right corner
fn = self.set_visible, inputs = None, outputs = overlay_img).then( # Makes the preview visible
fn = self.cerate_download_list, inputs = img_editor, outputs = masks_download).then(# Separates the color mask and put them into file list
fn = self.set_visible, inputs = None, outputs = masks_download) # Displays the download file list
This diff is collapsed.
from pathlib import Path
from abc import abstractmethod, ABC
from os import path, listdir
import gradio as gr
from .qim_theme import QimTheme
import qim3d.gui
# TODO: when offline it throws an error in cli
class BaseInterface(ABC):
"""
Annotation tool and Data explorer as those don't need any examples.
"""
def __init__(
self,
title: str,
height: int,
width: int = "100%",
verbose: bool = False,
custom_css: str = None,
):
"""
title: Is displayed in tab
height, width: If inline in launch method is True, sets the paramters of the widget. Inline defaults to True in py notebooks, otherwise is False
verbose: If True, updates are printed into terminal
custom_css: Only the name of the file in the css folder.
"""
self.title = title
self.height = height
self.width = width
self.verbose = bool(verbose)
self.interface = None
self.qim_dir = Path(qim3d.__file__).parents[0]
self.custom_css = (
path.join(self.qim_dir, "css", custom_css)
if custom_css is not None
else None
)
def set_visible(self):
return gr.update(visible=True)
def set_invisible(self):
return gr.update(visible=False)
def launch(self, img=None, force_light_mode: bool = True, **kwargs):
"""
img: If None, user can upload image after the interface is launched.
If defined, the interface will be launched with the image already there
This argument is used especially in jupyter notebooks, where you can launch
interface in loop with different picture every step
force_light_mode: The qim platform doesn't have night mode. The qim_theme thus
has option to display only light mode so it corresponds with the website. Preferably
will be removed as we add night mode to the website.
"""
# Create gradio interface
if img is not None:
self.img = img
self.interface = self.create_interface(force_light_mode=force_light_mode)
self.interface.launch(
quiet=not self.verbose,
height=self.height,
width=self.width,
favicon_path=Path(qim3d.__file__).parents[0]
/ "../docs/assets/qim3d-icon.svg",
**kwargs,
)
def clear(self):
"""Used to reset outputs with the clear button"""
return None
def create_interface(self, force_light_mode: bool = True, **kwargs):
# kwargs["img"] = self.img
with gr.Blocks(
theme=qim3d.gui.QimTheme(force_light_mode=force_light_mode),
title=self.title,
css=self.custom_css,
) as gradio_interface:
gr.Markdown(f"# {self.title}")
self.define_interface(**kwargs)
return gradio_interface
@abstractmethod
def define_interface(self, **kwargs):
pass
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
"""
def __init__(
self,
title: str,
height: int,
width: int,
verbose: bool = False,
custom_css: str = None,
):
super().__init__(title, height, width, verbose, custom_css)
self._set_examples_list()
def _set_examples_list(self):
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)]
This diff is collapsed.