Mentions légales du service

Skip to content
Snippets Groups Projects
soma_validation.py 9.96 KiB
Newer Older
import tkinter as tknt
DEBREUVE Eric's avatar
DEBREUVE Eric committed
from typing import Callable, Sequence, Tuple, Union
import matplotlib.cm as mpcm
import numpy as nmpy
import skimage.segmentation as sisg
from PIL import Image as plim
DEBREUVE Eric's avatar
DEBREUVE Eric committed
from PIL import ImageTk as pltk
array_t = nmpy.ndarray
image_t = plim.Image
DEBREUVE Eric's avatar
DEBREUVE Eric committed
tk_image_t = pltk.PhotoImage


STATIC_ROW_MIN_HEIGHT = 30


class soma_validation_window_t:
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        "gfp",
        "gfp_pil_image",
        "gfp_tk_image",
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        "lmap",
        "lmap_mip",
        "lmap_mip_4_display",
        "lmap_pil_image",
        "lmap_tk_image",
        "color_version",
        "with_cm",
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        "mip_axis_wgt",
        "gfp_wgt",
        "lmap_wgt",
        "cursor_nfo",
DEBREUVE Eric's avatar
DEBREUVE Eric committed
    gfp: array_t
    gfp_pil_image: image_t
    gfp_tk_image: tk_image_t
DEBREUVE Eric's avatar
DEBREUVE Eric committed
    lmap_mip: array_t
    lmap_mip_4_display: array_t
    lmap_pil_image: image_t
DEBREUVE Eric's avatar
DEBREUVE Eric committed
    lmap_tk_image: tk_image_t
    color_version: bool
    with_cm: str
DEBREUVE Eric's avatar
DEBREUVE Eric committed
    mip_axis_wgt: tknt.Menubutton
    gfp_wgt: tknt.Canvas
    lmap_wgt: tknt.Canvas
    cursor_nfo: tknt.Label
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        gfp: array_t,
        lmap: array_t,
        mip_axis: int = -1,
        color_version: bool = True,
        with_cm: str = None,
    ):
        """
        with_cm: "plasma" and "viridis" seem to be good options
        """
DEBREUVE Eric's avatar
DEBREUVE Eric committed

DEBREUVE Eric's avatar
DEBREUVE Eric committed
        # --- Creation of MIPs and Tk images
        (
            gfp_mip,
            lmap_mip,
            lmap_mip_4_display,
            gfp_pil_image,
            gfp_tk_image,
            lmap_pil_image,
            lmap_tk_image,
        ) = _MIPImages(
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            gfp,
            lmap,
            mip_axis,
            color_version,
            with_cm,
        gfp_pil_image_initial = gfp_pil_image
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        # ---- Creation of widgets
        if mip_axis < 0:
            mip_axis = gfp.ndim + mip_axis
        mip_axis_wgt = _MIPAxisChoiceWidget(
            mip_axis, self._ChangeMIPAxis, gfp.shape, main_window
        gfp_wgt = tknt.Label(main_window, image=gfp_tk_image)
        lmap_wgt = tknt.Label(main_window, image=lmap_tk_image)
        cursor_nfo = tknt.Label(main_window, text="")
        done_button = tknt.Button(main_window, text="Done", command=main_window.quit)
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        # --- Event management
        gfp_wgt.bind("<Configure>", self._OnResize)
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        lmap_wgt.bind("<Motion>", self._DisplaySomaLabel)
        lmap_wgt.bind("<Button-1>", self._DeleteSoma)

        # --- Widget placement in grid
        next_available_row = 0

        mip_axis_wgt.grid(row=next_available_row, column=0)
        next_available_row += 1

        gfp_wgt.grid(row=next_available_row, column=0)
        lmap_wgt.grid(row=next_available_row, column=1)
        next_available_row += 1
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        cursor_nfo.grid(row=next_available_row, column=0)
        done_button.grid(row=next_available_row, column=1)
        next_available_row += 1
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        # --- Window resize management
        main_window.rowconfigure(0, weight=1, minsize=STATIC_ROW_MIN_HEIGHT)
        main_window.rowconfigure(1, weight=10)
        main_window.rowconfigure(2, weight=1, minsize=STATIC_ROW_MIN_HEIGHT)
        main_window.columnconfigure(0, weight=1)
        main_window.columnconfigure(1, weight=1)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        # --- Saving required variables as object attributes
        for attribute in self.__class__.__slots__:
            setattr(self, attribute, eval(attribute))
    def LaunchValidation(self) -> int:
        self.main_window.mainloop()
        self.main_window.destroy()

        relabeled, _, _ = sisg.relabel_sequential(self.lmap)
        self.lmap[...] = relabeled

        return nmpy.amax(self.lmap)
    def _OnResize(self, event: tknt.EventType.Configure) -> None:
        """"""
        self.gfp_pil_image = self.gfp_pil_image_initial.resize((event.width, event.height))
        self.gfp_tk_image = pltk.PhotoImage(master=self.main_window, image=self.gfp_pil_image)
        self.gfp_wgt.configure(image=self.gfp_tk_image)

DEBREUVE Eric's avatar
DEBREUVE Eric committed
    def _ChangeMIPAxis(self, mip_axis: tknt.IntVar, *_, **__):
        """"""
        new_mip_axis = mip_axis.get()

        (
            gfp_mip,
            lmap_mip,
            lmap_mip_4_display,
            gfp_pil_image,
            gfp_tk_image,
            lmap_pil_image,
            lmap_tk_image,
        ) = _MIPImages(
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            self.gfp,
            self.lmap,
            new_mip_axis,
            self.color_version,
            self.with_cm,
        self.gfp_pil_image_initial = self.gfp_pil_image
        self.gfp_wgt.configure(image=gfp_tk_image)
        self.lmap_wgt.configure(image=lmap_tk_image)

        self.gfp_mip = gfp_mip
        self.gfp_pil_image = gfp_pil_image
        self.gfp_tk_image = gfp_tk_image
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        self.lmap_mip = lmap_mip
        self.lmap_mip_4_display = lmap_mip_4_display
        self.lmap_pil_image = lmap_pil_image
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        self.lmap_tk_image = lmap_tk_image

    def _DisplaySomaLabel(self, event: tknt.EventType.Motion) -> None:
        """"""
        row = event.y
        col = event.x
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            label = self.lmap_mip[row, col]
        except IndexError:
            # This problem appeared when pack was replaced with grid. Setting ipad? and pad? to zero when adding the
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            # lmap_wgt to the grid did not solve it. Is this a bug in TkInter grid?
            return
        self.cursor_nfo.configure(text=f"Label:{label}@{row}x{col}")
    def _DeleteSoma(self, event: tknt.EventType.ButtonPress) -> None:
        """"""
        row = event.y
        col = event.x
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            label = self.lmap_mip[row, col]
        except IndexError:
            # This problem appeared when pack was replaced with grid. Setting ipad? and pad? to zero when adding the
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            # lmap_wgt to the grid did not solve it. Is this a bug in TkInter grid?
        if label > 0:
            self.lmap[self.lmap == label] = 0
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            soma_bmap = self.lmap_mip == label
            self.lmap_mip[soma_bmap] = 0
            if self.lmap_mip_4_display.ndim == 1:
                self.lmap_mip_4_display[soma_bmap] = 0
DEBREUVE Eric's avatar
DEBREUVE Eric committed
                for channel in range(self.lmap_mip_4_display.shape[2]):
                    self.lmap_mip_4_display[..., channel][soma_bmap] = 0
            self.lmap_tk_image, self.lmap_pil_image = _TkImageFromNumpyArray(
                self.lmap_mip_4_display, self.main_window
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            )
            self.lmap_wgt.configure(image=self.lmap_tk_image)


def _MIPImages(
    gfp: array_t,
    lmap: array_t,
    mip_axis: int,
    color_version: bool,
    with_cm: str,
    parent: Union[tknt.Widget, tknt.Tk],
) -> Tuple[array_t, array_t, array_t, image_t, tk_image_t, image_t, tk_image_t]:
DEBREUVE Eric's avatar
DEBREUVE Eric committed
    """"""
    gfp_mip = nmpy.amax(gfp, axis=mip_axis)
    gfp_mip *= 255.0 / nmpy.amax(gfp_mip)

    lmap_mip = nmpy.amax(lmap, axis=mip_axis)

    if color_version:
        if with_cm is None:
            lmap_mip_4_display = _ColoredVersion(lmap_mip)
        else:
            lmap_mip_4_display = _ColoredVersionFromColormap(lmap_mip, with_cm)
    else:
        lmap_mip_4_display = _ScaledVersion(lmap_mip)

    gfp_tk_image, gfp_pil_image = _TkImageFromNumpyArray(gfp_mip, parent)
    lmap_tk_image, lmap_pil_image = _TkImageFromNumpyArray(lmap_mip_4_display, parent)

    return (
        gfp_mip,
        lmap_mip,
        lmap_mip_4_display,
        gfp_pil_image,
        gfp_tk_image,
        lmap_pil_image,
        lmap_tk_image,
    )


def _ScaledVersion(image: array_t, offset: int = 50) -> array_t:
    """
    offset: Value of darkest non-background intensity
    """
    scaling = (255.0 - offset) / nmpy.max(image)
    output = scaling * image + offset
    output[image == 0] = 0

    return output.astype(nmpy.uint8)


def _ColoredVersion(image: array_t) -> array_t:
    """"""
    max_label = nmpy.amax(image)

    labels = tuple(range(1, max_label + 1))
    half_length = int(round(0.5 * max_label))
    shuffled_labels = labels[half_length:] + labels[:half_length]
    shuffled_image = nmpy.zeros_like(image)
    for label, shuffled_label in enumerate(shuffled_labels):
        shuffled_image[image == label] = shuffled_label

    output = nmpy.dstack((image, shuffled_image, max_label - image))
    output = (255.0 / max_label) * output
    output[image == 0] = 0

    return output.astype(nmpy.uint8)


def _ColoredVersionFromColormap(image: array_t, colormap_name: str) -> array_t:
    """"""
    output = nmpy.zeros(image.shape + (3,), dtype=nmpy.uint8)

    LinearValueToRGB = mpcm.get_cmap(colormap_name)
    max_label = nmpy.amax(image)
    for label in range(1, max_label + 1):
        color_01 = LinearValueToRGB((label - 1.0) / (max_label - 1.0))
        color_255 = nmpy.round(255.0 * nmpy.array(color_01[:3]))
        output[image == label, :] = color_255

    return output


def _TkImageFromNumpyArray(
    array: array_t, parent: Union[tknt.Widget, tknt.Tk]
) -> Tuple[tk_image_t, image_t]:
    pil_image = plim.fromarray(array)
    tk_image = pltk.PhotoImage(master=parent, image=pil_image)
    return tk_image, pil_image
DEBREUVE Eric's avatar
DEBREUVE Eric committed


def _MIPAxisChoiceWidget(
    current_axis: int,
    Action: Callable,
    shape: Sequence[int],
    parent: Union[tknt.Widget, tknt.Tk],
) -> tknt.Menubutton:
    """"""
    title = f"MIP Axis [{','.join(str(_lgt) for _lgt in shape)}]"
    output = tknt.Menubutton(parent, text=title, relief="raised")

    menu = tknt.Menu(output, tearoff=False)
    entries = ("First dim", "Second dim", "Third dim")
    selected_mip_axis = tknt.IntVar()
    for idx, entry in enumerate(entries):
        menu.add_radiobutton(label=entry, value=idx, variable=selected_mip_axis)
    menu.invoke(current_axis)

    # Set action only after calling invoke to avoid redundant call at window creation
    TkAction = lambda *args, **kwargs: Action(selected_mip_axis, *args, **kwargs)
    selected_mip_axis.trace_add("write", TkAction)

    output["menu"] = menu