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

Merge branch 'new_layered_surface_segmentation' into 'main'

New Layered Surface Segmentation

See merge request !117
parents 4356b6c8 5d4f9dc7
No related branches found
No related tags found
1 merge request!117New Layered Surface Segmentation
Showing
with 707 additions and 13 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.
......@@ -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])
......@@ -10,7 +10,7 @@ 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
imagecodecs==2023.7.10
tifffile==2023.8.12
......@@ -30,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
......@@ -49,7 +49,7 @@ setup(
"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",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment