synapse_net.tools.cristae_analysis_widget
1import napari 2import numpy as np 3 4from napari.utils import progress 5from napari.utils.notifications import show_info 6from qtpy.QtWidgets import QWidget, QVBoxLayout, QPushButton 7 8from .base_widget import BaseWidget 9from ..cristae_analysis import ( 10 approximate_membrane, compute_mito_crista_statistics, detect_contact_sites, 11 _open_trimmed_mesh, _gap_radius, 12) 13 14 15class CristaeAnalysisWidget(BaseWidget): 16 """Napari widget for the cristae analysis (preview + full per-mitochondrion run). 17 18 ``_ORIENTATION_TO_METHOD`` maps the orientation dropdown labels to the ``method`` argument of 19 :func:`~synapse_net.cristae_analysis.compute_mito_crista_statistics`, and ``_MEMBRANE_TO_MODE`` maps 20 the membrane-mode labels to the ``membrane_mode`` argument of 21 :func:`~synapse_net.cristae_analysis.approximate_membrane`. The ``_*_LAYER`` name constants are 22 shared by the preview and the full run so re-previewing / running updates the same layers instead 23 of duplicating them. 24 """ 25 26 _ORIENTATION_FAST = "Fast (downsampled, approximate)" 27 _ORIENTATION_SKIP = "Skip (no orientation)" 28 _ORIENTATION_TO_METHOD = { 29 _ORIENTATION_FAST: "fast", 30 "Exact (full resolution)": "exact", 31 _ORIENTATION_SKIP: "skip", 32 } 33 34 _MEMBRANE_SLICE_2D = "2D per-slice (z-parallel)" 35 _MEMBRANE_TO_MODE = { 36 _MEMBRANE_SLICE_2D: "slice_2d", 37 "3D connected shell": "shell_3d", 38 } 39 40 def __init__(self): 41 super().__init__() 42 43 self.viewer = napari.current_viewer() 44 layout = QVBoxLayout() 45 46 self.crista_selector_name = "Crista Mask" 47 self.mito_selector_name = "Mito Segmentation" 48 49 self.crista_selector_widget = self._create_layer_selector( 50 self.crista_selector_name, layer_type="Labels", prefer_substring="cristae") 51 self.mito_selector_widget = self._create_layer_selector( 52 self.mito_selector_name, layer_type="Labels", prefer_substring="mitochondria") 53 54 self.settings = self._create_settings_widget() 55 56 self.preview_button = QPushButton("Preview Membrane && Junctions") 57 self.preview_button.clicked.connect(self.on_preview) 58 59 self.run_button = QPushButton("Run Cristae Analysis") 60 self.run_button.clicked.connect(self.on_run) 61 62 layout.addWidget(self.crista_selector_widget) 63 layout.addWidget(self.mito_selector_widget) 64 layout.addWidget(self.settings) 65 layout.addWidget(self.preview_button) 66 layout.addWidget(self.run_button) 67 68 self.setLayout(layout) 69 70 _MEMBRANE_LAYER = "Membrane Mask" 71 _MEMBRANE_MESH_LAYER = "Membrane Mesh" 72 _JUNCTION_LAYER = "Crista-Membrane Junctions" 73 74 def _create_settings_widget(self): 75 setting_values = QWidget() 76 setting_values.setLayout(QVBoxLayout()) 77 78 self.save_path, layout = self._add_path_param( 79 name="save_path", select_type="file", value="", 80 tooltip="Path to save the analysis results CSV file. An empty path will skip saving. " 81 "See docs/cristae_analysis.md for how each column is computed.", 82 ) 83 setting_values.layout().addLayout(layout) 84 85 self.voxel_size_param, layout = self._add_float_param( 86 "voxel_size", 0.0, min_val=0.0, max_val=100.0, 87 title="Voxel Size (nm, 0 = auto)", step=0.1, 88 tooltip="Voxel size of the input volume in nanometers. Set to 0 (default) to auto-detect from layer metadata.", 89 ) 90 setting_values.layout().addLayout(layout) 91 92 self.mm_thickness_param, layout = self._add_float_param( 93 "mm_thickness", 8.0, min_val=1.0, max_val=30.0, 94 title="Membrane Thickness (nm)", decimals=1, step=0.5, 95 tooltip="Thickness of the mitochondrial membrane shell in nanometers.", 96 ) 97 setting_values.layout().addLayout(layout) 98 99 self.border_gap_param, layout = self._add_float_param( 100 "border_gap", 0.0, min_val=0.0, max_val=100.0, 101 title="Border Gap (nm, 0 = same as membrane)", decimals=1, step=0.5, 102 tooltip="Distance from each volume face within which membrane voxels are suppressed. " 103 "Set to 0 to use the same value as Membrane Thickness.", 104 ) 105 setting_values.layout().addLayout(layout) 106 107 self.show_membranes_param = self._add_boolean_param( 108 "show_membranes", False, 109 title="Show Membrane Mesh", 110 tooltip="Add the eroded-mito (lumen) inner surface — the single-wall surface the junction " 111 "geodesics run along — as a mesh (napari Surface layer) after running.", 112 ) 113 setting_values.layout().addWidget(self.show_membranes_param) 114 115 self.orientation_param, layout = self._add_choice_param( 116 "orientation", self._ORIENTATION_SKIP, list(self._ORIENTATION_TO_METHOD.keys()), 117 title="Crista orientation", 118 tooltip="How to compute the crista orientation anisotropy — the most expensive stage " 119 "(structure tensor). All other metrics (surface areas, junction distances, " 120 "thickness) are identical regardless of this choice.\n" 121 "- Fast (downsampled, approximate): ~8x faster; a relative indicator only, not " 122 "comparable in magnitude to the exact value.\n" 123 "- Exact (full resolution): the true anisotropy (slowest).\n" 124 "- Skip (no orientation): fastest; leaves the orientation column empty.", 125 ) 126 setting_values.layout().addLayout(layout) 127 128 self.membrane_mode_param, layout = self._add_choice_param( 129 "membrane_mode", self._MEMBRANE_SLICE_2D, list(self._MEMBRANE_TO_MODE.keys()), 130 title="Membrane mode", 131 tooltip="How the membrane shell is approximated.\n" 132 "- 2D per-slice (z-parallel): erode each Z-slice independently in XY (no z-bleed); " 133 "the shell has no Z-caps and can fragment across slices (some junction pairs may " 134 "then have no along-membrane path).\n" 135 "- 3D connected shell: a single connected 3D shell including the Z-caps (no " 136 "fragmentation), somewhat slower; thickness acts in all axes.", 137 ) 138 setting_values.layout().addLayout(layout) 139 140 return self._make_collapsible(widget=setting_values, title="Advanced Settings") 141 142 def _read_inputs(self): 143 """Validate the selected layers/voxel size and read the shared run/preview parameters. 144 145 ``layer_scale``/``layer_translate`` are inherited from the source (crista) layer so the result 146 layers overlay the input correctly (e.g. when the raw data was loaded with a physical voxel 147 scale). 148 149 Returns (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, mm_thickness, 150 border_gap) or None (after showing a guidance message) if inputs are incomplete. 151 """ 152 crista_mask = self._get_layer_selector_data(self.crista_selector_name) 153 mito_seg = self._get_layer_selector_data(self.mito_selector_name) 154 if crista_mask is None or mito_seg is None: 155 show_info("Please select both a crista mask and a mito segmentation layer.") 156 return None 157 158 metadata = self._get_layer_selector_data(self.crista_selector_name, return_metadata=True) 159 voxel_size = self._handle_resolution(metadata, self.voxel_size_param, crista_mask.ndim, return_as_list=False) 160 if voxel_size is None: 161 show_info("Please provide a voxel size (or ensure layer metadata contains voxel_size).") 162 return None 163 164 ref_layer = self._get_layer_selector_layer(self.crista_selector_name) 165 layer_scale = None if ref_layer is None else ref_layer.scale 166 layer_translate = None if ref_layer is None else ref_layer.translate 167 168 mm_thickness = self.mm_thickness_param.value() 169 border_gap_val = self.border_gap_param.value() 170 border_gap = border_gap_val if border_gap_val > 0.0 else None 171 membrane_mode = self._MEMBRANE_TO_MODE[self.membrane_mode_param.currentText()] 172 return (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 173 mm_thickness, border_gap, membrane_mode) 174 175 def _compute_membrane_and_contacts(self, mito_seg, crista_mask, voxel_size, mm_thickness, 176 border_gap, membrane_mode): 177 """The cheap front-end shared by preview and run: membrane shell + crista-membrane junctions. 178 179 Also returns the border-trimmed lumen (eroded-mito interior) so the run can both display it 180 and feed it to the geodesic stage without recomputing the erosion. 181 """ 182 membrane_mask, lumen_mask = approximate_membrane( 183 mito_seg, voxel_size, 184 membrane_thickness_nm=mm_thickness, border_gap_nm=border_gap, 185 n_jobs=-1, 186 membrane_mode=membrane_mode, 187 return_lumen=True, 188 ) 189 contact_labels, contact_summary = detect_contact_sites( 190 crista_mask.astype(bool), membrane_mask, voxel_size 191 ) 192 return membrane_mask, lumen_mask, contact_labels, contact_summary 193 194 def on_preview(self): 195 """Compute and show ONLY the membrane + junctions (seconds) — the front-end of the pipeline — 196 so the user can tune Membrane Thickness / Border Gap before the expensive per-mito run. 197 198 Runs synchronously; :meth:`_computing` provides the busy feedback while it blocks. 199 """ 200 inputs = self._read_inputs() 201 if inputs is None: 202 return 203 (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 204 mm_thickness, border_gap, membrane_mode) = inputs 205 206 with self._computing( 207 self.preview_button, "Computing preview…", "Preview Membrane && Junctions", 208 "INFO: Previewing membrane & junctions...", 209 ): 210 pbar = progress(total=2, desc="Preview: membrane & junctions") 211 try: 212 membrane_mask, _lumen_mask, contact_labels, contact_summary = self._compute_membrane_and_contacts( 213 mito_seg, crista_mask, voxel_size, mm_thickness, border_gap, membrane_mode 214 ) 215 pbar.update(1) 216 self.add_or_update_labels( 217 self._MEMBRANE_LAYER, membrane_mask.astype(np.uint8), 218 scale=layer_scale, translate=layer_translate, opacity=0.4, 219 ) 220 if contact_labels.max() > 0: 221 self.add_or_update_labels( 222 self._JUNCTION_LAYER, contact_labels.astype(np.uint32), 223 scale=layer_scale, translate=layer_translate, 224 blending="translucent_no_depth", 225 ) 226 else: 227 show_info("INFO: No crista–membrane junctions detected at these settings.") 228 pbar.update(1) 229 show_info( 230 f"INFO: Preview — {int(membrane_mask.sum())} membrane voxels, " 231 f"{contact_summary['crista_junction_count']} junctions. " 232 "Adjust Membrane Thickness / Border Gap and preview again, or Run." 233 ) 234 finally: 235 pbar.close() 236 237 def on_run(self): 238 """Run the full per-mitochondrion cristae analysis and add the result layers + stats table. 239 240 Runs synchronously; :meth:`_computing` provides the busy feedback while it blocks. 241 """ 242 inputs = self._read_inputs() 243 if inputs is None: 244 return 245 (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 246 mm_thickness, border_gap, membrane_mode) = inputs 247 248 with self._computing( 249 self.run_button, "Computing analysis…", "Run Cristae Analysis", 250 "INFO: Approximating mitochondrial membrane & junctions...", 251 ): 252 membrane_mask, lumen_mask, contact_labels, contact_summary = self._compute_membrane_and_contacts( 253 mito_seg, crista_mask, voxel_size, mm_thickness, border_gap, membrane_mode 254 ) 255 256 method = self._ORIENTATION_TO_METHOD[self.orientation_param.currentText()] 257 show_info(f"INFO: Running cristae analysis per mitochondrion (orientation: {method})...") 258 259 # compute_mito_crista_statistics calls progress_callback once per mitochondrion, on this 260 # (GUI) thread, so the activity-dock bar can be created/updated here directly. 261 pbar = {"bar": None} 262 263 def _on_progress(done, total): 264 if pbar["bar"] is None: 265 pbar["bar"] = progress(total=total, desc="Cristae analysis") 266 pbar["bar"].update(1) 267 268 try: 269 stats_df = compute_mito_crista_statistics( 270 crista_mask, mito_seg, voxel_size, 271 membrane_mask=membrane_mask, 272 lumen_mask=lumen_mask, 273 membrane_thickness_nm=mm_thickness, 274 border_gap_nm=border_gap, 275 method=method, 276 membrane_mode=membrane_mode, 277 n_jobs=-1, 278 verbose=True, 279 progress_callback=_on_progress, 280 ) 281 finally: 282 if pbar["bar"] is not None: 283 pbar["bar"].close() 284 285 if self.show_membranes_param.isChecked(): 286 gap_radius = _gap_radius(voxel_size, mm_thickness, border_gap, mito_seg.ndim) 287 mesh = _open_trimmed_mesh( 288 lumen_mask, np.ones(mito_seg.ndim), gap_radius, np.ones((mito_seg.ndim, 2), dtype=bool) 289 ) 290 if mesh is not None: 291 verts, faces = mesh 292 self.add_or_update_surface( 293 self._MEMBRANE_MESH_LAYER, verts, faces, 294 scale=layer_scale, translate=layer_translate, 295 opacity=0.4, blending="translucent", 296 ) 297 else: 298 show_info("INFO: No membrane surface to display at these settings.") 299 300 if contact_labels.max() > 0: 301 self.add_or_update_labels( 302 self._JUNCTION_LAYER, contact_labels.astype(np.uint32), 303 scale=layer_scale, translate=layer_translate, 304 blending="translucent_no_depth", 305 ) 306 else: 307 show_info("INFO: No crista–membrane junctions detected — junction layer not added.") 308 309 mito_layer = self._get_layer_selector_layer(self.mito_selector_name) 310 self._add_properties_and_table(mito_layer, stats_df, save_path=self.save_path.text()) 311 312 n_mito = len(stats_df) 313 n_contacts = contact_summary["crista_junction_count"] 314 show_info( 315 f"INFO: Cristae analysis complete — {n_mito} mitochondria, " 316 f"{n_contacts} crista junction sites detected." 317 )
16class CristaeAnalysisWidget(BaseWidget): 17 """Napari widget for the cristae analysis (preview + full per-mitochondrion run). 18 19 ``_ORIENTATION_TO_METHOD`` maps the orientation dropdown labels to the ``method`` argument of 20 :func:`~synapse_net.cristae_analysis.compute_mito_crista_statistics`, and ``_MEMBRANE_TO_MODE`` maps 21 the membrane-mode labels to the ``membrane_mode`` argument of 22 :func:`~synapse_net.cristae_analysis.approximate_membrane`. The ``_*_LAYER`` name constants are 23 shared by the preview and the full run so re-previewing / running updates the same layers instead 24 of duplicating them. 25 """ 26 27 _ORIENTATION_FAST = "Fast (downsampled, approximate)" 28 _ORIENTATION_SKIP = "Skip (no orientation)" 29 _ORIENTATION_TO_METHOD = { 30 _ORIENTATION_FAST: "fast", 31 "Exact (full resolution)": "exact", 32 _ORIENTATION_SKIP: "skip", 33 } 34 35 _MEMBRANE_SLICE_2D = "2D per-slice (z-parallel)" 36 _MEMBRANE_TO_MODE = { 37 _MEMBRANE_SLICE_2D: "slice_2d", 38 "3D connected shell": "shell_3d", 39 } 40 41 def __init__(self): 42 super().__init__() 43 44 self.viewer = napari.current_viewer() 45 layout = QVBoxLayout() 46 47 self.crista_selector_name = "Crista Mask" 48 self.mito_selector_name = "Mito Segmentation" 49 50 self.crista_selector_widget = self._create_layer_selector( 51 self.crista_selector_name, layer_type="Labels", prefer_substring="cristae") 52 self.mito_selector_widget = self._create_layer_selector( 53 self.mito_selector_name, layer_type="Labels", prefer_substring="mitochondria") 54 55 self.settings = self._create_settings_widget() 56 57 self.preview_button = QPushButton("Preview Membrane && Junctions") 58 self.preview_button.clicked.connect(self.on_preview) 59 60 self.run_button = QPushButton("Run Cristae Analysis") 61 self.run_button.clicked.connect(self.on_run) 62 63 layout.addWidget(self.crista_selector_widget) 64 layout.addWidget(self.mito_selector_widget) 65 layout.addWidget(self.settings) 66 layout.addWidget(self.preview_button) 67 layout.addWidget(self.run_button) 68 69 self.setLayout(layout) 70 71 _MEMBRANE_LAYER = "Membrane Mask" 72 _MEMBRANE_MESH_LAYER = "Membrane Mesh" 73 _JUNCTION_LAYER = "Crista-Membrane Junctions" 74 75 def _create_settings_widget(self): 76 setting_values = QWidget() 77 setting_values.setLayout(QVBoxLayout()) 78 79 self.save_path, layout = self._add_path_param( 80 name="save_path", select_type="file", value="", 81 tooltip="Path to save the analysis results CSV file. An empty path will skip saving. " 82 "See docs/cristae_analysis.md for how each column is computed.", 83 ) 84 setting_values.layout().addLayout(layout) 85 86 self.voxel_size_param, layout = self._add_float_param( 87 "voxel_size", 0.0, min_val=0.0, max_val=100.0, 88 title="Voxel Size (nm, 0 = auto)", step=0.1, 89 tooltip="Voxel size of the input volume in nanometers. Set to 0 (default) to auto-detect from layer metadata.", 90 ) 91 setting_values.layout().addLayout(layout) 92 93 self.mm_thickness_param, layout = self._add_float_param( 94 "mm_thickness", 8.0, min_val=1.0, max_val=30.0, 95 title="Membrane Thickness (nm)", decimals=1, step=0.5, 96 tooltip="Thickness of the mitochondrial membrane shell in nanometers.", 97 ) 98 setting_values.layout().addLayout(layout) 99 100 self.border_gap_param, layout = self._add_float_param( 101 "border_gap", 0.0, min_val=0.0, max_val=100.0, 102 title="Border Gap (nm, 0 = same as membrane)", decimals=1, step=0.5, 103 tooltip="Distance from each volume face within which membrane voxels are suppressed. " 104 "Set to 0 to use the same value as Membrane Thickness.", 105 ) 106 setting_values.layout().addLayout(layout) 107 108 self.show_membranes_param = self._add_boolean_param( 109 "show_membranes", False, 110 title="Show Membrane Mesh", 111 tooltip="Add the eroded-mito (lumen) inner surface — the single-wall surface the junction " 112 "geodesics run along — as a mesh (napari Surface layer) after running.", 113 ) 114 setting_values.layout().addWidget(self.show_membranes_param) 115 116 self.orientation_param, layout = self._add_choice_param( 117 "orientation", self._ORIENTATION_SKIP, list(self._ORIENTATION_TO_METHOD.keys()), 118 title="Crista orientation", 119 tooltip="How to compute the crista orientation anisotropy — the most expensive stage " 120 "(structure tensor). All other metrics (surface areas, junction distances, " 121 "thickness) are identical regardless of this choice.\n" 122 "- Fast (downsampled, approximate): ~8x faster; a relative indicator only, not " 123 "comparable in magnitude to the exact value.\n" 124 "- Exact (full resolution): the true anisotropy (slowest).\n" 125 "- Skip (no orientation): fastest; leaves the orientation column empty.", 126 ) 127 setting_values.layout().addLayout(layout) 128 129 self.membrane_mode_param, layout = self._add_choice_param( 130 "membrane_mode", self._MEMBRANE_SLICE_2D, list(self._MEMBRANE_TO_MODE.keys()), 131 title="Membrane mode", 132 tooltip="How the membrane shell is approximated.\n" 133 "- 2D per-slice (z-parallel): erode each Z-slice independently in XY (no z-bleed); " 134 "the shell has no Z-caps and can fragment across slices (some junction pairs may " 135 "then have no along-membrane path).\n" 136 "- 3D connected shell: a single connected 3D shell including the Z-caps (no " 137 "fragmentation), somewhat slower; thickness acts in all axes.", 138 ) 139 setting_values.layout().addLayout(layout) 140 141 return self._make_collapsible(widget=setting_values, title="Advanced Settings") 142 143 def _read_inputs(self): 144 """Validate the selected layers/voxel size and read the shared run/preview parameters. 145 146 ``layer_scale``/``layer_translate`` are inherited from the source (crista) layer so the result 147 layers overlay the input correctly (e.g. when the raw data was loaded with a physical voxel 148 scale). 149 150 Returns (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, mm_thickness, 151 border_gap) or None (after showing a guidance message) if inputs are incomplete. 152 """ 153 crista_mask = self._get_layer_selector_data(self.crista_selector_name) 154 mito_seg = self._get_layer_selector_data(self.mito_selector_name) 155 if crista_mask is None or mito_seg is None: 156 show_info("Please select both a crista mask and a mito segmentation layer.") 157 return None 158 159 metadata = self._get_layer_selector_data(self.crista_selector_name, return_metadata=True) 160 voxel_size = self._handle_resolution(metadata, self.voxel_size_param, crista_mask.ndim, return_as_list=False) 161 if voxel_size is None: 162 show_info("Please provide a voxel size (or ensure layer metadata contains voxel_size).") 163 return None 164 165 ref_layer = self._get_layer_selector_layer(self.crista_selector_name) 166 layer_scale = None if ref_layer is None else ref_layer.scale 167 layer_translate = None if ref_layer is None else ref_layer.translate 168 169 mm_thickness = self.mm_thickness_param.value() 170 border_gap_val = self.border_gap_param.value() 171 border_gap = border_gap_val if border_gap_val > 0.0 else None 172 membrane_mode = self._MEMBRANE_TO_MODE[self.membrane_mode_param.currentText()] 173 return (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 174 mm_thickness, border_gap, membrane_mode) 175 176 def _compute_membrane_and_contacts(self, mito_seg, crista_mask, voxel_size, mm_thickness, 177 border_gap, membrane_mode): 178 """The cheap front-end shared by preview and run: membrane shell + crista-membrane junctions. 179 180 Also returns the border-trimmed lumen (eroded-mito interior) so the run can both display it 181 and feed it to the geodesic stage without recomputing the erosion. 182 """ 183 membrane_mask, lumen_mask = approximate_membrane( 184 mito_seg, voxel_size, 185 membrane_thickness_nm=mm_thickness, border_gap_nm=border_gap, 186 n_jobs=-1, 187 membrane_mode=membrane_mode, 188 return_lumen=True, 189 ) 190 contact_labels, contact_summary = detect_contact_sites( 191 crista_mask.astype(bool), membrane_mask, voxel_size 192 ) 193 return membrane_mask, lumen_mask, contact_labels, contact_summary 194 195 def on_preview(self): 196 """Compute and show ONLY the membrane + junctions (seconds) — the front-end of the pipeline — 197 so the user can tune Membrane Thickness / Border Gap before the expensive per-mito run. 198 199 Runs synchronously; :meth:`_computing` provides the busy feedback while it blocks. 200 """ 201 inputs = self._read_inputs() 202 if inputs is None: 203 return 204 (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 205 mm_thickness, border_gap, membrane_mode) = inputs 206 207 with self._computing( 208 self.preview_button, "Computing preview…", "Preview Membrane && Junctions", 209 "INFO: Previewing membrane & junctions...", 210 ): 211 pbar = progress(total=2, desc="Preview: membrane & junctions") 212 try: 213 membrane_mask, _lumen_mask, contact_labels, contact_summary = self._compute_membrane_and_contacts( 214 mito_seg, crista_mask, voxel_size, mm_thickness, border_gap, membrane_mode 215 ) 216 pbar.update(1) 217 self.add_or_update_labels( 218 self._MEMBRANE_LAYER, membrane_mask.astype(np.uint8), 219 scale=layer_scale, translate=layer_translate, opacity=0.4, 220 ) 221 if contact_labels.max() > 0: 222 self.add_or_update_labels( 223 self._JUNCTION_LAYER, contact_labels.astype(np.uint32), 224 scale=layer_scale, translate=layer_translate, 225 blending="translucent_no_depth", 226 ) 227 else: 228 show_info("INFO: No crista–membrane junctions detected at these settings.") 229 pbar.update(1) 230 show_info( 231 f"INFO: Preview — {int(membrane_mask.sum())} membrane voxels, " 232 f"{contact_summary['crista_junction_count']} junctions. " 233 "Adjust Membrane Thickness / Border Gap and preview again, or Run." 234 ) 235 finally: 236 pbar.close() 237 238 def on_run(self): 239 """Run the full per-mitochondrion cristae analysis and add the result layers + stats table. 240 241 Runs synchronously; :meth:`_computing` provides the busy feedback while it blocks. 242 """ 243 inputs = self._read_inputs() 244 if inputs is None: 245 return 246 (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 247 mm_thickness, border_gap, membrane_mode) = inputs 248 249 with self._computing( 250 self.run_button, "Computing analysis…", "Run Cristae Analysis", 251 "INFO: Approximating mitochondrial membrane & junctions...", 252 ): 253 membrane_mask, lumen_mask, contact_labels, contact_summary = self._compute_membrane_and_contacts( 254 mito_seg, crista_mask, voxel_size, mm_thickness, border_gap, membrane_mode 255 ) 256 257 method = self._ORIENTATION_TO_METHOD[self.orientation_param.currentText()] 258 show_info(f"INFO: Running cristae analysis per mitochondrion (orientation: {method})...") 259 260 # compute_mito_crista_statistics calls progress_callback once per mitochondrion, on this 261 # (GUI) thread, so the activity-dock bar can be created/updated here directly. 262 pbar = {"bar": None} 263 264 def _on_progress(done, total): 265 if pbar["bar"] is None: 266 pbar["bar"] = progress(total=total, desc="Cristae analysis") 267 pbar["bar"].update(1) 268 269 try: 270 stats_df = compute_mito_crista_statistics( 271 crista_mask, mito_seg, voxel_size, 272 membrane_mask=membrane_mask, 273 lumen_mask=lumen_mask, 274 membrane_thickness_nm=mm_thickness, 275 border_gap_nm=border_gap, 276 method=method, 277 membrane_mode=membrane_mode, 278 n_jobs=-1, 279 verbose=True, 280 progress_callback=_on_progress, 281 ) 282 finally: 283 if pbar["bar"] is not None: 284 pbar["bar"].close() 285 286 if self.show_membranes_param.isChecked(): 287 gap_radius = _gap_radius(voxel_size, mm_thickness, border_gap, mito_seg.ndim) 288 mesh = _open_trimmed_mesh( 289 lumen_mask, np.ones(mito_seg.ndim), gap_radius, np.ones((mito_seg.ndim, 2), dtype=bool) 290 ) 291 if mesh is not None: 292 verts, faces = mesh 293 self.add_or_update_surface( 294 self._MEMBRANE_MESH_LAYER, verts, faces, 295 scale=layer_scale, translate=layer_translate, 296 opacity=0.4, blending="translucent", 297 ) 298 else: 299 show_info("INFO: No membrane surface to display at these settings.") 300 301 if contact_labels.max() > 0: 302 self.add_or_update_labels( 303 self._JUNCTION_LAYER, contact_labels.astype(np.uint32), 304 scale=layer_scale, translate=layer_translate, 305 blending="translucent_no_depth", 306 ) 307 else: 308 show_info("INFO: No crista–membrane junctions detected — junction layer not added.") 309 310 mito_layer = self._get_layer_selector_layer(self.mito_selector_name) 311 self._add_properties_and_table(mito_layer, stats_df, save_path=self.save_path.text()) 312 313 n_mito = len(stats_df) 314 n_contacts = contact_summary["crista_junction_count"] 315 show_info( 316 f"INFO: Cristae analysis complete — {n_mito} mitochondria, " 317 f"{n_contacts} crista junction sites detected." 318 )
Napari widget for the cristae analysis (preview + full per-mitochondrion run).
_ORIENTATION_TO_METHOD maps the orientation dropdown labels to the method argument of
~synapse_net.cristae_analysis.compute_mito_crista_statistics(), and _MEMBRANE_TO_MODE maps
the membrane-mode labels to the membrane_mode argument of
~synapse_net.cristae_analysis.approximate_membrane(). The _*_LAYER name constants are
shared by the preview and the full run so re-previewing / running updates the same layers instead
of duplicating them.
195 def on_preview(self): 196 """Compute and show ONLY the membrane + junctions (seconds) — the front-end of the pipeline — 197 so the user can tune Membrane Thickness / Border Gap before the expensive per-mito run. 198 199 Runs synchronously; :meth:`_computing` provides the busy feedback while it blocks. 200 """ 201 inputs = self._read_inputs() 202 if inputs is None: 203 return 204 (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 205 mm_thickness, border_gap, membrane_mode) = inputs 206 207 with self._computing( 208 self.preview_button, "Computing preview…", "Preview Membrane && Junctions", 209 "INFO: Previewing membrane & junctions...", 210 ): 211 pbar = progress(total=2, desc="Preview: membrane & junctions") 212 try: 213 membrane_mask, _lumen_mask, contact_labels, contact_summary = self._compute_membrane_and_contacts( 214 mito_seg, crista_mask, voxel_size, mm_thickness, border_gap, membrane_mode 215 ) 216 pbar.update(1) 217 self.add_or_update_labels( 218 self._MEMBRANE_LAYER, membrane_mask.astype(np.uint8), 219 scale=layer_scale, translate=layer_translate, opacity=0.4, 220 ) 221 if contact_labels.max() > 0: 222 self.add_or_update_labels( 223 self._JUNCTION_LAYER, contact_labels.astype(np.uint32), 224 scale=layer_scale, translate=layer_translate, 225 blending="translucent_no_depth", 226 ) 227 else: 228 show_info("INFO: No crista–membrane junctions detected at these settings.") 229 pbar.update(1) 230 show_info( 231 f"INFO: Preview — {int(membrane_mask.sum())} membrane voxels, " 232 f"{contact_summary['crista_junction_count']} junctions. " 233 "Adjust Membrane Thickness / Border Gap and preview again, or Run." 234 ) 235 finally: 236 pbar.close()
Compute and show ONLY the membrane + junctions (seconds) — the front-end of the pipeline — so the user can tune Membrane Thickness / Border Gap before the expensive per-mito run.
Runs synchronously; _computing() provides the busy feedback while it blocks.
238 def on_run(self): 239 """Run the full per-mitochondrion cristae analysis and add the result layers + stats table. 240 241 Runs synchronously; :meth:`_computing` provides the busy feedback while it blocks. 242 """ 243 inputs = self._read_inputs() 244 if inputs is None: 245 return 246 (crista_mask, mito_seg, voxel_size, layer_scale, layer_translate, 247 mm_thickness, border_gap, membrane_mode) = inputs 248 249 with self._computing( 250 self.run_button, "Computing analysis…", "Run Cristae Analysis", 251 "INFO: Approximating mitochondrial membrane & junctions...", 252 ): 253 membrane_mask, lumen_mask, contact_labels, contact_summary = self._compute_membrane_and_contacts( 254 mito_seg, crista_mask, voxel_size, mm_thickness, border_gap, membrane_mode 255 ) 256 257 method = self._ORIENTATION_TO_METHOD[self.orientation_param.currentText()] 258 show_info(f"INFO: Running cristae analysis per mitochondrion (orientation: {method})...") 259 260 # compute_mito_crista_statistics calls progress_callback once per mitochondrion, on this 261 # (GUI) thread, so the activity-dock bar can be created/updated here directly. 262 pbar = {"bar": None} 263 264 def _on_progress(done, total): 265 if pbar["bar"] is None: 266 pbar["bar"] = progress(total=total, desc="Cristae analysis") 267 pbar["bar"].update(1) 268 269 try: 270 stats_df = compute_mito_crista_statistics( 271 crista_mask, mito_seg, voxel_size, 272 membrane_mask=membrane_mask, 273 lumen_mask=lumen_mask, 274 membrane_thickness_nm=mm_thickness, 275 border_gap_nm=border_gap, 276 method=method, 277 membrane_mode=membrane_mode, 278 n_jobs=-1, 279 verbose=True, 280 progress_callback=_on_progress, 281 ) 282 finally: 283 if pbar["bar"] is not None: 284 pbar["bar"].close() 285 286 if self.show_membranes_param.isChecked(): 287 gap_radius = _gap_radius(voxel_size, mm_thickness, border_gap, mito_seg.ndim) 288 mesh = _open_trimmed_mesh( 289 lumen_mask, np.ones(mito_seg.ndim), gap_radius, np.ones((mito_seg.ndim, 2), dtype=bool) 290 ) 291 if mesh is not None: 292 verts, faces = mesh 293 self.add_or_update_surface( 294 self._MEMBRANE_MESH_LAYER, verts, faces, 295 scale=layer_scale, translate=layer_translate, 296 opacity=0.4, blending="translucent", 297 ) 298 else: 299 show_info("INFO: No membrane surface to display at these settings.") 300 301 if contact_labels.max() > 0: 302 self.add_or_update_labels( 303 self._JUNCTION_LAYER, contact_labels.astype(np.uint32), 304 scale=layer_scale, translate=layer_translate, 305 blending="translucent_no_depth", 306 ) 307 else: 308 show_info("INFO: No crista–membrane junctions detected — junction layer not added.") 309 310 mito_layer = self._get_layer_selector_layer(self.mito_selector_name) 311 self._add_properties_and_table(mito_layer, stats_df, save_path=self.save_path.text()) 312 313 n_mito = len(stats_df) 314 n_contacts = contact_summary["crista_junction_count"] 315 show_info( 316 f"INFO: Cristae analysis complete — {n_mito} mitochondria, " 317 f"{n_contacts} crista junction sites detected." 318 )
Run the full per-mitochondrion cristae analysis and add the result layers + stats table.
Runs synchronously; _computing() provides the busy feedback while it blocks.