Skip to content

Commit

Permalink
Merge pull request #53 from zoccoler/patch_0_0_7
Browse files Browse the repository at this point in the history
Patch 0 0 7
  • Loading branch information
zoccoler authored Mar 14, 2024
2 parents a3574e0 + 9fa06aa commit df67ea3
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 96 deletions.
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ _Warning: In the current version, lazy loading with `.zarr` is available, but pr

![](https://github.com/zoccoler/napari-flim-phasor-plotter/raw/main/images/convert_to_zarr.png)

If you have multiple slices or time-points as separated files, you can choose a folder containing the files. In order for the plugin to properly build a stack, the file names must contain some indication about which slice or time-point they represent, i.e., **each file name should contain a `_t` and/or `_z` followerd by a number**.
If you have multiple slices or time-points as separated files, you can choose a folder containing the files. In order for the plugin to properly build a stack, the file names must contain some indication about which slice or time-point they represent, i.e., **each file name should contain a `_t` and/or `_z` followed by a number**.

Here are a few example templates:
- timelapse:
Expand All @@ -69,26 +69,25 @@ Here are a few example templates:

## Installation

You can install `napari-flim-phasor-plotter` via [pip]. Follow these steps from a terminal.
We recommend installing `napari-flim-phasor-plotter` with [mamba](https://mamba.readthedocs.io/en/latest/) after having [Miniforge](https://github.com/conda-forge/miniforge?tab=readme-ov-file#miniforge) installed in your computer. Follow these steps from a terminal.

We recommend using `mamba-forge` whenever possible. Click [here](https://github.com/conda-forge/miniforge#mambaforge) to choose the right download option for your OS.
**If you use `mamba-forge`, replace the `conda` term whenever you see it below with `mamba`.**
Click [here](https://github.com/conda-forge/miniforge?tab=readme-ov-file#download) to choose the right download option for your OS.

Create a conda environment:

conda create -n napari-flim-phasor-env python=3.9
mamba create -n napari-flim-phasor-env python=3.9

Activate the environment:

conda activate napari-flim-phasor-env
mamba activate napari-flim-phasor-env

Then install `napari` and `napari-clusturs-plotter` (plus git if on Windows):

conda install -c conda-forge napari==0.4.17 napari-clusters-plotter git pyqt
mamba install -c conda-forge napari napari-clusters-plotter git pyqt

_Optional: we **strongly** recommend having the `devbio-napari` plugin bundle also installed for post-processing. This can be done with:_
_Optional, but we **strongly** recommend having the `devbio-napari` plugin bundle also installed for post-processing. This can be done with:_

conda install -c conda-forge devbio-napari
mamba install -c conda-forge devbio-napari

Finally install `napari-flim-phasor-plotter` plugin with:

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = napari-flim-phasor-plotter
version = 0.0.6
version = 0.0.7
description = A plugin that performs phasor plot from TCSPC FLIM data.
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
2 changes: 1 addition & 1 deletion src/napari_flim_phasor_plotter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.0.6"
__version__ = "0.0.7"

from ._reader import napari_get_reader
from ._sample_data import load_seminal_receptacle_image, load_hazelnut_image, load_hazelnut_z_stack, load_lifetime_cat_synthtetic_single_image
Expand Down
19 changes: 14 additions & 5 deletions src/napari_flim_phasor_plotter/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def read_single_tif_file(path, channel_axis=0, ut_axis=1, timelapse=False, viewe
elif data.ndim == 4: # Assume (ut, z, y, x)
# Yields: (ut, t, z, y, x)
data = np.expand_dims(data, axis=-4)
# Add unidimensional channel axis
# Add unidimensional channel axis (WARNING: this already moves the ut axis) TODO: fix this
data = np.expand_dims(data, 0)
# if multichannel
else:
Expand Down Expand Up @@ -223,8 +223,9 @@ def flim_file_reader(path):
in napari along with other FLIM metadata, and layer_type is 'image'.
"""
from pathlib import Path
from tifffile import TiffFile
import tifffile
import numpy as np
from napari.utils. notifications import show_warning
# handle both a string and a list of strings
paths = [path] if isinstance(path, str) else path
# Use Path from pathlib
Expand All @@ -243,10 +244,18 @@ def flim_file_reader(path):
channel_axis = 0
# If .tif, check shape before loading pixels
if file_extension == '.tif' or file_extension == '.tiff':
tif = TiffFile(file_path)
shape = tif.shaped_metadata[0]['shape']
tif = tifffile.TiffFile(file_path)
shaped_metadata = tif.shaped_metadata
if len(shaped_metadata) > 0:
shape = tif.shaped_metadata[0]['shape']
else:
show_warning('Warning: Cannot determine shape from metadata. Loading full stack.')
image = tifffile.imread(file_path)
shape = image.shape
if len(shape) > 4: # stack (z or timelapse)
channel_axis = None
if len(shape) < 4: # single 2D image (ut, y, x)
channel_axis = None
imread = get_read_function_from_extension[file_extension]
# (ch, ut, y, x) or (ch, ut, t, z, y, x) in case of single tif stack
data, metadata_list = imread(file_path, channel_axis=channel_axis, viewer_exists=True)
Expand All @@ -260,7 +269,7 @@ def flim_file_reader(path):
data = data[non_empty_channel_indices]
metadata_list = [metadata_list[i] for i in non_empty_channel_indices if len(metadata_list) > 0]

summed_intensity_image = np.sum(data, axis=1, keepdims=True)
summed_intensity_image = np.sum(data, axis=1, keepdims=False)
# arguments for TCSPC stack
add_kwargs = {'channel_axis': 0, 'metadata': metadata_list}
layer_type = "image"
Expand Down
131 changes: 76 additions & 55 deletions src/napari_flim_phasor_plotter/_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def make_flim_phasor_plot(image_layer: "napari.layers.Image",
napari_viewer : napari.Viewer, optional
napari viewer instance, by default None
"""
import warnings
import numpy as np
import dask.array as da
import pandas as pd
Expand Down Expand Up @@ -119,59 +120,65 @@ def make_flim_phasor_plot(image_layer: "napari.layers.Image",
visible=False)

# Check if plotter was alrerady added to dock_widgets
# TO DO: avoid using private method access to napari_viewer.window._dock_widgets (will be deprecated)
dock_widgets_names = [key for key,
value in napari_viewer.window._dock_widgets.items()]
if 'Phasor Plotter Widget (napari-flim-phasor-plotter)' not in dock_widgets_names:
plotter_widget = PhasorPlotterWidget(napari_viewer)
napari_viewer.window.add_dock_widget(
plotter_widget, name='Phasor Plotter Widget (napari-flim-phasor-plotter)')
else:
widgets = napari_viewer.window._dock_widgets['Phasor Plotter Widget (napari-flim-phasor-plotter)']
plotter_widget = widgets.findChild(PhasorPlotterWidget)

# Get labels layer with labelled pixels (labels)
# Commented code below will work with napari-clusters-plotter 0.7.4 (not released yet) or 0.8.0 depending on the next version number
# plotter_widget.layer_select.value = [
# choice for choice in plotter_widget.layer_select.choices if choice.name.startswith("Labelled_pixels")][0]
plotter_widget.labels_select.value = [
choice for choice in plotter_widget.labels_select.choices if choice.name.startswith("Labelled_pixels")][0]
# Set G and S as features to plot (update_axes_list method clears Comboboxes)
plotter_widget.plot_x_axis.setCurrentIndex(1)
plotter_widget.plot_y_axis.setCurrentIndex(2)
plotter_widget.plotting_type.setCurrentIndex(1)
plotter_widget.log_scale.setChecked(True)

# Show parent (PlotterWidget) so that run function can run properly
plotter_widget.parent().show()
# Disconnect selector to reset collection of points in plotter
# (it gets reconnected when 'run' method is run)
plotter_widget.graphics_widget.selector.disconnect()
plotter_widget.run(labels_layer.features,
plotter_widget.plot_x_axis.currentText(),
plotter_widget.plot_y_axis.currentText())
plotter_widget.redefine_axes_limits(ensure_full_semi_circle_displayed=True)

# Update laser frequency spinbox
# TO DO: access and update widget in a better way
if 'Calculate Phasors (napari-flim-phasor-plotter)' in dock_widgets_names:
widgets = napari_viewer.window._dock_widgets[
'Calculate Phasors (napari-flim-phasor-plotter)']
laser_frequency_spinbox = widgets.children()[4].children()[
2].children()[-1]
# Set precision of spinbox based on number of decimals in laser_frequency
laser_frequency_spinbox.setDecimals(
str(laser_frequency)[::-1].find('.'))
laser_frequency_spinbox.setValue(laser_frequency)
# TODO: avoid using private method access to napari_viewer.window._dock_widgets (will be deprecated)
with warnings.catch_warnings():
warnings.simplefilter(action='ignore', category=FutureWarning)
dock_widgets_names = [key for key,
value in napari_viewer.window._dock_widgets.items()]
if 'Phasor Plotter Widget (napari-flim-phasor-plotter)' not in dock_widgets_names:
plotter_widget = PhasorPlotterWidget(napari_viewer)
napari_viewer.window.add_dock_widget(
plotter_widget, name='Phasor Plotter Widget (napari-flim-phasor-plotter)')
else:
widgets = napari_viewer.window._dock_widgets['Phasor Plotter Widget (napari-flim-phasor-plotter)']
plotter_widget = widgets.findChild(PhasorPlotterWidget)

# Get labels layer with labelled pixels (labels)
# Commented code below will work with napari-clusters-plotter 0.7.4 (not released yet) or 0.8.0 depending on the next version number
# plotter_widget.layer_select.value = [
# choice for choice in plotter_widget.layer_select.choices if choice.name.startswith("Labelled_pixels")][0]
for choice in plotter_widget.labels_select.choices:
if choice.name == 'Labelled_pixels_from_' + image_layer.name:
plotter_widget.labels_select.value = choice
break
# plotter_widget.labels_select.value = [
# choice for choice in plotter_widget.labels_select.choices if choice.name.startswith("Labelled_pixels")][0]
# Set G and S as features to plot (update_axes_list method clears Comboboxes)
plotter_widget.plot_x_axis.setCurrentIndex(1)
plotter_widget.plot_y_axis.setCurrentIndex(2)
plotter_widget.plotting_type.setCurrentIndex(1)
plotter_widget.log_scale.setChecked(True)

# Show parent (PlotterWidget) so that run function can run properly
plotter_widget.parent().show()
# Disconnect selector to reset collection of points in plotter
# (it gets reconnected when 'run' method is run)
plotter_widget.graphics_widget.selector.disconnect()
plotter_widget.run(labels_layer.features,
plotter_widget.plot_x_axis.currentText(),
plotter_widget.plot_y_axis.currentText())
plotter_widget.redefine_axes_limits(ensure_full_semi_circle_displayed=True)

# Update laser frequency spinbox
# TO DO: access and update widget in a better way
if 'Calculate Phasors (napari-flim-phasor-plotter)' in dock_widgets_names:
widgets = napari_viewer.window._dock_widgets[
'Calculate Phasors (napari-flim-phasor-plotter)']
laser_frequency_spinbox = widgets.children()[4].children()[
2].children()[-1]
# Set precision of spinbox based on number of decimals in laser_frequency
laser_frequency_spinbox.setDecimals(
str(laser_frequency)[::-1].find('.'))
laser_frequency_spinbox.setValue(laser_frequency)

return plotter_widget, labels_layer


@magic_factory
def apply_binning_widget(image_layer: "napari.types.ImageData",
def apply_binning_widget(image_layer: "napari.layers.Image",
bin_size: int = 2,
binning_3D: bool = True,
) -> "napari.types.ImageData":
) -> "napari.layers.Image":
"""Apply binning to image layer.
Parameters
Expand All @@ -186,10 +193,11 @@ def apply_binning_widget(image_layer: "napari.types.ImageData",
Returns
-------
image_binned : napari.types.ImageData
binned image
image_layer_binned : napari.layers.Image
binned layer
"""
import numpy as np
from napari.layers import Image
from napari_flim_phasor_plotter.filters import apply_binning
# Warning! This loads the image as a numpy array
# TODO: add support for dask arrays
Expand All @@ -198,7 +206,7 @@ def apply_binning_widget(image_layer: "napari.types.ImageData",
# Add dimensions if needed, to make it 5D (ut, time, z, y, x)
while len(image_binned.shape) < 5:
image_binned = np.expand_dims(image_binned, axis=0)
return image_binned
return Image(image_binned, scale=image_layer.scale, name=image_layer.name + f' binned {bin_size}')

def manual_label_extract(cluster_labels_layer: "napari.layers.Labels", label_number: int = 1) -> "napari.layers.Labels":
"""Extracts single label from labels layer
Expand All @@ -217,10 +225,14 @@ def manual_label_extract(cluster_labels_layer: "napari.layers.Labels", label_num
"""
import numpy as np
from napari.layers import Labels
from napari.utils import DirectLabelColormap
unitary_dims = [i for i, size in enumerate(np.asarray(cluster_labels_layer.data).shape) if size == 1]
labels_data = np.squeeze(np.asarray(cluster_labels_layer.data).copy())
labels_data[labels_data != label_number] = 0
# TODO: update to use DirectLabelColormap once napari-clusters-plotter has this issue fixed
label_color = cluster_labels_layer.color
return Labels(labels_data, color=label_color, name=f'Cluster Label #{label_number}')
new_scale = np.array([scale for i, scale in enumerate(cluster_labels_layer.scale) if i not in unitary_dims])
return Labels(labels_data, colormap=DirectLabelColormap(color_dict=label_color), name=f'Cluster Label #{label_number}', scale=new_scale)

def get_n_largest_cluster_labels(features_table: 'pandas.DataFrame', n: int=1, clustering_id: str='MANUAL_CLUSTER_ID') -> List[int]:
"""Get the labels of the n largest clusters in a features table
Expand Down Expand Up @@ -278,9 +290,11 @@ def split_n_largest_cluster_labels(labels_layer: "napari.layers.Labels", cluster
class Split_N_Largest_Cluster_Labels(Container):
"""Widget to split the n largest clusters from a labels layer
"""
from napari import layers as napari_layers
input_layer_types = (
"napari.layers.Labels",)
napari_layers.Labels,)
def __init__(self, viewer: "napari.viewer.Viewer"):
from napari import layers as napari_layers
self._viewer = viewer

# Create widgets
Expand Down Expand Up @@ -313,7 +327,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"):
# Connect all labels layer data change events to reset clustering id choices
# to ensure cluster id is up-to-date
for layer in self._viewer.layers:
if isinstance(layer, "napari.layers.Labels"):
if isinstance(layer, napari_layers.Labels):
layer.events.data.connect(self._clustering_id_combobox.reset_choices)

# Create cut button
Expand Down Expand Up @@ -358,14 +372,17 @@ def _on_run_clicked(self):
"""Run the widget
Creates new labels layers for each of the n largest clusters
if entriesd are valid
if entries are valid
"""
split_n_largest_cluster_labels(
cluster_individual_labels_layer_list = split_n_largest_cluster_labels(
labels_layer=self._labels_layer_combobox.value,
clusters_labels_layer=self._clusters_labels_layer_combobox.value,
clustering_id=self._clustering_id_combobox.value,
n=self._n_spinbox.value,
)
# Add new layers to viewer
for layer in cluster_individual_labels_layer_list:
self._viewer.add_layer(layer)

def smooth_cluster_mask(cluster_mask_layer: "napari.layers.Labels", fill_area_px: int = 64, smooth_radius: int = 3) -> "napari.layers.Labels":
"""Smooths a mask from a labels layer with morphological operations
Expand All @@ -387,6 +404,8 @@ def smooth_cluster_mask(cluster_mask_layer: "napari.layers.Labels", fill_area_px
from skimage import morphology
import numpy as np
from napari.layers import Labels
from napari.utils import DirectLabelColormap
unitary_dims = [i for i, size in enumerate(np.asarray(cluster_mask_layer.data).shape) if size == 1]
labels_data = np.squeeze(np.asarray(cluster_mask_layer.data))
# Fill holes based on area threshold
labels_data = morphology.area_closing(labels_data, fill_area_px)
Expand All @@ -396,5 +415,7 @@ def smooth_cluster_mask(cluster_mask_layer: "napari.layers.Labels", fill_area_px
labels_data = morphology.isotropic_opening(labels_data, smooth_radius)
# Restore label number
labels_data = labels_data.astype(cluster_mask_layer.data.dtype)*cluster_mask_layer.data.max()
# TODO: update to use DirectLabelColormap once napari-clusters-plotter has this issue fixed
label_color = cluster_mask_layer.color
return Labels(labels_data, color=label_color)
new_scale = np.array([scale for i, scale in enumerate(cluster_mask_layer.scale) if i not in unitary_dims])
return Labels(labels_data, colormap=DirectLabelColormap(color_dict=label_color), scale=new_scale, name=cluster_mask_layer.name + ' smoothed')
91 changes: 66 additions & 25 deletions src/napari_flim_phasor_plotter/notebooks/Example_workflow.ipynb

Large diffs are not rendered by default.

0 comments on commit df67ea3

Please sign in to comment.