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
 18from .pool_visualization import _visualize_vesicle_pools
 19
 20
 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    )
 72
 73
 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    )
108
109
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    _visualize_vesicle_pools(
132        args.input_path, args.vesicle_paths, args.table_paths, args.segmentation_paths, args.split_pools
133    )
134
135
136# TODO: handle kwargs
137def segmentation_cli():
138    parser = argparse.ArgumentParser(description="Run segmentation.")
139    parser.add_argument(
140        "--input_path", "-i", required=True,
141        help="The filepath to the mrc file or the directory containing the tomogram data."
142    )
143    parser.add_argument(
144        "--output_path", "-o", required=True,
145        help="The filepath to directory where the segmentations will be saved."
146    )
147    model_names = list(_get_model_registry().urls.keys())
148    model_names = ", ".join(model_names)
149    parser.add_argument(
150        "--model", "-m", required=True,
151        help=f"The model type. The following models are currently available: {model_names}"
152    )
153    parser.add_argument(
154        "--mask_path", help="The filepath to a tif file with a mask that will be used to restrict the segmentation."
155        "Can also be a directory with tifs if the filestructure matches input_path."
156    )
157    parser.add_argument("--input_key", "-k", required=False)
158    parser.add_argument(
159        "--force", action="store_true",
160        help="Whether to over-write already present segmentation results."
161    )
162    parser.add_argument(
163        "--tile_shape", type=int, nargs=3,
164        help="The tile shape for prediction, in ZYX order. Lower the tile shape if GPU memory is insufficient."
165    )
166    parser.add_argument(
167        "--halo", type=int, nargs=3,
168        help="The halo for prediction, in ZYX order. Increase the halo to minimize boundary artifacts."
169    )
170    parser.add_argument(
171        "--data_ext", default=".mrc", help="The extension of the tomogram data. By default .mrc."
172    )
173    parser.add_argument(
174        "--checkpoint", "-c", help="Path to a custom model, e.g. from domain adaptation.",
175    )
176    parser.add_argument(
177        "--segmentation_key", "-s",
178        help="If given, the outputs will be saved to an hdf5 file with this key. Otherwise they will be saved as tif.",
179    )
180    parser.add_argument(
181        "--scale", type=float,
182        help="The factor for rescaling the data before inference. "
183        "By default, the scaling factor will be derived from the voxel size of the input data. "
184        "If this parameter is given it will over-ride the default behavior. "
185    )
186    parser.add_argument(
187        "--verbose", "-v", action="store_true",
188        help="Whether to print verbose information about the segmentation progress."
189    )
190    parser.add_argument(
191        "--scalable", action="store_true", help="Use the scalable segmentation implementation. "
192        "Currently this only works for vesicles, mitochondria, or active zones."
193    )
194    parser.add_argument(
195        "--extra_input_path", default=None, help="Filepath to extra inputs, needed for cristae segmentation."
196    )
197    parser.add_argument(
198        "--extra_input_ext", default=".tif", help="File extension for the extra inputs, default is tif."
199    )
200    args = parser.parse_args()
201
202    if args.checkpoint is None:
203        model = get_model(args.model)
204    else:
205        checkpoint_path = args.checkpoint
206        if checkpoint_path.endswith("best.pt"):
207            checkpoint_path = os.path.split(checkpoint_path)[0]
208
209        if os.path.isdir(checkpoint_path):  # Load the model from a torch_em checkpoint.
210            model = torch_em.util.load_model(checkpoint=checkpoint_path)
211        else:
212            model = torch.load(checkpoint_path, weights_only=False)
213        assert model is not None, f"The model from {args.checkpoint} could not be loaded."
214
215    is_2d = "2d" in args.model
216    tiling = parse_tiling(args.tile_shape, args.halo, is_2d=is_2d)
217
218    # If the scale argument is not passed, then we get the average training resolution for the model.
219    # The inputs will then be scaled to match this resolution based on the voxel size from the mrc files.
220    if args.scale is None:
221        model_resolution = get_model_training_resolution(args.model)
222        model_resolution = tuple(model_resolution[ax] for ax in ("yx" if is_2d else "zyx"))
223        scale = None
224    # Otherwise, we set the model resolution to None and use the scaling factor provided by the user.
225    else:
226        model_resolution = None
227        scale = (2 if is_2d else 3) * (args.scale,)
228
229    if args.scalable:
230        if not args.model.startswith(("vesicle", "mito", "active")):
231            raise ValueError(
232                "The scalable segmentation implementation is currently only supported for "
233                f"vesicles, mitochondria, or active zones, not for {args.model}."
234            )
235        segmentation_function = partial(
236            scalable_segmentation, model=model, tiling=tiling, verbose=args.verbose
237        )
238        allocate_output = True
239
240    else:
241        segmentation_function = partial(
242            run_segmentation, model=model, model_type=args.model, verbose=args.verbose, tiling=tiling,
243        )
244        allocate_output = False
245
246    inference_helper(
247        args.input_path, args.output_path, segmentation_function,
248        mask_input_path=args.mask_path, force=args.force, data_ext=args.data_ext,
249        output_key=args.segmentation_key, model_resolution=model_resolution, scale=scale,
250        allocate_output=allocate_output, extra_input_path=args.extra_input_path,
251        extra_input_ext=args.extra_input_ext
252    )
253
254
255def cristae_analysis_helper(
256    crista_path, mito_path, output_root,
257    crista_key=None, mito_key=None,
258    voxel_size=None, tomogram_path=None,
259    membrane_thickness_nm=8.0, border_gap_nm=None,
260    method="skip", membrane_mode="slice_2d",
261    n_jobs=-1, force=False, verbose=False,
262):
263    """Batch-compute per-mitochondrion cristae statistics and save one CSV per input pair.
264
265    This is the headless equivalent of the napari cristae-analysis widget. It matches crista and
266    mitochondria segmentations by sorted order (a single file each, or two directories), computes
267    the statistics via :func:`synapse_net.cristae_analysis.compute_mito_crista_statistics`, and
268    writes the resulting table next to a mirrored input folder structure.
269
270    Args:
271        crista_path: Crista segmentation - a single file or a directory of them.
272        mito_path: Mitochondria instance segmentation - a single file or a directory of them.
273        output_root: Directory where the ``<stem>_cristae_analysis.csv`` tables are written. A single
274            input file writes directly into it; a directory input mirrors the nested folder structure.
275        crista_key: Internal dataset key for the crista segmentation. If None the crista files are
276            assumed to be tif, otherwise hdf5 with this key.
277        mito_key: Internal dataset key for the mitochondria segmentation, analogous to crista_key.
278        voxel_size: Voxel size in nm applied to every file. If None it is read per file from the
279            raw tomogram given via tomogram_path.
280        tomogram_path: Raw tomogram (mrc/rec) - a single file or a directory - used to read the
281            voxel size when voxel_size is None.
282        membrane_thickness_nm: Membrane shell thickness in nm.
283        border_gap_nm: Distance from the volume faces where the membrane is suppressed (nm).
284            Defaults to membrane_thickness_nm when None.
285        method: How the crista orientation anisotropy is computed ("skip", "fast" or "exact").
286        membrane_mode: How the membrane shell is built ("slice_2d" or "shell_3d").
287        n_jobs: Number of workers for the per-mitochondrion computation (-1 = all cores).
288        force: Whether to over-write already present result tables.
289        verbose: Whether to show a progress bar over the mitochondria of each file.
290    """
291    crista_files, crista_root = _get_file_paths(crista_path, ext=".h5" if crista_key else ".tif")
292    mito_files, _ = _get_file_paths(mito_path, ext=".h5" if mito_key else ".tif")
293    if len(crista_files) != len(mito_files):
294        raise ValueError(
295            f"The number of crista ({len(crista_files)}) and mitochondria ({len(mito_files)}) "
296            "segmentations does not match."
297        )
298
299    if voxel_size is not None:
300        voxel_sizes = [voxel_size] * len(crista_files)
301    elif tomogram_path is not None:
302        tomo_files, _ = _get_file_paths(tomogram_path, ext=(".mrc", ".rec"))
303        if len(tomo_files) != len(crista_files):
304            raise ValueError(
305                f"The number of tomograms ({len(tomo_files)}) does not match the number of "
306                f"crista segmentations ({len(crista_files)})."
307            )
308        voxel_sizes = [read_voxel_size(path) for path in tomo_files]
309    else:
310        raise ValueError("Provide either --voxel_size or --tomogram_path to determine the voxel size.")
311
312    for crista_file, mito_file, this_voxel_size in tqdm(
313        zip(crista_files, mito_files, voxel_sizes), total=len(crista_files), desc="Processing files"
314    ):
315        input_folder, input_name = os.path.split(crista_file)
316        fname = os.path.splitext(input_name)[0] + "_cristae_analysis.csv"
317        if crista_root is None:
318            output_path = os.path.join(output_root, fname)
319        else:
320            rel_folder = os.path.relpath(input_folder, crista_root)
321            output_path = os.path.join(output_root, rel_folder, fname)
322
323        if os.path.exists(output_path) and not force:
324            continue
325
326        crista = _load_segmentation(crista_file, crista_key)
327        mito = _load_segmentation(mito_file, mito_key)
328        stats_df = compute_mito_crista_statistics(
329            crista, mito, this_voxel_size,
330            membrane_thickness_nm=membrane_thickness_nm, border_gap_nm=border_gap_nm,
331            method=method, membrane_mode=membrane_mode, n_jobs=n_jobs, verbose=verbose,
332        )
333
334        os.makedirs(os.path.split(output_path)[0], exist_ok=True)
335        stats_df.to_csv(output_path, index=False)
336        print(f"Saved cristae analysis to {output_path}.")
337
338
339def cristae_analysis_cli():
340    parser = argparse.ArgumentParser(
341        description="Compute per-mitochondrion cristae statistics from a crista segmentation and a "
342        "mitochondria instance segmentation, and save the results as a CSV table. This is the "
343        "command-line equivalent of the napari cristae-analysis widget."
344    )
345    parser.add_argument(
346        "--crista_path", "-c", required=True,
347        help="The filepath to the crista segmentation, or a directory containing multiple of them."
348    )
349    parser.add_argument(
350        "--mito_path", "-m", required=True,
351        help="The filepath to the mitochondria instance segmentation, or a directory containing multiple of them."
352    )
353    parser.add_argument(
354        "--output_path", "-o", required=True,
355        help="The filepath to the directory where the result tables will be saved."
356    )
357    parser.add_argument(
358        "--crista_key",
359        help="The key in the crista segmentation file. If not given the crista segmentation is assumed to be tif. "
360        "If given, it is assumed to be an hdf5 file and the key is used to load the internal dataset."
361    )
362    parser.add_argument(
363        "--mito_key",
364        help="The key in the mitochondria segmentation file, analogous to --crista_key."
365    )
366    parser.add_argument(
367        "--voxel_size", type=float,
368        help="The voxel size in nm, applied to all inputs. If not given it is read from the raw tomogram "
369        "passed via --tomogram_path."
370    )
371    parser.add_argument(
372        "--tomogram_path",
373        help="The filepath to the raw tomogram (mrc/rec), or a directory of them, used to read the voxel size "
374        "when --voxel_size is not given."
375    )
376    parser.add_argument(
377        "--membrane_thickness", type=float, default=8.0,
378        help="The membrane shell thickness in nm. By default 8.0."
379    )
380    parser.add_argument(
381        "--border_gap", type=float, default=None,
382        help="The distance from the volume faces where the membrane is suppressed, in nm. "
383        "By default the same as the membrane thickness."
384    )
385    parser.add_argument(
386        "--method", default="skip", choices=["skip", "fast", "exact"],
387        help="How the crista orientation anisotropy is computed. 'skip' (default) does not compute it, "
388        "'fast' uses a downsampled crop (relative only), 'exact' uses the full-resolution structure tensor."
389    )
390    parser.add_argument(
391        "--membrane_mode", default="slice_2d", choices=["slice_2d", "shell_3d"],
392        help="How the membrane shell is built - 'slice_2d' (default, per-Z-slice) or 'shell_3d' (connected 3D shell)."
393    )
394    parser.add_argument(
395        "--n_jobs", type=int, default=-1,
396        help="The number of workers for the per-mitochondrion computation. By default -1 (all cores)."
397    )
398    parser.add_argument(
399        "--force", action="store_true",
400        help="Whether to over-write already present result tables."
401    )
402    parser.add_argument(
403        "--verbose", "-v", action="store_true",
404        help="Whether to show a progress bar over the mitochondria of each file."
405    )
406    args = parser.parse_args()
407
408    cristae_analysis_helper(
409        args.crista_path, args.mito_path, args.output_path,
410        crista_key=args.crista_key, mito_key=args.mito_key,
411        voxel_size=args.voxel_size, tomogram_path=args.tomogram_path,
412        membrane_thickness_nm=args.membrane_thickness, border_gap_nm=args.border_gap,
413        method=args.method, membrane_mode=args.membrane_mode,
414        n_jobs=args.n_jobs, force=args.force, verbose=args.verbose,
415    )
def imod_point_cli():
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    )
def imod_object_cli():
 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    )
def pool_visualization_cli():
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    _visualize_vesicle_pools(
133        args.input_path, args.vesicle_paths, args.table_paths, args.segmentation_paths, args.split_pools
134    )
def segmentation_cli():
138def segmentation_cli():
139    parser = argparse.ArgumentParser(description="Run segmentation.")
140    parser.add_argument(
141        "--input_path", "-i", required=True,
142        help="The filepath to the mrc file or the directory containing the tomogram data."
143    )
144    parser.add_argument(
145        "--output_path", "-o", required=True,
146        help="The filepath to directory where the segmentations will be saved."
147    )
148    model_names = list(_get_model_registry().urls.keys())
149    model_names = ", ".join(model_names)
150    parser.add_argument(
151        "--model", "-m", required=True,
152        help=f"The model type. The following models are currently available: {model_names}"
153    )
154    parser.add_argument(
155        "--mask_path", help="The filepath to a tif file with a mask that will be used to restrict the segmentation."
156        "Can also be a directory with tifs if the filestructure matches input_path."
157    )
158    parser.add_argument("--input_key", "-k", required=False)
159    parser.add_argument(
160        "--force", action="store_true",
161        help="Whether to over-write already present segmentation results."
162    )
163    parser.add_argument(
164        "--tile_shape", type=int, nargs=3,
165        help="The tile shape for prediction, in ZYX order. Lower the tile shape if GPU memory is insufficient."
166    )
167    parser.add_argument(
168        "--halo", type=int, nargs=3,
169        help="The halo for prediction, in ZYX order. Increase the halo to minimize boundary artifacts."
170    )
171    parser.add_argument(
172        "--data_ext", default=".mrc", help="The extension of the tomogram data. By default .mrc."
173    )
174    parser.add_argument(
175        "--checkpoint", "-c", help="Path to a custom model, e.g. from domain adaptation.",
176    )
177    parser.add_argument(
178        "--segmentation_key", "-s",
179        help="If given, the outputs will be saved to an hdf5 file with this key. Otherwise they will be saved as tif.",
180    )
181    parser.add_argument(
182        "--scale", type=float,
183        help="The factor for rescaling the data before inference. "
184        "By default, the scaling factor will be derived from the voxel size of the input data. "
185        "If this parameter is given it will over-ride the default behavior. "
186    )
187    parser.add_argument(
188        "--verbose", "-v", action="store_true",
189        help="Whether to print verbose information about the segmentation progress."
190    )
191    parser.add_argument(
192        "--scalable", action="store_true", help="Use the scalable segmentation implementation. "
193        "Currently this only works for vesicles, mitochondria, or active zones."
194    )
195    parser.add_argument(
196        "--extra_input_path", default=None, help="Filepath to extra inputs, needed for cristae segmentation."
197    )
198    parser.add_argument(
199        "--extra_input_ext", default=".tif", help="File extension for the extra inputs, default is tif."
200    )
201    args = parser.parse_args()
202
203    if args.checkpoint is None:
204        model = get_model(args.model)
205    else:
206        checkpoint_path = args.checkpoint
207        if checkpoint_path.endswith("best.pt"):
208            checkpoint_path = os.path.split(checkpoint_path)[0]
209
210        if os.path.isdir(checkpoint_path):  # Load the model from a torch_em checkpoint.
211            model = torch_em.util.load_model(checkpoint=checkpoint_path)
212        else:
213            model = torch.load(checkpoint_path, weights_only=False)
214        assert model is not None, f"The model from {args.checkpoint} could not be loaded."
215
216    is_2d = "2d" in args.model
217    tiling = parse_tiling(args.tile_shape, args.halo, is_2d=is_2d)
218
219    # If the scale argument is not passed, then we get the average training resolution for the model.
220    # The inputs will then be scaled to match this resolution based on the voxel size from the mrc files.
221    if args.scale is None:
222        model_resolution = get_model_training_resolution(args.model)
223        model_resolution = tuple(model_resolution[ax] for ax in ("yx" if is_2d else "zyx"))
224        scale = None
225    # Otherwise, we set the model resolution to None and use the scaling factor provided by the user.
226    else:
227        model_resolution = None
228        scale = (2 if is_2d else 3) * (args.scale,)
229
230    if args.scalable:
231        if not args.model.startswith(("vesicle", "mito", "active")):
232            raise ValueError(
233                "The scalable segmentation implementation is currently only supported for "
234                f"vesicles, mitochondria, or active zones, not for {args.model}."
235            )
236        segmentation_function = partial(
237            scalable_segmentation, model=model, tiling=tiling, verbose=args.verbose
238        )
239        allocate_output = True
240
241    else:
242        segmentation_function = partial(
243            run_segmentation, model=model, model_type=args.model, verbose=args.verbose, tiling=tiling,
244        )
245        allocate_output = False
246
247    inference_helper(
248        args.input_path, args.output_path, segmentation_function,
249        mask_input_path=args.mask_path, force=args.force, data_ext=args.data_ext,
250        output_key=args.segmentation_key, model_resolution=model_resolution, scale=scale,
251        allocate_output=allocate_output, extra_input_path=args.extra_input_path,
252        extra_input_ext=args.extra_input_ext
253    )
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', n_jobs=-1, force=False, verbose=False):
256def cristae_analysis_helper(
257    crista_path, mito_path, output_root,
258    crista_key=None, mito_key=None,
259    voxel_size=None, tomogram_path=None,
260    membrane_thickness_nm=8.0, border_gap_nm=None,
261    method="skip", membrane_mode="slice_2d",
262    n_jobs=-1, force=False, verbose=False,
263):
264    """Batch-compute per-mitochondrion cristae statistics and save one CSV per input pair.
265
266    This is the headless equivalent of the napari cristae-analysis widget. It matches crista and
267    mitochondria segmentations by sorted order (a single file each, or two directories), computes
268    the statistics via :func:`synapse_net.cristae_analysis.compute_mito_crista_statistics`, and
269    writes the resulting table next to a mirrored input folder structure.
270
271    Args:
272        crista_path: Crista segmentation - a single file or a directory of them.
273        mito_path: Mitochondria instance segmentation - a single file or a directory of them.
274        output_root: Directory where the ``<stem>_cristae_analysis.csv`` tables are written. A single
275            input file writes directly into it; a directory input mirrors the nested folder structure.
276        crista_key: Internal dataset key for the crista segmentation. If None the crista files are
277            assumed to be tif, otherwise hdf5 with this key.
278        mito_key: Internal dataset key for the mitochondria segmentation, analogous to crista_key.
279        voxel_size: Voxel size in nm applied to every file. If None it is read per file from the
280            raw tomogram given via tomogram_path.
281        tomogram_path: Raw tomogram (mrc/rec) - a single file or a directory - used to read the
282            voxel size when voxel_size is None.
283        membrane_thickness_nm: Membrane shell thickness in nm.
284        border_gap_nm: Distance from the volume faces where the membrane is suppressed (nm).
285            Defaults to membrane_thickness_nm when None.
286        method: How the crista orientation anisotropy is computed ("skip", "fast" or "exact").
287        membrane_mode: How the membrane shell is built ("slice_2d" or "shell_3d").
288        n_jobs: Number of workers for the per-mitochondrion computation (-1 = all cores).
289        force: Whether to over-write already present result tables.
290        verbose: Whether to show a progress bar over the mitochondria of each file.
291    """
292    crista_files, crista_root = _get_file_paths(crista_path, ext=".h5" if crista_key else ".tif")
293    mito_files, _ = _get_file_paths(mito_path, ext=".h5" if mito_key else ".tif")
294    if len(crista_files) != len(mito_files):
295        raise ValueError(
296            f"The number of crista ({len(crista_files)}) and mitochondria ({len(mito_files)}) "
297            "segmentations does not match."
298        )
299
300    if voxel_size is not None:
301        voxel_sizes = [voxel_size] * len(crista_files)
302    elif tomogram_path is not None:
303        tomo_files, _ = _get_file_paths(tomogram_path, ext=(".mrc", ".rec"))
304        if len(tomo_files) != len(crista_files):
305            raise ValueError(
306                f"The number of tomograms ({len(tomo_files)}) does not match the number of "
307                f"crista segmentations ({len(crista_files)})."
308            )
309        voxel_sizes = [read_voxel_size(path) for path in tomo_files]
310    else:
311        raise ValueError("Provide either --voxel_size or --tomogram_path to determine the voxel size.")
312
313    for crista_file, mito_file, this_voxel_size in tqdm(
314        zip(crista_files, mito_files, voxel_sizes), total=len(crista_files), desc="Processing files"
315    ):
316        input_folder, input_name = os.path.split(crista_file)
317        fname = os.path.splitext(input_name)[0] + "_cristae_analysis.csv"
318        if crista_root is None:
319            output_path = os.path.join(output_root, fname)
320        else:
321            rel_folder = os.path.relpath(input_folder, crista_root)
322            output_path = os.path.join(output_root, rel_folder, fname)
323
324        if os.path.exists(output_path) and not force:
325            continue
326
327        crista = _load_segmentation(crista_file, crista_key)
328        mito = _load_segmentation(mito_file, mito_key)
329        stats_df = compute_mito_crista_statistics(
330            crista, mito, this_voxel_size,
331            membrane_thickness_nm=membrane_thickness_nm, border_gap_nm=border_gap_nm,
332            method=method, membrane_mode=membrane_mode, n_jobs=n_jobs, verbose=verbose,
333        )
334
335        os.makedirs(os.path.split(output_path)[0], exist_ok=True)
336        stats_df.to_csv(output_path, index=False)
337        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.csv tables 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").
  • 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():
340def cristae_analysis_cli():
341    parser = argparse.ArgumentParser(
342        description="Compute per-mitochondrion cristae statistics from a crista segmentation and a "
343        "mitochondria instance segmentation, and save the results as a CSV table. This is the "
344        "command-line equivalent of the napari cristae-analysis widget."
345    )
346    parser.add_argument(
347        "--crista_path", "-c", required=True,
348        help="The filepath to the crista segmentation, or a directory containing multiple of them."
349    )
350    parser.add_argument(
351        "--mito_path", "-m", required=True,
352        help="The filepath to the mitochondria instance segmentation, or a directory containing multiple of them."
353    )
354    parser.add_argument(
355        "--output_path", "-o", required=True,
356        help="The filepath to the directory where the result tables will be saved."
357    )
358    parser.add_argument(
359        "--crista_key",
360        help="The key in the crista segmentation file. If not given the crista segmentation is assumed to be tif. "
361        "If given, it is assumed to be an hdf5 file and the key is used to load the internal dataset."
362    )
363    parser.add_argument(
364        "--mito_key",
365        help="The key in the mitochondria segmentation file, analogous to --crista_key."
366    )
367    parser.add_argument(
368        "--voxel_size", type=float,
369        help="The voxel size in nm, applied to all inputs. If not given it is read from the raw tomogram "
370        "passed via --tomogram_path."
371    )
372    parser.add_argument(
373        "--tomogram_path",
374        help="The filepath to the raw tomogram (mrc/rec), or a directory of them, used to read the voxel size "
375        "when --voxel_size is not given."
376    )
377    parser.add_argument(
378        "--membrane_thickness", type=float, default=8.0,
379        help="The membrane shell thickness in nm. By default 8.0."
380    )
381    parser.add_argument(
382        "--border_gap", type=float, default=None,
383        help="The distance from the volume faces where the membrane is suppressed, in nm. "
384        "By default the same as the membrane thickness."
385    )
386    parser.add_argument(
387        "--method", default="skip", choices=["skip", "fast", "exact"],
388        help="How the crista orientation anisotropy is computed. 'skip' (default) does not compute it, "
389        "'fast' uses a downsampled crop (relative only), 'exact' uses the full-resolution structure tensor."
390    )
391    parser.add_argument(
392        "--membrane_mode", default="slice_2d", choices=["slice_2d", "shell_3d"],
393        help="How the membrane shell is built - 'slice_2d' (default, per-Z-slice) or 'shell_3d' (connected 3D shell)."
394    )
395    parser.add_argument(
396        "--n_jobs", type=int, default=-1,
397        help="The number of workers for the per-mitochondrion computation. By default -1 (all cores)."
398    )
399    parser.add_argument(
400        "--force", action="store_true",
401        help="Whether to over-write already present result tables."
402    )
403    parser.add_argument(
404        "--verbose", "-v", action="store_true",
405        help="Whether to show a progress bar over the mitochondria of each file."
406    )
407    args = parser.parse_args()
408
409    cristae_analysis_helper(
410        args.crista_path, args.mito_path, args.output_path,
411        crista_key=args.crista_key, mito_key=args.mito_key,
412        voxel_size=args.voxel_size, tomogram_path=args.tomogram_path,
413        membrane_thickness_nm=args.membrane_thickness, border_gap_nm=args.border_gap,
414        method=args.method, membrane_mode=args.membrane_mode,
415        n_jobs=args.n_jobs, force=args.force, verbose=args.verbose,
416    )