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
  • 3D_UNet
  • 3d_watershed
  • conv_zarr_tiff_folders
  • convert_tiff_folders
  • layered_surface_segmentation
  • main
  • memmap_txrm
  • notebook_update
  • notebooks
  • notebooksv1
  • optimize_scaleZYXdask
  • save_files_function
  • scaleZYX_mean
  • test
  • threshold-exploration
  • tr_val_te_splits
  • v0.2.0
  • v0.3.0
  • v0.3.1
  • v0.3.2
  • v0.3.3
  • v0.3.9
  • v0.4.0
  • v0.4.1
24 results

Target

Select target project
  • QIM/tools/qim3d
1 result
Select Git revision
  • 3D_UNet
  • 3d_watershed
  • conv_zarr_tiff_folders
  • convert_tiff_folders
  • layered_surface_segmentation
  • main
  • memmap_txrm
  • notebook_update
  • notebooks
  • notebooksv1
  • optimize_scaleZYXdask
  • save_files_function
  • scaleZYX_mean
  • test
  • threshold-exploration
  • tr_val_te_splits
  • v0.2.0
  • v0.3.0
  • v0.3.1
  • v0.3.2
  • v0.3.3
  • v0.3.9
  • v0.4.0
  • v0.4.1
24 results
Show changes
Commits on Source (7)
Showing
with 871 additions and 49 deletions
docs/assets/screenshots/GUI-layers.png

223 KiB

docs/assets/screenshots/layers.png

77 KiB

docs/assets/screenshots/segmented_layers.png

83.1 KiB

......@@ -30,6 +30,8 @@ This offers quick interactions, making it ideal for tasks that require efficienc
| `--data-explorer` | Starts the Data Explorer |
| `--iso3d` | Starts the 3D Isosurfaces visualization |
| `--local-thickness` | Starts the Local thickness tool |
| `--anotation-tool` | Starts the annotation tool |
| `--layers` | Starts the tool for segmenting layers |
| `--host` | Desired host for the server. By default runs on `0.0.0.0` |
| `--platform` | Uses the Qim platform API for a unique path and port depending on the username |
......
......@@ -36,3 +36,7 @@ For details see [here](cli.md#qim3d-gui).
::: qim3d.gui.annotation_tool
options:
members: False
::: qim3d.gui.layers2d
options:
members: False
\ No newline at end of file
......@@ -14,6 +14,8 @@ Here, we provide functionalities designed specifically for 3D image analysis and
- maximum
- minimum
- tophat
- get_lines
- segment_layers
- create_mesh
::: qim3d.processing.Pipeline
......
......@@ -34,6 +34,9 @@ def main():
gui_parser.add_argument(
"--local-thickness", action="store_true", help="Run local thickness tool."
)
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"
......@@ -137,6 +140,8 @@ def main():
interface_class = qim3d.gui.annotation_tool.Interface
elif args.local_thickness:
interface_class = qim3d.gui.local_thickness.Interface
elif args.layers:
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"
......
qim3d/examples/slice_218x193.png

21.9 KiB

......@@ -4,6 +4,7 @@ from . import data_explorer
from . import iso3d
from . import local_thickness
from . import annotation_tool
from . import layers2d
from .qim_theme import QimTheme
......
......@@ -48,6 +48,9 @@ class BaseInterface(ABC):
def set_invisible(self):
return gr.update(visible=False)
def change_visibility(self, is_visible):
return gr.update(visible = is_visible)
def launch(self, img=None, force_light_mode: bool = True, **kwargs):
"""
img: If None, user can upload image after the interface is launched.
......
This diff is collapsed.
......@@ -5,6 +5,7 @@ Exporting data to different formats.
import os
import math
import shutil
import logging
import numpy as np
import zarr
......@@ -23,6 +24,9 @@ from ome_zarr import scale
from scipy.ndimage import zoom
from typing import Any, Callable, Iterator, List, Tuple, Union
import dask.array as da
import dask
from dask.distributed import Client, LocalCluster
from skimage.transform import (
resize,
)
......@@ -54,13 +58,112 @@ class OMEScaler(
log.info(f"- Scale 0: {rv[-1].shape}")
for i in range(self.max_layer):
downscale_ratio = (1 / self.downscale) ** (i + 1)
rv.append(zoom(base, zoom=downscale_ratio, order=self.order))
rv.append(zoom(rv[-1], zoom=1 / self.downscale, order=self.order))
log.info(f"- Scale {i+1}: {rv[-1].shape}")
return list(rv)
def scaleZYXdask(self, base):
"""Downsample using :func:`scipy.ndimage.zoom`."""
"""
Downsample a 3D volume using Dask and scipy.ndimage.zoom.
This method performs multi-scale downsampling on a 3D dataset, generating image pyramids. It processes the data in chunks using Dask.
Args:
base (dask.array): The 3D array (volume) to be downsampled. Must be a Dask array for chunked processing.
Returns:
list of dask.array: A list of downsampled volumes, where each element represents a different scale. The first element corresponds to the original resolution, and subsequent elements represent progressively downsampled versions.
The downsampling process occurs scale by scale, using the following steps:
- For each scale, the array is resized based on the downscale factor, computed as a function of the current scale level.
- The `scipy.ndimage.zoom` function is used to perform interpolation, with chunk-wise processing handled by Dask's `map_blocks` function.
- The output is rechunked to match the input volume's original chunk size.
"""
def resize_zoom(vol, scale_factors, order, scaled_shape):
# Get the chunksize needed so that all the blocks match the new shape
# This snippet comes from the original OME-Zarr-python library
better_chunksize = tuple(
np.maximum(
1, np.round(np.array(vol.chunksize) * scale_factors) / scale_factors
).astype(int)
)
log.debug(f"better chunk size: {better_chunksize}")
# Compute the chunk size after the downscaling
new_chunk_size = tuple(
np.ceil(np.multiply(better_chunksize, scale_factors)).astype(int)
)
log.debug(
f"orginal chunk size: {vol.chunksize}, chunk size after downscale: {new_chunk_size}"
)
def resize_chunk(chunk, scale_factors, order):
#print(f"zoom factors: {scale_factors}")
resized_chunk = zoom(
chunk,
zoom=scale_factors,
order=order,
mode="grid-constant",
grid_mode=True,
)
#print(f"resized chunk shape: {resized_chunk.shape}")
return resized_chunk
output_slices = tuple(slice(0, d) for d in scaled_shape)
# Testing new shape
predicted_shape = np.multiply(vol.shape, scale_factors)
log.debug(f"predicted shape: {predicted_shape}")
scaled_vol = da.map_blocks(
resize_chunk,
vol,
scale_factors,
order,
chunks=new_chunk_size,
)[output_slices]
# Rechunk the output to match the input
# This is needed because chunks were scaled down
scaled_vol = scaled_vol.rechunk(vol.chunksize)
return scaled_vol
rv = [base]
log.info(f"- Scale 0: {rv[-1].shape}")
for i in range(self.max_layer):
log.debug(f"\nScale {i+1}\n{'-'*32}")
# Calculate the downscale factor for this scale
downscale_factor = 1 / (self.downscale ** (i + 1))
scaled_shape = tuple(
np.ceil(np.multiply(base.shape, downscale_factor)).astype(int)
)
log.debug(f"target shape: {scaled_shape}")
downscale_rate = tuple(np.divide(rv[-1].shape, scaled_shape).astype(float))
log.debug(f"downscale rate: {downscale_rate}")
scale_factors = tuple(np.divide(1, downscale_rate))
log.debug(f"scale factors: {scale_factors}")
log.debug("\nResizing volume chunk-wise")
scaled_vol = resize_zoom(rv[-1], scale_factors, self.order, scaled_shape)
rv.append(scaled_vol)
log.info(f"- Scale {i+1}: {rv[-1].shape}")
return list(rv)
def scaleZYXdask_legacy(self, base):
"""Downsample using the original OME-Zarr python library"""
rv = [base]
log.info(f"- Scale 0: {rv[-1].shape}")
......@@ -69,7 +172,9 @@ class OMEScaler(
scaled_shape = tuple(
base.shape[j] // (self.downscale ** (i + 1)) for j in range(3)
)
rv.append(dask_resize(base, scaled_shape, order=self.order))
scaled = dask_resize(base, scaled_shape, order=self.order)
rv.append(scaled)
log.info(f"- Scale {i+1}: {rv[-1].shape}")
return list(rv)
......@@ -78,27 +183,30 @@ class OMEScaler(
def export_ome_zarr(
path,
data,
chunk_size=100,
chunk_size=256,
downsample_rate=2,
order=0,
order=1,
replace=False,
method="scaleZYX",
progress_bar: bool = True,
progress_bar_repeat_time="auto",
):
"""
Export image data to OME-Zarr format with pyramidal downsampling.
Export 3D image data to OME-Zarr format with pyramidal downsampling.
Automatically calculates the number of downsampled scales such that the smallest scale fits within the specified `chunk_size`.
This function generates a multi-scale OME-Zarr representation of the input data, which is commonly used for large imaging datasets. The downsampled scales are calculated such that the smallest scale fits within the specified `chunk_size`.
Args:
path (str): The directory where the OME-Zarr data will be stored.
data (np.ndarray): The image data to be exported.
chunk_size (int, optional): The size of the chunks for storing data. Defaults to 100.
downsample_rate (int, optional): Factor by which to downsample the data for each scale. Must be greater than 1. Defaults to 2.
order (int, optional): Interpolation order to use when downsampling. Defaults to 0 (nearest-neighbor).
data (np.ndarray or dask.array): The 3D image data to be exported. Supports both NumPy and Dask arrays.
chunk_size (int, optional): The size of the chunks for storing data. This affects both the original data and the downsampled scales. Defaults to 256.
downsample_rate (int, optional): The factor by which to downsample the data for each scale. Must be greater than 1. Defaults to 2.
order (int, optional): The interpolation order to use when downsampling. Defaults to 1 (linear). Use 0 for a faster nearest-neighbor interpolation.
replace (bool, optional): Whether to replace the existing directory if it already exists. Defaults to False.
progress_bar (bool, optional): Whether to display progress while writing data to disk. Defaults to True.
method (str, optional): The method used for downsampling. If set to "dask", Dask arrays are used for chunking and downsampling. Defaults to "scaleZYX".
progress_bar (bool, optional): Whether to display a progress bar during export. Defaults to True.
progress_bar_repeat_time (str or int, optional): The repeat interval (in seconds) for updating the progress bar. Defaults to "auto".
Raises:
ValueError: If the directory already exists and `replace` is False.
ValueError: If `downsample_rate` is less than or equal to 1.
......@@ -111,9 +219,10 @@ def export_ome_zarr(
data = downloader.Snail.Escargot(load_file=True)
qim3d.io.export_ome_zarr("Escargot.zarr", data, chunk_size=100, downsample_rate=2)
```
Returns:
None: This function writes the OME-Zarr data to the specified directory and does not return any value.
"""
# Check if directory exists
......@@ -146,30 +255,47 @@ def export_ome_zarr(
store = parse_url(path, mode="w").store
root = zarr.group(store=store)
fmt = CurrentFormat()
log.info("Creating a multi-scale pyramid")
mip, axes = _create_mip(image=data, fmt=fmt, scaler=scaler, axes="zyx")
# Check if we want to process using Dask
if "dask" in method and not isinstance(data, da.Array):
log.info("\nConverting input data to Dask array")
data = da.from_array(data, chunks=(chunk_size, chunk_size, chunk_size))
log.info(f" - shape...: {data.shape}\n - chunks..: {data.chunksize}\n")
elif "dask" in method and isinstance(data, da.Array):
log.info("\nInput data will be rechunked")
data = data.rechunk((chunk_size, chunk_size, chunk_size))
log.info(f" - shape...: {data.shape}\n - chunks..: {data.chunksize}\n")
log.info("Calculating the multi-scale pyramid")
# Generate multi-scale pyramid
mip = scaler.func(data)
log.info("Writing data to disk")
kwargs = dict(
pyramid=mip,
group=root,
fmt=fmt,
axes=axes,
fmt=CurrentFormat(),
axes="zyx",
name=None,
compute=True,
storage_options=dict(chunks=(chunk_size, chunk_size, chunk_size)),
)
if progress_bar:
n_chunks = get_n_chunks(
shapes=(scaled_data.shape for scaled_data in mip),
dtypes = (scaled_data.dtype for scaled_data in mip)
dtypes=(scaled_data.dtype for scaled_data in mip),
)
with OmeZarrExportProgressBar(path = path, n_chunks = n_chunks, reapeat_time=progress_bar_repeat_time):
with OmeZarrExportProgressBar(
path=path, n_chunks=n_chunks, reapeat_time=progress_bar_repeat_time
):
write_multiscale(**kwargs)
else:
write_multiscale(**kwargs)
log.info("All done!")
log.info("\nAll done!")
return
......
......@@ -4,4 +4,5 @@ from .detection import blob_detection
from .filters import *
from .operations import *
from .cc import get_3d_cc
from .layers2d import segment_layers, get_lines
from .mesh import create_mesh
import numpy as np
from slgbuilder import GraphObject
from slgbuilder import MaxflowBuilder
def segment_layers(data:np.ndarray, inverted:bool = False, n_layers:int = 1, delta:float = 1, min_margin:int = 10, max_margin:int = None, wrap:bool = False):
"""
Works on 2D and 3D data.
Light one function wrapper around slgbuilder https://github.com/Skielex/slgbuilder to do layer segmentation
Now uses only MaxflowBuilder for solving.
Args:
data: 2D or 3D array on which it will be computed
inverted: if True, it will invert the brightness of the image
n_layers: How many layers are we looking for (result in a layer and background)
delta: Smoothness parameter
min_margin: If we want more layers, we have to have a margin otherwise they are all going to be exactly the same
max_margin: Maximum margin between layers
wrap: If True, starting and ending point of the border between layers are at the same level
Returns:
segmentations: list of numpy arrays, even if n_layers == 1, each array is only 0s and 1s, 1s segmenting this specific layer
Raises:
TypeError: If Data is not np.array, if n_layers is not integer.
ValueError: If n_layers is less than 1, if delta is negative or zero
Example:
Example is only shown on 2D image, but segment_layers can also take 3D structures.
```python
import qim3d
layers_image = qim3d.io.load('layers3d.tif')[:,:,0]
layers = qim3d.processing.segment_layers(layers_image, n_layers = 2)
layer_lines = qim3d.processing.get_lines(layers)
from matplotlib import pyplot as plt
plt.imshow(layers_image, cmap='gray')
plt.axis('off')
for layer_line in layer_lines:
plt.plot(layer_line, linewidth = 3)
```
![layer_segmentation](assets/screenshots/layers.png)
![layer_segmentation](assets/screenshots/segmented_layers.png)
"""
if isinstance(data, np.ndarray):
data = data.astype(np.int32)
if inverted:
data = ~data
else:
raise TypeError(F"Data has to be type np.ndarray. Your data is of type {type(data)}")
helper = MaxflowBuilder()
if not isinstance(n_layers, int):
raise TypeError(F"Number of layers has to be positive integer. You passed {type(n_layers)}")
if n_layers == 1:
layer = GraphObject(data)
helper.add_object(layer)
elif n_layers > 1:
layers = [GraphObject(data) for _ in range(n_layers)]
helper.add_objects(layers)
for i in range(len(layers)-1):
helper.add_layered_containment(layers[i], layers[i+1], min_margin=min_margin, max_margin=max_margin)
else:
raise ValueError(F"Number of layers has to be positive integer. You passed {n_layers}")
helper.add_layered_boundary_cost()
if delta > 1:
delta = int(delta)
elif delta <= 0:
raise ValueError(F'Delta has to be positive number. You passed {delta}')
helper.add_layered_smoothness(delta=delta, wrap = bool(wrap))
helper.solve()
if n_layers == 1:
segmentations =[helper.what_segments(layer)]
else:
segmentations = [helper.what_segments(l).astype(np.int32) for l in layers]
return segmentations
def get_lines(segmentations:list|np.ndarray) -> list:
"""
Expects list of arrays where each array is 2D segmentation with only 2 classes. This function gets the border between those two
so it could be plotted. Used with qim3d.processing.segment_layers
Args:
segmentations: list of arrays where each array is 2D segmentation with only 2 classes
Returns:
segmentation_lines: list of 1D numpy arrays
"""
segmentation_lines = [np.argmin(s, axis=0) - 0.5 for s in segmentations]
return segmentation_lines
\ No newline at end of file
......@@ -200,7 +200,7 @@ def fade_mask(
def overlay_rgb_images(
background: np.ndarray, foreground: np.ndarray, alpha: float = 0.5
background: np.ndarray, foreground: np.ndarray, alpha: float = 0.5, hide_black:bool = True,
) -> np.ndarray:
"""
Overlay an RGB foreground onto an RGB background using alpha blending.
......@@ -209,6 +209,7 @@ def overlay_rgb_images(
background (numpy.ndarray): The background RGB image.
foreground (numpy.ndarray): The foreground RGB image (usually masks).
alpha (float, optional): The alpha value for blending. Defaults to 0.5.
hide_black (bool, optional): If True, black pixels will have alpha value 0, so the black won't be visible. Used for segmentation where we don't care about background. Defaults to True.
Returns:
composite (numpy.ndarray): The composite RGB image with overlaid foreground.
......@@ -218,18 +219,36 @@ def overlay_rgb_images(
Note:
- The function performs alpha blending to overlay the foreground onto the background.
- It ensures that the background and foreground have the same shape before blending.
- It ensures that the background and foreground have the same first two dimensions (image size matches).
- It can handle greyscale images, values from 0 to 1, raw values which are negative or bigger than 255.
- It calculates the maximum projection of the foreground and blends them onto the background.
- Brightness outside the foreground is adjusted to maintain consistency with the background.
"""
# Igonore alpha in case its there
background = background[..., :3]
foreground = foreground[..., :3]
def to_uint8(image:np.ndarray):
if np.min(image) < 0:
image = image - np.min(image)
maxim = np.max(image)
if maxim > 255:
image = (image / maxim)*255
elif maxim <= 1:
image = image*255
if image.ndim == 2:
image = np.repeat(image[..., None], 3, -1)
elif image.ndim == 3:
image = image[..., :3] # Ignoring alpha channel
else:
raise ValueError(F'Input image can not have higher dimension than 3. Yours have {image.ndim}')
return image.astype(np.uint8)
background = to_uint8(background)
foreground = to_uint8(foreground)
# Ensure both images have the same shape
if background.shape != foreground.shape:
raise ValueError("Input images must have the same shape")
raise ValueError(F"Input images must have the same first two dimensions. But background is of shape {background.shape} and foreground is of shape {foreground.shape}")
# Perform alpha blending
foreground_max_projection = np.amax(foreground, axis=2)
......@@ -240,11 +259,18 @@ def overlay_rgb_images(
foreground_max_projection = foreground_max_projection / np.max(
foreground_max_projection
)
# Check alpha validity
if alpha < 0:
raise ValueError(F'Alpha has to be positive number. You used {alpha}')
elif alpha > 1:
alpha = 1
# If the pixel is black, its alpha value is set to 0, so it has no effect on the image
if hide_black:
alpha = np.full((background.shape[0], background.shape[1],1), alpha)
alpha[np.apply_along_axis(lambda x: (x == [0,0,0]).all(), axis = 2, arr = foreground)] = 0
composite = background * (1 - alpha) + foreground * alpha
composite = np.clip(composite, 0, 255).astype("uint8")
# Adjust brightness outside foreground
composite = composite + (background * (1 - alpha)) * (1 - foreground_max_projection)
return composite.astype("uint8")
\ No newline at end of file
import argparse
from qim3d.gui import data_explorer, iso3d, annotation_tool, local_thickness, layers2d
def main():
parser = argparse.ArgumentParser(description='Qim3d command-line interface.')
subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
# subcommands
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('--layers2d', action='store_true', help='Run layers2d.')
gui_parser.add_argument('--host', default='0.0.0.0', help='Desired host.')
args = parser.parse_args()
if args.subcommand == 'gui':
arghost = args.host
if args.data_explorer:
data_explorer.run_interface(arghost)
elif args.iso3d:
iso3d.run_interface(arghost)
elif args.annotation_tool:
annotation_tool.run_interface(arghost)
elif args.local_thickness:
local_thickness.run_interface(arghost)
elif args.layers2d:
layers2d.run_interface(arghost)
if __name__ == '__main__':
main()
\ No newline at end of file
......@@ -13,3 +13,4 @@ from .local_thickness_ import local_thickness
from .structure_tensor import vectors
from .metrics import plot_metrics, grid_overview, grid_pred, vol_masked
from .preview import image_preview
from . import layers2d
""" Provides a collection of visualisation functions for the Layers2d class."""
import io
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
def image_with_lines(image:np.ndarray, lines: list, line_thickness:float|int) -> Image:
"""
Plots the image and plots the lines on top of it. Then extracts it as PIL.Image and in the same size as the input image was.
Paramters:
-----------
image: Image on which we put the lines
lines: list of 1D arrays to be plotted on top of the image
line_thickness: how thick is the line supposed to be
Returns:
----------
image_with_lines:
"""
fig, ax = plt.subplots()
ax.imshow(image, cmap = 'gray')
ax.axis('off')
for line in lines:
ax.plot(line, linewidth = line_thickness)
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
plt.close()
buf.seek(0)
return Image.open(buf).resize(size = image.squeeze().shape[::-1])
albumentations>=1.3.1
gradio>=4.27.0
gradio==4.44
h5py>=3.9.0
localthickness>=0.1.2
matplotlib>=3.8.0
......@@ -10,9 +10,10 @@ Pillow>=10.0.1
plotly>=5.14.1
scipy>=1.11.2
seaborn>=0.12.2
pydicom>=2.4.4
pydicom==2.4.4
setuptools>=68.0.0
tifffile>=2023.4.12
imagecodecs==2023.7.10
tifffile==2023.8.12
torch>=2.0.1
torchvision>=0.15.2
torchinfo>=1.8.0
......@@ -29,3 +30,4 @@ zarr>=2.18.2
ome_zarr>=0.9.0
dask-image>=2024.5.3
trimesh>=4.4.9
slgbuilder>=0.2.1
......@@ -45,11 +45,11 @@ setup(
],
python_requires=">=3.10",
install_requires=[
"gradio>=4.27.0",
"gradio==4.44",
"h5py>=3.9.0",
"localthickness>=0.1.2",
"matplotlib>=3.8.0",
"pydicom>=2.4.4",
"pydicom==2.4.4",
"numpy>=1.26.0",
"outputformat>=0.1.3",
"Pillow>=10.0.1",
......@@ -57,7 +57,8 @@ setup(
"scipy>=1.11.2",
"seaborn>=0.12.2",
"setuptools>=68.0.0",
"tifffile>=2023.4.12",
"tifffile==2023.8.12",
"imagecodecs==2023.7.10",
"tqdm>=4.65.0",
"nibabel>=5.2.0",
"ipywidgets>=8.1.2",
......