From c4012fb2490a8e50d003e92606108a5cfc30dae9 Mon Sep 17 00:00:00 2001 From: Marcelo Zoccoler Date: Mon, 1 Jul 2024 11:47:51 +0200 Subject: [PATCH 1/6] Force cluster to be redrawn and start as histogram with log scale --- src/napari_flim_phasor_plotter/_plotting.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/napari_flim_phasor_plotter/_plotting.py b/src/napari_flim_phasor_plotter/_plotting.py index 6d51533..9f2e3e0 100644 --- a/src/napari_flim_phasor_plotter/_plotting.py +++ b/src/napari_flim_phasor_plotter/_plotting.py @@ -26,19 +26,26 @@ def __init__(self, napari_viewer): self.advanced_options_container.layout().addWidget(self.tau_lines_container) self.tau_lines_button.clicked.connect(self.on_show_hide_tau_lines) + # Start with histogram plot + self.plotting_type.setCurrentIndex(1) + # Start with log scale + self.log_scale.setChecked(True) + def run(self, features, plot_x_axis_name, plot_y_axis_name, - plot_cluster_name=None, + plot_cluster_name, redraw_cluster_image=True, + force_redraw=True, ensure_full_semi_circle_displayed=False ): super().run(features=features, plot_x_axis_name=plot_x_axis_name, plot_y_axis_name=plot_y_axis_name, plot_cluster_name=plot_cluster_name, - redraw_cluster_image=redraw_cluster_image,) + redraw_cluster_image=redraw_cluster_image, + force_redraw=force_redraw) if self.tau_lines_button.isChecked(): self.add_tau_lines_from_widget() self.redefine_axes_limits(ensure_full_semi_circle_displayed=ensure_full_semi_circle_displayed) From bb6aa2aea8b73598846ed1d726789d5a0c92e102 Mon Sep 17 00:00:00 2001 From: Marcelo Zoccoler Date: Mon, 1 Jul 2024 11:48:09 +0200 Subject: [PATCH 2/6] fix layer creation after selection test --- .../_tests/test_widget.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/napari_flim_phasor_plotter/_tests/test_widget.py b/src/napari_flim_phasor_plotter/_tests/test_widget.py index 5b062db..9e6b10f 100644 --- a/src/napari_flim_phasor_plotter/_tests/test_widget.py +++ b/src/napari_flim_phasor_plotter/_tests/test_widget.py @@ -1,7 +1,6 @@ from napari_flim_phasor_plotter._widget import make_flim_phasor_plot from napari_flim_phasor_plotter._synthetic import make_synthetic_flim_data from napari_flim_phasor_plotter._synthetic import create_time_array -from napari_flim_phasor_plotter._plotting import PhasorPlotterWidget import numpy as np import pandas as pd # make_napari_viewer is a pytest fixture that returns a napari viewer object @@ -63,7 +62,7 @@ def test_make_flim_phasor_plot_and_plotter(make_napari_viewer, capsys): assert plotter_widget is not None assert len(viewer.layers) == 2 - assert list(labels_layer.features.columns) == ['label', 'G', 'S', 'pixel_x_coordinates', 'pixel_y_coordinates', 'pixel_z_coordinates', 'frame'] + assert list(labels_layer.features.columns) == list(output_table.columns) assert labels_layer.features.shape == (7, 7) assert np.allclose(labels_layer.features.values, output_table.values, rtol=0, atol=1e-5) @@ -74,13 +73,14 @@ def test_make_flim_phasor_plot_and_plotter(make_napari_viewer, capsys): plotter_widget.tau_lines_line_edit_widget.setText('0.1, 1') plotter_widget.tau_lines_button.click() - # TODO: Fix the following test (manually it works) # # Select region around 1 ns - # plotter_widget.graphics_widget.selector.onselect(input_selection_vertices) + plotter_widget.graphics_widget.selector.onselect(input_selection_vertices) # # Check if the selected cluster image is displayed - # assert len(viewer.layers) == 3 - # phasor_clusters_layer = viewer.layers[-1] - # assert np.allclose(phasor_clusters_layer.data, output_selected_cluster_image, rtol=0, atol=1e-5) + assert len(viewer.layers) == 3 + phasor_clusters_layer = viewer.layers[-1] + assert phasor_clusters_layer.name.startswith( + 'Phasor_clusters_from_') + assert np.allclose(phasor_clusters_layer.data, output_selected_cluster_image, rtol=0, atol=1e-5) From 9c17402cec070836785fcde7425ddf1b7ea606de Mon Sep 17 00:00:00 2001 From: Marcelo Zoccoler Date: Mon, 1 Jul 2024 11:49:22 +0200 Subject: [PATCH 3/6] match run function arguments (don't redraw at first since there are no selected clusters yet) --- src/napari_flim_phasor_plotter/_widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/napari_flim_phasor_plotter/_widget.py b/src/napari_flim_phasor_plotter/_widget.py index c2b0d62..cc0d4d5 100644 --- a/src/napari_flim_phasor_plotter/_widget.py +++ b/src/napari_flim_phasor_plotter/_widget.py @@ -159,6 +159,8 @@ def make_flim_phasor_plot(image_layer: "napari.layers.Image", plotter_widget.run(features=labels_layer.features, plot_x_axis_name=plotter_widget.plot_x_axis.currentText(), plot_y_axis_name=plotter_widget.plot_y_axis.currentText(), + plot_cluster_name=plotter_widget.plot_cluster_id.currentText(), + redraw_cluster_image=False, ensure_full_semi_circle_displayed=True) # Update laser frequency spinbox From 8836c38d7e744751f4e29fefe992ef4d290b0c5a Mon Sep 17 00:00:00 2001 From: Marcelo Zoccoler Date: Mon, 1 Jul 2024 11:49:59 +0200 Subject: [PATCH 4/6] bump version --- src/napari_flim_phasor_plotter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_flim_phasor_plotter/__init__.py b/src/napari_flim_phasor_plotter/__init__.py index 4d02687..5e9d5a1 100644 --- a/src/napari_flim_phasor_plotter/__init__.py +++ b/src/napari_flim_phasor_plotter/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.7" +__version__ = "0.1.0" 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 From a40fe8206b7f3bc5ee057ac8877c0f69ab07c009 Mon Sep 17 00:00:00 2001 From: Marcelo Zoccoler Date: Mon, 1 Jul 2024 15:25:24 +0200 Subject: [PATCH 5/6] get_n_largest_cluster_labels function return list instead of numpy array --- src/napari_flim_phasor_plotter/_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/napari_flim_phasor_plotter/_widget.py b/src/napari_flim_phasor_plotter/_widget.py index cc0d4d5..794a202 100644 --- a/src/napari_flim_phasor_plotter/_widget.py +++ b/src/napari_flim_phasor_plotter/_widget.py @@ -262,7 +262,7 @@ def get_n_largest_cluster_labels(features_table: 'pandas.DataFrame', n: int=1, c if 'MANUAL' in clustering_id: sorted_cluster_ids = sorted_cluster_ids[sorted_cluster_ids != 1] # remove unselected clusters when selection is manual - return sorted_cluster_ids[:n] + return sorted_cluster_ids[:n].tolist() def split_n_largest_cluster_labels(labels_layer: "napari.layers.Labels", clusters_labels_layer: "napari.layers.Labels", clustering_id: str, n: int=1) -> List["napari.layers.Labels"]: """Split the n largest clusters from a labels layer inot new layers @@ -270,7 +270,7 @@ def split_n_largest_cluster_labels(labels_layer: "napari.layers.Labels", cluster Parameters ---------- labels_layer : napari.layers.Labels - labels layer + labels layer with features table clusters_labels_layer : napari.layers.Labels labels layer with clusters clustering_id : str From a242eab68612a71d7598027d93ae170ed5fea3b5 Mon Sep 17 00:00:00 2001 From: Marcelo Zoccoler Date: Mon, 1 Jul 2024 15:51:31 +0200 Subject: [PATCH 6/6] Add tests for post-processing widgets and functions, and set a few global inputs --- .../_tests/test_widget.py | 139 ++++++++++++++++-- 1 file changed, 124 insertions(+), 15 deletions(-) diff --git a/src/napari_flim_phasor_plotter/_tests/test_widget.py b/src/napari_flim_phasor_plotter/_tests/test_widget.py index 9e6b10f..6bdd008 100644 --- a/src/napari_flim_phasor_plotter/_tests/test_widget.py +++ b/src/napari_flim_phasor_plotter/_tests/test_widget.py @@ -6,34 +6,67 @@ # make_napari_viewer is a pytest fixture that returns a napari viewer object # capsys is a pytest fixture that captures stdout and stderr output streams +# Global inputs +laser_frequency = 40 # MHz +amplitude = 1 +tau_list = [0.1, 0.2, 0.5, 1, 2, 5, 10, 25, 40] # ns +# Obs: after running calculate phasors with default thresholding (10), 0.1 and 0.2 associated pixels are excluded/masked out because they end up with low photon budget (summed intensity < 10) given the low amplitude and fast decay -def test_make_flim_phasor_plot_and_plotter(make_napari_viewer, capsys): - output_table = pd.DataFrame({ +table = pd.DataFrame({ 'label': [1, 2, 3, 4, 5, 6, 7], 'G': [0.984806, 0.941213, 0.799235, 0.388781, 0.137519, 0.025144, 0.010088], 'S': [0.120760, 0.233658, 0.399003, 0.485904, 0.342826, 0.154997, 0.098370], 'pixel_x_coordinates': [2, 0, 1, 2, 0, 1, 2], 'pixel_y_coordinates': [0, 1, 1, 1, 2, 2, 2], 'pixel_z_coordinates': [0, 0, 0, 0, 0, 0, 0], - 'frame': [0, 0, 0, 0, 0, 0, 0] + 'frame': [0, 0, 0, 0, 0, 0, 0], }) +labelled_pixels_masked = np.array( + [ + [ + [ + [0, 0, 1], # 0 represents masked pixels after default thresholding (10) + [2, 3, 4], + [5, 6, 7], + ] + ] + ] +) + +phasor_clusters_labels = np.array( + [ + [ + [ + [0, 0, 2], # 0 represents noise or masked pixels + [1, 1, 2], # 1 represents not selected pixels + [3, 3, 3], # 2 and 3 represent the first 2 selected clusters labels (in table they are 1 and 2) + ] + ] + ] + ) +# Manual clusters column contains phasor cluster labels, where labelled pixels are present, minus 1 because 0 is reserved for noise clusters and 1 for not selected pixels +manual_clusters_column = pd.Series(phasor_clusters_labels[labelled_pixels_masked>0]-1, name='MANUAL_CLUSTER_ID', index=table.index) +table_with_clusters = pd.concat([table, manual_clusters_column], axis=1) + +def test_make_flim_phasor_plot_and_plotter(make_napari_viewer, capsys): + # Inputs for manual selection input_selection_vertices = np.array( [ [0.940, 0.23], [0.940, 0.24], [0.945, 0.24], - [0.945, 0.23] + [0.945, 0.23], ] ) # square vertices with G and S coordinates around 1 ns - - output_selected_cluster_image = np.array( + # Expected output array after manual selection + output_selected_cluster_labels = np.array( [ [ [ [0, 0, 0], [2, 0, 0], - [0, 0, 0] + [0, 0, 0], ] ] ] @@ -41,9 +74,7 @@ def test_make_flim_phasor_plot_and_plotter(make_napari_viewer, capsys): # Test make_flim_phasor_plot viewer = make_napari_viewer() - laser_frequency = 40 # MHz - amplitude = 1 - tau_list = [0.1, 0.2, 0.5, 1, 2, 5, 10, 25, 40] # nss + number_of_time_points = 1000 time_array = create_time_array(laser_frequency, number_of_time_points) @@ -56,16 +87,18 @@ def test_make_flim_phasor_plot_and_plotter(make_napari_viewer, capsys): # create the widget my_widget = make_flim_phasor_plot() - # execute our function + # execute function plotter_widget, labels_layer = my_widget() # Check if the plotter widget was created assert plotter_widget is not None - + # Check if outputs match expected values assert len(viewer.layers) == 2 - assert list(labels_layer.features.columns) == list(output_table.columns) + assert list(labels_layer.features.columns) == list(table.columns) assert labels_layer.features.shape == (7, 7) + assert labels_layer.name.startswith( + 'Labelled_pixels_from_') assert np.allclose(labels_layer.features.values, - output_table.values, rtol=0, atol=1e-5) + table.values, rtol=0, atol=1e-5) # Test plotter widget options and selection @@ -81,6 +114,82 @@ def test_make_flim_phasor_plot_and_plotter(make_napari_viewer, capsys): phasor_clusters_layer = viewer.layers[-1] assert phasor_clusters_layer.name.startswith( 'Phasor_clusters_from_') - assert np.allclose(phasor_clusters_layer.data, output_selected_cluster_image, rtol=0, atol=1e-5) + assert np.allclose(phasor_clusters_layer.data, output_selected_cluster_labels, rtol=0, atol=1e-5) +def test_manual_label_extract(): + from napari_flim_phasor_plotter._widget import manual_label_extract + from napari.layers import Labels + # Inputs + selected_label = 2 + # Expected output array + output_selected_cluster_labels = np.array( + [ + [0, 0, 2], + [0, 0, 2], + [0, 0, 0], + ] + ) + + labels_layer = Labels(phasor_clusters_labels) + selected_label_layer = manual_label_extract(labels_layer, selected_label) + + assert selected_label_layer.data.shape == output_selected_cluster_labels.shape # squeeze dimensions to match other processing tools + assert np.array_equal(selected_label_layer.data, output_selected_cluster_labels) + +def test_get_n_largest_cluster_labels(): + from napari_flim_phasor_plotter._widget import get_n_largest_cluster_labels + # Input + number_of_clusters = 2 + # Expected outputs + expected_list_of_cluster_labels = [3, 2] # lergest cluster is 3, second largest is 2 (1 represents not selected pixels) + + list_of_cluster_labels = get_n_largest_cluster_labels(table_with_clusters, number_of_clusters) + + assert len(list_of_cluster_labels) == number_of_clusters + assert list(list_of_cluster_labels) == expected_list_of_cluster_labels + +def test_Split_N_Largest_Cluster_Labels(make_napari_viewer): + from napari_flim_phasor_plotter._widget import Split_N_Largest_Cluster_Labels + from napari.layers import Labels + + viewer = make_napari_viewer() + # Inputs + labelled_pixels_layer = Labels(labelled_pixels_masked, features=table_with_clusters) + phasor_clusters_layer = Labels(phasor_clusters_labels) + number_of_clusters = 2 + clustering_id = manual_clusters_column.name + # Expected output arrays + largest_cluster_labels = np.array( + [ + [0, 0, 0], + [0, 0, 0], + [3, 3, 3], + ] + ) + second_largest_cluster_labels = np.array( + [ + [0, 0, 2], + [0, 0, 2], + [0, 0, 0], + ] + ) + + # Add layers to viewer + viewer.add_layer(labelled_pixels_layer) + viewer.add_layer(phasor_clusters_layer) + n_layers_in_viewer = 2 + # Set widget options + split_n_largest_widget = Split_N_Largest_Cluster_Labels(viewer) + split_n_largest_widget._labels_layer_combobox.value = labelled_pixels_layer + split_n_largest_widget._clusters_labels_layer_combobox.value = phasor_clusters_layer + split_n_largest_widget._n_spinbox.value = number_of_clusters + split_n_largest_widget._clustering_id_combobox.value = clustering_id + # Run widget + split_n_largest_widget._on_run_clicked() + # Check new layers are created + assert len(viewer.layers) == n_layers_in_viewer + number_of_clusters + # Check that first layer created contains only largest cluster label + assert np.array_equal(viewer.layers[-2].data, largest_cluster_labels) + # Check that second layer created contain only second largest cluster label + assert np.array_equal(viewer.layers[-1].data, second_largest_cluster_labels)