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

Merge branch 'gui-refactoring' into 'main'

GUI refactoring to make it more readable and easier to maintain

See merge request !104
parents eb290d1e d77dfdbb
No related branches found
No related tags found
1 merge request!104GUI refactoring to make it more readable and easier to maintain
/* 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*/
}
......@@ -26,56 +26,36 @@ import tempfile
import gradio as gr
import numpy as np
import qim3d.utils
from PIL import Image
from qim3d.io import load, save
from qim3d.io.logger import log
from .qim_theme import QimTheme
from pathlib import Path
import qim3d
from qim3d.utils import overlay_rgb_images
from .interface import BaseInterface
#TODO: img in launch should be self.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"
)
class Session:
def __init__(self):
self.username = getpass.getuser()
self.temp_dir = os.path.join(tempfile.gettempdir(), f"qim-{self.username}")
self.name_suffix = name_suffix
self.img = img
self.n_masks = 3
self.img_editor = None
self.masks_rgb = None
self.mask_names = {0: "red", 1: "green", 2: "blue"}
self.temp_files = []
self.temp_dir = None
class Interface:
def __init__(self):
self.verbose = False
self.title = "Annotation tool"
self.height = 768
self.interface = None
self.username = getpass.getuser()
self.temp_dir = os.path.join(tempfile.gettempdir(), f"qim-{self.username}")
self.name_suffix = None
def launch(self, img=None, force_light_mode:bool = True, **kwargs):
# Create gradio interfaces
# img = "/tmp/qim-fima/2dimage.png"
self.interface = self.create_interface(img, force_light_mode=force_light_mode)
# Set gradio verbose level
if self.verbose:
quiet = False
else:
quiet = True
self.interface.launch(
quiet=quiet,
height=self.height,
# width=self.width,
favicon_path = Path(qim3d.__file__).parents[0] / "gui/images/qim_platform-icon.svg",
**kwargs,
)
return
def get_result(self):
# Get the temporary files from gradio
......@@ -94,13 +74,62 @@ class Interface:
return masks
def set_visible(self):
return gr.update(visible=True)
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}",
]
def create_interface(self, img=None, force_light_mode:bool = False):
# Clean up old files
try:
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)
with gr.Blocks(theme = QimTheme(force_light_mode = force_light_mode), title=self.title) as gradio_interface:
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)",
......@@ -111,16 +140,15 @@ class Interface:
default_size=10,
)
with gr.Row():
with gr.Column(scale=6):
with gr.Column(scale=6, ):
img_editor = gr.ImageEditor(
value=(
{
"background": img,
"layers": [Image.new("RGBA", img.shape, (0, 0, 0, 0))],
"background": self.img,
"layers": [Image.new("RGBA", self.img.shape, (0, 0, 0, 0))],
"composite": None,
}
if img is not None
if self.img is not None
else None
),
type="numpy",
......@@ -142,139 +170,16 @@ class Interface:
)
with gr.Row():
masks_download = gr.File(
label="Download masks",
visible=False,
label="Download masks", visible=False
)
temp_dir = gr.Textbox(value=self.temp_dir, visible=False)
name_suffix = gr.Textbox(value=self.name_suffix, visible=False)
session = gr.State([])
operations = Operations()
# fmt: off
img_editor.change(
fn=operations.start_session, inputs=[img_editor,temp_dir, name_suffix] , outputs=session).then(
fn=operations.preview, inputs=session, outputs=overlay_img).then(
fn=self.set_visible, inputs=None, outputs=overlay_img).then(
fn=operations.separate_masks, inputs=session, outputs=[session, masks_download]).then(
fn=self.set_visible, inputs=None, outputs=masks_download)
# fmt: on
return gradio_interface
class Operations:
def start_session(self, *args):
session = Session()
session.img_editor = args[0]
session.temp_dir = args[1]
session.mask_names = {
0: f"red{args[2]}",
1: f"green{args[2]}",
2: f"blue{args[2]}",
}
# Clean up old files
try:
files = os.listdir(session.temp_dir)
for filename in files:
# Check if "mask" is in the filename
if "mask" and args[2] in filename:
file_path = os.path.join(session.temp_dir, filename)
os.remove(file_path)
except FileNotFoundError:
files = None
return session
def overlay_images(self, background, masks, alpha=0.5):
"""Overlay multiple RGB masks onto an RGB background image using alpha blending.
Args:
background (numpy.ndarray): The background RGB image with shape (height, width, 3).
masks (numpy.ndarray): The RGB mask images with shape (num_masks, height, width, 3).
alpha (float, optional): The alpha value for blending. Defaults to 0.5.
Returns:
numpy.ndarray: The composite image with overlaid masks.
Raises:
ValueError: If input images have different shapes.
Note:
- The function performs alpha blending to overlay the masks onto the background.
- It ensures that the background and masks have the same shape before blending.
- It calculates the maximum projection of the masks and blends them onto the background.
- Brightness outside the masks is adjusted to maintain consistency with the background.
"""
# Igonore alpha in case its there
background = background[..., :3]
masks = masks[..., :3]
# Ensure both images have the same shape
if background.shape != masks.shape:
raise ValueError("Input images must have the same shape")
# Perform alpha blending
masks_max_projection = np.amax(masks, axis=2)
masks_max_projection = np.stack((masks_max_projection,) * 3, axis=-1)
# Normalize if we have something
if np.max(masks_max_projection) > 0:
masks_max_projection = masks_max_projection / np.max(masks_max_projection)
composite = background * (1 - alpha) + masks * alpha
composite = np.clip(composite, 0, 255).astype("uint8")
# Adjust brightness outside masks
composite = composite + (background * (1 - alpha)) * (1 - masks_max_projection)
return composite.astype("uint8")
def preview(self, session):
background = session.img_editor["background"]
masks = session.img_editor["layers"][0]
overlay_image = qim3d.utils.img.overlay_rgb_images(background, masks)
return overlay_image
def separate_masks(self, session):
masks_rgb = session.img_editor["layers"][0]
mask_threshold = 200 # This value is based
mask_list = []
files_list = []
# Go through each channel
for idx in np.arange(session.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_{session.mask_names[idx]}.tif"
if not os.path.exists(session.temp_dir):
os.makedirs(session.temp_dir)
filepath = os.path.join(session.temp_dir, filename)
files_list.append(filepath)
save(filepath, mask, replace=True)
session.temp_files.append(filepath)
return session, files_list
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
def run_interface(host="0.0.0.0"):
gradio_interface = Interface().create_interface()
qim3d.utils.internal_tools.run_gradio_app(gradio_interface, host)
if __name__ == "__main__":
# Creates interface
run_interface()
This diff is collapsed.
from pathlib import Path
from abc import abstractmethod, ABC
from os import path
import gradio as gr
from .qim_theme import QimTheme
from qim3d.utils import internal_tools
import qim3d
#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] / "gui/images/qim_platform-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 = 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"):
internal_tools.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):
examples = [
"blobs_256x256x256.tif",
"fly_150x256x256.tif",
"cement_128x128x128.tif",
"NT_10x200x100.tif",
"NT_128x128x128.tif",
"shell_225x128x128.tif",
"bone_128x128x128.tif",
]
self.img_examples = []
for example in examples:
self.img_examples.append(
[path.join(self.qim_dir, "img_examples", example)]
)
......@@ -15,57 +15,47 @@ app.launch()
```
"""
import os
import gradio as gr
import numpy as np
import os
from qim3d.utils import internal_tools
from qim3d.io import DataLoader
from qim3d.io.logger import log
from .qim_theme import QimTheme
import plotly.graph_objects as go
from scipy import ndimage
from pathlib import Path
import qim3d
from qim3d.io import load
from qim3d.io.logger import log
from .interface import InterfaceWithExamples
class Interface:
def __init__(self):
self.show_header = False
self.verbose = False
self.title = "Isosurfaces for 3D visualization"
self.interface = None
self.plot_height = 768
self.height = 1024
self.width = 960
# Data examples
current_dir = os.path.dirname(os.path.abspath(__file__))
examples_dir = ["..", "img_examples"]
examples = [
"blobs_256x256x256.tif",
"fly_150x256x256.tif",
"cement_128x128x128.tif",
"NT_10x200x100.tif",
"NT_128x128x128.tif",
"shell_225x128x128.tif",
"bone_128x128x128.tif",
]
self.img_examples = []
for example in examples:
self.img_examples.append(
[os.path.join(current_dir, *examples_dir, example)]
)
def clear(self):
"""Used to reset the plot with the clear button"""
return None
#TODO img in launch should be self.img
class Interface(InterfaceWithExamples):
def __init__(self,
verbose:bool = False,
plot_height:int = 768,
img = None):
def load_data(self, filepath):
# TODO: Add support for multiple files
self.vol = DataLoader().load_tiff(filepath)
super().__init__(title = "Isosurfaces for 3D visualization",
height = 1024,
width = 960,
verbose = verbose)
def resize_vol(self):
self.interface = None
self.img = img
self.plot_height = plot_height
def load_data(self, gradiofile):
try:
self.vol = load(gradiofile.name)
assert self.vol.ndim == 3
except AttributeError:
raise gr.Error("You have to select a file")
except ValueError:
raise gr.Error("Unsupported file format")
except AssertionError:
raise gr.Error(F"File has to be 3D structure. Your structure has {self.vol.ndim} dimension{'' if self.vol.ndim == 1 else 's'}")
def resize_vol(self, display_size):
"""Resizes the loaded volume to the display size"""
# Get original size
......@@ -77,7 +67,7 @@ class Interface:
# Resize for display
self.vol = ndimage.zoom(
input=self.vol,
zoom=self.display_size / max_size,
zoom = display_size / max_size,
order=0,
prefilter=False,
)
......@@ -94,15 +84,37 @@ class Interface:
# Write Plotly figure to disk
fig.write_html(filename)
def create_fig(self):
def create_fig(self,
gradio_file,
display_size,
opacity,
opacityscale,
only_wireframe,
min_value,
max_value,
surface_count,
colormap,
show_colorbar,
reversescale,
flip_z,
show_axis,
show_ticks,
show_caps,
show_z_slice,
slice_z_location,
show_y_slice,
slice_y_location,
show_x_slice,
slice_x_location,):
# Load volume
self.load_data(self.gradio_file.name)
self.load_data(gradio_file)
# Resize data for display size
self.resize_vol()
self.resize_vol(display_size)
# Flip Z
if self.flip_z:
if flip_z:
self.vol = np.flip(self.vol, axis=0)
# Create 3D grid
......@@ -110,7 +122,7 @@ class Interface:
0 : self.display_size_z, 0 : self.display_size_y, 0 : self.display_size_x
]
if self.only_wireframe:
if only_wireframe:
surface_fill = 0.2
else:
surface_fill = 1.0
......@@ -121,103 +133,68 @@ class Interface:
y = Y.flatten(),
x = X.flatten(),
value = self.vol.flatten(),
isomin=self.min_value * np.max(self.vol),
isomax=self.max_value * np.max(self.vol),
isomin = min_value * np.max(self.vol),
isomax = max_value * np.max(self.vol),
cmin = np.min(self.vol),
cmax = np.max(self.vol),
opacity=self.opacity,
opacityscale=self.opacityscale,
surface_count=self.surface_count,
colorscale=self.colormap,
opacity = opacity,
opacityscale = opacityscale,
surface_count = surface_count,
colorscale = colormap,
slices_z = dict(
show=self.show_z_slice,
locations=[int(self.display_size_z * self.slice_z_location)],
show = show_z_slice,
locations = [int(self.display_size_z * slice_z_location)],
),
slices_y = dict(
show=self.show_y_slice,
locations=[int(self.display_size_y * self.slice_y_location)],
show = show_y_slice,
locations=[int(self.display_size_y * slice_y_location)],
),
slices_x = dict(
show=self.show_x_slice,
locations=[int(self.display_size_x * self.slice_x_location)],
show = show_x_slice,
locations = [int(self.display_size_x * slice_x_location)],
),
surface = dict(fill=surface_fill),
caps = dict(
x_show=self.show_caps,
y_show=self.show_caps,
z_show=self.show_caps,
x_show = show_caps,
y_show = show_caps,
z_show = show_caps,
),
showscale=self.show_colorbar,
showscale = show_colorbar,
colorbar=dict(
thickness=8, outlinecolor="#fff", len=0.5, orientation="h"
),
reversescale=self.reversescale,
reversescale = reversescale,
hoverinfo = "skip",
)
)
fig.update_layout(
scene_xaxis_showticklabels=self.show_ticks,
scene_yaxis_showticklabels=self.show_ticks,
scene_zaxis_showticklabels=self.show_ticks,
scene_xaxis_visible=self.show_axis,
scene_yaxis_visible=self.show_axis,
scene_zaxis_visible=self.show_axis,
scene_xaxis_showticklabels = show_ticks,
scene_yaxis_showticklabels = show_ticks,
scene_zaxis_showticklabels = show_ticks,
scene_xaxis_visible = show_axis,
scene_yaxis_visible = show_axis,
scene_zaxis_visible = show_axis,
scene_aspectmode="data",
height=self.plot_height,
hovermode=False,
scene_camera_eye=dict(x=2.0, y=-2.0, z=1.5),
)
return fig
def process(self, *args):
# Get args passed by Gradio
# TODO: solve this in an automated way
# Could Gradio pass kwargs instead of args?
self.gradio_file = args[0]
self.display_size = args[1]
self.opacity = args[2]
self.opacityscale = args[3]
self.only_wireframe = args[4]
self.min_value = args[5]
self.max_value = args[6]
self.surface_count = args[7]
self.colormap = args[8]
self.show_colorbar = args[9]
self.reversescale = args[10]
self.flip_z = args[11]
self.show_axis = args[12]
self.show_ticks = args[13]
self.show_caps = args[14]
self.show_z_slice = args[15]
self.slice_z_location = args[16]
self.show_y_slice = args[17]
self.slice_y_location = args[18]
self.show_x_slice = args[19]
self.slice_x_location = args[20]
# Create output figure
fig = self.create_fig()
# Save it to disk
self.save_fig(fig, "iso3d.html")
return fig, "iso3d.html"
filename = "iso3d.html"
self.save_fig(fig, filename)
return fig, filename
def remove_unused_file(self):
# Remove localthickness.tif file from working directory
# as it otherwise is not deleted
os.remove("iso3d.html")
def create_interface(self, force_light_mode:bool = True):
# Create gradio app
def define_interface(self, **kwargs):
with gr.Blocks(theme = QimTheme(force_light_mode = force_light_mode), title = self.title) as gradio_interface:
if self.show_header:
gr.Markdown(
"""
# 3D Visualization (isosurfaces)
This tool uses Plotly Volume (https://plotly.com/python/3d-volume-plots/) to create iso surfaces from voxels based on their intensity levels.
To optimize performance when generating visualizations, set the number of voxels (_display resolution_) and isosurfaces (_total surfaces_) to lower levels.
"""
......@@ -281,7 +258,7 @@ class Interface:
0.0, 1.0, step=0.05, label="Max value", value=1
)
with gr.Tab("Slices"):
with gr.Tab("Slices") as slices:
show_z_slice = gr.Checkbox(value=False, label="Show Z slice")
slice_z_location = gr.Slider(
0.0, 1.0, step=0.05, value=0.5, label="Position"
......@@ -376,10 +353,9 @@ class Interface:
outputs = [volvizplot, plot_download]
# Session for user data
session = gr.State([])
#####################################
# Listeners
#####################################
# Clear button
for gr_obj in outputs:
......@@ -387,44 +363,9 @@ class Interface:
# Run button
# fmt: off
btn_run.click(
fn=self.process, inputs=inputs, outputs=outputs).success(
fn=self.create_fig, inputs = inputs, outputs = outputs).success(
fn=self.remove_unused_file).success(
fn=self.make_visible, inputs=None, outputs=plot_download)
# fmt: on
return gradio_interface
def make_visible(self):
return gr.update(visible=True)
def launch(self, force_light_mode:bool = True, **kwargs):
# Show header
if self.show_header:
internal_tools.gradio_header(self.title, self.port)
# Create gradio interface
self.interface = self.create_interface(force_light_mode=force_light_mode)
# Set gradio verbose level
if self.verbose:
quiet = False
else:
quiet = True
self.interface.launch(
quiet=quiet,
height=self.height,
width=self.width,
favicon_path = Path(qim3d.__file__).parents[0] / "gui/images/qim_platform-icon.svg",
**kwargs,
)
def run_interface(host = "0.0.0.0"):
gradio_interface = Interface().create_interface()
internal_tools.run_gradio_app(gradio_interface,host)
fn=self.set_visible, inputs=None, outputs=plot_download)
if __name__ == "__main__":
# Creates interface
run_interface()
\ No newline at end of file
Interface().run_interface()
\ No newline at end of file
......@@ -32,105 +32,34 @@ app.launch()
```
"""
import os
# matplotlib.use("Agg")
import matplotlib.pyplot as plt
import gradio as gr
import numpy as np
import os
from qim3d.utils import internal_tools
from qim3d.io import DataLoader
from qim3d.io.logger import log
import tifffile
import plotly.express as px
from scipy import ndimage
import outputformat as ouf
import plotly.graph_objects as go
import localthickness as lt
from qim3d.io import load
from .interface import InterfaceWithExamples
# matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path
import qim3d
from .qim_theme import QimTheme
class Interface:
def __init__(self):
self.show_header = False
self.verbose = False
self.title = "Local thickness"
self.plot_height = 768
self.height = 1024
self.width = 960
# Data examples
current_dir = os.path.dirname(os.path.abspath(__file__))
examples_dir = ["..", "img_examples"]
examples = [
"blobs_256x256x256.tif",
"cement_128x128x128.tif",
"bone_128x128x128.tif",
"NT_10x200x100.tif",
]
self.img_examples = []
for example in examples:
self.img_examples.append(
[os.path.join(current_dir, *examples_dir, example)]
)
def clear(self):
"""Used to reset the plot with the clear button"""
return None
def make_visible(self):
return gr.update(visible=True)
def start_session(self, *args):
session = Session()
session.verbose = self.verbose
session.interface = "gradio"
class Interface(InterfaceWithExamples):
def __init__(self,
img = None,
verbose:bool = False,
plot_height:int = 768,
figsize:int = 6):
# Get the args passed by gradio
session.data = args[0]
session.lt_scale = args[1]
session.threshold = args[2]
session.dark_objects = args[3]
session.nbins = args[4]
session.zpos = args[5]
session.cmap_originals = args[6]
session.cmap_lt = args[7]
super().__init__(title = "Local thickness",
height = 1024,
width = 960,
verbose = verbose)
return session
def update_session_zpos(self, session, zpos):
session.zpos = zpos
return session
def launch(self, img=None, force_light_mode:bool = True, **kwargs):
# Show header
if self.show_header:
internal_tools.gradio_header(self.title, self.port)
# Create gradio interfaces
self.interface = self.create_interface(img=img, force_light_mode=force_light_mode)
# Set gradio verbose level
if self.verbose:
quiet = False
else:
quiet = True
self.interface.launch(
quiet=quiet,
height=self.height,
width=self.width,
favicon_path = Path(qim3d.__file__).parents[0] / "gui/images/qim_platform-icon.svg",
**kwargs
)
return
self.plot_height = plot_height
self.figsize = figsize
self.img = img
def get_result(self):
# Get the temporary files from gradio
......@@ -150,25 +79,24 @@ class Interface:
file_idx = np.argmax(creation_time_list)
# Load the temporary file
vol_lt = DataLoader().load(temp_path_list[file_idx])
vol_lt = load(temp_path_list[file_idx])
return vol_lt
def create_interface(self, img=None, force_light_mode:bool = True):
with gr.Blocks(theme = QimTheme(force_light_mode=force_light_mode), title = self.title) as gradio_interface:
def define_interface(self):
gr.Markdown(
"# 3D Local thickness \n Interface for _Fast local thickness in 3D and 2D_ (https://github.com/vedranaa/local-thickness)"
"Interface for _Fast local thickness in 3D and 2D_ (https://github.com/vedranaa/local-thickness)"
)
with gr.Row():
with gr.Column(scale=1, min_width=320):
if img is not None:
data = gr.State(value=img)
if self.img is not None:
data = gr.State(value=self.img)
else:
with gr.Tab("Input"):
data = gr.File(
show_label=False,
value=img,
value=self.img,
)
with gr.Tab("Examples"):
gr.Examples(examples=self.img_examples, inputs=data)
......@@ -204,7 +132,7 @@ class Interface:
dark_objects = gr.Checkbox(
value=False,
label="Dark objects",
info="Inverts the image before trhesholding. Use in case your foreground is darker than the background.",
info="Inverts the image before thresholding. Use in case your foreground is darker than the background.",
)
with gr.Tab("Display options"):
......@@ -234,16 +162,6 @@ class Interface:
with gr.Column(scale=1, min_width=64):
btn_clear = gr.Button("Clear", variant = "stop")
inputs = [
data,
lt_scale,
threshold,
dark_objects,
nbins,
zpos,
cmap_original,
cmap_lt,
]
with gr.Column(scale=4):
with gr.Row():
......@@ -278,95 +196,78 @@ class Interface:
visible=False,
)
# Pipelines
pipeline = Pipeline()
pipeline.verbose = self.verbose
# Session
session = gr.State([])
# Run button
# fmt: off
viz_input = lambda zpos, cmap: self.show_slice(self.vol, zpos, self.vmin, self.vmax, cmap)
viz_binary = lambda zpos, cmap: self.show_slice(self.vol_binary, zpos, None, None, cmap)
viz_output = lambda zpos, cmap: self.show_slice(self.vol_thickness, zpos, self.vmin_lt, self.vmax_lt, cmap)
# Ouput gradio objects
outputs = [input_vol, output_vol, binary_vol, histogram, lt_output]
btn.click(
fn=self.process_input, inputs = [data, dark_objects], outputs = []).success(
fn=viz_input, inputs = [zpos, cmap_original], outputs = input_vol).success(
fn=self.make_binary, inputs = threshold, outputs = []).success(
fn=viz_binary, inputs = [zpos, cmap_original], outputs = binary_vol).success(
fn=self.compute_localthickness, inputs = lt_scale, outputs = []).success(
fn=viz_output, inputs = [zpos, cmap_lt], outputs = output_vol).success(
fn=self.thickness_histogram, inputs = nbins, outputs = histogram).success(
fn=self.save_lt, inputs = [], outputs = lt_output).success(
fn=self.remove_unused_file).success(
fn=self.set_visible, inputs= [], outputs=lt_output)
# Clear button
outputs = [input_vol, output_vol, binary_vol, histogram, lt_output]
for gr_obj in outputs:
btn_clear.click(fn=self.clear, inputs=None, outputs=gr_obj)
# Run button
# fmt: off
btn.click(
fn=self.start_session, inputs=inputs, outputs=session).success(
fn=pipeline.process_input, inputs=session, outputs=session).success(
fn=pipeline.input_viz, inputs=session, outputs=input_vol).success(
fn=pipeline.make_binary, inputs=session, outputs=session).success(
fn=pipeline.binary_viz, inputs=session, outputs=binary_vol).success(
fn=pipeline.compute_localthickness, inputs=session, outputs=session).success(
fn=pipeline.output_viz, inputs=session, outputs=output_vol).success(
fn=pipeline.thickness_histogram, inputs=session, outputs=histogram).success(
fn=pipeline.save_lt, inputs=session, outputs=lt_output).success(
fn=pipeline.remove_unused_file).success(
fn=self.make_visible, inputs=None, outputs=lt_output)
btn_clear.click(fn = self.set_invisible, inputs = [], outputs = lt_output)
# Event listeners
zpos.change(
fn=self.update_session_zpos, inputs=[session, zpos], outputs=session, show_progress=False).success(
fn=pipeline.input_viz, inputs=session, outputs=input_vol, show_progress=False).success(
fn=pipeline.binary_viz, inputs=session, outputs=binary_vol,show_progress=False).success(
fn=pipeline.output_viz, inputs=session, outputs=output_vol,show_progress=False)
fn=viz_input, inputs = [zpos, cmap_original], outputs=input_vol, show_progress=False).success(
fn=viz_binary, inputs = [zpos, cmap_original], outputs=binary_vol, show_progress=False).success(
fn=viz_output, inputs = [zpos, cmap_lt], outputs=output_vol, show_progress=False)
cmap_original.change(
fn=viz_input, inputs = [zpos, cmap_original],outputs=input_vol, show_progress=False).success(
fn=viz_binary, inputs = [zpos, cmap_original], outputs=binary_vol, show_progress=False)
cmap_lt.change(
fn=viz_output, inputs = [zpos, cmap_lt], outputs=output_vol, show_progress=False
)
nbins.change(
fn = self.thickness_histogram, inputs = nbins, outputs = histogram
)
# fmt: on
return gradio_interface
class Session:
def __init__(self):
self.interface = None
self.verbose = None
self.show_ticks = False
self.show_axis = True
# Args from gradio
self.data = None
self.lt_scale = None
self.threshold = 0.5
self.dark_objects = False
self.flip_z = True
self.nbins = 25
self.reversescale = False
# From pipeline
self.vol = None
self.vol_binary = None
self.vol_thickness = None
self.zpos = 0
self.vmin = None
self.vmax = None
self.vmin_lt = None
self.vmax_lt = None
class Pipeline:
def __init__(self):
self.figsize = 6
def process_input(self, session):
#######################################################
#
# PIPELINE
#
#######################################################
def process_input(self, data, dark_objects):
# Load volume
try:
session.vol = DataLoader().load(session.data.name)
except:
session.vol = session.data
self.vol = load(data.name)
assert self.vol.ndim == 3
except AttributeError:
self.vol = data
except AssertionError:
raise gr.Error(F"File has to be 3D structure. Your structure has {self.vol.ndim} dimension{'' if self.vol.ndim == 1 else 's'}")
if session.dark_objects:
session.vol = np.invert(session.vol)
if dark_objects:
self.vol = np.invert(self.vol)
# Get min and max values for visualization
session.vmin = np.min(session.vol)
session.vmax = np.max(session.vol)
self.vmin = np.min(self.vol)
self.vmax = np.max(self.vol)
return session
def show_slice(self, vol, z_idx, vmin=None, vmax=None, cmap="viridis"):
def show_slice(self, vol, zpos, vmin=None, vmax=None, cmap="viridis"):
plt.close()
z_idx = int(zpos * (vol.shape[0] - 1))
fig, ax = plt.subplots(figsize=(self.figsize, self.figsize))
ax.imshow(vol[z_idx], interpolation="nearest", cmap=cmap, vmin=vmin, vmax=vmax)
......@@ -377,60 +278,24 @@ class Pipeline:
return fig
def input_viz(self, session):
# Generate input visualization
z_idx = int(session.zpos * (session.vol.shape[0] - 1))
fig = self.show_slice(
vol=session.vol,
z_idx=z_idx,
cmap=session.cmap_originals,
vmin=session.vmin,
vmax=session.vmax,
)
return fig
def make_binary(self, session):
def make_binary(self, threshold):
# Make a binary volume
# Nothing fancy, but we could add new features here
session.vol_binary = session.vol > (session.threshold * np.max(session.vol))
self.vol_binary = self.vol > (threshold * np.max(self.vol))
return session
def binary_viz(self, session):
# Generate input visualization
z_idx = int(session.zpos * (session.vol_binary.shape[0] - 1))
fig = self.show_slice(
vol=session.vol_binary, z_idx=z_idx, cmap=session.cmap_originals
)
return fig
def compute_localthickness(self, session):
session.vol_thickness = lt.local_thickness(session.vol_binary, session.lt_scale)
def compute_localthickness(self, lt_scale):
self.vol_thickness = lt.local_thickness(self.vol_binary, lt_scale)
# Valus for visualization
session.vmin_lt = np.min(session.vol_thickness)
session.vmax_lt = np.max(session.vol_thickness)
return session
def output_viz(self, session):
# Generate input visualization
z_idx = int(session.zpos * (session.vol_thickness.shape[0] - 1))
fig = self.show_slice(
vol=session.vol_thickness,
z_idx=z_idx,
cmap=session.cmap_lt,
vmin=session.vmin_lt,
vmax=session.vmax_lt,
)
return fig
self.vmin_lt = np.min(self.vol_thickness)
self.vmax_lt = np.max(self.vol_thickness)
def thickness_histogram(self, session):
def thickness_histogram(self, nbins):
# Ignore zero thickness
non_zero_values = session.vol_thickness[session.vol_thickness > 0]
non_zero_values = self.vol_thickness[self.vol_thickness > 0]
# Calculate histogram
vol_hist, bin_edges = np.histogram(non_zero_values, session.nbins)
vol_hist, bin_edges = np.histogram(non_zero_values, nbins)
fig, ax = plt.subplots(figsize=(6, 4))
......@@ -447,10 +312,10 @@ class Pipeline:
return fig
def save_lt(self, session):
def save_lt(self):
filename = "localthickness.tif"
# Save output image in a temp space
tifffile.imwrite(filename, session.vol_thickness)
tifffile.imwrite(filename, self.vol_thickness)
return filename
......@@ -459,10 +324,5 @@ class Pipeline:
# as it otherwise is not deleted
os.remove('localthickness.tif')
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
Interface().run_interface()
\ No newline at end of file
......@@ -119,30 +119,24 @@ def main():
if args.subcommand == "gui":
arghost = args.host
inbrowser = not args.no_browser # Should automatically open in browser
interface = None
if args.data_explorer:
if args.platform:
data_explorer.run_interface(arghost)
else:
interface = data_explorer.Interface()
interface.launch(inbrowser=inbrowser, force_light_mode=False)
interface_class = data_explorer.Interface
elif args.iso3d:
if args.platform:
iso3d.run_interface(arghost)
else:
interface = iso3d.Interface()
interface.launch(inbrowser=inbrowser, force_light_mode=False)
interface_class = iso3d.Interface
elif args.annotation_tool:
if args.platform:
annotation_tool.run_interface(arghost)
else:
interface = annotation_tool.Interface()
interface.launch(inbrowser=inbrowser, force_light_mode=False)
interface_class = annotation_tool.Interface
elif args.local_thickness:
interface_class = 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:
local_thickness.run_interface(arghost)
interface.run_interface(host = arghost)
else:
interface = local_thickness.Interface()
interface.launch(inbrowser = inbrowser, force_light_mode = False)
elif args.subcommand == "viz":
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment