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
  • main
  • notebooksv1
  • scaleZYX_mean
  • notebooks
  • convert_tiff_folders
  • test
  • notebook_update
  • threshold-exploration
  • optimize_scaleZYXdask
  • layered_surface_segmentation
  • conv_zarr_tiff_folders
  • 3d_watershed
  • tr_val_te_splits
  • save_files_function
  • memmap_txrm
  • 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
No results found
Select Git revision
  • 3D_UNet
  • main
  • notebooksv1
  • scaleZYX_mean
  • notebooks
  • convert_tiff_folders
  • test
  • notebook_update
  • threshold-exploration
  • optimize_scaleZYXdask
  • layered_surface_segmentation
  • conv_zarr_tiff_folders
  • 3d_watershed
  • tr_val_te_splits
  • save_files_function
  • memmap_txrm
  • 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 3

7 files
+ 1113
1460
Compare changes
  • Side-by-side
  • Inline

Files

+12 −0
Original line number Diff line number Diff line
/* 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*/
}



Original line number Diff line number Diff line
@@ -26,56 +26,34 @@ 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 qim3d.gui.interface import BaseInterface

# TODO: img in launch should be self.img

class Session:
    def __init__(self):
        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(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 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,
        )
        self.name_suffix = name_suffix
        self.img = img

        return
        self.n_masks = 3
        self.img_editor = None
        self.masks_rgb = None
        self.temp_files = []

    def get_result(self):
        # Get the temporary files from gradio
@@ -94,13 +72,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}",
        ]

        # 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)

        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

    def create_interface(self, img=None, force_light_mode:bool = False):
            # 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)

        with gr.Blocks(theme = QimTheme(force_light_mode = force_light_mode), title=self.title) as gradio_interface:
                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 +138,17 @@ 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",
@@ -138,143 +166,17 @@ class Interface:

                with gr.Row():
                    overlay_img = gr.Image(
                            show_download_button=False, show_label=False, visible=False,
                        )
                    with gr.Row():
                        masks_download = gr.File(
                            label="Download masks",
                        show_download_button=False,
                        show_label=False,
                        visible=False,
                    )
                with gr.Row():
                    masks_download = gr.File(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


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()
            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

qim3d/gui/interface.py

0 → 100644
+113 −0
Original line number Diff line number Diff line
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)]
            )
+254 −313
Original line number Diff line number Diff line
@@ -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 qim3d.gui.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
Original line number Diff line number Diff line
@@ -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 qim3d.gui.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
+15 −21
Original line number Diff line number Diff line
@@ -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":