Skip to content

Commit

Permalink
Finish vesicle pool widget
Browse files Browse the repository at this point in the history
  • Loading branch information
constantinpape committed Dec 7, 2024
1 parent 14d4fe9 commit 9a69484
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 87 deletions.
6 changes: 3 additions & 3 deletions synapse_net/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ contributions:
# Commands for widgets.
- id: synapse_net.segment
python_name: synapse_net.tools.segmentation_widget:SegmentationWidget
title: Segment
title: Segmentation
- id: synapse_net.distance_measure
python_name: synapse_net.tools.distance_measure_widget:DistanceMeasureWidget
title: Distance Measurement
Expand All @@ -21,7 +21,7 @@ contributions:
title: Morphology Analysis
- id: synapse_net.vesicle_pooling
python_name: synapse_net.tools.vesicle_pool_widget:VesiclePoolWidget
title: Vesicle Pooling
title: Pool Assignment

# Commands for sample data.
- id: synapse_net.sample_data_tem_2d
Expand All @@ -47,7 +47,7 @@ contributions:
- command: synapse_net.morphology
display_name: Morphology Analysis
- command: synapse_net.vesicle_pooling
display_name: Vesicle Pooling
display_name: Pool Assignment

sample_data:
- command: synapse_net.sample_data_tem_2d
Expand Down
22 changes: 16 additions & 6 deletions synapse_net/tools/base_widget.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
from pathlib import Path

import napari
Expand All @@ -16,6 +17,16 @@
add_table, get_table = None, None


class _SilencePrint:
def __enter__(self):
self._original_stdout = sys.stdout
sys.stdout = open(os.devnull, "w")

def __exit__(self, exc_type, exc_val, exc_tb):
sys.stdout.close()
sys.stdout = self._original_stdout


class BaseWidget(QWidget):
def __init__(self):
super().__init__()
Expand Down Expand Up @@ -316,18 +327,17 @@ def _save_table(self, save_path, data):
return file_path

def _add_properties_and_table(self, layer, table_data, save_path=""):
if layer.properties:
layer.properties.update(table_data)
else:
layer.properties = table_data
layer.properties = table_data

if add_table is not None:
table = get_table(layer, self.viewer)
if table is None:
add_table(layer, self.viewer)
with _SilencePrint():
add_table(layer, self.viewer)
else:
# FIXME updating the table does not yet work
table.update_content()
with _SilencePrint():
table.update_content()
# table_dict = table_data.to_dict()
# table_dict["index"] = table_dict["label"]
# table.set_content(table_dict)
Expand Down
187 changes: 109 additions & 78 deletions synapse_net/tools/vesicle_pool_widget.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Dict

import napari
import napari.layers
import napari.viewer
Expand All @@ -13,59 +15,59 @@
COLORMAP = ["red", "blue", "yellow", "cyan", "purple", "magenta", "orange", "green"]


# TODO Make selection of the distance layers optional and add a second distance layer.
class VesiclePoolWidget(BaseWidget):
def __init__(self):
super().__init__()

self.viewer = napari.current_viewer()
layout = QVBoxLayout()

self.image_selector_name = "Distances to Structure"
self.image_selector_name1 = "Vesicles Segmentation"
# Create the image selection dropdown.
self.image_selector_widget = self._create_layer_selector(self.image_selector_name, layer_type="Shapes")
self.segmentation1_selector_widget = self._create_layer_selector(self.image_selector_name1, layer_type="Labels")

# Create new layer name.
self.pool_layer_name_param, pool_layer_name_layout = self._add_string_param(
name="Output Layer Name", value="",
)

# Create pool name.
self.pool_name_param, pool_name_layout = self._add_string_param(
name="Vesicle Pool", value="",
)

# Create query string
# Create the selectors for the layers:
# 1. Selector for the labels layer with vesicles.
self.vesicle_selector_name = "Vesicle Segmentation"
self.vesicle_selector_widget = self._create_layer_selector(self.vesicle_selector_name, layer_type="Labels")
# 2. Selector for a distance layer.
self.dist_selector_name1 = "Distances to Structure"
self.dist_selector_widget1 = self._create_layer_selector(self.dist_selector_name1, layer_type="Shapes")

# Add the selector widgets to the layout.
layout.addWidget(self.vesicle_selector_widget)
layout.addWidget(self.dist_selector_widget1)

# Create the UI elements for defining the vesicle pools:
# The name of the output name, the name of the vesicle pool, and the criterion for the pool.
self.pool_layer_name_param, pool_layer_name_layout = self._add_string_param(name="Layer Name", value="")
self.pool_name_param, pool_name_layout = self._add_string_param(name="Vesicle Pool", value="")
self.query_param, query_layout = self._add_string_param(
name="Criterion", value="",
tooltip="Enter a comma separated query string (e.g., 'radius > 15, distance > 250') "
tooltip="Enter a comma separated criterion (e.g., 'radius > 15, distance > 250') "
"Possible filters: radius, distance, area, intensity_max, intensity_mean, intensity_min, intensity_std"
)

# Create advanced settings.
self.settings = self._create_settings_widget()

# Create and connect buttons.
self.measure_button1 = QPushButton("Create Vesicle Pool")
self.measure_button1.clicked.connect(self.on_pool_vesicles)

# Add the widgets to the layout.
layout.addWidget(self.image_selector_widget)
layout.addWidget(self.segmentation1_selector_widget)
layout.addLayout(query_layout)
layout.addLayout(pool_layer_name_layout)
layout.addLayout(pool_name_layout)
layout.addWidget(self.measure_button1)
layout.addLayout(query_layout)

# Create the UI elements for advanced settings and the run button.
self.settings = self._create_settings_widget()
self.measure_button = QPushButton("Create Vesicle Pool")
self.measure_button.clicked.connect(self.on_pool_vesicles)
layout.addWidget(self.settings)
layout.addWidget(self.measure_button)

self.setLayout(layout)

# The colormap for displaying the vesicle pools.
self.pool_colors = {}

def on_pool_vesicles(self):
distances_layer = self._get_layer_selector_layer(self.image_selector_name)
distances = distances_layer.properties
segmentation = self._get_layer_selector_data(self.image_selector_name1)
morphology_layer = self._get_layer_selector_layer(self.image_selector_name1)
morphology = morphology_layer.properties
segmentation = self._get_layer_selector_data(self.vesicle_selector_name)
morphology = self._get_layer_selector_layer(self.vesicle_selector_name).properties
if not morphology:
morphology = None

distance_layer = self._get_layer_selector_layer(self.dist_selector_name1)
distances = None if distance_layer is None else distance_layer.properties

if segmentation is None:
show_info("INFO: Please choose a segmentation.")
Expand All @@ -76,61 +78,91 @@ def on_pool_vesicles(self):
query = self.query_param.text()

if self.pool_layer_name_param.text() == "":
show_info("INFO: Please enter a new layer name.")
show_info("INFO: Please enter a name for the pool layer.")
return
pool_layer_name = self.pool_layer_name_param.text()
if self.pool_name_param.text() == "":
show_info("INFO: Please enter a pooled group name.")
show_info("INFO: Please enter a name for the vesicle pool.")
return
pool_name = self.pool_name_param.text()

if distances is None:
show_info("INFO: Distances layer could not be found or has no values.")
return
pool_color = self.pool_color_param.text()
self._compute_vesicle_pool(segmentation, distances, morphology, pool_layer_name, pool_name, query, pool_color)

self._compute_vesicle_pool(segmentation, distances, morphology, pool_layer_name, pool_name, query)

def _compute_vesicle_pool(self, segmentation, distances, morphology, pool_layer_name, pool_name, query):
"""
Compute a vesicle pool based on the provided query parameters.
def _update_pool_colors(self, pool_name, pool_color):
if pool_color == "":
next_color_id = len(self.pool_colors)
next_color = COLORMAP[next_color_id]
else:
# We could check here that this is a valid color.
next_color = pool_color
self.pool_colors[pool_name] = next_color

def _compute_vesicle_pool(
self,
segmentation: np.ndarray,
distances: Dict,
morphology: Dict,
pool_layer_name: str,
pool_name: str,
query: str,
pool_color: str,
):
"""Compute a vesicle pool based on the provided query parameters.
Args:
segmentation (array): Segmentation data (e.g., labeled regions).
distances (dict): Properties from the distances layer.
morphology (dict): Properties from the morphology layer.
pool_layer_name (str): Name for the new layer to be created.
pool_name (str): Name for the pooled group to be assigned.
query (dict): Query parameters.
segmentation: Segmentation data (e.g., labeled regions).
distances: Properties from the distances layer.
morphology: Properties from the morphology layer.
pool_layer_name: Name for the new layer to be created.
pool_name: Name for the pooled group to be assigned.
query: Query parameters.
pool_color: Optional color for the vesicle pool.
"""
distance_ids = distances.get("label", [])
morphology_ids = morphology.get("label", [])

# Ensure that IDs are identical.
if set(distance_ids) != set(morphology_ids):
show_info("ERROR: The IDs in distances and morphology are not identical.")
# Check which of the properties are present and construct the combined properties based on this.
if distances is None and morphology is None: # No properties were given -> we can't do anything.
show_info("ERROR: Neither distances nor vesicle morphology were found.")
return

# Create a merged dataframe from the dataframes which are relevant for the criterion.
# TODO: select the dataframes more dynamically depending on the criterion defined by the user.
distances = pd.DataFrame(distances)
morphology = pd.DataFrame(morphology)
merged_df = morphology.merge(distances, left_on="label", right_on="label", suffixes=("_morph", "_dist"))
elif distances is None and morphology is not None: # Only morphology props were found.
merged_df = pd.DataFrame(morphology).drop(columns=["index"])
elif distances is not None and morphology is None: # Only distances were found.
merged_df = pd.DataFrame(distances).drop(columns=["index"])
else: # Both were found.
distance_ids = distances.get("label", [])
morphology_ids = morphology.get("label", [])

# Ensure that IDs are identical.
if set(distance_ids) != set(morphology_ids):
show_info("ERROR: The IDs in distances and morphology are not identical.")
return

# Create a merged dataframe from the dataframes which are relevant for the criterion.
distances = pd.DataFrame(distances).drop(columns=["index"])
morphology = pd.DataFrame(morphology).drop(columns=["index"])
merged_df = morphology.merge(distances, left_on="label", right_on="label", suffixes=("_morph", "_dist"))

# Assign the vesicles to the current pool by filtering the mergeddataframe based on the query.
filtered_df = self._parse_query(query, merged_df)
if len(filtered_df) == 0:
show_info("No vesicles were found matching the condition.")
return
pool_vesicle_ids = filtered_df.label.values.tolist()
vesicles_in_pool = len(pool_vesicle_ids)

# Check if this layer was already created in a previous pool assignment.
if pool_layer_name in self.viewer.layers:
# If yes then load the previous pool assignments and merge them with the new pool assignments
pool_layer = self.viewer.layers[pool_layer_name]
pool_properties = pd.DataFrame.from_dict(pool_layer.properties)

pool_names = pd.unique(pool_properties.pool).tolist()
pool_names = pd.unique(pool_properties.pool)
if pool_name in pool_names:
show_info(f"Updating pool '{pool_name}' with {vesicles_in_pool} vesicles.")
# This pool has already been assigned and we changed the criterion.
# Its old assignment has to be over-written, remove the rows for this pool.
pool_properties = pool_properties[pool_properties.pool != pool_name]
else:
show_info(f"Creating pool '{pool_name}' with {vesicles_in_pool} vesicles.")

# Combine the vesicle ids corresponding to the previous assignment with the
# assignment for the new / current pool.
Expand All @@ -146,6 +178,7 @@ def _compute_vesicle_pool(self, segmentation, distances, morphology, pool_layer_
pool_values = [id_to_pool_name[ves_id] for ves_id in pool_assignments]

else:
show_info(f"Creating pool '{pool_name}' with {vesicles_in_pool} vesicles.")
# Otherwise, this is the first pool assignment.
pool_assignments = pool_vesicle_ids
pool_values = [pool_name] * len(pool_assignments)
Expand All @@ -161,32 +194,27 @@ def _compute_vesicle_pool(self, segmentation, distances, morphology, pool_layer_
col for col in pool_properties.columns
if col not in ("x", "y", "z", "begin-x", "begin-y", "begin-z", "end-x", "end-y", "end-z")
]
pool_properties = pool_properties[keep_columns].reset_index()
pool_properties = pool_properties[keep_columns]
# Add a colun for the pool.
pool_properties.insert(1, "pool", pool_values)

# Create the colormap to group the pools in the layer rendering.
# This can lead to color switches: if a new pool gets added which starts with
# a letter that's earlier in the alphabet the color will switch.
# To avoid this the user has to specify the pool color (not yet implemented, see next todo).
pool_names = np.unique(pool_values).tolist()
# TODO: add setting so that users can over-ride the color for a pool.
pool_colors = {pname: COLORMAP[pool_names.index(pname)] for pname in pool_names}
# Update the colormap to display the pools.
self._update_pool_colors(pool_name, pool_color)

# Assign the vesicle ids to their pool color.
vesicle_colors = {
label_id: pool_colors[pname] for label_id, pname
in zip(pool_properties.label.values, pool_properties.pool.values)
label_id: self.pool_colors[pname] for label_id, pname in zip(
pool_properties.label.values, pool_properties.pool.values
)
}
vesicle_colors[None] = "gray"

# TODO print some messages
# Add or replace the pool layer and properties.
if pool_layer_name in self.viewer.layers:
# message about added or over-ridden pool, including number of vesicles in pool
pool_layer = self.viewer.layers[pool_layer_name]
pool_layer.data = vesicle_pools
pool_layer.colormap = vesicle_colors
else:
# message about new pool, including number of vesicles in pool
pool_layer = self.viewer.add_labels(vesicle_pools, name=pool_layer_name, colormap=vesicle_colors)

self._add_properties_and_table(pool_layer, pool_properties, save_path=self.save_path.text())
Expand Down Expand Up @@ -220,5 +248,8 @@ def _create_settings_widget(self):
self.save_path, layout = self._add_path_param(name="Save Table", select_type="file", value="")
setting_values.layout().addLayout(layout)

self.pool_color_param, layout = self._add_string_param(name="Pool Color", value="")
setting_values.layout().addLayout(layout)

settings = self._make_collapsible(widget=setting_values, title="Advanced Settings")
return settings

0 comments on commit 9a69484

Please sign in to comment.