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.
......
"""
The GUI can be launched directly from the command line:
```bash
qim3d gui --layers
```
Or launched from a python script
```python
import qim3d
layers = qim3d.gui.layers2d.Interface()
app = layers.launch()
```
![gui-layers](assets/screenshots/GUI-layers.png)
"""
import os
import gradio as gr
import numpy as np
from .interface import BaseInterface
# from qim3d.processing import layers2d as l2d
from qim3d.processing import overlay_rgb_images, segment_layers, get_lines
from qim3d.io import load
from qim3d.viz.layers2d import image_with_lines
#TODO figure out how not update anything and go through processing when there are no data loaded
# So user could play with the widgets but it doesnt throw error
# Right now its only bypassed with several if statements
# I opened an issue here https://github.com/gradio-app/gradio/issues/9273
X = 'X'
Y = 'Y'
Z = 'Z'
AXES = {X:2, Y:1, Z:0}
DEFAULT_PLOT_TYPE = 'Segmentation mask'
SEGMENTATION_COLORS = np.array([[0, 255, 255], # Cyan
[255, 195, 0], # Yellow Orange
[199, 0, 57], # Dark orange
[218, 247, 166], # Light green
[255, 0, 255], # Magenta
[65, 105, 225], # Royal blue
[138, 43, 226], # Blue violet
[255, 0, 0], #Red
])
class Interface(BaseInterface):
def __init__(self):
super().__init__("Layered surfaces 2D", 1080)
self.data = None
# It important to keep the name of the attributes like this (including the capital letter) becuase of
# accessing the attributes via __dict__
self.X_slice = None
self.Y_slice = None
self.Z_slice = None
self.X_segmentation = None
self.Y_segmentation = None
self.Z_segmentation = None
self.plot_type = DEFAULT_PLOT_TYPE
self.error = False
def define_interface(self):
with gr.Row():
with gr.Column(scale=1, min_width=320):
with gr.Row():
with gr.Column(scale=99, min_width=128):
base_path = gr.Textbox(
max_lines=1,
container=False,
label="Base path",
value=os.getcwd(),
)
with gr.Column(scale=1, min_width=36):
reload_base_path = gr.Button(value="")
explorer = gr.FileExplorer(
ignore_glob="*/.*",
root_dir=os.getcwd(),
label=os.getcwd(),
render=True,
file_count="single",
interactive=True,
height = 230,
)
with gr.Group():
with gr.Row():
axis = gr.Radio(
choices = [Z, Y, X],
value = Z,
label = 'Layer axis',
info = 'Specifies in which direction are the layers. The order of axes is ZYX',)
with gr.Row():
wrap = gr.Checkbox(
label = "Lines start and end at the same level.",
info = "Used when segmenting layers of unfolded image."
)
is_inverted = gr.Checkbox(
label="Invert image before processing",
info="The algorithm effectively flips the gradient.",
)
with gr.Row():
delta = gr.Slider(
minimum=0,
maximum=5,
value=0.75,
step=0.01,
interactive = True,
label="Delta value",
info="The lower the delta is, the more accurate the gradient calculation will be. However, the calculation takes longer to execute. Delta above 1 is rounded down to closest lower integer",
)
with gr.Row():
min_margin = gr.Slider(
minimum=1,
maximum=50,
value=10,
step=1,
interactive = True,
label="Min margin",
info="Minimum margin between layers to be detected in the image.",
)
with gr.Row():
n_layers = gr.Slider(
minimum=1,
maximum=len(SEGMENTATION_COLORS) - 1,
value=2,
step=1,
interactive=True,
label="Number of layers",
info="Number of layers to be detected in the image",
)
# with gr.Row():
# btn_run = gr.Button("Run Layers2D", variant = 'primary')
# Output panel: Plots
"""
60em if plot is alone
30em if two of them
20em if all of them are visible
When one slicing axis is made unvisible we want the other two images to be bigger
For some reason, gradio changes their width but not their height. So we have to
change their height manually
"""
self.heights = ['60em', '30em', '20em'] # em units are relative to the parent,
with gr.Column(scale=2,):
# with gr.Row(): # Source image outputs
# input_image_kwargs = lambda axis: dict(
# show_label = True,
# label = F'Slice along {axis}-axis',
# visible = True,
# height = self.heights[2]
# )
# input_plot_x = gr.Image(**input_image_kwargs('X'))
# input_plot_y = gr.Image(**input_image_kwargs('Y'))
# input_plot_z = gr.Image(**input_image_kwargs('Z'))
with gr.Row(): # Detected layers outputs
output_image_kwargs = lambda axis: dict(
show_label = True,
label = F'Detected layers {axis}-axis',
visible = True,
height = self.heights[2]
)
output_plot_x = gr.Image(**output_image_kwargs('X'))
output_plot_y = gr.Image(**output_image_kwargs('Y'))
output_plot_z = gr.Image(**output_image_kwargs('Z'))
with gr.Row(): # Axis position sliders
slider_kwargs = lambda axis: dict(
minimum = 0,
maximum = 1,
value = 0.5,
step = 0.01,
label = F'{axis} position',
info = F'The 3D image is sliced along {axis}-axis'
)
x_pos = gr.Slider(**slider_kwargs('X'))
y_pos = gr.Slider(**slider_kwargs('Y'))
z_pos = gr.Slider(**slider_kwargs('Z'))
with gr.Row():
x_check = gr.Checkbox(value = True, interactive=True, label = 'Show X slice')
y_check = gr.Checkbox(value = True, interactive=True, label = 'Show Y slice')
z_check = gr.Checkbox(value = True, interactive=True, label = 'Show Z slice')
with gr.Row():
with gr.Group():
plot_type = gr.Radio(
choices= (DEFAULT_PLOT_TYPE, 'Segmentation lines',),
value = DEFAULT_PLOT_TYPE,
interactive = True,
show_label=False
)
alpha = gr.Slider(
minimum=0,
maximum = 1,
step = 0.01,
label = 'Alpha value',
show_label=True,
value = 0.5,
visible = True,
interactive=True
)
line_thickness = gr.Slider(
minimum=0.1,
maximum = 5,
value = 2,
label = 'Line thickness',
show_label = True,
visible = False,
interactive = True
)
with gr.Row():
btn_run = gr.Button("Run Layers2D", variant = 'primary')
positions = [x_pos, y_pos, z_pos]
process_inputs = [axis, is_inverted, delta, min_margin, n_layers, wrap]
plotting_inputs = [axis, alpha, line_thickness]
# input_plots = [input_plot_x, input_plot_y, input_plot_z]
output_plots = [output_plot_x, output_plot_y, output_plot_z]
visibility_check_inputs = [x_check, y_check, z_check]
spinner_loading = gr.Text("Loading data...", visible=False)
spinner_running = gr.Text("Running pipeline...", visible=False)
reload_base_path.click(
fn=self.update_explorer,inputs=base_path, outputs=explorer)
plot_type.change(
self.change_plot_type, inputs = plot_type, outputs = [alpha, line_thickness]).then(
fn = self.plot_output_img_all, inputs = plotting_inputs, outputs = output_plots
)
gr.on(
triggers = [alpha.release, line_thickness.release],
fn = self.plot_output_img_all, inputs = plotting_inputs, outputs = output_plots
)
"""
Difference between btn_run.click and the other triggers below is only loading the data.
To make it easier to maintain, I created 'update_component' variable. Its value is completely
unimportant. It exists only to be changed after loading the data which triggers further processing
which is the same for button click and the other triggers
"""
update_component = gr.State(True)
btn_run.click(
fn=self.set_spinner, inputs=spinner_loading, outputs=btn_run).then(
fn=self.load_data, inputs = [base_path, explorer]).then(
fn = lambda state: not state, inputs = update_component, outputs = update_component)
gr.on(
triggers= (axis.change, is_inverted.change, delta.release, min_margin.release, n_layers.release, update_component.change, wrap.change),
fn=self.set_spinner, inputs = spinner_running, outputs=btn_run).then(
fn=self.process_all, inputs = [*positions, *process_inputs]).then(
# fn=self.plot_input_img_all, outputs = input_plots, show_progress='hidden').then(
fn=self.plot_output_img_all, inputs = plotting_inputs, outputs = output_plots, show_progress='hidden').then(
fn=self.set_relaunch_button, inputs=[], outputs=btn_run)
# Chnages visibility and sizes of the plots - gives user the option to see only some of the images and in bigger scale
gr.on(
triggers=[x_check.change, y_check.change, z_check.change],
fn = self.change_row_visibility, inputs = visibility_check_inputs, outputs = positions).then(
# fn = self.change_row_visibility, inputs = visibility_check_inputs, outputs = input_plots).then(
fn = self.change_plot_size, inputs = visibility_check_inputs, outputs = output_plots)
# for axis, slider, input_plot, output_plot in zip(['x','y','z'], positions, input_plots, output_plots):
for axis, slider, output_plot in zip([X,Y,Z], positions, output_plots):
slider.change(
self.process_wrapper(axis), inputs = [slider, *process_inputs]).then(
# self.plot_input_img_wrapper(axis), outputs = input_plot).then(
self.plot_output_img_wrapper(axis), inputs = plotting_inputs, outputs = output_plot)
def change_plot_type(self, plot_type, ):
self.plot_type = plot_type
if plot_type == 'Segmentation lines':
return gr.update(visible = False), gr.update(visible = True)
else:
return gr.update(visible = True), gr.update(visible = False)
def change_plot_size(self, x_check, y_check, z_check):
"""
Based on how many plots are we displaying (controlled by checkboxes in the bottom) we define
also their height because gradio doesn't do it automatically. The values of heights were set just by eye.
They are defines before defining the plot in 'define_interface'
"""
index = x_check + y_check + z_check - 1
height = self.heights[index] # also used to define heights of plots in the begining
return gr.update(height = height, visible= x_check), gr.update(height = height, visible = y_check), gr.update(height = height, visible = z_check)
def change_row_visibility(self, x_check, y_check, z_check):
return self.change_visibility(x_check), self.change_visibility(y_check), self.change_visibility(z_check)
def update_explorer(self, new_path):
# Refresh the file explorer object
new_path = os.path.expanduser(new_path)
# In case we have a directory
if os.path.isdir(new_path):
return gr.update(root_dir=new_path, label=new_path)
elif os.path.isfile(new_path):
parent_dir = os.path.dirname(new_path)
file_name = str(os.path.basename(new_path))
return gr.update(root_dir=parent_dir, label=parent_dir, value=file_name)
else:
raise ValueError("Invalid path")
def set_relaunch_button(self):
return gr.update(value=f"Relaunch", interactive=True)
def set_spinner(self, message):
if self.error:
return gr.Button()
# spinner icon/shows the user something is happeing
return gr.update(value=f"{message}", interactive=False)
def load_data(self, base_path, explorer):
if base_path and os.path.isfile(base_path):
file_path = base_path
elif explorer and os.path.isfile(explorer):
file_path = explorer
else:
raise gr.Error("Invalid file path")
try:
self.data = load(
file_path,
progress_bar=False
)
except Exception as error_message:
raise gr.Error(
f"Failed to load the image: {error_message}"
) from error_message
def process_all(self, x_pos:float, y_pos:float, z_pos:float, axis:str, inverted:bool, delta:float, min_margin:int, n_layers:int, wrap:bool):
self.process_wrapper(X)(x_pos, axis, inverted, delta, min_margin, n_layers, wrap)
self.process_wrapper(Y)(y_pos, axis, inverted, delta, min_margin, n_layers, wrap)
self.process_wrapper(Z)(z_pos, axis, inverted, delta, min_margin, n_layers, wrap)
def process_wrapper(self, slicing_axis:str):
"""
The function behaves the same in all 3 directions, however we have to know in which direction we are now.
Thus we have this wrapper function, where we pass the slicing axis - in which axis are we indexing the data
and we return a function working in that direction
"""
slice_key = F'{slicing_axis}_slice'
seg_key = F'{slicing_axis}_segmentation'
slicing_axis_int = AXES[slicing_axis]
def process(pos:float, segmenting_axis:str, inverted:bool, delta:float, min_margin:int, n_layers:int, wrap:bool):
"""
Parameters:
-----------
pos: Relative position of a slice from data
segmenting_axis: In which direction we want to detect layers
inverted: If we want use inverted gradient
delta: Smoothness parameter
min_margin: What is the minimum distance between layers. If it was 0, all layers would be the same
n_layers: How many layer boarders we want to find
wrap: If True, the starting point and end point will be at the same level. Useful when segmenting unfolded images.
"""
slice = self.get_slice(pos, slicing_axis_int)
self.__dict__[slice_key] = slice
if segmenting_axis == slicing_axis:
self.__dict__[seg_key] = None
else:
if self.is_transposed(slicing_axis, segmenting_axis):
slice = np.rot90(slice)
self.__dict__[seg_key] = segment_layers(slice, inverted = inverted, n_layers = n_layers, delta = delta, min_margin = min_margin, wrap = wrap)
return process
def is_transposed(self, slicing_axis:str, segmenting_axis:str):
"""
Checks if the desired direction of segmentation is the same if the image would be submitted to segmentation as is.
If it is not, we have to rotate it before we put it to segmentation algorithm
"""
remaining_axis = F"{X}{Y}{Z}".replace(slicing_axis, '').replace(segmenting_axis, '')
return AXES[segmenting_axis] > AXES[remaining_axis]
def get_slice(self, pos:float, axis:int):
idx = int(pos * (self.data.shape[axis] - 1))
return np.take(self.data, idx, axis = axis)
# def plot_input_img_wrapper(self, axis:str):
# slice_key = F'{axis.lower()}_slice'
# def plot_input_img():
# slice = self.__dict__[slice_key]
# slice = slice + np.abs(np.min(slice))
# slice = slice / np.max(slice)
# return slice
# return plot_input_img
# def plot_input_img_all(self):
# x_plot = self.plot_input_img_wrapper('x')()
# y_plot = self.plot_input_img_wrapper('y')()
# z_plot = self.plot_input_img_wrapper('z')()
# return x_plot, y_plot, z_plot
def plot_output_img_wrapper(self, slicing_axis:str):
slice_key = F'{slicing_axis}_slice'
seg_key = F'{slicing_axis}_segmentation'
def plot_output_img(segmenting_axis:str, alpha:float, line_thickness:float):
slice = self.__dict__[slice_key]
seg = self.__dict__[seg_key]
if seg is None: # In case segmenting axis si the same as slicing axis
return slice
if self.plot_type == DEFAULT_PLOT_TYPE:
n_layers = len(seg) + 1
seg = np.sum(seg, axis = 0)
seg = np.repeat(seg[..., None], 3, axis = -1)
for i in range(n_layers):
seg[seg[:,:,0] == i, :] = SEGMENTATION_COLORS[i]
if self.is_transposed(slicing_axis, segmenting_axis):
seg = np.rot90(seg, k = 3)
# slice = 255 * (slice/np.max(slice))
# return image_with_overlay(np.repeat(slice[..., None], 3, -1), seg, alpha)
return overlay_rgb_images(slice, seg, alpha)
else:
lines = get_lines(seg)
if self.is_transposed(slicing_axis, segmenting_axis):
return image_with_lines(np.rot90(slice), lines, line_thickness).rotate(270, expand = True)
else:
return image_with_lines(slice, lines, line_thickness)
return plot_output_img
def plot_output_img_all(self, segmenting_axis:str, alpha:float, line_thickness:float):
x_output = self.plot_output_img_wrapper(X)(segmenting_axis, alpha, line_thickness)
y_output = self.plot_output_img_wrapper(Y)(segmenting_axis, alpha, line_thickness)
z_output = self.plot_output_img_wrapper(Z)(segmenting_axis, alpha, line_thickness)
return x_output, y_output, z_output
if __name__ == "__main__":
Interface().run_interface()
\ No newline at end of file
......@@ -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