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
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())
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