synapse_net.tools.cli
1import argparse 2import os 3from functools import partial 4 5import torch 6import torch_em 7from tqdm import tqdm 8 9from ..cristae_analysis import compute_mito_crista_statistics 10from ..file_utils import read_voxel_size 11from ..imod.to_imod import ( 12 _get_file_paths, _load_segmentation, export_helper, 13 write_segmentation_to_imod_as_points, write_segmentation_to_imod, 14) 15from ..inference.inference import _get_model_registry, get_model, get_model_training_resolution, run_segmentation 16from ..inference.scalable_segmentation import scalable_segmentation 17from ..inference.util import inference_helper, parse_tiling 18 19 20def imod_point_cli(): 21 parser = argparse.ArgumentParser( 22 description="Convert a vesicle segmentation to an IMOD point model, " 23 "corresponding to a sphere for each vesicle in the segmentation." 24 ) 25 parser.add_argument( 26 "--input_path", "-i", required=True, 27 help="The filepath to the mrc file or the directory containing the tomogram data." 28 ) 29 parser.add_argument( 30 "--segmentation_path", "-s", required=True, 31 help="The filepath to the file or the directory containing the segmentations." 32 ) 33 parser.add_argument( 34 "--output_path", "-o", required=True, 35 help="The filepath to directory where the segmentations will be saved." 36 ) 37 parser.add_argument( 38 "--segmentation_key", "-k", 39 help="The key in the segmentation files. If not given we assume that the segmentations are stored as tif." 40 "If given, we assume they are stored as hdf5 files, and use the key to load the internal dataset." 41 ) 42 parser.add_argument( 43 "--min_radius", type=float, default=10.0, 44 help="The minimum vesicle radius in nm. Objects that are smaller than this radius will be exclded from the export." # noqa 45 ) 46 parser.add_argument( 47 "--radius_factor", type=float, default=1.0, 48 help="A factor for scaling the sphere radius for the export. " 49 "This can be used to fit the size of segmented vesicles to the best matching spheres.", 50 ) 51 parser.add_argument( 52 "--force", action="store_true", 53 help="Whether to over-write already present export results." 54 ) 55 args = parser.parse_args() 56 57 export_function = partial( 58 write_segmentation_to_imod_as_points, 59 min_radius=args.min_radius, 60 radius_factor=args.radius_factor, 61 ) 62 63 export_helper( 64 input_path=args.input_path, 65 segmentation_path=args.segmentation_path, 66 output_root=args.output_path, 67 export_function=export_function, 68 force=args.force, 69 segmentation_key=args.segmentation_key, 70 ) 71 72 73def imod_object_cli(): 74 parser = argparse.ArgumentParser( 75 description="Convert segmented objects to close contour IMOD models." 76 ) 77 parser.add_argument( 78 "--input_path", "-i", required=True, 79 help="The filepath to the mrc file or the directory containing the tomogram data." 80 ) 81 parser.add_argument( 82 "--segmentation_path", "-s", required=True, 83 help="The filepath to the file or the directory containing the segmentations." 84 ) 85 parser.add_argument( 86 "--output_path", "-o", required=True, 87 help="The filepath to directory where the segmentations will be saved." 88 ) 89 parser.add_argument( 90 "--segmentation_key", "-k", 91 help="The key in the segmentation files. If not given we assume that the segmentations are stored as tif." 92 "If given, we assume they are stored as hdf5 files, and use the key to load the internal dataset." 93 ) 94 parser.add_argument( 95 "--force", action="store_true", 96 help="Whether to over-write already present export results." 97 ) 98 args = parser.parse_args() 99 export_helper( 100 input_path=args.input_path, 101 segmentation_path=args.segmentation_path, 102 output_root=args.output_path, 103 export_function=write_segmentation_to_imod, 104 force=args.force, 105 segmentation_key=args.segmentation_key, 106 ) 107 108 109def pool_visualization_cli(): 110 parser = argparse.ArgumentParser(description="Load tomogram data, vesicle pools and additional segmentations for viualization.") # noqa 111 parser.add_argument( 112 "--input_path", "-i", required=True, 113 help="The filepath to the mrc file containing the tomogram data." 114 ) 115 parser.add_argument( 116 "--vesicle_paths", "-v", required=True, nargs="+", 117 help="The filepath(s) to the tif file(s) containing the vesicle segmentation." 118 ) 119 parser.add_argument( 120 "--table_paths", "-t", required=True, nargs="+", 121 help="The filepath(s) to the table(s) with the vesicle pool assignments." 122 ) 123 parser.add_argument( 124 "-s", "--segmentation_paths", nargs="+", help="Filepaths for additional segmentations." 125 ) 126 parser.add_argument( 127 "--split_pools", action="store_true", help="Whether to split the pools into individual layers.", 128 ) 129 args = parser.parse_args() 130 131 # Imported here because it needs napari, which is an optional dependency. 132 from .pool_visualization import _visualize_vesicle_pools 133 134 _visualize_vesicle_pools( 135 args.input_path, args.vesicle_paths, args.table_paths, args.segmentation_paths, args.split_pools 136 ) 137 138 139# TODO: handle kwargs 140def segmentation_cli(): 141 parser = argparse.ArgumentParser(description="Run segmentation.") 142 parser.add_argument( 143 "--input_path", "-i", required=True, 144 help="The filepath to the mrc file or the directory containing the tomogram data." 145 ) 146 parser.add_argument( 147 "--output_path", "-o", required=True, 148 help="The filepath to directory where the segmentations will be saved." 149 ) 150 model_names = list(_get_model_registry().urls.keys()) 151 model_names = ", ".join(model_names) 152 parser.add_argument( 153 "--model", "-m", required=True, 154 help=f"The model type. The following models are currently available: {model_names}" 155 ) 156 parser.add_argument( 157 "--mask_path", help="The filepath to a tif file with a mask that will be used to restrict the segmentation." 158 "Can also be a directory with tifs if the filestructure matches input_path." 159 ) 160 parser.add_argument("--input_key", "-k", required=False) 161 parser.add_argument( 162 "--force", action="store_true", 163 help="Whether to over-write already present segmentation results." 164 ) 165 parser.add_argument( 166 "--tile_shape", type=int, nargs=3, 167 help="The tile shape for prediction, in ZYX order. Lower the tile shape if GPU memory is insufficient." 168 ) 169 parser.add_argument( 170 "--halo", type=int, nargs=3, 171 help="The halo for prediction, in ZYX order. Increase the halo to minimize boundary artifacts." 172 ) 173 parser.add_argument( 174 "--data_ext", default=".mrc", help="The extension of the tomogram data. By default .mrc." 175 ) 176 parser.add_argument( 177 "--checkpoint", "-c", help="Path to a custom model, e.g. from domain adaptation.", 178 ) 179 parser.add_argument( 180 "--segmentation_key", "-s", 181 help="If given, the outputs will be saved to an hdf5 file with this key. Otherwise they will be saved as tif.", 182 ) 183 parser.add_argument( 184 "--scale", type=float, 185 help="The factor for rescaling the data before inference. " 186 "By default, the scaling factor will be derived from the voxel size of the input data. " 187 "If this parameter is given it will over-ride the default behavior. " 188 ) 189 parser.add_argument( 190 "--verbose", "-v", action="store_true", 191 help="Whether to print verbose information about the segmentation progress." 192 ) 193 parser.add_argument( 194 "--scalable", action="store_true", help="Use the scalable segmentation implementation. " 195 "Currently this only works for vesicles, mitochondria, or active zones." 196 ) 197 parser.add_argument( 198 "--extra_input_path", default=None, help="Filepath to extra inputs, needed for cristae segmentation." 199 ) 200 parser.add_argument( 201 "--extra_input_ext", default=".tif", help="File extension for the extra inputs, default is tif." 202 ) 203 args = parser.parse_args() 204 205 if args.checkpoint is None: 206 model = get_model(args.model) 207 else: 208 checkpoint_path = args.checkpoint 209 if checkpoint_path.endswith("best.pt"): 210 checkpoint_path = os.path.split(checkpoint_path)[0] 211 212 if os.path.isdir(checkpoint_path): # Load the model from a torch_em checkpoint. 213 model = torch_em.util.load_model(checkpoint=checkpoint_path) 214 else: 215 model = torch.load(checkpoint_path, weights_only=False) 216 assert model is not None, f"The model from {args.checkpoint} could not be loaded." 217 218 is_2d = "2d" in args.model 219 tiling = parse_tiling(args.tile_shape, args.halo, is_2d=is_2d) 220 221 # If the scale argument is not passed, then we get the average training resolution for the model. 222 # The inputs will then be scaled to match this resolution based on the voxel size from the mrc files. 223 if args.scale is None: 224 model_resolution = get_model_training_resolution(args.model) 225 model_resolution = tuple(model_resolution[ax] for ax in ("yx" if is_2d else "zyx")) 226 scale = None 227 # Otherwise, we set the model resolution to None and use the scaling factor provided by the user. 228 else: 229 model_resolution = None 230 scale = (2 if is_2d else 3) * (args.scale,) 231 232 if args.scalable: 233 if not args.model.startswith(("vesicle", "mito", "active")): 234 raise ValueError( 235 "The scalable segmentation implementation is currently only supported for " 236 f"vesicles, mitochondria, or active zones, not for {args.model}." 237 ) 238 segmentation_function = partial( 239 scalable_segmentation, model=model, tiling=tiling, verbose=args.verbose 240 ) 241 allocate_output = True 242 243 else: 244 segmentation_function = partial( 245 run_segmentation, model=model, model_type=args.model, verbose=args.verbose, tiling=tiling, 246 ) 247 allocate_output = False 248 249 inference_helper( 250 args.input_path, args.output_path, segmentation_function, 251 mask_input_path=args.mask_path, force=args.force, data_ext=args.data_ext, 252 output_key=args.segmentation_key, model_resolution=model_resolution, scale=scale, 253 allocate_output=allocate_output, extra_input_path=args.extra_input_path, 254 extra_input_ext=args.extra_input_ext 255 ) 256 257 258def cristae_analysis_helper( 259 crista_path, mito_path, output_root, 260 crista_key=None, mito_key=None, 261 voxel_size=None, tomogram_path=None, 262 membrane_thickness_nm=8.0, border_gap_nm=None, 263 method="skip", membrane_mode="slice_2d", 264 junction_mode="overlap", max_extension_nm=None, terminus_nm=None, min_junction_volume_nm3=None, 265 n_jobs=-1, force=False, verbose=False, 266): 267 """Batch-compute per-mitochondrion cristae statistics and save one CSV per input pair. 268 269 This is the headless equivalent of the napari cristae-analysis widget. It matches crista and 270 mitochondria segmentations by sorted order (a single file each, or two directories), computes 271 the statistics via :func:`synapse_net.cristae_analysis.compute_mito_crista_statistics`, and 272 writes the resulting table next to a mirrored input folder structure. 273 274 Args: 275 crista_path: Crista segmentation - a single file or a directory of them. 276 mito_path: Mitochondria instance segmentation - a single file or a directory of them. 277 output_root: Directory where the ``<stem>_cristae_analysis.csv`` tables are written. A single 278 input file writes directly into it; a directory input mirrors the nested folder structure. 279 crista_key: Internal dataset key for the crista segmentation. If None the crista files are 280 assumed to be tif, otherwise hdf5 with this key. 281 mito_key: Internal dataset key for the mitochondria segmentation, analogous to crista_key. 282 voxel_size: Voxel size in nm applied to every file. If None it is read per file from the 283 raw tomogram given via tomogram_path. 284 tomogram_path: Raw tomogram (mrc/rec) - a single file or a directory - used to read the 285 voxel size when voxel_size is None. 286 membrane_thickness_nm: Membrane shell thickness in nm. 287 border_gap_nm: Distance from the volume faces where the membrane is suppressed (nm). 288 Defaults to membrane_thickness_nm when None. 289 method: How the crista orientation anisotropy is computed ("skip", "fast" or "exact"). 290 membrane_mode: How the membrane shell is built ("slice_2d" or "shell_3d"). 291 junction_mode: Which junction detector fills crista_junction_count - "overlap" (the direct 292 crista-membrane intersection) or "skeleton" (crista regions reaching close to the inner 293 boundary membrane near a crista terminus). 294 max_extension_nm: How far in nm a crista may fall short of the inner boundary membrane surface 295 and still count ("skeleton" mode only). Defaults to membrane_thickness_nm when None. 296 terminus_nm: A near-membrane crista region counts only if it lies within this distance in nm 297 of a crista terminus - the free end of the cleaned-up crista skeleton ("skeleton" mode 298 only), which rejects a crista running alongside the membrane. Defaults to 20 nm when None. 299 min_junction_volume_nm3: Smallest junction volume in nm^3 that counts ("skeleton" mode only). 300 Defaults to 50 when None. 301 n_jobs: Number of workers for the per-mitochondrion computation (-1 = all cores). 302 force: Whether to over-write already present result tables. 303 verbose: Whether to show a progress bar over the mitochondria of each file. 304 """ 305 crista_files, crista_root = _get_file_paths(crista_path, ext=".h5" if crista_key else ".tif") 306 mito_files, _ = _get_file_paths(mito_path, ext=".h5" if mito_key else ".tif") 307 if len(crista_files) != len(mito_files): 308 raise ValueError( 309 f"The number of crista ({len(crista_files)}) and mitochondria ({len(mito_files)}) " 310 "segmentations does not match." 311 ) 312 313 if voxel_size is not None: 314 voxel_sizes = [voxel_size] * len(crista_files) 315 elif tomogram_path is not None: 316 tomo_files, _ = _get_file_paths(tomogram_path, ext=(".mrc", ".rec")) 317 if len(tomo_files) != len(crista_files): 318 raise ValueError( 319 f"The number of tomograms ({len(tomo_files)}) does not match the number of " 320 f"crista segmentations ({len(crista_files)})." 321 ) 322 voxel_sizes = [read_voxel_size(path) for path in tomo_files] 323 else: 324 raise ValueError("Provide either --voxel_size or --tomogram_path to determine the voxel size.") 325 326 for crista_file, mito_file, this_voxel_size in tqdm( 327 zip(crista_files, mito_files, voxel_sizes), total=len(crista_files), desc="Processing files" 328 ): 329 input_folder, input_name = os.path.split(crista_file) 330 fname = os.path.splitext(input_name)[0] + "_cristae_analysis.csv" 331 if crista_root is None: 332 output_path = os.path.join(output_root, fname) 333 else: 334 rel_folder = os.path.relpath(input_folder, crista_root) 335 output_path = os.path.join(output_root, rel_folder, fname) 336 337 if os.path.exists(output_path) and not force: 338 continue 339 340 crista = _load_segmentation(crista_file, crista_key) 341 mito = _load_segmentation(mito_file, mito_key) 342 stats_df = compute_mito_crista_statistics( 343 crista, mito, this_voxel_size, 344 membrane_thickness_nm=membrane_thickness_nm, border_gap_nm=border_gap_nm, 345 method=method, membrane_mode=membrane_mode, 346 junction_mode=junction_mode, max_extension_nm=max_extension_nm, 347 terminus_nm=terminus_nm, min_junction_volume_nm3=min_junction_volume_nm3, 348 n_jobs=n_jobs, verbose=verbose, 349 ) 350 351 os.makedirs(os.path.split(output_path)[0], exist_ok=True) 352 stats_df.to_csv(output_path, index=False) 353 print(f"Saved cristae analysis to {output_path}.") 354 355 356def cristae_analysis_cli(): 357 parser = argparse.ArgumentParser( 358 description="Compute per-mitochondrion cristae statistics from a crista segmentation and a " 359 "mitochondria instance segmentation, and save the results as a CSV table. This is the " 360 "command-line equivalent of the napari cristae-analysis widget." 361 ) 362 parser.add_argument( 363 "--crista_path", "-c", required=True, 364 help="The filepath to the crista segmentation, or a directory containing multiple of them." 365 ) 366 parser.add_argument( 367 "--mito_path", "-m", required=True, 368 help="The filepath to the mitochondria instance segmentation, or a directory containing multiple of them." 369 ) 370 parser.add_argument( 371 "--output_path", "-o", required=True, 372 help="The filepath to the directory where the result tables will be saved." 373 ) 374 parser.add_argument( 375 "--crista_key", 376 help="The key in the crista segmentation file. If not given the crista segmentation is assumed to be tif. " 377 "If given, it is assumed to be an hdf5 file and the key is used to load the internal dataset." 378 ) 379 parser.add_argument( 380 "--mito_key", 381 help="The key in the mitochondria segmentation file, analogous to --crista_key." 382 ) 383 parser.add_argument( 384 "--voxel_size", type=float, 385 help="The voxel size in nm, applied to all inputs. If not given it is read from the raw tomogram " 386 "passed via --tomogram_path." 387 ) 388 parser.add_argument( 389 "--tomogram_path", 390 help="The filepath to the raw tomogram (mrc/rec), or a directory of them, used to read the voxel size " 391 "when --voxel_size is not given." 392 ) 393 parser.add_argument( 394 "--membrane_thickness", type=float, default=8.0, 395 help="The membrane shell thickness in nm. By default 8.0." 396 ) 397 parser.add_argument( 398 "--border_gap", type=float, default=None, 399 help="The distance from the volume faces where the membrane is suppressed, in nm. " 400 "By default the same as the membrane thickness." 401 ) 402 parser.add_argument( 403 "--method", default="skip", choices=["skip", "fast", "exact"], 404 help="How the crista orientation anisotropy is computed. 'skip' (default) does not compute it, " 405 "'fast' uses a downsampled crop (relative only), 'exact' uses the full-resolution structure tensor." 406 ) 407 parser.add_argument( 408 "--membrane_mode", default="slice_2d", choices=["slice_2d", "shell_3d"], 409 help="How the membrane shell is built - 'slice_2d' (default, per-Z-slice) or 'shell_3d' (connected 3D shell)." 410 ) 411 parser.add_argument( 412 "--junction_mode", default="overlap", choices=["overlap", "skeleton"], 413 help="Which junction detector fills crista_junction_count. 'overlap' (default) counts the " 414 "connected components of the direct crista-membrane intersection, so a crista that stops " 415 "short of the membrane scores no junction. 'skeleton' counts crista regions that come within " 416 "--max_extension of the membrane near a crista terminus, so it tolerates a crista segmented " 417 "short of the membrane. 'skeleton' requires 3D data." 418 ) 419 parser.add_argument( 420 "--max_extension", type=float, default=None, 421 help="How far in nm a crista may fall short of the inner boundary membrane surface and still " 422 "count as a junction (--junction_mode skeleton only). By default the same as the membrane " 423 "thickness." 424 ) 425 parser.add_argument( 426 "--terminus_distance", type=float, default=None, 427 help="How close in nm a near-membrane crista region must be to a crista terminus - a free end " 428 "of the cleaned-up crista skeleton - to count as a junction (--junction_mode skeleton only). " 429 "This is what separates a crista ending at the membrane from one running alongside it. By " 430 "default 20 nm." 431 ) 432 parser.add_argument( 433 "--min_junction_volume", type=float, default=None, 434 help="The smallest junction volume in nm^3 that counts (--junction_mode skeleton only). This " 435 "only removes specks; it does not address the fact that skeleton mode over-counts on densely " 436 "packed cristae - see docs/cristae_analysis.md. By default 50." 437 ) 438 parser.add_argument( 439 "--n_jobs", type=int, default=-1, 440 help="The number of workers for the per-mitochondrion computation. By default -1 (all cores)." 441 ) 442 parser.add_argument( 443 "--force", action="store_true", 444 help="Whether to over-write already present result tables." 445 ) 446 parser.add_argument( 447 "--verbose", "-v", action="store_true", 448 help="Whether to show a progress bar over the mitochondria of each file." 449 ) 450 args = parser.parse_args() 451 452 cristae_analysis_helper( 453 args.crista_path, args.mito_path, args.output_path, 454 crista_key=args.crista_key, mito_key=args.mito_key, 455 voxel_size=args.voxel_size, tomogram_path=args.tomogram_path, 456 membrane_thickness_nm=args.membrane_thickness, border_gap_nm=args.border_gap, 457 method=args.method, membrane_mode=args.membrane_mode, 458 junction_mode=args.junction_mode, max_extension_nm=args.max_extension, 459 terminus_nm=args.terminus_distance, min_junction_volume_nm3=args.min_junction_volume, 460 n_jobs=args.n_jobs, force=args.force, verbose=args.verbose, 461 )
def
imod_point_cli():
21def imod_point_cli(): 22 parser = argparse.ArgumentParser( 23 description="Convert a vesicle segmentation to an IMOD point model, " 24 "corresponding to a sphere for each vesicle in the segmentation." 25 ) 26 parser.add_argument( 27 "--input_path", "-i", required=True, 28 help="The filepath to the mrc file or the directory containing the tomogram data." 29 ) 30 parser.add_argument( 31 "--segmentation_path", "-s", required=True, 32 help="The filepath to the file or the directory containing the segmentations." 33 ) 34 parser.add_argument( 35 "--output_path", "-o", required=True, 36 help="The filepath to directory where the segmentations will be saved." 37 ) 38 parser.add_argument( 39 "--segmentation_key", "-k", 40 help="The key in the segmentation files. If not given we assume that the segmentations are stored as tif." 41 "If given, we assume they are stored as hdf5 files, and use the key to load the internal dataset." 42 ) 43 parser.add_argument( 44 "--min_radius", type=float, default=10.0, 45 help="The minimum vesicle radius in nm. Objects that are smaller than this radius will be exclded from the export." # noqa 46 ) 47 parser.add_argument( 48 "--radius_factor", type=float, default=1.0, 49 help="A factor for scaling the sphere radius for the export. " 50 "This can be used to fit the size of segmented vesicles to the best matching spheres.", 51 ) 52 parser.add_argument( 53 "--force", action="store_true", 54 help="Whether to over-write already present export results." 55 ) 56 args = parser.parse_args() 57 58 export_function = partial( 59 write_segmentation_to_imod_as_points, 60 min_radius=args.min_radius, 61 radius_factor=args.radius_factor, 62 ) 63 64 export_helper( 65 input_path=args.input_path, 66 segmentation_path=args.segmentation_path, 67 output_root=args.output_path, 68 export_function=export_function, 69 force=args.force, 70 segmentation_key=args.segmentation_key, 71 )
def
imod_object_cli():
74def imod_object_cli(): 75 parser = argparse.ArgumentParser( 76 description="Convert segmented objects to close contour IMOD models." 77 ) 78 parser.add_argument( 79 "--input_path", "-i", required=True, 80 help="The filepath to the mrc file or the directory containing the tomogram data." 81 ) 82 parser.add_argument( 83 "--segmentation_path", "-s", required=True, 84 help="The filepath to the file or the directory containing the segmentations." 85 ) 86 parser.add_argument( 87 "--output_path", "-o", required=True, 88 help="The filepath to directory where the segmentations will be saved." 89 ) 90 parser.add_argument( 91 "--segmentation_key", "-k", 92 help="The key in the segmentation files. If not given we assume that the segmentations are stored as tif." 93 "If given, we assume they are stored as hdf5 files, and use the key to load the internal dataset." 94 ) 95 parser.add_argument( 96 "--force", action="store_true", 97 help="Whether to over-write already present export results." 98 ) 99 args = parser.parse_args() 100 export_helper( 101 input_path=args.input_path, 102 segmentation_path=args.segmentation_path, 103 output_root=args.output_path, 104 export_function=write_segmentation_to_imod, 105 force=args.force, 106 segmentation_key=args.segmentation_key, 107 )
def
pool_visualization_cli():
110def pool_visualization_cli(): 111 parser = argparse.ArgumentParser(description="Load tomogram data, vesicle pools and additional segmentations for viualization.") # noqa 112 parser.add_argument( 113 "--input_path", "-i", required=True, 114 help="The filepath to the mrc file containing the tomogram data." 115 ) 116 parser.add_argument( 117 "--vesicle_paths", "-v", required=True, nargs="+", 118 help="The filepath(s) to the tif file(s) containing the vesicle segmentation." 119 ) 120 parser.add_argument( 121 "--table_paths", "-t", required=True, nargs="+", 122 help="The filepath(s) to the table(s) with the vesicle pool assignments." 123 ) 124 parser.add_argument( 125 "-s", "--segmentation_paths", nargs="+", help="Filepaths for additional segmentations." 126 ) 127 parser.add_argument( 128 "--split_pools", action="store_true", help="Whether to split the pools into individual layers.", 129 ) 130 args = parser.parse_args() 131 132 # Imported here because it needs napari, which is an optional dependency. 133 from .pool_visualization import _visualize_vesicle_pools 134 135 _visualize_vesicle_pools( 136 args.input_path, args.vesicle_paths, args.table_paths, args.segmentation_paths, args.split_pools 137 )
def
segmentation_cli():
141def segmentation_cli(): 142 parser = argparse.ArgumentParser(description="Run segmentation.") 143 parser.add_argument( 144 "--input_path", "-i", required=True, 145 help="The filepath to the mrc file or the directory containing the tomogram data." 146 ) 147 parser.add_argument( 148 "--output_path", "-o", required=True, 149 help="The filepath to directory where the segmentations will be saved." 150 ) 151 model_names = list(_get_model_registry().urls.keys()) 152 model_names = ", ".join(model_names) 153 parser.add_argument( 154 "--model", "-m", required=True, 155 help=f"The model type. The following models are currently available: {model_names}" 156 ) 157 parser.add_argument( 158 "--mask_path", help="The filepath to a tif file with a mask that will be used to restrict the segmentation." 159 "Can also be a directory with tifs if the filestructure matches input_path." 160 ) 161 parser.add_argument("--input_key", "-k", required=False) 162 parser.add_argument( 163 "--force", action="store_true", 164 help="Whether to over-write already present segmentation results." 165 ) 166 parser.add_argument( 167 "--tile_shape", type=int, nargs=3, 168 help="The tile shape for prediction, in ZYX order. Lower the tile shape if GPU memory is insufficient." 169 ) 170 parser.add_argument( 171 "--halo", type=int, nargs=3, 172 help="The halo for prediction, in ZYX order. Increase the halo to minimize boundary artifacts." 173 ) 174 parser.add_argument( 175 "--data_ext", default=".mrc", help="The extension of the tomogram data. By default .mrc." 176 ) 177 parser.add_argument( 178 "--checkpoint", "-c", help="Path to a custom model, e.g. from domain adaptation.", 179 ) 180 parser.add_argument( 181 "--segmentation_key", "-s", 182 help="If given, the outputs will be saved to an hdf5 file with this key. Otherwise they will be saved as tif.", 183 ) 184 parser.add_argument( 185 "--scale", type=float, 186 help="The factor for rescaling the data before inference. " 187 "By default, the scaling factor will be derived from the voxel size of the input data. " 188 "If this parameter is given it will over-ride the default behavior. " 189 ) 190 parser.add_argument( 191 "--verbose", "-v", action="store_true", 192 help="Whether to print verbose information about the segmentation progress." 193 ) 194 parser.add_argument( 195 "--scalable", action="store_true", help="Use the scalable segmentation implementation. " 196 "Currently this only works for vesicles, mitochondria, or active zones." 197 ) 198 parser.add_argument( 199 "--extra_input_path", default=None, help="Filepath to extra inputs, needed for cristae segmentation." 200 ) 201 parser.add_argument( 202 "--extra_input_ext", default=".tif", help="File extension for the extra inputs, default is tif." 203 ) 204 args = parser.parse_args() 205 206 if args.checkpoint is None: 207 model = get_model(args.model) 208 else: 209 checkpoint_path = args.checkpoint 210 if checkpoint_path.endswith("best.pt"): 211 checkpoint_path = os.path.split(checkpoint_path)[0] 212 213 if os.path.isdir(checkpoint_path): # Load the model from a torch_em checkpoint. 214 model = torch_em.util.load_model(checkpoint=checkpoint_path) 215 else: 216 model = torch.load(checkpoint_path, weights_only=False) 217 assert model is not None, f"The model from {args.checkpoint} could not be loaded." 218 219 is_2d = "2d" in args.model 220 tiling = parse_tiling(args.tile_shape, args.halo, is_2d=is_2d) 221 222 # If the scale argument is not passed, then we get the average training resolution for the model. 223 # The inputs will then be scaled to match this resolution based on the voxel size from the mrc files. 224 if args.scale is None: 225 model_resolution = get_model_training_resolution(args.model) 226 model_resolution = tuple(model_resolution[ax] for ax in ("yx" if is_2d else "zyx")) 227 scale = None 228 # Otherwise, we set the model resolution to None and use the scaling factor provided by the user. 229 else: 230 model_resolution = None 231 scale = (2 if is_2d else 3) * (args.scale,) 232 233 if args.scalable: 234 if not args.model.startswith(("vesicle", "mito", "active")): 235 raise ValueError( 236 "The scalable segmentation implementation is currently only supported for " 237 f"vesicles, mitochondria, or active zones, not for {args.model}." 238 ) 239 segmentation_function = partial( 240 scalable_segmentation, model=model, tiling=tiling, verbose=args.verbose 241 ) 242 allocate_output = True 243 244 else: 245 segmentation_function = partial( 246 run_segmentation, model=model, model_type=args.model, verbose=args.verbose, tiling=tiling, 247 ) 248 allocate_output = False 249 250 inference_helper( 251 args.input_path, args.output_path, segmentation_function, 252 mask_input_path=args.mask_path, force=args.force, data_ext=args.data_ext, 253 output_key=args.segmentation_key, model_resolution=model_resolution, scale=scale, 254 allocate_output=allocate_output, extra_input_path=args.extra_input_path, 255 extra_input_ext=args.extra_input_ext 256 )
def
cristae_analysis_helper( crista_path, mito_path, output_root, crista_key=None, mito_key=None, voxel_size=None, tomogram_path=None, membrane_thickness_nm=8.0, border_gap_nm=None, method='skip', membrane_mode='slice_2d', junction_mode='overlap', max_extension_nm=None, terminus_nm=None, min_junction_volume_nm3=None, n_jobs=-1, force=False, verbose=False):
259def cristae_analysis_helper( 260 crista_path, mito_path, output_root, 261 crista_key=None, mito_key=None, 262 voxel_size=None, tomogram_path=None, 263 membrane_thickness_nm=8.0, border_gap_nm=None, 264 method="skip", membrane_mode="slice_2d", 265 junction_mode="overlap", max_extension_nm=None, terminus_nm=None, min_junction_volume_nm3=None, 266 n_jobs=-1, force=False, verbose=False, 267): 268 """Batch-compute per-mitochondrion cristae statistics and save one CSV per input pair. 269 270 This is the headless equivalent of the napari cristae-analysis widget. It matches crista and 271 mitochondria segmentations by sorted order (a single file each, or two directories), computes 272 the statistics via :func:`synapse_net.cristae_analysis.compute_mito_crista_statistics`, and 273 writes the resulting table next to a mirrored input folder structure. 274 275 Args: 276 crista_path: Crista segmentation - a single file or a directory of them. 277 mito_path: Mitochondria instance segmentation - a single file or a directory of them. 278 output_root: Directory where the ``<stem>_cristae_analysis.csv`` tables are written. A single 279 input file writes directly into it; a directory input mirrors the nested folder structure. 280 crista_key: Internal dataset key for the crista segmentation. If None the crista files are 281 assumed to be tif, otherwise hdf5 with this key. 282 mito_key: Internal dataset key for the mitochondria segmentation, analogous to crista_key. 283 voxel_size: Voxel size in nm applied to every file. If None it is read per file from the 284 raw tomogram given via tomogram_path. 285 tomogram_path: Raw tomogram (mrc/rec) - a single file or a directory - used to read the 286 voxel size when voxel_size is None. 287 membrane_thickness_nm: Membrane shell thickness in nm. 288 border_gap_nm: Distance from the volume faces where the membrane is suppressed (nm). 289 Defaults to membrane_thickness_nm when None. 290 method: How the crista orientation anisotropy is computed ("skip", "fast" or "exact"). 291 membrane_mode: How the membrane shell is built ("slice_2d" or "shell_3d"). 292 junction_mode: Which junction detector fills crista_junction_count - "overlap" (the direct 293 crista-membrane intersection) or "skeleton" (crista regions reaching close to the inner 294 boundary membrane near a crista terminus). 295 max_extension_nm: How far in nm a crista may fall short of the inner boundary membrane surface 296 and still count ("skeleton" mode only). Defaults to membrane_thickness_nm when None. 297 terminus_nm: A near-membrane crista region counts only if it lies within this distance in nm 298 of a crista terminus - the free end of the cleaned-up crista skeleton ("skeleton" mode 299 only), which rejects a crista running alongside the membrane. Defaults to 20 nm when None. 300 min_junction_volume_nm3: Smallest junction volume in nm^3 that counts ("skeleton" mode only). 301 Defaults to 50 when None. 302 n_jobs: Number of workers for the per-mitochondrion computation (-1 = all cores). 303 force: Whether to over-write already present result tables. 304 verbose: Whether to show a progress bar over the mitochondria of each file. 305 """ 306 crista_files, crista_root = _get_file_paths(crista_path, ext=".h5" if crista_key else ".tif") 307 mito_files, _ = _get_file_paths(mito_path, ext=".h5" if mito_key else ".tif") 308 if len(crista_files) != len(mito_files): 309 raise ValueError( 310 f"The number of crista ({len(crista_files)}) and mitochondria ({len(mito_files)}) " 311 "segmentations does not match." 312 ) 313 314 if voxel_size is not None: 315 voxel_sizes = [voxel_size] * len(crista_files) 316 elif tomogram_path is not None: 317 tomo_files, _ = _get_file_paths(tomogram_path, ext=(".mrc", ".rec")) 318 if len(tomo_files) != len(crista_files): 319 raise ValueError( 320 f"The number of tomograms ({len(tomo_files)}) does not match the number of " 321 f"crista segmentations ({len(crista_files)})." 322 ) 323 voxel_sizes = [read_voxel_size(path) for path in tomo_files] 324 else: 325 raise ValueError("Provide either --voxel_size or --tomogram_path to determine the voxel size.") 326 327 for crista_file, mito_file, this_voxel_size in tqdm( 328 zip(crista_files, mito_files, voxel_sizes), total=len(crista_files), desc="Processing files" 329 ): 330 input_folder, input_name = os.path.split(crista_file) 331 fname = os.path.splitext(input_name)[0] + "_cristae_analysis.csv" 332 if crista_root is None: 333 output_path = os.path.join(output_root, fname) 334 else: 335 rel_folder = os.path.relpath(input_folder, crista_root) 336 output_path = os.path.join(output_root, rel_folder, fname) 337 338 if os.path.exists(output_path) and not force: 339 continue 340 341 crista = _load_segmentation(crista_file, crista_key) 342 mito = _load_segmentation(mito_file, mito_key) 343 stats_df = compute_mito_crista_statistics( 344 crista, mito, this_voxel_size, 345 membrane_thickness_nm=membrane_thickness_nm, border_gap_nm=border_gap_nm, 346 method=method, membrane_mode=membrane_mode, 347 junction_mode=junction_mode, max_extension_nm=max_extension_nm, 348 terminus_nm=terminus_nm, min_junction_volume_nm3=min_junction_volume_nm3, 349 n_jobs=n_jobs, verbose=verbose, 350 ) 351 352 os.makedirs(os.path.split(output_path)[0], exist_ok=True) 353 stats_df.to_csv(output_path, index=False) 354 print(f"Saved cristae analysis to {output_path}.")
Batch-compute per-mitochondrion cristae statistics and save one CSV per input pair.
This is the headless equivalent of the napari cristae-analysis widget. It matches crista and
mitochondria segmentations by sorted order (a single file each, or two directories), computes
the statistics via synapse_net.cristae_analysis.compute_mito_crista_statistics(), and
writes the resulting table next to a mirrored input folder structure.
Arguments:
- crista_path: Crista segmentation - a single file or a directory of them.
- mito_path: Mitochondria instance segmentation - a single file or a directory of them.
- output_root: Directory where the
<stem>_cristae_analysis.csvtables are written. A single input file writes directly into it; a directory input mirrors the nested folder structure. - crista_key: Internal dataset key for the crista segmentation. If None the crista files are assumed to be tif, otherwise hdf5 with this key.
- mito_key: Internal dataset key for the mitochondria segmentation, analogous to crista_key.
- voxel_size: Voxel size in nm applied to every file. If None it is read per file from the raw tomogram given via tomogram_path.
- tomogram_path: Raw tomogram (mrc/rec) - a single file or a directory - used to read the voxel size when voxel_size is None.
- membrane_thickness_nm: Membrane shell thickness in nm.
- border_gap_nm: Distance from the volume faces where the membrane is suppressed (nm). Defaults to membrane_thickness_nm when None.
- method: How the crista orientation anisotropy is computed ("skip", "fast" or "exact").
- membrane_mode: How the membrane shell is built ("slice_2d" or "shell_3d").
- junction_mode: Which junction detector fills crista_junction_count - "overlap" (the direct crista-membrane intersection) or "skeleton" (crista regions reaching close to the inner boundary membrane near a crista terminus).
- max_extension_nm: How far in nm a crista may fall short of the inner boundary membrane surface and still count ("skeleton" mode only). Defaults to membrane_thickness_nm when None.
- terminus_nm: A near-membrane crista region counts only if it lies within this distance in nm of a crista terminus - the free end of the cleaned-up crista skeleton ("skeleton" mode only), which rejects a crista running alongside the membrane. Defaults to 20 nm when None.
- min_junction_volume_nm3: Smallest junction volume in nm^3 that counts ("skeleton" mode only). Defaults to 50 when None.
- n_jobs: Number of workers for the per-mitochondrion computation (-1 = all cores).
- force: Whether to over-write already present result tables.
- verbose: Whether to show a progress bar over the mitochondria of each file.
def
cristae_analysis_cli():
357def cristae_analysis_cli(): 358 parser = argparse.ArgumentParser( 359 description="Compute per-mitochondrion cristae statistics from a crista segmentation and a " 360 "mitochondria instance segmentation, and save the results as a CSV table. This is the " 361 "command-line equivalent of the napari cristae-analysis widget." 362 ) 363 parser.add_argument( 364 "--crista_path", "-c", required=True, 365 help="The filepath to the crista segmentation, or a directory containing multiple of them." 366 ) 367 parser.add_argument( 368 "--mito_path", "-m", required=True, 369 help="The filepath to the mitochondria instance segmentation, or a directory containing multiple of them." 370 ) 371 parser.add_argument( 372 "--output_path", "-o", required=True, 373 help="The filepath to the directory where the result tables will be saved." 374 ) 375 parser.add_argument( 376 "--crista_key", 377 help="The key in the crista segmentation file. If not given the crista segmentation is assumed to be tif. " 378 "If given, it is assumed to be an hdf5 file and the key is used to load the internal dataset." 379 ) 380 parser.add_argument( 381 "--mito_key", 382 help="The key in the mitochondria segmentation file, analogous to --crista_key." 383 ) 384 parser.add_argument( 385 "--voxel_size", type=float, 386 help="The voxel size in nm, applied to all inputs. If not given it is read from the raw tomogram " 387 "passed via --tomogram_path." 388 ) 389 parser.add_argument( 390 "--tomogram_path", 391 help="The filepath to the raw tomogram (mrc/rec), or a directory of them, used to read the voxel size " 392 "when --voxel_size is not given." 393 ) 394 parser.add_argument( 395 "--membrane_thickness", type=float, default=8.0, 396 help="The membrane shell thickness in nm. By default 8.0." 397 ) 398 parser.add_argument( 399 "--border_gap", type=float, default=None, 400 help="The distance from the volume faces where the membrane is suppressed, in nm. " 401 "By default the same as the membrane thickness." 402 ) 403 parser.add_argument( 404 "--method", default="skip", choices=["skip", "fast", "exact"], 405 help="How the crista orientation anisotropy is computed. 'skip' (default) does not compute it, " 406 "'fast' uses a downsampled crop (relative only), 'exact' uses the full-resolution structure tensor." 407 ) 408 parser.add_argument( 409 "--membrane_mode", default="slice_2d", choices=["slice_2d", "shell_3d"], 410 help="How the membrane shell is built - 'slice_2d' (default, per-Z-slice) or 'shell_3d' (connected 3D shell)." 411 ) 412 parser.add_argument( 413 "--junction_mode", default="overlap", choices=["overlap", "skeleton"], 414 help="Which junction detector fills crista_junction_count. 'overlap' (default) counts the " 415 "connected components of the direct crista-membrane intersection, so a crista that stops " 416 "short of the membrane scores no junction. 'skeleton' counts crista regions that come within " 417 "--max_extension of the membrane near a crista terminus, so it tolerates a crista segmented " 418 "short of the membrane. 'skeleton' requires 3D data." 419 ) 420 parser.add_argument( 421 "--max_extension", type=float, default=None, 422 help="How far in nm a crista may fall short of the inner boundary membrane surface and still " 423 "count as a junction (--junction_mode skeleton only). By default the same as the membrane " 424 "thickness." 425 ) 426 parser.add_argument( 427 "--terminus_distance", type=float, default=None, 428 help="How close in nm a near-membrane crista region must be to a crista terminus - a free end " 429 "of the cleaned-up crista skeleton - to count as a junction (--junction_mode skeleton only). " 430 "This is what separates a crista ending at the membrane from one running alongside it. By " 431 "default 20 nm." 432 ) 433 parser.add_argument( 434 "--min_junction_volume", type=float, default=None, 435 help="The smallest junction volume in nm^3 that counts (--junction_mode skeleton only). This " 436 "only removes specks; it does not address the fact that skeleton mode over-counts on densely " 437 "packed cristae - see docs/cristae_analysis.md. By default 50." 438 ) 439 parser.add_argument( 440 "--n_jobs", type=int, default=-1, 441 help="The number of workers for the per-mitochondrion computation. By default -1 (all cores)." 442 ) 443 parser.add_argument( 444 "--force", action="store_true", 445 help="Whether to over-write already present result tables." 446 ) 447 parser.add_argument( 448 "--verbose", "-v", action="store_true", 449 help="Whether to show a progress bar over the mitochondria of each file." 450 ) 451 args = parser.parse_args() 452 453 cristae_analysis_helper( 454 args.crista_path, args.mito_path, args.output_path, 455 crista_key=args.crista_key, mito_key=args.mito_key, 456 voxel_size=args.voxel_size, tomogram_path=args.tomogram_path, 457 membrane_thickness_nm=args.membrane_thickness, border_gap_nm=args.border_gap, 458 method=args.method, membrane_mode=args.membrane_mode, 459 junction_mode=args.junction_mode, max_extension_nm=args.max_extension, 460 terminus_nm=args.terminus_distance, min_junction_volume_nm3=args.min_junction_volume, 461 n_jobs=args.n_jobs, force=args.force, verbose=args.verbose, 462 )