Skip to content
Snippets Groups Projects
Commit 5d4f9dc7 authored by s233039's avatar s233039 Committed by fima
Browse files

New Layered Surface Segmentation

parent 85014a4b
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
tifffile>=2023.4.12
torch>=2.0.1
......@@ -29,3 +29,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