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