Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions synaptic_reconstruction/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ contributions:
- id: synaptic_reconstruction.morphology
python_name: synaptic_reconstruction.tools.morphology_widget:MorphologyWidget
title: Morphology Analysis
- id: synaptic_reconstruction.vesicle_pooling
python_name: synaptic_reconstruction.tools.vesicle_pool_widget:VesiclePoolWidget
title: Vesicle Pooling

readers:
- command: synaptic_reconstruction.file_reader
Expand All @@ -32,3 +35,5 @@ contributions:
display_name: Distance Measurement
- command: synaptic_reconstruction.morphology
display_name: Morphology Analysis
- command: synaptic_reconstruction.vesicle_pooling
display_name: Vesicle Pooling
18 changes: 17 additions & 1 deletion synaptic_reconstruction/tools/base_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ def _create_layer_selector(self, selector_name, layer_type="Image"):
layer_filter = napari.layers.Image
elif layer_type == "Labels":
layer_filter = napari.layers.Labels
elif layer_type == "Shapes":
layer_filter = napari.layers.Shapes
else:
raise ValueError("layer_type must be either 'Image' or 'Labels'.")

selector_widget = QtWidgets.QWidget()
image_selector = QtWidgets.QComboBox()
layer_label = QtWidgets.QLabel(f"{selector_name} Layer:")
layer_label = QtWidgets.QLabel(f"{selector_name}:")

# Populate initial options
self._update_selector(selector=image_selector, layer_filter=layer_filter)
Expand All @@ -61,6 +63,20 @@ def _update_selector(self, selector, layer_filter):
image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)] # if isinstance(layer, napari.layers.Image)
selector.addItems(image_layers)

def _get_layer_selector_layer(self, selector_name):
"""Return the layer currently selected in a given selector."""
if selector_name in self.layer_selectors:
selector_widget = self.layer_selectors[selector_name]

# Retrieve the QComboBox from the QWidget's layout
image_selector = selector_widget.layout().itemAt(1).widget()

if isinstance(image_selector, QComboBox):
selected_layer_name = image_selector.currentText()
if selected_layer_name in self.viewer.layers:
return self.viewer.layers[selected_layer_name]
return None # Return None if layer not found

def _get_layer_selector_data(self, selector_name, return_metadata=False):
"""Return the data for the layer currently selected in a given selector."""
if selector_name in self.layer_selectors:
Expand Down
5 changes: 4 additions & 1 deletion synaptic_reconstruction/tools/distance_measure_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ def _add_lines_and_table(self, lines, properties, table_data, name):
blending="additive",
properties=properties,
)

line_layer.properties["distances"] = table_data

if add_table is not None:
add_table(line_layer, self.viewer)

Expand Down Expand Up @@ -136,7 +139,7 @@ def on_measure_pairwise(self):
resolution = metadata.get("voxel_size", None)
if resolution is not None:
resolution = [v for v in resolution.values()]
# if user input is present override metadata
# if user input is present, override metadata
if self.voxel_size_param.value() != 0.0: # changed from default
resolution = segmentation.ndim * [self.voxel_size_param.value()]

Expand Down
36 changes: 20 additions & 16 deletions synaptic_reconstruction/tools/morphology_widget.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import os

import napari
import napari.layers
import napari.viewer
Expand Down Expand Up @@ -69,7 +67,7 @@ def _create_shapes_layer(self, table_data, name="Shapes Layer"):
if 'z' not in table_data.columns
else table_data[['x', 'y', 'z']].to_numpy()
)
radii = table_data['radii'].to_numpy()
radii = table_data['radius'].to_numpy()

if coords.shape[1] == 2:
# For 2D data, create circular outlines using trigonometric functions
Expand Down Expand Up @@ -131,7 +129,7 @@ def _to_table_data(self, coords, radii, props):
table_data = {
'label_id': [prop.label for prop in props],
**{col: coords[:, i] for i, col in enumerate(col_names)},
'radii': radii,
'radius': radii,
'intensity_max': [prop.intensity_max for prop in props],
'intensity_mean': [prop.intensity_mean for prop in props],
'intensity_min': [prop.intensity_min for prop in props],
Expand All @@ -156,7 +154,11 @@ def _add_table(self, coords, radii, props, name="Shapes Layer"):
table_data = self._to_table_data(coords, radii, props)

# Add the shapes layer
layer = self._create_shapes_layer(table_data, name)
layer = self._get_layer_selector_layer(self.image_selector_name1)
if layer.properties:
layer.properties = layer.properties.update(table_data)
else:
layer.properties = table_data

if add_table is not None:
add_table(layer, self.viewer)
Expand All @@ -179,7 +181,7 @@ def on_measure_vesicle_morphology(self):
return

# get metadata from layer if available
metadata = self._get_layer_selector_data(self.image_selector_name1, return_metadata=True)
metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
resolution = metadata.get("voxel_size", None)
if resolution is not None:
resolution = [v for v in resolution.values()]
Expand All @@ -194,18 +196,19 @@ def on_measure_vesicle_morphology(self):
resolution=resolution,
props=props,
)
self._add_table(coords, radii, props, name="Vesicles")
self._add_table(coords, radii, props, name="Vesicles Morphology")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proper spelling is "Vesicle Morphology".


def on_measure_structure_morphology(self):
"""add the structure measurements to the segmentation layer (via properties)
"""
Add the structure measurements to the segmentation layer (via properties)
and visualize the properties table
"""
segmentation = self._get_layer_selector_data(self.image_selector_name1)
if segmentation is None:
show_info("INFO: Please choose a segmentation.")
return
# get metadata from layer if available
metadata = self._get_layer_selector_data(self.image_selector_name1, return_metadata=True)
metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
resolution = metadata.get("voxel_size", None)
if resolution is not None:
resolution = [v for v in resolution.values()]
Expand All @@ -217,19 +220,20 @@ def on_measure_structure_morphology(self):
self._add_table_structure(morphology)

def _add_table_structure(self, morphology):
# add properties to segmentation layer
segmentation_selector_widget = self.layer_selectors[self.image_selector_name1]
image_selector = segmentation_selector_widget.layout().itemAt(1).widget()
selected_layer_name = image_selector.currentText()
self.viewer.layers[selected_layer_name].properties = morphology
layer = self._get_layer_selector_layer(self.image_selector_name1)
# Add properties to layer for add_table function
if layer.properties:
layer.properties = layer.properties.update(morphology)
else:
layer.properties = morphology

# Add a table layer to the Napari viewer
if add_table is not None:
add_table(self.viewer.layers[selected_layer_name], self.viewer)
add_table(layer, self.viewer)

# Save table to file if save path is provided
if self.save_path.text() != "":
file_path = _save_table(self.save_path.text(), self._to_table_data_structure(morphology))
file_path = _save_table(self.save_path.text(), morphology)
show_info(f"INFO: Added table and saved file to {file_path}.")
else:
print("INFO: Table added to viewer.")
Expand Down
219 changes: 219 additions & 0 deletions synaptic_reconstruction/tools/vesicle_pool_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import napari
import napari.layers
import napari.viewer
import numpy as np
import pandas as pd

from napari.utils.notifications import show_info
from qtpy.QtWidgets import QWidget, QVBoxLayout, QPushButton

from .base_widget import BaseWidget

try:
from napari_skimage_regionprops import add_table
except ImportError:
add_table = None


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.new_layer_name_param, new_layer_name_layout = self._add_string_param(
name="New Layer Name",
value="",
)

# pooled group name
self.pooled_group_name_param, pooled_group_name_layout = self._add_string_param(
name="Pooled Group Name",
value="",
)

# Create query string
self.query_param, query_layout = self._add_string_param(
name="Query String",
value="",
tooltip="Enter a comma separated query string (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(new_layer_name_layout)
layout.addLayout(pooled_group_name_layout)
layout.addWidget(self.measure_button1)

self.setLayout(layout)

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

if segmentation is None:
show_info("INFO: Please choose a segmentation.")
return
if self.query_param.text() == "":
show_info("INFO: Please enter a query string.")
return
# resolve string
query = self.query_param.text()

if self.new_layer_name_param.text() == "":
show_info("INFO: Please enter a new layer name.")
return
new_layer_name = self.new_layer_name_param.text()
if self.pooled_group_name_param.text() == "":
show_info("INFO: Please enter a pooled group name.")
return
pooled_group_name = self.pooled_group_name_param.text()

if distances is None:
show_info("INFO: Distances layer could not be found or has no values.")
return
self._compute_vesicle_pool(segmentation, distances, morphology, new_layer_name, pooled_group_name, query)

def _compute_vesicle_pool(self, segmentation, distances, morphology, new_layer_name, pooled_group_name, query):
"""
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.
new_layer_name (str): Name for the new layer to be created.
pooled_group_name (str): Name for the pooled group to be assigned.
query (dict): Query parameters.
"""

distances_ids = distances.get("id", [])
morphology_ids = morphology.get("label_id", [])

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

distances = pd.DataFrame(distances)
morphology = pd.DataFrame(morphology)

# Merge dataframes on the 'id' column
merged_df = morphology.merge(distances, left_on="label_id", right_on="id", suffixes=("_morph", "_dist"))

# Apply the query string to filter the data
filtered_df = self._parse_query(query, merged_df)

# Extract valid vesicle IDs
valid_vesicle_ids = filtered_df["label_id"].tolist()

new_layer_data = np.zeros(segmentation.shape, dtype=np.uint8)
pool_id = 1
layer = None

# check if group already exists
if new_layer_name in self.viewer.layers:
layer = self.viewer.layers[new_layer_name]
if pooled_group_name not in layer.properties["pool"]:
new_layer_data = layer.data
pool_id = len(np.unique(layer.properties["pool"])) + 1
# compute vesicles with new pool_id and properties
for vesicle_id in valid_vesicle_ids:
new_layer_data[segmentation == vesicle_id] = pool_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few problems here:

  • You set the value in the layer to the pool_id. Instead we should keep the vesicle ids intact, but just filter out the vesicles that are not part of any pool.
  • The whole pool assignment and property logic could be simplified a lot, see the general comment for how.
  • The way you do this is in general inefficient. Instead of solving this with a loop you can for example do
filtered_vesicle_mask = ~np.isin(segmentation, valid_vesicle_ids)
new_layer_data[filtered_vesicle_mask] = 0

new_properties = {
"id": valid_vesicle_ids,
"radius": filtered_df["radius"].tolist(),
"distance": filtered_df["distance"].tolist(),
"pool": [pooled_group_name] * len(valid_vesicle_ids)
}
if new_layer_name in self.viewer.layers:
layer = self.viewer.layers[new_layer_name]
# override current vesicles with new pooled vesicles
if pooled_group_name in layer.properties["pool"]:
layer.data = new_layer_data
layer.properties = new_properties
show_info(f"Vesicle pool '{pooled_group_name}' overriden with {len(valid_vesicle_ids)} vesicles.")
else:
# add new vesicles and pool to existing layer
current_properties = pd.DataFrame(layer.properties)
new_properties = pd.DataFrame(new_properties)
merged = pd.concat([current_properties, new_properties], ignore_index=True)
layer.data = new_layer_data
layer.properties = merged
show_info(f"Vesicle pool '{pooled_group_name}' updated with {len(valid_vesicle_ids)} vesicles.")
else:
# Create a new layer in the viewer
self.viewer.add_labels(
new_layer_data,
name=new_layer_name,
properties=new_properties
)
show_info(
f"Added new layer '{new_layer_name}' with {len(valid_vesicle_ids)} "
f"vesicles in group '{pooled_group_name}'."
)
if add_table is not None:
add_table(self.viewer.layers[new_layer_name], self.viewer)
return {
"id": valid_vesicle_ids,
"radius": filtered_df["radius"].tolist(),
"distance": filtered_df["distance"].tolist(),
}

def _parse_query(self, query, data):
"""
Parse and apply a query string to filter data.
Args:
query (str): Comma-separated query string (e.g., "radius > 15, distance > 250").
data (pd.DataFrame): DataFrame containing the data to filter.
Returns:
pd.DataFrame: Filtered DataFrame.
"""
filters = query.split(",") # Split the query into individual conditions
filters = [f.strip() for f in filters] # Remove extra spaces
for condition in filters:
try:
# Apply each condition to filter the DataFrame
data = data.query(condition)
except Exception as e:
print(f"Failed to apply condition '{condition}': {e}")
continue
return data

def _create_settings_widget(self):
setting_values = QWidget()
setting_values.setLayout(QVBoxLayout())

self.save_path, layout = self._add_path_param(name="Save Table", select_type="file", value="")
setting_values.layout().addLayout(layout)

self.voxel_size_param, layout = self._add_float_param(
"voxel_size", 0.0, min_val=0.0, max_val=100.0,
)
setting_values.layout().addLayout(layout)

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