synapse_net.tools.morphology_widget

  1import napari
  2import napari.layers
  3import napari.viewer
  4import numpy as np
  5import pandas as pd
  6from skimage.measure import regionprops
  7
  8from napari.utils.notifications import show_info
  9from qtpy.QtWidgets import QWidget, QVBoxLayout, QPushButton
 10
 11from .base_widget import BaseWidget
 12from synapse_net.imod.to_imod import convert_segmentation_to_spheres
 13from synapse_net.morphology import compute_object_morphology
 14
 15
 16class MorphologyWidget(BaseWidget):
 17    def __init__(self):
 18        super().__init__()
 19
 20        self.viewer = napari.current_viewer()
 21        layout = QVBoxLayout()
 22
 23        self.image_selector_name = "Raw Image"
 24        self.image_selector_name1 = "Segmentation"
 25        # Create the image selection dropdown.
 26        self.image_selector_widget = self._create_layer_selector(self.image_selector_name, layer_type="Image")
 27        self.segmentation1_selector_widget = self._create_layer_selector(self.image_selector_name1, layer_type="Labels")
 28
 29        # Create advanced settings.
 30        self.settings = self._create_settings_widget()
 31
 32        # Create and connect buttons.
 33        self.measure_button1 = QPushButton("Measure Vesicle Morphology")
 34        self.measure_button1.clicked.connect(self.on_measure_vesicle_morphology)
 35
 36        self.measure_button2 = QPushButton("Measure Structure Morphology")
 37        self.measure_button2.clicked.connect(self.on_measure_structure_morphology)
 38
 39        # Add the widgets to the layout.
 40        layout.addWidget(self.image_selector_widget)
 41        layout.addWidget(self.segmentation1_selector_widget)
 42        layout.addWidget(self.settings)
 43        layout.addWidget(self.measure_button1)
 44        layout.addWidget(self.measure_button2)
 45
 46        self.setLayout(layout)
 47
 48    def _create_shapes_layer(self, table_data, name="Shapes Layer"):
 49        """
 50        Create and add a Shapes layer to the Napari viewer using table data.
 51
 52        Args:
 53            table_data (pd.DataFrame): The table data containing coordinates, radii, and properties.
 54            name (str): Name of the layer.
 55
 56        Returns:
 57            Shapes layer: The created Napari Shapes layer.
 58        """
 59        coords = (
 60            table_data[['x', 'y']].to_numpy()
 61            if 'z' not in table_data.columns
 62            else table_data[['x', 'y', 'z']].to_numpy()
 63        )
 64        radii = table_data['radius'].to_numpy()
 65
 66        if coords.shape[1] == 2:
 67            # For 2D data, create circular outlines using trigonometric functions
 68            outlines = [
 69                np.column_stack((
 70                    coords[i, 0] + radii[i] * np.cos(np.linspace(0, 2 * np.pi, 100)),
 71                    coords[i, 1] + radii[i] * np.sin(np.linspace(0, 2 * np.pi, 100))
 72                )) for i in range(len(coords))
 73            ]
 74        elif coords.shape[1] == 3:
 75            # For 3D data, create spherical outlines using latitude and longitude
 76            theta = np.linspace(0, 2 * np.pi, 50)  # Longitude
 77            phi = np.linspace(0, np.pi, 25)       # Latitude
 78            sphere_points = np.array([
 79                [
 80                    coords[i, 0] + radii[i] * np.sin(p) * np.cos(t),
 81                    coords[i, 1] + radii[i] * np.sin(p) * np.sin(t),
 82                    coords[i, 2] + radii[i] * np.cos(p)
 83                ]
 84                for i in range(len(coords))
 85                for t in theta for p in phi
 86            ])
 87            outlines = [
 88                sphere_points[i * len(theta) * len(phi):(i + 1) * len(theta) * len(phi)]
 89                for i in range(len(coords))
 90            ]
 91        else:
 92            raise ValueError("Coordinate dimensionality must be 2 or 3.")
 93
 94        # Add the shapes layer with properties
 95        layer = self.viewer.add_shapes(
 96            outlines,
 97            name=name,
 98            shape_type="polygon",  # Use "polygon" for closed shapes like circles
 99            edge_width=2,
100            edge_color="red",
101            face_color="transparent",
102            blending="additive",
103            properties=table_data.to_dict(orient='list'),  # Attach table data as properties
104        )
105        return layer
106
107    def _to_table_data(self, coords, radii, props):
108        """
109        Create a table of data including coordinates, radii, and intensity statistics.
110
111        Args:
112            coords (np.ndarray): Array of 2D or 3D coordinates.
113            radii (np.ndarray): Array of radii corresponding to the coordinates.
114            props (list): List of properties containing intensity statistics.
115
116        Returns:
117            pd.DataFrame: The formatted table data.
118        """
119        assert len(coords) == len(radii), f"Shape mismatch: {coords.shape}, {radii.shape}"
120
121        # Define columns based on dimension (2D or 3D)
122        col_names = ["x", "y"] if coords.shape[1] == 2 else ["x", "y", "z"]
123        table_data = {
124            "label": [prop.label for prop in props],
125            **{col: coords[:, i] for i, col in enumerate(col_names)},
126            "radius": radii,
127            "intensity_max": [prop.intensity_max for prop in props],
128            "intensity_mean": [prop.intensity_mean for prop in props],
129            "intensity_min": [prop.intensity_min for prop in props],
130            "intensity_std": [prop.intensity_std for prop in props],
131        }
132
133        return pd.DataFrame(table_data)
134
135    def on_measure_vesicle_morphology(self):
136        segmentation = self._get_layer_selector_data(self.image_selector_name1)
137        image = self._get_layer_selector_data(self.image_selector_name)
138        if segmentation is None:
139            show_info("INFO: Please choose a segmentation.")
140            return
141        if image is None:
142            show_info("INFO: Please choose an image.")
143            return
144
145        # Get the resolution / voxel size.
146        metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
147        resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
148
149        # Compute the mophology parameter.
150        props = regionprops(label_image=segmentation, intensity_image=image)
151        coords, radii = convert_segmentation_to_spheres(
152            segmentation=segmentation,
153            resolution=resolution,
154            props=props,
155        )
156
157        # Create table data and add the properties and table to the layer.
158        table_data = self._to_table_data(coords, radii, props)
159        layer = self._get_layer_selector_layer(self.image_selector_name1)
160        self._add_properties_and_table(layer, table_data, self.save_path.text())
161
162    def on_measure_structure_morphology(self):
163        """
164        Add the structure measurements to the segmentation layer (via properties)
165        and visualize the properties table
166        """
167        segmentation = self._get_layer_selector_data(self.image_selector_name1)
168        if segmentation is None:
169            show_info("INFO: Please choose a segmentation.")
170            return
171        # get metadata from layer if available
172        metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
173        resolution = metadata.get("voxel_size", None)
174        if resolution is not None:
175            resolution = [v for v in resolution.values()]
176        morphology = compute_object_morphology(
177            object_=segmentation, structure_name=self.image_selector_name1, resolution=resolution
178        )
179
180        # Add the properties to the layer and add/save the table.
181        layer = self._get_layer_selector_layer(self.image_selector_name1)
182        self._add_properties_and_table(layer, morphology, self.save_path.text())
183
184    def _create_settings_widget(self):
185        setting_values = QWidget()
186        setting_values.setLayout(QVBoxLayout())
187
188        self.save_path, layout = self._add_path_param(name="Save Table", select_type="file", value="")
189        setting_values.layout().addLayout(layout)
190
191        self.voxel_size_param, layout = self._add_float_param(
192            "voxel_size", 0.0, min_val=0.0, max_val=100.0,
193        )
194        setting_values.layout().addLayout(layout)
195
196        settings = self._make_collapsible(widget=setting_values, title="Advanced Settings")
197        return settings
class MorphologyWidget(synapse_net.tools.base_widget.BaseWidget):
 17class MorphologyWidget(BaseWidget):
 18    def __init__(self):
 19        super().__init__()
 20
 21        self.viewer = napari.current_viewer()
 22        layout = QVBoxLayout()
 23
 24        self.image_selector_name = "Raw Image"
 25        self.image_selector_name1 = "Segmentation"
 26        # Create the image selection dropdown.
 27        self.image_selector_widget = self._create_layer_selector(self.image_selector_name, layer_type="Image")
 28        self.segmentation1_selector_widget = self._create_layer_selector(self.image_selector_name1, layer_type="Labels")
 29
 30        # Create advanced settings.
 31        self.settings = self._create_settings_widget()
 32
 33        # Create and connect buttons.
 34        self.measure_button1 = QPushButton("Measure Vesicle Morphology")
 35        self.measure_button1.clicked.connect(self.on_measure_vesicle_morphology)
 36
 37        self.measure_button2 = QPushButton("Measure Structure Morphology")
 38        self.measure_button2.clicked.connect(self.on_measure_structure_morphology)
 39
 40        # Add the widgets to the layout.
 41        layout.addWidget(self.image_selector_widget)
 42        layout.addWidget(self.segmentation1_selector_widget)
 43        layout.addWidget(self.settings)
 44        layout.addWidget(self.measure_button1)
 45        layout.addWidget(self.measure_button2)
 46
 47        self.setLayout(layout)
 48
 49    def _create_shapes_layer(self, table_data, name="Shapes Layer"):
 50        """
 51        Create and add a Shapes layer to the Napari viewer using table data.
 52
 53        Args:
 54            table_data (pd.DataFrame): The table data containing coordinates, radii, and properties.
 55            name (str): Name of the layer.
 56
 57        Returns:
 58            Shapes layer: The created Napari Shapes layer.
 59        """
 60        coords = (
 61            table_data[['x', 'y']].to_numpy()
 62            if 'z' not in table_data.columns
 63            else table_data[['x', 'y', 'z']].to_numpy()
 64        )
 65        radii = table_data['radius'].to_numpy()
 66
 67        if coords.shape[1] == 2:
 68            # For 2D data, create circular outlines using trigonometric functions
 69            outlines = [
 70                np.column_stack((
 71                    coords[i, 0] + radii[i] * np.cos(np.linspace(0, 2 * np.pi, 100)),
 72                    coords[i, 1] + radii[i] * np.sin(np.linspace(0, 2 * np.pi, 100))
 73                )) for i in range(len(coords))
 74            ]
 75        elif coords.shape[1] == 3:
 76            # For 3D data, create spherical outlines using latitude and longitude
 77            theta = np.linspace(0, 2 * np.pi, 50)  # Longitude
 78            phi = np.linspace(0, np.pi, 25)       # Latitude
 79            sphere_points = np.array([
 80                [
 81                    coords[i, 0] + radii[i] * np.sin(p) * np.cos(t),
 82                    coords[i, 1] + radii[i] * np.sin(p) * np.sin(t),
 83                    coords[i, 2] + radii[i] * np.cos(p)
 84                ]
 85                for i in range(len(coords))
 86                for t in theta for p in phi
 87            ])
 88            outlines = [
 89                sphere_points[i * len(theta) * len(phi):(i + 1) * len(theta) * len(phi)]
 90                for i in range(len(coords))
 91            ]
 92        else:
 93            raise ValueError("Coordinate dimensionality must be 2 or 3.")
 94
 95        # Add the shapes layer with properties
 96        layer = self.viewer.add_shapes(
 97            outlines,
 98            name=name,
 99            shape_type="polygon",  # Use "polygon" for closed shapes like circles
100            edge_width=2,
101            edge_color="red",
102            face_color="transparent",
103            blending="additive",
104            properties=table_data.to_dict(orient='list'),  # Attach table data as properties
105        )
106        return layer
107
108    def _to_table_data(self, coords, radii, props):
109        """
110        Create a table of data including coordinates, radii, and intensity statistics.
111
112        Args:
113            coords (np.ndarray): Array of 2D or 3D coordinates.
114            radii (np.ndarray): Array of radii corresponding to the coordinates.
115            props (list): List of properties containing intensity statistics.
116
117        Returns:
118            pd.DataFrame: The formatted table data.
119        """
120        assert len(coords) == len(radii), f"Shape mismatch: {coords.shape}, {radii.shape}"
121
122        # Define columns based on dimension (2D or 3D)
123        col_names = ["x", "y"] if coords.shape[1] == 2 else ["x", "y", "z"]
124        table_data = {
125            "label": [prop.label for prop in props],
126            **{col: coords[:, i] for i, col in enumerate(col_names)},
127            "radius": radii,
128            "intensity_max": [prop.intensity_max for prop in props],
129            "intensity_mean": [prop.intensity_mean for prop in props],
130            "intensity_min": [prop.intensity_min for prop in props],
131            "intensity_std": [prop.intensity_std for prop in props],
132        }
133
134        return pd.DataFrame(table_data)
135
136    def on_measure_vesicle_morphology(self):
137        segmentation = self._get_layer_selector_data(self.image_selector_name1)
138        image = self._get_layer_selector_data(self.image_selector_name)
139        if segmentation is None:
140            show_info("INFO: Please choose a segmentation.")
141            return
142        if image is None:
143            show_info("INFO: Please choose an image.")
144            return
145
146        # Get the resolution / voxel size.
147        metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
148        resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
149
150        # Compute the mophology parameter.
151        props = regionprops(label_image=segmentation, intensity_image=image)
152        coords, radii = convert_segmentation_to_spheres(
153            segmentation=segmentation,
154            resolution=resolution,
155            props=props,
156        )
157
158        # Create table data and add the properties and table to the layer.
159        table_data = self._to_table_data(coords, radii, props)
160        layer = self._get_layer_selector_layer(self.image_selector_name1)
161        self._add_properties_and_table(layer, table_data, self.save_path.text())
162
163    def on_measure_structure_morphology(self):
164        """
165        Add the structure measurements to the segmentation layer (via properties)
166        and visualize the properties table
167        """
168        segmentation = self._get_layer_selector_data(self.image_selector_name1)
169        if segmentation is None:
170            show_info("INFO: Please choose a segmentation.")
171            return
172        # get metadata from layer if available
173        metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
174        resolution = metadata.get("voxel_size", None)
175        if resolution is not None:
176            resolution = [v for v in resolution.values()]
177        morphology = compute_object_morphology(
178            object_=segmentation, structure_name=self.image_selector_name1, resolution=resolution
179        )
180
181        # Add the properties to the layer and add/save the table.
182        layer = self._get_layer_selector_layer(self.image_selector_name1)
183        self._add_properties_and_table(layer, morphology, self.save_path.text())
184
185    def _create_settings_widget(self):
186        setting_values = QWidget()
187        setting_values.setLayout(QVBoxLayout())
188
189        self.save_path, layout = self._add_path_param(name="Save Table", select_type="file", value="")
190        setting_values.layout().addLayout(layout)
191
192        self.voxel_size_param, layout = self._add_float_param(
193            "voxel_size", 0.0, min_val=0.0, max_val=100.0,
194        )
195        setting_values.layout().addLayout(layout)
196
197        settings = self._make_collapsible(widget=setting_values, title="Advanced Settings")
198        return settings

QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

viewer
image_selector_name
image_selector_name1
image_selector_widget
segmentation1_selector_widget
settings
measure_button1
measure_button2
def on_measure_vesicle_morphology(self):
136    def on_measure_vesicle_morphology(self):
137        segmentation = self._get_layer_selector_data(self.image_selector_name1)
138        image = self._get_layer_selector_data(self.image_selector_name)
139        if segmentation is None:
140            show_info("INFO: Please choose a segmentation.")
141            return
142        if image is None:
143            show_info("INFO: Please choose an image.")
144            return
145
146        # Get the resolution / voxel size.
147        metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
148        resolution = self._handle_resolution(metadata, self.voxel_size_param, segmentation.ndim)
149
150        # Compute the mophology parameter.
151        props = regionprops(label_image=segmentation, intensity_image=image)
152        coords, radii = convert_segmentation_to_spheres(
153            segmentation=segmentation,
154            resolution=resolution,
155            props=props,
156        )
157
158        # Create table data and add the properties and table to the layer.
159        table_data = self._to_table_data(coords, radii, props)
160        layer = self._get_layer_selector_layer(self.image_selector_name1)
161        self._add_properties_and_table(layer, table_data, self.save_path.text())
def on_measure_structure_morphology(self):
163    def on_measure_structure_morphology(self):
164        """
165        Add the structure measurements to the segmentation layer (via properties)
166        and visualize the properties table
167        """
168        segmentation = self._get_layer_selector_data(self.image_selector_name1)
169        if segmentation is None:
170            show_info("INFO: Please choose a segmentation.")
171            return
172        # get metadata from layer if available
173        metadata = self._get_layer_selector_data(self.image_selector_name, return_metadata=True)
174        resolution = metadata.get("voxel_size", None)
175        if resolution is not None:
176            resolution = [v for v in resolution.values()]
177        morphology = compute_object_morphology(
178            object_=segmentation, structure_name=self.image_selector_name1, resolution=resolution
179        )
180
181        # Add the properties to the layer and add/save the table.
182        layer = self._get_layer_selector_layer(self.image_selector_name1)
183        self._add_properties_and_table(layer, morphology, self.save_path.text())

Add the structure measurements to the segmentation layer (via properties) and visualize the properties table