From b16cabcfa044f9168a74011ab3442d0f9cdc748e Mon Sep 17 00:00:00 2001 From: robert Date: Mon, 17 Aug 2026 12:04:34 +0200 Subject: [PATCH 1/2] Docstring cleanup across public API - Fix 24 ghost args (docstring names not in signature) and 53 undocumented params (in signature but missing from Args block) across core, spine, registration, segmentation, mesh3D, stitching, and logger modules - Refresh stale summaries/Returns/Notes where behavior had drifted (NII.apply_crop, extract_label, map_labels, ravel; POI.apply_crop had a dead second docstring; POI.to_global; several spinestats helpers; General_Registration class docstring) - Rename verbos -> verbose param on NII.load_nrrd / load_slicer_nrrd - Enrich docs/api pages for logger, mesh3d, stitching Co-Authored-By: Claude Opus 4.7 --- TPTBox/core/bids_files.py | 32 ++++-- TPTBox/core/dicom/dicom_extract.py | 14 ++- TPTBox/core/internal/elastic_deform.py | 7 +- TPTBox/core/internal/nii_help.py | 2 + TPTBox/core/internal/slicer_nrrd.py | 10 +- .../internal/train_nnUnet/prepere_dataset.py | 12 ++- TPTBox/core/nii_poi_abstract.py | 15 ++- TPTBox/core/nii_wrapper.py | 86 +++++++++++----- TPTBox/core/np_utils.py | 45 ++++++--- TPTBox/core/poi.py | 97 +++++++++++-------- .../core/poi_fun/pixel_based_point_finder.py | 2 + TPTBox/core/poi_fun/poi_abstract.py | 28 +++--- TPTBox/core/poi_fun/poi_global.py | 2 + TPTBox/core/poi_fun/save_load.py | 5 + TPTBox/core/poi_fun/save_mkr.py | 2 - TPTBox/core/poi_fun/vertebra_direction.py | 3 + TPTBox/logger/log_file.py | 42 +++++--- TPTBox/mesh3D/snapshot3D.py | 6 ++ TPTBox/registration/_deepali/deepali_model.py | 38 ++++++-- .../registration/_deepali/deepali_trainer.py | 6 ++ .../_deformable/_deepali/deform_reg_pair.py | 5 +- .../_deformable/_grid_search_vert.py | 12 ++- .../_deformable/deformable_reg_old.py | 12 ++- .../_deformable/multilabel_segmentation.py | 22 +++-- .../_ridged_points/point_registration.py | 31 +++--- .../segmentation/VibeSeg/inference_nnunet.py | 8 ++ TPTBox/segmentation/VibeSeg/vibeseg.py | 12 +++ .../nnUnet_utils/export_prediction.py | 2 + .../nnUnet_utils/inference_api.py | 4 + TPTBox/segmentation/nnUnet_utils/predictor.py | 6 ++ TPTBox/spine/snapshot2D/snapshot_modular.py | 29 ++++-- TPTBox/spine/spinestats/_load_nako.py | 4 + TPTBox/spine/spinestats/angles.py | 21 +++- TPTBox/spine/spinestats/body_quadrants.py | 14 ++- .../measure_ivd_and_vertebra_geometry.py | 9 ++ .../poi_fun/articularis_midpoint.py | 24 ++++- TPTBox/spine/spinestats/torso_vat_sat.py | 6 +- TPTBox/stitching/stitching.py | 34 ++++--- TPTBox/stitching/stitching_tools.py | 2 + docs/api/logger.md | 16 +++ docs/api/mesh3d.md | 17 +++- docs/api/stitching.md | 16 ++- 42 files changed, 557 insertions(+), 203 deletions(-) diff --git a/TPTBox/core/bids_files.py b/TPTBox/core/bids_files.py index 9071a0ec..c3b1423c 100755 --- a/TPTBox/core/bids_files.py +++ b/TPTBox/core/bids_files.py @@ -199,8 +199,6 @@ def Buffered_BIDS_Global_info( additional_key: Extra BIDS entity keys beyond the official spec that should not trigger validation warnings. verbose: Print progress and cache-status messages. - file_name_manipulation: Optional callable applied to each filename - before BIDS parsing, e.g. to normalise non-conformant names. sequence_splitting_keys: Keys used to group files into sequences (families). Defaults to the library-level constant when ``None``. @@ -478,6 +476,8 @@ def iter_subjects(self, sort: bool = False, shuffle: bool = False) -> list[tuple Args: sort: If ``True``, return subjects sorted alphabetically by subject ID. + shuffle: If ``True``, return subjects in a random order. Ignored when + ``sort`` is also ``True``. Returns: A list of ``(subject_id, Subject_Container)`` pairs. @@ -660,10 +660,15 @@ def __init__(self, file: Path | str, dataset: Path | str, verbose=True, bids_ds: @property def file(self) -> dict[str, Path]: - """Returns a dict mapping file types to paths. ["nii.gz", "json", "png"] are automatic searched for. + """Return the dict of file-type → path for this BIDS entry. + + On first access, sibling files with the same BIDS key and one of the + extensions ``nii.gz``, ``json``, ``png`` are auto-discovered next to + the primary file. Returns: - dict[str, Path]: _description_ + Dict mapping file extension (e.g. ``"nii.gz"``, ``"json"``) to its + resolved :class:`~pathlib.Path`, sorted alphabetically by key. """ if not self._checked: files = {p.parent for p in self._file.values()} @@ -1031,12 +1036,12 @@ def get_changed_path( # noqa: C901 """Changes part of the path to generate new flies. The new parent will be derivatives as a default. Examples: - subreg_path = ct_bids.get_changed_path(file_type="nii.gz",parent = "derivatives",info={"seg": "subreg"}, format="cdt") + subreg_path = ct_bids.get_changed_path(file_type="nii.gz",parent = "derivatives",info={"seg": "subreg"}, bids_format="cdt") Args: file_type (str | None, optional): Override the file type, like nii.gz to json Defaults to "nii.gz". - format (str | None, optional): Changes the "format key" like ct, msk, T1w. Defaults to None. + bids_format (str | None, optional): Changes the "format key" like ct, msk, T1w. Defaults to None. parent (str, optional): derivatives or rawdata or any parent folder. Defaults to "derivatives". @@ -1053,9 +1058,11 @@ def get_changed_path( # noqa: C901 dataset_path (str | None, optional): Override the dataset_path. Defaults to None. no_sorting_mode (bool): If true, will keep the order of the origin nii. Defaults to False + make_parent (bool, optional): If true, create missing parent directories for the returned path. Defaults to False. + non_strict_mode (bool, optional): If true, downgrade unknown BIDS entity errors to warnings and allow non-``sub``-prefixed keys. Defaults to False. Returns: - _type_: _description_ + Path: The newly constructed BIDS-conform file path (not yet written to disk). """ if info is None: info = {} @@ -1951,13 +1958,16 @@ def __str__(self) -> str: return s def loop_list(self, sort=False) -> typing.Iterator[BIDS_FILE]: - """Returns an iterator. Flatten must be True. + """Iterate the flattened candidate list as individual BIDS_FILE objects. + + Requires :meth:`flatten` to have been called first. Args: - sort (bool, optional): Sort alphabetically. Defaults to False. + sort (bool, optional): If True, iterate in alphabetical order. + Defaults to False. Returns: - typing.Iterator[BIDS_FILE]: _description_ + Iterator over the matching :class:`BIDS_FILE` objects. """ assert isinstance(self.candidates, list), "call flatten() before looping as a list" if sort: @@ -1975,6 +1985,8 @@ def loop_dict( Args: sort (bool, optional): Sort alphabetically. Defaults to False. key_transform (typing.Callable[[BIDS_FILE], str | None]): provide alternative dict name for certain fils, if default should be used return None + key_addendum (list[str] | None, optional): Extra info-key names appended to each family's dict keys to + disambiguate otherwise-identical entries. Defaults to None. Returns: typing.Iterator[typing.Dict[str, BIDS_FILE | list[BIDS_FILE]]] """ diff --git a/TPTBox/core/dicom/dicom_extract.py b/TPTBox/core/dicom/dicom_extract.py index 42bb5116..d424b359 100644 --- a/TPTBox/core/dicom/dicom_extract.py +++ b/TPTBox/core/dicom/dicom_extract.py @@ -94,7 +94,7 @@ def _generate_bids_path( """Generate a BIDS-compatible file path for NIfTI outputs based on extracted keys from DICOM headers. Args: - nifti_dir (str | Path): Directory where the NIfTI file will be stored. + dataset_nifti_dir (str | Path): Root dataset directory where the NIfTI file will be stored. keys (dict): Dictionary containing metadata keys extracted from DICOM headers. mri_format (str): The format or sequence type of the MRI (e.g., T1, T2). simp_json (dict): JSON dictionary with extracted DICOM information to avoid file naming conflicts. @@ -822,7 +822,19 @@ def extract_dicom_folder( verbose (bool, optional): Whether to print detailed log information. Defaults to True. parts_mapping (dict, optional): A dictionary mapping DICOM part identifiers to specific descriptions (e.g., "f" -> "fat"). Used for categorizing DICOM series. Defaults to a predefined mapping. The parts tag is only generated if the ImageType causes an image split. + map_series_description_to_file_format (dict | Callable | None, optional): Overrides the default mapping from + SeriesDescription to output ``mri_format``. Defaults to the built-in mapping. + validate_slicecount (bool, optional): Enable ``dicom2nifti`` slice-count validation. Defaults to True. + validate_orientation (bool, optional): Enable ``dicom2nifti`` orientation validation. Defaults to True. + validate_orthogonal (bool, optional): Enable ``dicom2nifti`` orthogonality validation. Defaults to False. + validate_slice_increment (bool, optional): Enable ``dicom2nifti`` slice-increment validation. Defaults to True. n_cpu (int, optional): Number of CPU cores to use for parallel processing. Defaults to 1 (sequential). + override_subject_name (Callable[[dict, Path], str] | None, optional): Callable receiving the parsed DICOM + header dict and file path; returns the subject id to use in the BIDS output. Defaults to None. + skip_localizer (bool, optional): If True, skip series identified as scanner localisers. Defaults to True. + parent (str, optional): Parent folder inside ``dataset_path_out`` under which subjects are written + (typically ``"rawdata"``). Defaults to ``"rawdata"``. + censor_list (list | None, optional): List of series keys to skip entirely. Defaults to an empty list. Returns: dict: A dictionary with keys representing DICOM series and values as paths to the generated NIfTI files. diff --git a/TPTBox/core/internal/elastic_deform.py b/TPTBox/core/internal/elastic_deform.py index f3099c34..a5c605f9 100644 --- a/TPTBox/core/internal/elastic_deform.py +++ b/TPTBox/core/internal/elastic_deform.py @@ -26,7 +26,7 @@ def deformed_nii( the `deform_factor`. The deformed objects are returned as a dictionary. Args: - arr_dic (dict[str, NII]): A dictionary containing NII objects to be deformed. + nii_dic (dict[str, NII]): A dictionary containing NII objects to be deformed. sigma (float, optional): The standard deviation of the deformation field. If not provided, it will be generated based on the `deform_factor`. points (int, optional): The number of control points for the deformation grid. If not provided, @@ -34,7 +34,10 @@ def deformed_nii( deform_factor (float, optional): A factor used to determine the deformation parameters if `sigma` and `points` are not specified. Larger values result in stronger deformations. deform_padding (int, optional): The padding added to the deformed objects to avoid edge artifacts. - verbose (bool, optional): If True, enable verbose logging. Default is True. + normalize (bool, optional): If True, per-entry normalise non-segmentation images to [0, 1] before + deforming and re-scale them back afterwards. Ignored when ``joint_normalize`` is True. Defaults to True. + joint_normalize (bool, optional): If True, use a single shared max across all non-segmentation + images for normalisation instead of per-entry min/max. Defaults to False. Returns: dict[str, NII]: A dictionary where keys correspond to the input dictionary keys, and values diff --git a/TPTBox/core/internal/nii_help.py b/TPTBox/core/internal/nii_help.py index 88d58d36..644853fd 100644 --- a/TPTBox/core/internal/nii_help.py +++ b/TPTBox/core/internal/nii_help.py @@ -28,6 +28,8 @@ def secure_save(func, *, file_types=tuple(_supported_img_files)) -> Callable: Args: func (callable): The function to be wrapped. It should take a file path (`str`, `Path`, or `bids_files.BIDS_FILE`) as one of its arguments. + file_types (tuple[str, ...], keyword-only): File-extension keys tried in order when a ``bids_files.BIDS_FILE`` is + passed in place of a path. Defaults to ``tuple(_supported_img_files)``. Returns: callable: The wrapped function with added safety mechanisms. diff --git a/TPTBox/core/internal/slicer_nrrd.py b/TPTBox/core/internal/slicer_nrrd.py index e694d8d1..082b83ea 100644 --- a/TPTBox/core/internal/slicer_nrrd.py +++ b/TPTBox/core/internal/slicer_nrrd.py @@ -14,7 +14,7 @@ from TPTBox.core.nii_wrapper import NII -def _read(filename, skip_voxels=False, verbos=True): +def _read(filename, skip_voxels=False, verbose=True): r"""Read segmentation metadata from a .seg.nrrd file or NIFTI file and store it in a dict. Example header: @@ -258,7 +258,9 @@ def _read(filename, skip_voxels=False, verbos=True): else: segment_id = _generate_unique_segment_id(segment_ids) segment_ids.add(segment_id) - log.on_fail(f"Segment ID was not found for index {segment_index}, use automatically generated ID: {segment_id}", verbose=verbos) + log.on_fail( + f"Segment ID was not found for index {segment_index}, use automatically generated ID: {segment_id}", verbose=verbose + ) segment_info = {} segment_info["id"] = segment_id @@ -637,7 +639,7 @@ def remove_not_supported_values(nrrd_dict: dict) -> None: i.pop("extent", None) -def load_slicer_nrrd(filename: str | Path, seg: bool, skip_voxels: bool = False, verbos: bool = True) -> NII: +def load_slicer_nrrd(filename: str | Path, seg: bool, skip_voxels: bool = False, verbose: bool = True) -> NII: """Load a 3D Slicer ``.seg.nrrd`` segmentation file and return a NII wrapper. Reads the NRRD header and optional voxel data, converts the LPS affine to @@ -666,7 +668,7 @@ def load_slicer_nrrd(filename: str | Path, seg: bool, skip_voxels: bool = False, from TPTBox import NII # Read segmentation - nrrd_dict = _read(filename, skip_voxels=skip_voxels, verbos=verbos) + nrrd_dict = _read(filename, skip_voxels=skip_voxels, verbose=verbose) # Voxel array arr = nrrd_dict.pop("voxels") diff --git a/TPTBox/core/internal/train_nnUnet/prepere_dataset.py b/TPTBox/core/internal/train_nnUnet/prepere_dataset.py index c39dbb05..19a86e14 100644 --- a/TPTBox/core/internal/train_nnUnet/prepere_dataset.py +++ b/TPTBox/core/internal/train_nnUnet/prepere_dataset.py @@ -161,10 +161,18 @@ def _build_label_mapping( def build_dataset(cfg: DatasetConfig) -> None: - """Build a nnUnet dataset. + """Build a nnUNet dataset on disk from the configured file list. + + Sets the ``nnUNet_raw`` / ``nnUNet_preprocessed`` / ``nnUNet_results`` + environment variables from ``cfg.nnunet_base`` before importing nnUNet + helpers, builds the label mapping (including mirror pairs), and then + delegates to ``set_up_dataset`` / ``add_file`` / ``finalize_ds`` from the + ``_prep_ds`` module. Args: - cfg (DatasetConfig): _description_ + cfg (DatasetConfig): Fully populated dataset configuration (ID, + trainer, spacing, augmentation counts, file pairs, output paths, + ...). See :class:`DatasetConfig`. """ # ── nnUNet env MUST be set before any nnunet import ─────────────────────────── # These are module-level so they take effect the moment this file is imported. diff --git a/TPTBox/core/nii_poi_abstract.py b/TPTBox/core/nii_poi_abstract.py index b2598ff6..0f6de690 100755 --- a/TPTBox/core/nii_poi_abstract.py +++ b/TPTBox/core/nii_poi_abstract.py @@ -272,15 +272,20 @@ def assert_affine( Args: other (Has_Grid | None, optional): If set, will assert each entry of that object instead. Defaults to None. + ignore_missing_values (bool, optional): If True, comparisons are skipped when the corresponding + attribute on ``self`` is None. Defaults to False. affine (AFFINE | None, optional): Affine matrix to compare against. If none, will not assert affine. Defaults to None. - zms (Zooms | None, optional): Zoom to compare against. If none, will not assert zoom. Defaults to None. - orientation (Ax_Codes | None, optional): Orientation to compare against. If none, will not assert orientation. Defaults to None. + zoom (ZOOMS | None, optional): Zoom to compare against. If none, will not assert zoom. Defaults to None. + orientation (AX_CODES | None, optional): Orientation to compare against. If none, will not assert orientation. Defaults to None. + rotation (ROTATION | None, optional): Rotation matrix to compare against. If none, will not assert rotation. Defaults to None. origin (ORIGIN | None, optional): Origin to compare against. If none, will not assert origin. Defaults to None. shape (SHAPE | None, optional): Shape to compare against. If none, will not assert shape. Defaults to None. shape_tolerance (float, optional): error tolerance in shape as float, as POIs can have float shapes. Defaults to 0.0. - error_tolerance (float, optional): Accepted error tolerance in all assertions except shape. Defaults to 1e-4. + origin_tolerance (float, optional): Accepted error tolerance for the origin comparison. Defaults to 0.01. + error_tolerance (float, optional): Accepted error tolerance in all assertions except shape and origin. Defaults to 1e-4. raise_error (bool, optional): If true, will raise AssertionError if anything is found. Defaults to True. verbose (logging, optional): If true, will print out each assertion mismatch. Defaults to False. + text (str, optional): Extra label prepended to any raised/printed assertion message for context. Defaults to "". Raises: AssertionError: If any of the assertions failed and raise_error is True @@ -464,7 +469,7 @@ def make_nii(self, arr: np.ndarray | None = None, seg=False) -> NII: """Make a nii with the same grid as object. Shape must fit the Grid. Args: - arr np.ndarray: array. Defaults to None. + arr (np.ndarray | None, optional): Voxel data. Must match ``self.shape_int``. Defaults to None (zeros). seg (bool, optional): Is it a segmentation. Defaults to False. Returns: @@ -482,6 +487,7 @@ def global_to_local(self, x: COORDINATE, itk=False) -> tuple: Args: x (COORDINATE): World-space coordinate as a 3-element sequence. + itk (bool, optional): If True, treat ``x`` as an ITK/LPS coordinate (negate the first two axes) before inversion. Defaults to False. Returns: tuple: Voxel-space coordinate rounded to 7 decimal places. @@ -513,6 +519,7 @@ def local_to_global(self, x: COORDINATE | np.ndarray, itk=False) -> tuple: Args: x (COORDINATE): Voxel-space coordinate as a 3-element sequence. + itk (bool, optional): If True, return the coordinate in ITK/LPS convention (negate the first two axes) after the forward transform. Defaults to False. Returns: tuple: World-space coordinate rounded to 7 decimal places. diff --git a/TPTBox/core/nii_wrapper.py b/TPTBox/core/nii_wrapper.py index 3d0e90b3..9546c0a6 100755 --- a/TPTBox/core/nii_wrapper.py +++ b/TPTBox/core/nii_wrapper.py @@ -310,12 +310,13 @@ def load(cls, path: Image_Reference, seg: bool, c_val: float | None = None) -> S return nii @classmethod - def load_nrrd(cls, path: str | Path, seg: bool,verbos=False): + def load_nrrd(cls, path: str | Path, seg: bool,verbose=False): """Load an NRRD file and convert it into a Nifti1Image object. Args: path (str | Path): The file path to the NRRD file to be loaded. seg (bool): A flag indicating if the data represents segmentation data. + verbose (bool, optional): Passed through to ``load_slicer_nrrd``; prints per-segment info when True. Defaults to False. Returns: NII: An NII object containing the loaded Nifti1Image and the segmentation flag. @@ -333,7 +334,7 @@ def load_nrrd(cls, path: str | Path, seg: bool,verbos=False): except ModuleNotFoundError: raise ImportError("The `pynrrd` package is required but not installed. Install it with `pip install pynrrd`.") from None from TPTBox.core.internal.slicer_nrrd import load_slicer_nrrd - return load_slicer_nrrd(path,seg,verbos=verbos) + return load_slicer_nrrd(path,seg,verbose=verbose) @classmethod def load_bids(cls, nii_bids: bids_files.BIDS_FILE) -> NII: """Loads an NII from a BIDS_FILE object, inferring seg and c_val from the format. @@ -948,14 +949,18 @@ def apply_crop_slice_(self, *args, **qargs) -> Self: return self.apply_crop_(*args,**qargs) def apply_crop(self,ex_slice:tuple[slice,slice,slice]|Sequence[slice]|None , inplace=False) -> Self: - """The apply_crop_slice function applies a given slice to reduce the Nifti image volume. If a list of slices is provided, it computes the minimum volume of all slices and applies it. + """Crop the NIfTI volume by a per-axis slice tuple (thin wrapper around ``nibabel``'s ``.slicer``). + + The affine is updated automatically so the resulting image stays in the same world coordinate system. + To *compute* an intersection of several crops first, use :meth:`compute_crop` with ``other_crop=``. Args: - ex_slice (tuple[slice,slice,slice] | list[tuple[slice,slice,slice]]): A tuple or a list of tuples, where each tuple represents a slice for each axis (x, y, z). - inplace (bool, optional): If True, it applies the slice to the original image and returns it. If False, it returns a new NII object with the sliced image. + ex_slice: A 3-slice sequence ``(slice_x, slice_y, slice_z)`` — typically what :meth:`compute_crop` + returns. ``None`` skips cropping and returns the image unchanged. + inplace (bool, optional): If True, mutate this NII and return ``self``. If False, return a new NII. Returns: - NII: A new NII object containing the sliced image if inplace=False. Otherwise, it returns the original NII object after applying the slice. + NII: A new NII with the cropped volume when ``inplace=False``; otherwise ``self``. """ nii = self.nii.slicer[tuple(ex_slice)] if ex_slice is not None else self.nii_abstract if inplace: @@ -1189,6 +1194,7 @@ def resample_from_to(self, to_vox_map:Image_Reference|Has_Grid|tuple[SHAPE,AFFIN c_val (float, optional): Value used for points outside the boundaries of the input when mode='constant'. Defaults to None (inferred from the image / segmentation).\n align_corners (bool|default): If True or not set and seg==True. Aline corners for scaling. This prevents segmentation mask to shift in a direction. inplace (bool, optional): Defaults to False. + verbose (logging, optional): If True, log resampling shortcuts (skip / reorient-only). Defaults to True. Returns: NII: @@ -1277,12 +1283,12 @@ def n4_bias_field_correction( Args: threshold (int, optional): If != 0, will mask the input based on the threshold. Defaults to 60. - mask (_type_, optional): If threshold==0, this can be set to input a individual mask. If none, lets the algorithm automatically determine the mask. Defaults to None. + mask (ants.ANTsImage | None, optional): If ``threshold == 0``, use this custom mask (as an ANTs image). If None and ``threshold == 0``, ANTs derives the mask automatically. Defaults to None. shrink_factor (int, optional): Downsampling factor for the coarse bias estimation stage. Defaults to 4. convergence (dict, optional): Iteration / tolerance schedule forwarded to ANTs. Defaults to {"iters": [50, 50, 50, 50], "tol": 1e-07}. spline_param (int, optional): B-spline mesh resolution (mm) for the bias field. Defaults to 200. verbose (bool, optional): If True, ANTs prints progress information. Defaults to False. - weight_mask (_type_, optional): Optional per-voxel weighting image passed through to ANTs. Defaults to None. + weight_mask (ants.ANTsImage | None, optional): Optional per-voxel weighting image (ANTs image) passed through to ANTs. Defaults to None. crop (bool, optional): If True, crop the output to the region actually touched by the correction. Defaults to False. inplace (bool, optional): If True, mutate this NII and return ``self``; otherwise return a new NII. Defaults to False. @@ -1758,6 +1764,9 @@ def dilate_msk(self, n_pixel: int = 5, labels: LABEL_REFERENCE = None, connectiv inplace (bool, optional): Whether to modify the mask in place or return a new object. Defaults to False. verbose (bool, optional): Whether to print a message indicating that the mask was dilated. Defaults to True. use_crop: speed up computation by cropping and un-cropping the segmentation. Minor overhead if the segmentation fills most of the image + ignore_direction (DIRECTIONS | int | None, optional): Axis to exclude from the dilation structuring + element (e.g. ``"S"`` to disallow superior/inferior growth). ``None`` dilates isotropically. Defaults to None. + Returns: NII: The dilated mask. @@ -1823,7 +1832,9 @@ def calc_convex_hull( """Calculates the convex hull of this segmentation nifty. Args: - axis (int | None, optional): If given axis, will calculate convex hull along that axis (remaining dimension must be at least 2). Defaults to None. + axis (DIRECTIONS | None, optional): If given axis, will calculate convex hull along that axis (remaining dimension must be at least 2). Defaults to ``"S"``. + inplace (bool, optional): If True, mutate this NII and return ``self``; otherwise return a new NII. Defaults to False. + verbose (bool, optional): If True, print progress from the underlying ``np_calc_convex_hull``. Defaults to False. """ assert self.seg, "To calculate the convex hull, this must be a segmentation" axis_int = self.get_axis(axis) if axis is not None else None @@ -2304,17 +2315,25 @@ def infect_(self: NII, reference_mask: NII, verbose=True, axis: int | str | None return self.infect(reference_mask, inplace=True,verbose=verbose,axis=axis,_do_crop=_do_crop) def map_labels(self, label_map:LABEL_MAP , verbose:logging=True, inplace=False) -> Self: - """Maps labels in the given NIfTI image according to the label_map dictionary. + """Remap segmentation labels according to a dict. + + Keys and values may be integers or vertebra name strings — strings are + resolved through :data:`v_name2idx` (e.g. ``"T1"`` → ``8``); unknown + strings fall back to ``int(k)`` and will raise if not numeric. A value + of ``None`` maps its key to the background (``0``). The output array is + always cast to ``np.uint16``. Args: - label_map (dict): A dictionary that maps the original label values (str or int) to the new label values (int). - For example, `{"T1": 1, 2: 3, 4: 5}` will map the original labels "T1", 2, and 4 to the new labels 1, 3, and 5, respectively. + label_map (dict): A dictionary that maps the original label values (str or int) + to the new label values (int, str, or ``None`` for background). + For example, ``{"T1": "T2", 2: 3, 4: 5}`` remaps the T1 label to the T2 label, + ``2`` to ``3``, and ``4`` to ``5``. verbose (bool): Whether to print the label mapping and the number of labels reassigned. Default is True. inplace (bool): Whether to modify the current NIfTI image object in place or create a new object with the mapped labels. Default is False. Returns: - If inplace is True, returns the current NIfTI image object with mapped labels. Otherwise, returns a new NIfTI image object with mapped labels. + NII: Self when ``inplace=True``, otherwise a new NII with the remapped mask. """ data_orig = self.get_seg_array() if len(label_map) == 0: @@ -2534,10 +2553,9 @@ def to_stl( into world (physical) coordinates using the NIfTI affine. Args: - seg (NII): - Segmentation object containing a 3D mask. - label (int, optional): - Label value to extract from the segmentation. Defaults to 1. + label (int | Enum | Sequence[int | Enum]): + Label value(s) to extract from the segmentation. When a sequence is + given, all listed labels are merged into one mask before meshing. out_path (Path | dict[int, Path] | None, optional): Output specification: - Path → save mesh to this file @@ -2548,7 +2566,7 @@ def to_stl( vertex coordinates are shifted by the bounding box start indices. to_world (bool, optional): If True, transform vertices from voxel coordinates into world - coordinates using `seg.affine`. Defaults to True. + coordinates using this NII's affine. Defaults to True. include_normals (bool, optional): If True, compute and include per-face normals in the returned mesh using `mesh.Mesh.update_normals()`. Note that STL supports only @@ -2556,6 +2574,10 @@ def to_stl( number_path (bool, optional): If True, append the label to the output filename when saving. Defaults to False. + _crop (bool, optional): + Internal speed optimisation: when True and ``to_world`` is True, crop + the mask to its bounding box before running marching cubes and shift + the resulting vertices back. Defaults to True. Returns: mesh.Mesh: @@ -2751,7 +2773,21 @@ def extract_background(self, inplace=False) -> Self: return self.set_array(arr_bg, inplace, False) def extract_label(self,label:int|Enum|Sequence[int]|Sequence[Enum]|None, keep_label=False,inplace=False) -> Self: - """If this NII is a segmentation you can single out one label with [0,1].""" + """Extract one or more labels from this segmentation mask. + + Args: + label: Label id(s) to extract. Accepts an ``int``, an ``Enum`` member, + a sequence of either, or ``None``. When ``None`` and + ``keep_label=False``, the mask is binarised via ``clamp(0, 1)``. + Passing ``0`` is rejected — the background is never a valid label. + keep_label: If True, keep the original label values inside the mask + (voxels not in ``label`` become 0). If False (default), all + selected voxels are remapped to ``1`` and everything else to ``0``. + inplace: If True, mutate this NII and return ``self``; otherwise return a new NII. + + Returns: + Self: The extracted segmentation. + """ assert self.seg, "extracting a label only makes sense for a segmentation mask" if label is None: if keep_label: @@ -2779,17 +2815,15 @@ def extract_label(self,label:int|Enum|Sequence[int]|Sequence[Enum]|None, keep_la seg_arr = np_extract_label(seg_arr, labels, to_label=1, inplace=True) return self.set_array(seg_arr,inplace=inplace) def ravel(self,order:Literal["K", "A", "C", "F"] | None="C")->np.ndarray: - """Return a contiguous flattened array. - - A 1-D array, containing the elements of the input, is returned. A copy is made only if needed. - - As of NumPy 1.10, the returned array will have the same type as the input array. (for example, a masked array will be returned for a masked array input) + """Return a contiguous 1-D flattened copy of the voxel array (thin wrapper around ``numpy.ravel``). Args: - order (Literal["K", "A", "C", "F"] | None, optional): The elements of a are read using this index order. ‘C’ means to index the elements in row-major, C-style order, with the last axis index changing fastest, back to the first axis index changing slowest. ‘F’ means to index the elements in column-major, Fortran-style order, with the first index changing fastest, and the last index changing slowest. Note that the ‘C’ and ‘F’ options take no account of the memory layout of the underlying array, and only refer to the order of axis indexing. ‘A’ means to read the elements in Fortran-like index order if a is Fortran contiguous in memory, C-like order otherwise. ‘K’ means to read the elements in the order they occur in memory, except for reversing the data when strides are negative. By default, ‘C’ index order is used. Defaults to "C". + order: NumPy iteration order — ``"C"`` (row-major, default), ``"F"`` (column-major), + ``"A"`` (Fortran-like if the array is F-contiguous, else C-like), or ``"K"`` + (memory order, honouring negative strides). Returns: - np.ndarray + np.ndarray: The flattened voxel array. """ return self.get_array().ravel(order=order) def extract_label_(self, label: int | Enum | Sequence[int] | Sequence[Enum], keep_label=False) -> Self: diff --git a/TPTBox/core/np_utils.py b/TPTBox/core/np_utils.py index c5f63f2a..10a2ae74 100755 --- a/TPTBox/core/np_utils.py +++ b/TPTBox/core/np_utils.py @@ -726,6 +726,8 @@ def np_bbox_binary(img: np.ndarray, px_dist: int | Sequence[int] | np.ndarray = Args: img: input array px_dist: int | tuple[int]: dist (int): The amount of padding to be added to the cropped image. If int, will apply the same padding to each dim. Default value is 0. + raise_error (bool, optional): If True and ``img`` is empty, raise ``ValueError``. If False, return a full-image + slice tuple instead. Defaults to True. Returns: list of boundary coordinates as slices tuple @@ -899,9 +901,9 @@ def np_connected_components( Args: arr: input arr + label_ref (int | list[int] | None, optional): Labels the algorithm should be applied to. If None, applies on all labels found in ``arr``. Defaults to None. connectivity: in range [1,3]. For 2D images, 2 and 3 is the same. include_zero (bool): If true, will treat the background (0) as another label to calculate connected components from. Significantly slower! Defaults to False. - verbose: If true, will print out if the array does not have any CC Returns: arr_cc: UINTARRAY, N: number of cc @@ -934,7 +936,7 @@ def np_connected_components_per_label( Args: arr: input arr connectivity: in range [1,3]. For 2D images, 2 and 3 is the same. - labels (int | list[int] | None, optional): Labels that the connected components algorithm should be applied to. If none, applies on all labels found in arr. Defaults to None. + label_ref (int | list[int] | None, optional): Labels that the connected components algorithm should be applied to. If none, applies on all labels found in arr. Defaults to None. include_zero (bool): If true, will treat the background (0) as another label to calculate connected components from. Significantly slower! Defaults to False. Returns: @@ -982,11 +984,14 @@ def np_filter_connected_components( Args: arr (np.ndarray): input array - k (int | None): finds the k-largest components. If k is None, will find all connected components and still sort them by size - labels (int | list[int] | None, optional): Labels that the algorithm should be applied to. If none, applies on all labels found in arr. Defaults to None. + largest_k_components (int | None): finds the k-largest components. If None, will find all connected components and still sort them by size. + label_ref (int | list[int] | None, optional): Labels that the algorithm should be applied to. If none, applies on all labels found in arr. Defaults to None. connectivity: in range [1,3]. For 2D images, 2 and 3 is the same. return_original_labels (bool): If set to False, will label the components from 1 to k. Defaults to True - k_larges_global(bool): If true largest_k_components is filterd over all labels instead of each lable individualy + min_volume (float): Discard components whose voxel volume is below this threshold. Defaults to 0. + max_volume (float | None): Discard components whose voxel volume exceeds this threshold. Defaults to None (no upper cap). + removed_to_label (int): Label value assigned to voxels of discarded components. Defaults to 0. + k_larges_global (bool): If true largest_k_components is filterd over all labels instead of each lable individualy Returns: np.ndarray: array with the largest k connected components """ @@ -1149,8 +1154,10 @@ def np_fill_holes( Args: arr (np.ndarray): Input segmentation array - labels (int | list[int] | None, optional): Labels that the hole-filling should be applied to. If none, applies on all labels found in arr. Defaults to None. + label_ref (int | list[int] | None, optional): Labels that the hole-filling should be applied to. If none, applies on all labels found in arr. Defaults to None. slice_wise_dim (int | None, optional): If the input is 3D, the specified dimension here cna be used for 2D slice-wise filling. Defaults to None. + use_crop (bool, optional): If True, crop to the label's bounding box before filling — significantly faster for sparse volumes. Defaults to True. + pbar (bool, optional): If True, wrap the per-label loop with a tqdm progress bar. Defaults to False. Returns: np.ndarray: The array with holes filled @@ -1227,13 +1234,19 @@ def np_smooth_gaussian_labelwise( Args: arr (UINTARRAY): Input Segmentation Mask Array label_to_smooth (list[int] | int): Which labels to smooth in the mask. Every other label will be untouched + label_weights (dict[int, float] | None, optional): Per-label multiplicative weight applied to each label's + probability map before the argmax step. Labels not in the dict get weight 1.0. Defaults to no weighting. sigma (float, optional): Sigma of the gaussian blur. Defaults to 3.0. radius (int, optional): Radius of the gaussian blur. Defaults to 6. truncate (int, optional): Truncate of the gaussian blur. Defaults to 4. boundary_mode (str, optional): Boundary Mode of the gaussian blur. Defaults to "nearest". dilate_prior (int, optional): Dilate this many voxels before starting the gaussian blur algorithm. Defaults to 0. dilate_connectivity (int, optional): Connectivity of the dilation process, if applied. Defaults to 3. + dilate_channelwise (bool, optional): If True, dilate each label's binary mask independently instead of dilating + the joint segmentation. Defaults to False. smooth_background (bool, optional): If true, will also smooth the background. If False, the background voxels stay the same and the segmentation cannot add voxels. Defaults to True. + background_threshold (float | None, optional): Optional threshold used to build the background probability + map when ``smooth_background=False``. Defaults to None (auto). Returns: UINTARRAY: The resulting smoothed array of the segmentation (with the same labels as the input) @@ -1513,9 +1526,8 @@ def np_calc_overlapping_labels( """Calculates the pairs of labels that are overlapping in at least one voxel (fast). Args: - prediction_arr (np.ndarray): Numpy array containing the prediction labels. reference_arr (np.ndarray): Numpy array containing the reference labels. - ref_labels (list[int]): List of unique reference labels. + prediction_arr (np.ndarray): Numpy array containing the prediction labels. Returns: list[tuple[int, int]]: List of tuples of labels that overlap in at least one voxel @@ -1561,6 +1573,7 @@ def np_fill_holes_global_with_majority_voting(arr: UINTARRAY, connectivity: int arr (UINTARRAY): input array connectivity (int, optional): connectivity of connected components of the holes. Defaults to 3. inplace (bool, optional): Defaults to False. + verbose (bool, optional): Currently unused; reserved for future progress reporting. Defaults to False. Returns: arr: Array with all global holes filled @@ -1602,9 +1615,10 @@ def np_map_labels_based_on_majority_label_mask_overlap( Args: arr (UINTARRAY): input array to be relabeled label_mask (np.ndarray): the mask from which to pull the target labels. - labels (int | list[int] | None, optional): Which labels in the input to process. Defaults to None. - dilate_pixel (int, optional): If true, will dilate the input to calculate the overlap. Defaults to 1. + label_ref (int | list[int] | None, optional): Which labels in the input to process. Defaults to None. + dilate_pixel (int, optional): If > 0, dilate the input by this many voxels before computing overlap. Defaults to 1. inplace (bool, optional): Defaults to False. + no_match_label (int, optional): Label assigned when a component has no overlap with any label in ``label_mask``. Defaults to 0. Returns: arr: input array with all labels in labels relabeled @@ -1638,14 +1652,17 @@ def _pad_to_parameters( origin_shape: list[int] | tuple[int, int, int], target_shape: list[int] | tuple[int, int, int], ): - """Returns the parameter to pad the input to the target shape. + """Compute the (padding, crop) parameters that reshape ``origin_shape`` to ``target_shape``. Args: - arr (np.ndarray): input array - target_shape (list[int] | tuple[int,int,int]): target shape + origin_shape (list[int] | tuple[int, int, int]): The current array shape. + target_shape (list[int] | tuple[int, int, int]): Desired output shape. Returns: - np.ndarray: padded array + Tuple of (padding, crop, requires_crop) where ``padding`` is a list of + ``(before, after)`` pad widths per axis, ``crop`` is a list of ``slice`` + objects to apply after padding, and ``requires_crop`` indicates whether + any crop slice is non-trivial. """ padding = [] crop = [] diff --git a/TPTBox/core/poi.py b/TPTBox/core/poi.py index 6f3d6c30..eaf38b19 100755 --- a/TPTBox/core/poi.py +++ b/TPTBox/core/poi.py @@ -236,6 +236,7 @@ def local_to_global(self, x: COORDINATE, itk_coords=False) -> COORDINATE: Args: x (Coordinate | list[float]): The local coordinate(s) to convert. + itk_coords (bool, optional): If True, return the coordinate in ITK/LPS convention (negate the first two axes) after the forward transform. Defaults to False. Returns: Coordinate: The converted global coordinate(s). @@ -321,41 +322,32 @@ def apply_crop_reverse( ) def apply_crop(self: Self, o_shift: tuple[slice, slice, slice] | Sequence[slice], inplace=False) -> Self: - """Adjust POI coordinates after a crop operation by shifting the origin. + """Adjust POI coordinates for a crop applied to the underlying image. - Points outside the cropped frame are NOT removed. - See :meth:`~TPTBox.NII.compute_crop_slice`. + When you crop an image you must also update the attached POIs. There are no + boundaries to move, but the origin must be shifted so that the new voxel + (0, 0, 0) matches the same world coordinate as before. ``self.shape`` and + ``self.origin`` are updated accordingly. Points outside the cropped frame + are **not** removed — filter them separately via :meth:`filter_points_inside_shape`. Args: - o_shift (tuple[slice, slice, slice]): translation of the origin, cause by the crop - inplace (bool, optional): inplace. Defaults to True. + o_shift (tuple[slice, slice, slice]): Per-axis slices encoding the crop + (the same slice tuple you would pass to ``NII.apply_crop``). + ``slice.start`` gives the origin translation per axis. + inplace (bool, optional): If True, mutate this POI and return ``self``; + otherwise return a new POI. Defaults to False. Returns: - Self - """ - """Crop the POIs based on the given origin shift due to the image crop. - - When you crop an image, you have to also crop the POIs. - There are actually no boundaries to be moved, but the origin must be moved to the new 0, 0, 0. - Points outside the frame are NOT removed. See NII.compute_crop_slice(). - - Args: - o_shift (tuple[slice, slice, slice]): Translation of the origin caused by the crop. - inplace (bool, optional): If True, perform the operation in-place. Defaults to False. + POI: The updated POI (same object when ``inplace=True``). - Returns: - Centroids: If inplace is True, returns the modified self. Otherwise, returns a new Centroids object. - - Notes: - The input 'o_shift' should be a tuple of slices for each dimension, specifying the crop range. - The 'shape' and 'origin' attributes are updated based on the crop information. Raises: - AttributeError: If the old deprecated format for 'o_shift' (a tuple of floats) is used. + DeprecationWarning: If ``o_shift`` is the legacy tuple-of-floats format + instead of the tuple-of-slices format. Examples: - >>> POI_obj = Centroids(...) + >>> poi_obj = POI(...) >>> crop_slice = (slice(10, 20), slice(5, 15), slice(0, 8)) - >>> new_POIs = POI_obj.crop(crop_slice) + >>> new_pois = poi_obj.apply_crop(crop_slice) """ origin: COORDINATE = None # type: ignore shape = None # type: ignore @@ -422,6 +414,7 @@ def shift_all_coordinates( translation_vector: Per-axis slices encoding the origin shift, or ``None`` to return ``self`` unchanged. inplace (bool, optional): Whether to modify in place. Defaults to True. + **kwargs: Extra keyword arguments forwarded to :meth:`apply_crop`. Returns: Self: The updated POI (same object when ``inplace=True``). @@ -594,17 +587,23 @@ def rescale_(self, voxel_spacing: ZOOMS = (1, 1, 1), decimals=3, verbose: loggin return self.rescale(voxel_spacing=voxel_spacing, decimals=decimals, verbose=verbose, inplace=True) def to_global(self, itk_coords=False) -> POI_Global: - """Converts the Centroids object to a global POI_Global object. + """Convert this voxel-space POI to a global (world-space) :class:`POI_Global`. + + The zoom, rotation, and origin of this POI are used to build the new + :class:`POI_Global`; the actual per-point conversion happens lazily as points + are read from it. ``level_one_info``, ``level_two_info``, and ``info`` are + forwarded (``info`` is deep-copied). - This method converts the local POI coordinates to global coordinates using the Centroids' zoom, - rotation, and origin attributes and returns a new POI_Global object. + Args: + itk_coords (bool, optional): If True, produce coordinates in ITK/LPS + convention (first two axes negated) instead of RAS. Defaults to False. Returns: - POI_Global: A new POI_Global object with the converted global POI coordinates. + POI_Global: A new POI_Global with the same points expressed in world space. Examples: - >>> POI_obj = Centroids(...) - >>> global_obj = POI_obj.to_global() + >>> poi_obj = POI(...) + >>> global_obj = poi_obj.to_global() """ from TPTBox import POI_Global @@ -642,8 +641,10 @@ def save( out_path (Path | str): The path where the JSON file will be saved. make_parents (bool, optional): If True, create any necessary parent directories for the output file. Defaults to False. - verbose (bool, optional): If True, print status messages to the console. Defaults to True. + additional_info (dict | None, optional): Extra key/value pairs merged into the JSON's info block. Defaults to None. save_hint: 0 Default, 1 Gruber, 2 POI (readable), 10 ISO-POI (outdated) + resample_reference (Has_Grid | None, optional): If given, resample the POI to this grid before saving. Defaults to None. + verbose (bool, optional): If True, print status messages to the console. Defaults to True. Returns: None @@ -669,6 +670,8 @@ def make_point_cloud_nii(self, affine=None, s=8, sphere=True) -> tuple[NII, NII] affine (np.ndarray, optional): The affine transformation matrix for the NIfTI image. Defaults to None. If None, the POI object's affine will be used. s (int, optional): The neighborhood size. Defaults to 8. + sphere (bool, optional): If True, place a sphere of radius ``s`` (in millimetres) around each POI; + if False, use a cubic ``s``-voxel neighbourhood. Defaults to True. Returns: tuple[NII, NII]: A tuple containing two NII objects representing the point cloud for regions and subregions. @@ -833,6 +836,10 @@ def load(cls, poi: POI_Reference, reference: Has_Grid | None = None, allow_globa - Tuple[Image_Reference, Image_Reference, list[int]]: A tuple containing two Image_Reference objects and a list of integers representing the POI data. - POI: An existing POI object to be loaded. + reference (Has_Grid | None, optional): Grid used to resample / anchor the loaded POI when its saved + grid is missing or differs. Defaults to None. + allow_global (bool, optional): If True, allow loading a :class:`POI_Global` file into this local + :class:`POI` (it will be converted to voxel space via ``reference``). Defaults to False. Returns: POI: The loaded Centroids object. @@ -952,7 +959,8 @@ def calc_poi_from_two_segs( verbose (bool, optional): Whether to print verbose output during the computation. override (bool, optional): Whether to overwrite any existing centroids file at `out_path`. decimals (int, optional): The number of decimal places to round the computed centroid coordinates to. - additional_folder (bool, optional): Whether to add a `/ctd/` folder to the path generated for the output file. + check_every_point (bool, optional): If True, re-computes centroids even when ``out_path`` already exists, + so that every point is verified against the source segmentation. Defaults to True. Returns: Centroids: The computed centroids, as a `Centroids` object. @@ -1056,14 +1064,17 @@ def calc_poi_from_subreg_vert( """Calculates the POIs of a subregion within a vertebral mask. This function is spine opinionated, the general implementation is "calc_poi_from_two_masks". Args: - vert_msk (Image_Reference): A vertebral mask image reference. + vert (Image_Reference): A vertebral mask image reference. subreg (Image_Reference): An image reference for the subregion of interest. - decimals (int, optional): Number of decimal places to round the output coordinates to. Defaults to 1. + buffer_file (str | Path | None, optional): Cache file used by the ``@_buffer_it`` wrapper — if present it is loaded and extended instead of recomputing from scratch. Defaults to None. + save_buffer_file (bool, optional): If True, persist the resulting POI back to ``buffer_file`` when new points were added. Defaults to False. + decimals (int, optional): Number of decimal places to round the output coordinates to. Defaults to 2. subreg_id (int | Location | list[int | Location], optional): The ID(s) of the subregion(s) to calculate POIs for. Defaults to 50. - axcodes_to (Ax_Codes | None, optional): A tuple of axis codes indicating the target orientation of the images. Defaults to None. verbose (bool, optional): Whether to print progress messages. Defaults to False. - fixed_offset (int, optional): A fixed offset value to add to the calculated POI coordinates. Defaults to 0. extend_to (POI | None, optional): An existing POI object to extend with the new POI values. Defaults to None. + _vert_ids (list[int] | None, optional): Restrict computation to this subset of vertebra ids. Defaults to None (all present). + _print_phases (bool, optional): Internal debug flag that prints per-phase timing information. Defaults to False. + _orientation_version (int, optional): Internal switch selecting the orientation-computation implementation to use. Defaults to 0. Returns: POI: A POI object containing the calculated POI coordinates. @@ -1297,9 +1308,15 @@ def calc_centroids( Args: msk (Image_Reference): An `Image_Reference` object representing the input mask image. decimals (int, optional): An optional integer specifying the number of decimal places to round the centroid coordinates to (default is 3). - vert_id (int, optional): An optional integer specifying the fixed vertical dimension for the centroids (default is -1). - subreg_id (int, optional): An optional integer specifying the fixed subregion dimension for the centroids (default is 50). + first_stage (int | Abstract_lvl, optional): Value stored in the first (region) coordinate of each POI key. + Use ``-1`` to instead take the value from the mask label. Defaults to -1. + second_stage (int | Abstract_lvl, optional): Value stored in the second (subregion) coordinate of each POI key. + Use ``-1`` to instead take the value from the mask label. Defaults to 50. extend_to (POI, optional): An optional `POI` object to add the calculated centroids to (default is None). + inplace (bool, optional): If True and ``extend_to`` is provided, mutate it in place instead of copying. Defaults to False. + bar (bool, optional): If True, show a progress bar over the label loop when ``_crop`` is False. Defaults to False. + _crop (bool, optional): Internal fast path: compute all centroids with a single ``np_center_of_mass`` call + over the whole mask instead of per-label extraction. Defaults to True. Returns: POI: A `POI` object containing the calculated centroid coordinates. @@ -1310,7 +1327,7 @@ def calc_centroids( Notes: - The function calculates the centroid coordinates of each region in the mask image. - The centroid coordinates are rounded to the specified number of decimal places. - - The fixed dimensions for the centroids can be specified using `vert_id` and `subreg_id`. + - Exactly one of ``first_stage`` and ``second_stage`` must be ``-1``; the other is stored as a fixed value. - If `extend_to` is provided, the calculated centroids will be added to the existing object and the updated object will be returned. - The region label is assumed to be an integer. - NaN values in the binary mask are ignored. diff --git a/TPTBox/core/poi_fun/pixel_based_point_finder.py b/TPTBox/core/poi_fun/pixel_based_point_finder.py index 25cee246..b5e6ba63 100644 --- a/TPTBox/core/poi_fun/pixel_based_point_finder.py +++ b/TPTBox/core/poi_fun/pixel_based_point_finder.py @@ -61,6 +61,8 @@ def max_distance_ray_cast_pixel_level( normal_vector_points (Union[Tuple[Location, Location], DIRECTIONS], optional): Points defining the normal vector or the direction. Defaults to "R". start_point (Location, optional): Starting point of the ray. Defaults to Location.Vertebra_Corpus. + two_sided (bool, optional): If True, cast rays in both the positive and negative normal direction and keep + the further hit. Defaults to False. log (Logger_Interface, optional): Logger interface. Defaults to _log. Returns: diff --git a/TPTBox/core/poi_fun/poi_abstract.py b/TPTBox/core/poi_fun/poi_abstract.py index 8b4bb86d..d0151a19 100755 --- a/TPTBox/core/poi_fun/poi_abstract.py +++ b/TPTBox/core/poi_fun/poi_abstract.py @@ -615,14 +615,14 @@ def fit_spline( location: int | Enum | list[int] | list[Enum] | None = Location.Vertebra_Corpus, vertebra=False, ) -> tuple[np.ndarray, np.ndarray]: - """Fits a spline interpolation through a set of centroids and calculates the first derivative of the spline curve. + """Fits a spline interpolation through the centroids of this POI and calculates the first derivative of the spline curve. Args: - centroids (POI): A set of centroids to interpolate. smoothness (int, optional): Smoothing parameter for the spline interpolation. Default is 10. samples_per_poi (int, optional): Number of sample points to generate per centroid. Default is 20. - location (int, optional): Location parameter for subregion extraction. Default is 50. - vertebra (bool, optional): Indicates whether to perform VertebraCentroids sorting. Default is True. + location (int | Enum | list | None, optional): Subregion to extract before fitting; pass None to fit + against all points in this POI. Defaults to ``Location.Vertebra_Corpus``. + vertebra (bool, optional): If True, sort points by ``Vertebra_Instance.order_dict()`` before fitting. Default is False. Returns: tuple[np.ndarray, np.ndarray]: A tuple containing two NumPy arrays: @@ -942,12 +942,14 @@ def calculate_distances_poi(self, target_point: Self, keep_zoom=False) -> dict[t """Calculate the distances between all points and each centroid in local spacing of the first POI. Args: - target_point (Tuple[float, float, float]): The target point represented as a tuple of x, y, and z coordinates. + target_point (Self): The other POI whose coordinates are compared against ``self``. + keep_zoom (bool, optional): If True, keep both POIs in their current (local) space; if False and + either POI is local, convert both to global (millimetre) space first. Defaults to False. Returns: - Dict[Tuple[int, int], float]: A dictionary containing the distances between the target point and each centroid. - The keys are tuples of two integers representing the region and subregion labels of the centroids, - and the values are the distances (in millimeters) between the target point and each centroid. + Dict[Tuple[int, int], float]: A dictionary containing the distances between matching centroids. + The keys are tuples of two integers representing the region and subregion labels, + and the values are the distances (in the space chosen by ``keep_zoom``) between the paired points. """ assert self.is_global == target_point.is_global if not keep_zoom and not self.is_global: @@ -1082,13 +1084,13 @@ def join_right_(self, pois: Self) -> Self: def join_right(self, *args, **qargs) -> Self: """Right-join another POI set into this one, overwriting existing values. - Args: - pois (Self): Another set of points (centroids) to be combined. - inplace (bool, optional): If True, the operation is performed in-place on the current set. - If False, a new set is created. Default is True. + Thin wrapper around :meth:`join_left` with ``_right_join=True``; forwards + ``*args`` and ``**qargs`` unchanged. See :meth:`join_left` for the full + signature (``pois``, ``inplace``, ...). Returns: - Self: The combined set of centroids, either in-place or as a new set, depending on the 'inplace' parameter. + Self: The combined set of centroids, either in-place or as a new set, + depending on the forwarded ``inplace`` argument. """ return self.join_left(*args, **qargs, _right_join=True) diff --git a/TPTBox/core/poi_fun/poi_global.py b/TPTBox/core/poi_fun/poi_global.py index 07b4c9ef..83d1ae9d 100755 --- a/TPTBox/core/poi_fun/poi_global.py +++ b/TPTBox/core/poi_fun/poi_global.py @@ -180,6 +180,8 @@ def to_other(self, msk: Has_Grid, verbose=False) -> poi.POI: Args: msk (Union[poi.POI, poi.NII]): The reference to the other coordinate system. + verbose (bool, optional): If True, take the per-point (non-batched) code path so individual + ``global_to_local`` calls can log. Defaults to False. Returns: poi.POI: The converted POI. diff --git a/TPTBox/core/poi_fun/save_load.py b/TPTBox/core/poi_fun/save_load.py index 5af3a985..e0bdb78e 100644 --- a/TPTBox/core/poi_fun/save_load.py +++ b/TPTBox/core/poi_fun/save_load.py @@ -94,9 +94,12 @@ def save_poi( """Saves the POIs to a JSON file. Args: + poi (POI | POI_Global): The POI object to serialise. out_path (Path | str): The path where the JSON file will be saved. make_parents (bool, optional): If True, create any necessary parent directories for the output file. Defaults to False. + additional_info (dict | None, optional): Extra key/value pairs merged into the JSON's info block. Defaults to None. + resample_reference (Has_Grid | None, optional): If given, resample the POI to this grid before saving. Defaults to None. verbose (bool, optional): If True, print status messages to the console. Defaults to True. save_hint: 0 Default, 1 Gruber, 2 POI (readable), 10 ISO-POI (outdated) @@ -305,6 +308,8 @@ def load_poi(ctd_path: POI_Reference, verbose=True) -> POI | POI_Global: # noqa - vert: str, the name of the vertebra. - subreg: str, the name of the subregion. - ids: list[int | Location], a list of integers and/or Location objects used to filter the POIs. + verbose (bool, optional): Currently unused; kept for API compatibility with the surrounding save/load + helpers. Defaults to True. Returns: A Centroids object containing the loaded POIs. diff --git a/TPTBox/core/poi_fun/save_mkr.py b/TPTBox/core/poi_fun/save_mkr.py index 4aa21fe0..bf0eb44a 100644 --- a/TPTBox/core/poi_fun/save_mkr.py +++ b/TPTBox/core/poi_fun/save_mkr.py @@ -396,8 +396,6 @@ def get_desc(self: POI_Global, region: int, subregion: int) -> tuple[str, str, s or the level-one-info enum name. Args: - self: The ``POI_Global`` instance providing ``info``, - ``level_one_info``, and ``level_two_info``. region: Region (vertebra) integer label. subregion: Subregion integer label. diff --git a/TPTBox/core/poi_fun/vertebra_direction.py b/TPTBox/core/poi_fun/vertebra_direction.py index b1214800..babec962 100644 --- a/TPTBox/core/poi_fun/vertebra_direction.py +++ b/TPTBox/core/poi_fun/vertebra_direction.py @@ -90,6 +90,9 @@ def calc_orientation_of_vertebra_PIR( do_fill_back (bool, optional): Whether to fill back. Defaults to False. spine_plot_path (None | str, optional): Path to spine plot. Defaults to None. save_normals_in_info (bool, optional): Whether to save normals in info. Defaults to False. + method (Literal["spline", "endplate"], optional): Strategy for the inferior-direction estimate — ``"endplate"`` + uses vertebral endplate landmarks when available, ``"spline"`` uses the local spinal spline tangent. + Defaults to ``"endplate"``. Returns: Tuple[POI, NII | None]: Point of interest and filled back NII. diff --git a/TPTBox/logger/log_file.py b/TPTBox/logger/log_file.py index e4abb7b6..6950162c 100755 --- a/TPTBox/logger/log_file.py +++ b/TPTBox/logger/log_file.py @@ -70,15 +70,19 @@ def print( self._log(_clean_all_color_from_text(string), end=end, ltype=ltype) def _preprocess_text(self, text: tuple[str, ...], ltype=Log_Type.TEXT, ignore_prefix: bool = False) -> str: - """Processes given text parts, converting manually specified datatypes, and adds the prefix and ltype corresponding color. + """Convert text parts to a single string with logger prefix applied. + + Datatype-aware coercion is applied per element via :func:`datatype_to_string` + and ``Log_Type`` markers embedded in ``text`` are stripped. Args: - text (tuple[str, ...]): _description_ - type (_type_, optional): _description_. Defaults to Log_Type.TEXT. - ignore_prefix (bool, optional): _description_. Defaults to False. + text: Text parts to join. ``Log_Type`` entries are removed before joining. + ltype: Log type used to derive the prefix color when it is not already + present at the beginning of the joined string. + ignore_prefix: If True, suppress prepending the logger prefix. Returns: - str: _description_ + The fully assembled log line, ready to be written to the log target. """ text_list: list[str] = [datatype_to_string(t, ltype) for t in text if not isinstance(t, Log_Type)] string = str.join(" ", text_list) @@ -107,14 +111,19 @@ def _prefix_indentation_level(self) -> str: string = " " + "-" * ((indentation_level * 3) - 2) + " " return string - def _get_logger_prefix(self, ltype: Log_Type = Log_Type.TEXT): - """Returns the prefix based on indentation level and log_type. + def _get_logger_prefix(self, ltype: Log_Type = Log_Type.TEXT) -> str: + """Return the indented prefix string for a log line. + + Combines the current indentation marker with either the user-provided + ``self.prefix`` (if set) or the color-coded default prefix associated + with ``ltype``. Args: - type (Log_Type, optional): _description_. Defaults to Log_Type.TEXT. + ltype: Log type whose default prefix is used when ``self.prefix`` + is None. Returns: - _type_: _description_ + The prefix string to prepend to the log message. """ indent: str = self._prefix_indentation_level() if self.prefix is not None: @@ -335,15 +344,20 @@ def create_from_bids( default_verbose: bool = False, override_prefix: str | None = None, ): - """Creates a logger object based on metadata from a BIDS_FILE. + """Create a :class:`Logger` whose folder is derived from a ``BIDS_FILE``. + + The log directory is placed inside the BIDS dataset root of ``bids_file``. Args: - bids_file (BIDS_FILE): _description_ - log_filename (str | dict[str, str]): _description_ - default_verbose (bool, optional): _description_. Defaults to False. + bids_file: BIDS file used to locate the parent dataset directory. + log_filename: Log filename, or a dict of BIDS-conform key/value pairs + that will be joined into a filename. + default_verbose: Default verbose behavior for subsequent calls. + override_prefix: If set, uses this string as the log prefix instead + of the automatically chosen one. Returns: - _type_: _description_ + A new :class:`Logger` writing into the BIDS dataset's ``logs`` folder. """ path = bids_file.dataset return Logger(path, log_filename, default_verbose=default_verbose, prefix=override_prefix) diff --git a/TPTBox/mesh3D/snapshot3D.py b/TPTBox/mesh3D/snapshot3D.py index f0c01f02..26d3ffec 100644 --- a/TPTBox/mesh3D/snapshot3D.py +++ b/TPTBox/mesh3D/snapshot3D.py @@ -62,6 +62,8 @@ def make_snapshot3D( Defaults to the minimum zoom of the image. width_factor: Multiplier applied to the per-view pixel width. scale_factor: PNG magnification factor passed to fury's record function. + debug: If True, disable the off-screen ``Xvfb`` wrapper and render to a + visible window — useful for interactive troubleshooting. verbose: If True, logs the output path after saving. crop: If True, crops the image to its bounding box before rendering. png_magnify: Window pixel density multiplier for the fury renderer. @@ -162,6 +164,10 @@ def make_snapshot3D_parallel( scale_factor: PNG magnification factor. override: If False, skips images whose output file already exists. crop: If True, crops each image to its bounding box before rendering. + opacity: Per-label opacity mapping forwarded to :func:`make_snapshot3D`. + ``1`` is fully opaque, ``0`` invisible. + debug: If True, forwards ``debug=True`` to :func:`make_snapshot3D` so each + worker renders to a visible window instead of ``Xvfb``. """ ress = [] with Pool(cpus) as p: # type: ignore diff --git a/TPTBox/registration/_deepali/deepali_model.py b/TPTBox/registration/_deepali/deepali_model.py index 523256e6..11b5065e 100644 --- a/TPTBox/registration/_deepali/deepali_model.py +++ b/TPTBox/registration/_deepali/deepali_model.py @@ -177,11 +177,17 @@ def _warp_points( """Warp points using a spatial transform. Args: - points (list): List of points to warp: (b,n) b points with n coordinates. + points (list): List of points to warp: (b, n) b points with n coordinates. + axes (Axes): Coordinate system that ``points`` are expressed in. + to_axes (Axes): Coordinate system requested for the returned points. + grid (Deepali_Grid): Grid used as the reference for ``axes``. + to_grid (Deepali_Grid): Grid used as the reference for ``to_axes``. transform (SpatialTransform): Spatial transform to apply. - align_corners (bool): Whether to align corners during warping. - device (torch.device, optional): Device to perform computation on. Defaults to default_device. + device (torch.device, optional): Device to perform computation on. Defaults to ``default_device``. inverse (bool, optional): Whether to apply the inverse transform. Defaults to True. + + Returns: + torch.Tensor: The warped points on CPU. """ with torch.inference_mode(): data = torch.Tensor(points) @@ -195,13 +201,24 @@ def _warp_points( class General_Registration(DeepaliPairwiseImageTrainer): - """A class for performing deformable registration between a fixed and moving image. + """Deep-learning-based pairwise image registration built on top of DeepALI. + + Wraps :class:`DeepaliPairwiseImageTrainer` with TPTBox ``NII``/``POI`` I/O. + The registration flavour (rigid / affine / SVFFD / …) is chosen via + ``transform_name`` and matched deepali transform class; the default + ``"SVFFD"`` produces a non-rigid B-spline transform. Registration runs in + the constructor when ``auto_run=True``; results are then applied through + :meth:`transform_nii`, :meth:`transform_poi`, or :meth:`transform_points`. Attributes: - transform (torch.Tensor): The transformation matrix resulting from the registration. - ref_nii (NII): Reference NII object used for registration. - grid (torch.Tensor): Target grid for image warping. - mov (NII): Processed version of the moving image. + target_grid (Grid): Grid of the fixed / reference image the transform + is defined on. Used as the default target grid in + :meth:`transform_nii` / :meth:`transform_poi`. + input_grid (Grid): Grid of the moving image (before resampling). + source_landmarks_poi (POI | None): Landmarks on the moving image, if any. + target_landmarks_poi (POI | None): Landmarks on the fixed image, if any. + transform (SpatialTransform): Fitted spatial transform (available after + training completes; inherited from the trainer base class). """ def __init__( @@ -442,6 +459,11 @@ def transform_nii( Args: img (NII): The NII image to be transformed. + gpu (int | None, optional): GPU index override. Defaults to the device used during registration. + ddevice (DEVICES | None, optional): Device family override (e.g. ``"cuda"``). Defaults to the registration device. + target (Has_Grid | None, optional): Target grid to resample the output into. Defaults to ``self.target_grid``. + align_corners (bool, optional): Whether to align corners when converting grids to Deepali grids. Defaults to True. + inverse (bool, optional): Apply the inverse transform. XOR-combined with ``self._is_inverted``. Defaults to False. Returns: NII: The transformed image as an NII object. diff --git a/TPTBox/registration/_deepali/deepali_trainer.py b/TPTBox/registration/_deepali/deepali_trainer.py index d7749865..0f49085b 100644 --- a/TPTBox/registration/_deepali/deepali_trainer.py +++ b/TPTBox/registration/_deepali/deepali_trainer.py @@ -114,6 +114,10 @@ def __init__( Args: source (Union[Image, PathStr]): The source image or file path. target (Union[Image, PathStr]): The target image or file path. + source_seg (Union[Image, PathStr] | None): Optional segmentation of the source image, used as an auxiliary + target by segmentation-based loss terms (e.g. Dice, MSE-on-labels). Defaults to None. + target_seg (Union[Image, PathStr] | None): Optional segmentation of the target image, used as an auxiliary + target by segmentation-based loss terms. Defaults to None. source_pset (optional): Source point set for point-based registration. Defaults to None. target_pset (optional): Target point set for point-based registration. Defaults to None. source_landmarks (optional): Source landmark points for registration. Defaults to None. @@ -156,6 +160,8 @@ def __init__( - Override `on_optimizer` for finer control. Defaults to "Adam". lr (float): Learning rate for the optimizer. Defaults to 0.01. + lr_end_factor (float | None): If set, wraps the optimiser in a ``LinearLR`` scheduler that + decays the LR to ``lr_end_factor * lr`` over the run. Defaults to None (no decay). optim_args (optional): Additional optimizer arguments (excluding learning rate). Defaults to None. smooth_grad (float): Smoothing factor applied to gradients. Defaults to 0.0. diff --git a/TPTBox/registration/_deformable/_deepali/deform_reg_pair.py b/TPTBox/registration/_deformable/_deepali/deform_reg_pair.py index 40836e31..1db97f8c 100644 --- a/TPTBox/registration/_deformable/_deepali/deform_reg_pair.py +++ b/TPTBox/registration/_deformable/_deepali/deform_reg_pair.py @@ -246,8 +246,9 @@ def new_loss_terms(config: dict[str, Any]) -> dict[str, Module]: r"""Instantiate terms of registration loss. Args: - config: Preparsed configuration of loss terms. - target_tree: Target vessel centerline tree. + config: Preparsed configuration mapping ``key`` to loss dict with + ``"name"`` and any per-loss keyword arguments. ``"weight"`` is + stripped and handled elsewhere. Returns: Mapping from channel or loss name to loss module instance. diff --git a/TPTBox/registration/_deformable/_grid_search_vert.py b/TPTBox/registration/_deformable/_grid_search_vert.py index 6815ce68..438b3144 100644 --- a/TPTBox/registration/_deformable/_grid_search_vert.py +++ b/TPTBox/registration/_deformable/_grid_search_vert.py @@ -55,13 +55,19 @@ def main_vert_test(): def get_femurs(img: Image_Reference, seg_id=13): - """Returns left (2) and right (1). + """Split the ``seg_id`` mask in ``img`` into its left and right femur components. + + Uses the two largest connected components of the extracted label, computes + their centroids, and relabels so left becomes ``2`` and right becomes ``1`` + in RAS orientation. Args: - img (Image_Reference): _description_ + img (Image_Reference): Segmentation image (or reference) containing the femur label. + seg_id (int, optional): Label id of the femur in ``img``. Defaults to 13. Returns: - _type_: _description_ + tuple[NII, POI]: The relabeled connected-component mask and a POI object with + the femur centroids plus one auxiliary superior point per side. """ nii = to_nii(img, True) # Extract Femurs diff --git a/TPTBox/registration/_deformable/deformable_reg_old.py b/TPTBox/registration/_deformable/deformable_reg_old.py index e0ed05d2..d8b19a45 100644 --- a/TPTBox/registration/_deformable/deformable_reg_old.py +++ b/TPTBox/registration/_deformable/deformable_reg_old.py @@ -112,11 +112,18 @@ def __init__( Args: fixed_image (Image_Reference): The fixed image to which the moving image is registered. moving_image (Image_Reference): The moving image to be registered. + fixed_image_seg (Image_Reference | None): Optional segmentation of the fixed image, used as an auxiliary target. + moving_image_seg (Image_Reference | None): Optional segmentation of the moving image, resampled to ``reference_image``. normalize (Literal["MRI", "CT"] | None): Normalization type; supports "MRI" or "CT" or no normalization. quantile (float): Quantile for intensity normalization; recommended 0.95 for MRI. reference_image (Image_Reference | None): Optional reference image for resampling. - device (Device | None): The computational device for the process, default is CUDA. align_corners (bool): Whether to align the corners during grid resampling. + verbose (int): Verbosity level passed through to the deformable trainer. + config (Path | str | dict): Path to a JSON config file, or an already-loaded config dict. + spacing_type (int): Finest-spacing selection strategy: 1 → fixed spacing, 2 → moving spacing, + 3 → per-axis max, 4 → per-axis min, other → let the trainer decide. + gpu (int): GPU index used when ``ddevice='cuda'``. + ddevice (DEVICES): The computational device family for the process, default is ``"cuda"``. """ self.gpu = gpu self.ddevice: DEVICES = ddevice @@ -176,6 +183,9 @@ def transform_nii(self, img: NII, gpu: int | None = None, ddevice: DEVICES | Non Args: img (NII): The NII image to be transformed. + gpu (int | None, optional): GPU index override. Defaults to the device used during registration. + ddevice (DEVICES | None, optional): Device family override (e.g. ``"cuda"``). Defaults to the registration device. + target (Has_Grid | None, optional): Target grid to resample the output into. Defaults to ``self.target_grid``. Returns: NII: The transformed image as an NII object. diff --git a/TPTBox/registration/_deformable/multilabel_segmentation.py b/TPTBox/registration/_deformable/multilabel_segmentation.py index 46d153c7..c27fb9f0 100644 --- a/TPTBox/registration/_deformable/multilabel_segmentation.py +++ b/TPTBox/registration/_deformable/multilabel_segmentation.py @@ -60,25 +60,31 @@ def __init__( # noqa: C901 """Initialize a multi-stage registration pipeline from an atlas to a target image. Args: - target (NII): Target image segmentation (e.g., from a subject). - atlas (NII): Atlas image segmentation (e.g., a reference or template). - target_img (NII): Target image if None the segmentation is used as an image. - atlas_img (NII): Atlas image if None the segmentation is used as an image. + target_seg (NII): Target image segmentation (e.g., from a subject). + atlas_seg (NII): Atlas image segmentation (e.g., a reference or template). + target_img (NII | None): Target intensity image; if None the segmentation is used as an image. + atlas_img (NII | None): Atlas intensity image; if None the segmentation is used as an image. poi_cms (POI | None): POI centroids of the atlas, used for initial point registration. same_side (bool): Whether atlas and target represent the same body side. verbose (int): Verbosity level for logging. gpu (int): GPU device ID (only relevant if using GPU). ddevice (DEVICES): Device type ('cuda' or 'cpu'). - loss_terms (dict): Dictionary of loss terms for deformable registration. - weights (dict): Weights for the loss terms. + loss_terms (dict | None): Dictionary of loss terms for deformable registration. + weights (dict | None): Weights for the loss terms. lr (float): Learning rate for deformable registration optimizer. + lr_end_factor (float | None): If set, exponentially decay the LR by this final factor across steps. max_steps (int): Maximum optimization steps. - min_delta (float): Minimum delta for convergence. + min_delta (float | list[float]): Minimum delta for convergence (per pyramid level if a list is given). pyramid_levels (int): Number of resolution levels in multi-scale deformable registration. coarsest_level (int): Coarsest level index. finest_level (int): Finest level index. + crop (bool): If True, crop both target and atlas to their combined bounding box before registration. cms_ids (list | None): List of segmentation labels used to extract POI centroids. poi_target_cms (POI | None): Optional precomputed centroids for the target image. + max_history (int): Number of past deformable-registration parameter snapshots to keep for rollback. + change_after_point_reg (Callable): Hook applied to ``(target_seg, atlas_seg, target_img, atlas_img)`` + between the point-based and deformable stages; defaults to identity. + tether_distance (float): Distance parameter for the segmentation-tether loss. **args: Additional keyword arguments passed to Deformable_Registration. Raises: @@ -314,6 +320,8 @@ def transform_nii(self, nii_atlas: NII, allow_only_same_grid_as_moving: bool = T nii_atlas: Atlas image to be transformed (must share the atlas grid). allow_only_same_grid_as_moving: If True, assert that *nii_atlas* matches the grid of the moving image used during point registration. + only_rigid: If True, apply only the point-based rigid registration and skip + the deformable stage. Defaults to False. Returns: Transformed ``NII`` aligned with the original target image space. diff --git a/TPTBox/registration/_ridged_points/point_registration.py b/TPTBox/registration/_ridged_points/point_registration.py index f158481d..df4a9649 100644 --- a/TPTBox/registration/_ridged_points/point_registration.py +++ b/TPTBox/registration/_ridged_points/point_registration.py @@ -40,22 +40,29 @@ def __init__( zooms=None, leave_worst_percent_out=0.0, ): - """Use two Centroids object to compute a ridged_points registration. + """Fit a rigid (versor) point-based registration between two POI sets. + + Only keys that are present in both ``poi_fixed`` and ``poi_moving`` (and + not listed in ``exclusion``) are used for the fit. The empty NIfTI grids + of the two POIs are used as the SimpleITK reference images. Args: - ctd_fixed (Centroids): _description_ - ctd_movig (Centroids): _description_ - representative_fixed (Image_Reference, optional): _description_. Defaults to None. - representative_movig (Image_Reference, optional): _description_. Defaults to None. - exclusion (list, optional): _description_. Defaults to []. - log (_type_, optional): _description_. Defaults to No_Logger(). - verbose (bool, optional): _description_. Defaults to True. + poi_fixed: Reference POI (target of the registration). + poi_moving: Moving POI whose coordinates are aligned to + ``poi_fixed``. + exclusion: Vertebra-level keys (first tuple element) to skip during + fitting. Defaults to no exclusion. + log: Logger used to emit progress and diagnostics. + verbose: If True, forwards verbose logging to ``log``. + ax_code: Optional target axis code (e.g. ``("R", "A", "S")``); + ``poi_fixed`` is reoriented to it before fitting. + zooms: Optional target voxel spacing; ``poi_fixed`` is rescaled to + it before fitting. ``(-1, -1, -1)`` disables rescaling. + leave_worst_percent_out: Fraction in ``[0, 1)`` of point pairs with + the largest post-fit residual to discard before re-fitting. Raises: - ValueError: Require at least two points - - Returns: - Resample_Filter + ValueError: If fewer than two shared points remain after filtering. """ assert leave_worst_percent_out < 1.0 assert leave_worst_percent_out >= 0.0 diff --git a/TPTBox/segmentation/VibeSeg/inference_nnunet.py b/TPTBox/segmentation/VibeSeg/inference_nnunet.py index d1402e51..edf2732b 100644 --- a/TPTBox/segmentation/VibeSeg/inference_nnunet.py +++ b/TPTBox/segmentation/VibeSeg/inference_nnunet.py @@ -32,6 +32,8 @@ def get_ds_info(idx: int, _model_path: str | Path | None = None, exit_one_fail: ``None``, the bundled default path is used. exit_one_fail: If ``True``, call :func:`sys.exit` when the dataset is not found; otherwise return ``None``. + logger: Logger used for the "Please add Dataset ..." failure message. + Defaults to the module-level ``Reflection_Logger``. Returns: Parsed ``dataset.json`` dictionary for the requested dataset. @@ -161,6 +163,11 @@ def run_inference_on_file( the cost of holding the model in GPU memory between calls. The GPU cache is also left warm (no ``empty_cache``) so the allocator can reuse buffers across images. + auto_download: If ``True``, download missing model weights on first use. + Forced to ``True`` when ``model_path`` is ``None``. + fail_on_missing_memory: If ``True``, raise an error when the estimated + GPU memory exceeds the available memory instead of waiting. + logger: Logger used for all progress and error output. Returns: A tuple ``(seg_nii, softmax_logits)`` where ``seg_nii`` is the @@ -432,6 +439,7 @@ def run_VibeSeg( max_folds: Limit the number of folds used for ensemble averaging. _model_path: Override for the default model weights directory. step_size: Sliding-window step size fraction. + logger: Logger used for progress and error output. **_kargs: Additional keyword arguments forwarded to :func:`run_inference_on_file`. diff --git a/TPTBox/segmentation/VibeSeg/vibeseg.py b/TPTBox/segmentation/VibeSeg/vibeseg.py index bbe49aa4..49222ffd 100644 --- a/TPTBox/segmentation/VibeSeg/vibeseg.py +++ b/TPTBox/segmentation/VibeSeg/vibeseg.py @@ -115,6 +115,7 @@ def run_vibeseg( keep_size: If True, keep the model's native output resolution instead of resampling back to the input image space. memory_max: MAX GPU memory in MB. Changes the super-batches are used. Might speed up inference. At least 8000 + model_path: Optional override for the model weights directory. Defaults to the bundled model path. **args: Additional keyword arguments forwarded to ``run_inference_on_file``. Returns: @@ -169,6 +170,17 @@ def run_nnunet( gpu: GPU device index to use for inference. ddevice: Compute device: ``"cuda"``, ``"cpu"``, or ``"mps"``. dataset_id: nnU-Net dataset identifier. + model_path: Optional override for the model weights directory. Defaults to the bundled path. + auto_download: If True, download missing model weights on first use. Defaults to False. + keep_size: If True, keep the model's native output resolution instead of resampling back. Defaults to False. + fill_holes: If True, fill holes in the output segmentation. Defaults to False. + logits: If True, also return raw softmax logits. Defaults to False. + mapping: Optional label remap dict applied to the segmentation. Defaults to None. + crop: If True, crop input images to their foreground bounding box before inference. Defaults to False. + max_folds: Limit the number of folds used for ensemble averaging. Defaults to None (use all). + mode: Resampling mode when mapping the output back to input space. Defaults to ``"nearest"``. + padd: Number of voxels to pad the image before inference. Defaults to 0. + key_ResEnc: Glob key used to locate ResEnc-style model folders under the dataset directory. **args: Additional keyword arguments forwarded to ``run_inference_on_file``. """ run_inference_on_file( diff --git a/TPTBox/segmentation/nnUnet_utils/export_prediction.py b/TPTBox/segmentation/nnUnet_utils/export_prediction.py index 013edaeb..a7503fdd 100755 --- a/TPTBox/segmentation/nnUnet_utils/export_prediction.py +++ b/TPTBox/segmentation/nnUnet_utils/export_prediction.py @@ -185,6 +185,8 @@ def convert_predicted_logits_to_segmentation_with_correct_shape( return_probabilities: Reserved for future use. Raises :class:`NotImplementedError` if ``True``. num_threads_torch: Number of threads used by PyTorch during resampling. + device: Torch device on which the argmax runs; ``None`` uses the tensor's current device. + logger: Logger used for progress messages from the argmax GPU-fallback path. Returns: Integer segmentation array with dtype ``np.uint8`` or ``np.uint16`` diff --git a/TPTBox/segmentation/nnUnet_utils/inference_api.py b/TPTBox/segmentation/nnUnet_utils/inference_api.py index d8d43dfd..c423e092 100755 --- a/TPTBox/segmentation/nnUnet_utils/inference_api.py +++ b/TPTBox/segmentation/nnUnet_utils/inference_api.py @@ -73,6 +73,9 @@ def load_inf_model( tile_batch_size: Number of sliding-window tiles per network forward pass. ``1`` reproduces the original per-tile path; larger values batch tiles to improve GPU utilisation at higher peak memory. + fail_on_missing_memory: If True, raise an error when the estimated GPU memory + exceeds the available memory instead of waiting. + logger: Logger used for progress and error output. Returns: Initialised ``nnUNetPredictor`` ready for inference. @@ -219,6 +222,7 @@ def run_inference( passing it to the model. logits: If True, return raw softmax logits. Currently not implemented and will raise ``NotImplementedError``. + logger: Logger used for progress and error output. verbose: Unused; reserved for future logging support. Raises: diff --git a/TPTBox/segmentation/nnUnet_utils/predictor.py b/TPTBox/segmentation/nnUnet_utils/predictor.py index c760b084..9a90266d 100755 --- a/TPTBox/segmentation/nnUnet_utils/predictor.py +++ b/TPTBox/segmentation/nnUnet_utils/predictor.py @@ -160,6 +160,7 @@ def initialize_from_trained_model_folder( cache_state_dicts: If ``True``, load all fold weights onto the device up-front and cache the network instances. Reduces per-sample latency at the cost of GPU memory. + logger: Logger used for progress and error output. """ if isinstance(use_folds, str): use_folds = [use_folds] # type: ignore @@ -342,6 +343,7 @@ def predict_single_npy_array( save_or_return_probabilities: If ``True``, also return softmax probabilities in addition to the label map. Currently raises :class:`NotImplementedError` inside the conversion step. + logger: Logger used for progress and error output. Returns: The predicted segmentation array (or a tuple with probabilities if @@ -402,6 +404,7 @@ def predict_logits_from_preprocessed_data(self, data: torch.Tensor, attempts: in Args: data: Preprocessed image tensor with shape ``(C, X, Y, Z)``. attempts: Number of retry attempts on GPU OOM before raising. + logger: Logger used for progress and error output. Returns: Averaged raw logits tensor with shape @@ -572,6 +575,9 @@ def predict_sliding_window_return_logits( input_image: Image tensor with shape ``(C, X, Y, Z)``. network: Optional network instance to use. Defaults to ``self.network``. + idx: Fold index used only for progress reporting; forwarded to the + logger so multi-fold runs can be traced. + logger: Logger used for progress and error output. Returns: Aggregated logit array with shape ``(num_classes, X, Y, Z)`` diff --git a/TPTBox/spine/snapshot2D/snapshot_modular.py b/TPTBox/spine/snapshot2D/snapshot_modular.py index dae553b1..2dc11e48 100755 --- a/TPTBox/spine/snapshot2D/snapshot_modular.py +++ b/TPTBox/spine/snapshot2D/snapshot_modular.py @@ -144,6 +144,7 @@ def sag_cor_curve_projection( Args: ctd_list: given Centroids img_data: given img_data + ctd_fallback: Fallback POI used if ``ctd_list`` has 3 or fewer points at ``curve_location`` to interpolate. cor_savgol_filter: If true, will perform the savgol filter also in coronal view curve_location: Location of the curve's centroids to be used. @@ -1062,15 +1063,29 @@ def create_snapshot( # noqa: C901 dpi=96, verbose: bool = False, ) -> None: - """Create virtual dx, sagittal, and coronal curved-planar CT snapshots with mask overlay. + """Render one or more :class:`Snapshot_Frame`s into a single figure and save it. + + Each frame independently selects its views (sagittal / coronal / axial), + slice mode (regular slice, MIP, curve-projection, …), and overlay style; + this function only handles layout, common pre-processing (crop / reorient + / resample), and file I/O. The output file format is inferred from + ``snp_path``'s suffix (typically ``.png`` or ``.jpg``). Args: - snp_path (str): Path to the new jpg - frames (List[Snapshot_Frame]): List of Images - crop (bool): crop output to vertebral masks (seg-vert). Defaults to False. - check (bool): if true, check if snap is present and do not re-create. Defaults to False. - to_ax (Orientation): Sets the Orientation. Can be used for flipping the image or fixing false rotations of the original inputs. - dpi (int): Set the resolution. + snp_path (str | Path | list[str | Path]): Destination path, or list of paths + when several output files should share the same rendered figure + (useful for writing both a ``.png`` and a ``.jpg``). + frames (list[Snapshot_Frame]): One frame per row in the output figure. + ``None`` entries are silently skipped. + crop (bool): Crop each frame to the vertebra segmentation bounding box + before rendering. Defaults to False. + check (bool): If True and every ``snp_path`` already exists on disk, + return without re-rendering. Defaults to False. + to_ax (Orientation): Reorientation applied to every frame's image / + segmentation / POI before rendering. Use to flip axes or correct + rotated inputs. Defaults to ``("I", "P", "L")``. + dpi (int): Matplotlib DPI for the output figure. Defaults to 96. + verbose (bool, optional): If True, log the output path and progress. Defaults to False. """ # Checks if snaps already exists, does nothing if true and check is true exist = all(Path(i).is_file() for i in snp_path) if isinstance(snp_path, list) else Path(snp_path).is_file() diff --git a/TPTBox/spine/spinestats/_load_nako.py b/TPTBox/spine/spinestats/_load_nako.py index 81b83645..9b77f311 100644 --- a/TPTBox/spine/spinestats/_load_nako.py +++ b/TPTBox/spine/spinestats/_load_nako.py @@ -190,6 +190,10 @@ def loop_over_repaired_nako( dataset: Root path of the NAKO BIDS dataset. test: If True, restrict scanning to a single hard-coded subject subtree for quick runs. verbose: Log each subject id as it is processed. + sort: If True, iterate subjects in alphabetical order (see :meth:`BIDS_Global_info.iter_subjects`). + test_key: Path substring passed to the BIDS scanner's ``filter_file`` when ``test=True``; only paths + containing this substring are indexed. Defaults to a hard-coded example subject. + baseline_metadata: Path to the NAKO baseline CSV used to look up height metadata. Yields: Dict mapping short keys to ``BIDS_FILE`` entries for one subject. diff --git a/TPTBox/spine/spinestats/angles.py b/TPTBox/spine/spinestats/angles.py index 81d46d2a..ce2fda4d 100644 --- a/TPTBox/spine/spinestats/angles.py +++ b/TPTBox/spine/spinestats/angles.py @@ -254,7 +254,10 @@ def compute_angel_between_two_points_( - "I" for Inferior. vert_id1_mv (MoveTo, optional): MoveTo instance indicating the position to consider for the first vertebra. Defaults to MoveTo.CENTER. vert_id2_mv (MoveTo, optional): MoveTo instance indicating the position to consider for the second vertebra. Defaults to MoveTo.CENTER. - project_2d (bool, optional): If True, computes the 2D projection of the angle. Defaults to False. + project_2D (bool, optional): If True, computes the 2D projection of the angle. Defaults to False. + use_ivd_direction (bool, optional): For coronal/right-directed angles, use the IVD direction (via + ``Location.Vertebra_Disc_Inferior``) instead of the vertebra direction for lumbar/thoracic ids + beyond ``IVD_MORE_ACCURATE``. Defaults to False. Returns: float | None: The computed angle in degrees. Returns None if either vertebra ID is invalid. @@ -384,7 +387,7 @@ def compute_lordosis_and_kyphosis(poi: POI, project_2D=True) -> dict[str, float Args: poi (POI): The points of interest object containing 3D coordinates for various vertebrae. It must include the vertebra direction information for proper calculation. (Location.Vertebra_Direction_Posterior) - project_2d (bool): If True, the calculation is done in 2D projection; otherwise, in 3D. Defaults to False. + project_2D (bool): If True, the calculation is done in 2D projection; otherwise, in 3D. Defaults to True. Returns: dict: A dictionary containing the following key-value pairs: @@ -403,7 +406,7 @@ def compute_lordosis_and_kyphosis(poi: POI, project_2D=True) -> dict[str, float Example: To compute the spinal angles for a given POI object: - >>> angles = compute_lordosis_and_kyphosis(poi, project_2d=True) + >>> angles = compute_lordosis_and_kyphosis(poi, project_2D=True) >>> print(angles) {'cervical_lordosis': 30.5, 'thoracic_kyphosis': 35.0, 'lumbar_lordosis': 45.2} """ @@ -480,7 +483,9 @@ def compute_max_cobb_angle( If not provided, defaults to all cervical, thoracic, and lumbar vertebrae. vert_id1_mv (MoveTo): Enum indicating the move direction for the first vertebra (default is MoveTo.TOP). vert_id2_mv (MoveTo): Enum indicating the move direction for the second vertebra (default is MoveTo.BOTTOM). - project_2d (bool): If True, the calculation is done in 2D projection; otherwise, in 3D. Defaults to False. + project_2D (bool): If True, the calculation is done in 2D projection; otherwise, in 3D. Defaults to True. + use_ivd_direction (bool, optional): For lumbar/thoracic ids beyond ``IVD_MORE_ACCURATE``, use the IVD direction + (via ``Location.Vertebra_Disc_Inferior``) instead of the vertebra direction. Defaults to False. Returns: tuple: A tuple containing the following elements: @@ -501,7 +506,7 @@ def compute_max_cobb_angle( Example: To compute the maximum Cobb angle for a given POI object: - >>> max_angle, from_vert, to_vert, apex = compute_max_cobb_angle(poi, project_2d=True) + >>> max_angle, from_vert, to_vert, apex = compute_max_cobb_angle(poi, project_2D=True) >>> print(f"Max Angle: {max_angle}, From: {from_vert}, To: {to_vert}, Apex: {apex}") Max Angle: 35.6, From: 3, To: 12, Apex: 7 """ @@ -595,6 +600,7 @@ def compute_max_cobb_angle_multi( vert_id1_mv (MoveTo): Enum indicating the move direction for the first vertebra (default is MoveTo.TOP). vert_id2_mv (MoveTo): Enum indicating the move direction for the second vertebra (default is MoveTo.BOTTOM). use_ivd_direction: Uses the IVD direction instead of the Vertebra direction for Lumbar and Thorax region. + project_2D (bool, optional): If True, the calculation is done in 2D projection; otherwise, in 3D. Defaults to True. Returns: list: A list of tuples, each containing: @@ -723,6 +729,7 @@ def plot_compute_lordosis_and_kyphosis( img (Image_Reference): The reference image on which to plot the angles and lines. seg (Image_Reference | None): The segmentation image reference. Optional, can be None. line_len (int): The length of the lines representing the vertebrae directions (default is 100). + project_2D (bool, optional): If True, the angles are computed in the 2D sagittal projection; otherwise in 3D. Defaults to True. Returns: tuple: A tuple containing: @@ -808,6 +815,9 @@ def plot_cobb_angle( threshold_deg (int): The angle threshold in degrees above which cobb angles are considered for plotting. vert_id1_mv (MoveTo): The MoveTo option for the first vertebra in each angle calculation. vert_id2_mv (MoveTo): The MoveTo option for the second vertebra in each angle calculation. + use_ivd_direction (bool, optional): For lumbar/thoracic ids beyond ``IVD_MORE_ACCURATE``, use the IVD direction + (via ``Location.Vertebra_Disc_Inferior``) instead of the vertebra direction. Defaults to False. + project_2D (bool, optional): If True, the underlying Cobb angles are computed as a 2D projection; otherwise in 3D. Defaults to True. Returns: tuple: A tuple containing: @@ -906,6 +916,7 @@ def plot_cobb_and_lordosis_and_kyphosis( seg (Image_Reference | None): The segmentation image reference. Optional, can be None. line_len (int): The length of the lines representing the vertebrae directions (default is 100). threshold_deg (int): The threshold angle in degrees to identify significant Cobb angles (default is 10). + project_2D (bool, optional): If True, the underlying angles are computed as a 2D projection; otherwise in 3D. Defaults to True. Returns: tuple: A tuple containing: diff --git a/TPTBox/spine/spinestats/body_quadrants.py b/TPTBox/spine/spinestats/body_quadrants.py index 4326b397..7e707f63 100644 --- a/TPTBox/spine/spinestats/body_quadrants.py +++ b/TPTBox/spine/spinestats/body_quadrants.py @@ -66,17 +66,23 @@ def make_quadrants( vert_ids : list of int or None, optional List of vertebra IDs to process. If None, all vertebrae found in the segmentation are processed. + mask_ids : tuple of int, optional + Spine sub-label ids that count as "vertebral body" and are intersected + with each vertebra mask before partitioning. Defaults to ``(49, 50, 52)``. + erode : int, optional + Number of voxels to erode the per-vertebra body mask before computing + the quantile bins (helps drop thin border voxels). ``0`` disables + erosion. Defaults to 0. Returns: ------- - Image_Reference + NII An image where each vertebral body voxel is labeled with a value from 1 to 27, representing its anatomical subregion. - Raises: + Notes: ------ - None - Vertebrae missing required POIs are silently skipped. + Vertebrae missing required POIs are silently skipped — no exception is raised. Examples: -------- diff --git a/TPTBox/spine/spinestats/measure_ivd_and_vertebra_geometry.py b/TPTBox/spine/spinestats/measure_ivd_and_vertebra_geometry.py index 169b686a..f16ce481 100644 --- a/TPTBox/spine/spinestats/measure_ivd_and_vertebra_geometry.py +++ b/TPTBox/spine/spinestats/measure_ivd_and_vertebra_geometry.py @@ -84,6 +84,10 @@ def measure_ivd_and_vertebra_geometry( orientation and, for the signal ratio, the spinal canal reference region. + buffer_poi : Path, optional + Cache file where the intermediate POI results are read from / written + to (via ``calc_poi_from_subreg_vert``). ``None`` disables caching. + step_size_mm : float, default=0.5 Grid spacing (in mm) used when sampling height/diameter profiles across the structure's surface. Smaller values are more accurate @@ -99,6 +103,11 @@ def measure_ivd_and_vertebra_geometry( module docstring for details. Any value other than ``100`` is currently treated like vertebra mode for the minimum-label check. + erode : int, default=1 + Number of voxels to erode each structure mask before computing the + peak-centered T2 signal statistics. Reduces contamination from + partial-volume voxels at the boundary. Set to 0 to disable erosion. + Returns: ------- dict[int, dict[str, float]] diff --git a/TPTBox/spine/spinestats/poi_fun/articularis_midpoint.py b/TPTBox/spine/spinestats/poi_fun/articularis_midpoint.py index 85cb25b7..2cbe5ca7 100644 --- a/TPTBox/spine/spinestats/poi_fun/articularis_midpoint.py +++ b/TPTBox/spine/spinestats/poi_fun/articularis_midpoint.py @@ -19,7 +19,29 @@ def calc_all_facet_joint_pois( surface_tolerance_mm: float = 1.5, log: Logger_Interface = _log, ) -> POI: - """Call :func:`calc_facet_joint_pois` for every vertebra present in ``vert``.""" + """Compute facet-joint midpoints for every vertebra present in ``vert``. + + Iterates ids in ascending order (skipping C1) up to id 29 and delegates + each pair to :func:`_calc_facet_joint_pois`, which places a midpoint POI + between the Inferior_Articular process of the upper vertebra and the + Superior_Articular process of its lower neighbour. + + Args: + vert: Vertebra instance segmentation (image reference). + subreg: Subregion / semantic segmentation on the same grid as ``vert``. + ids: Optional override for the two POI subregion ids the midpoints + are stored under. Defaults to + ``{"left": Articular_Process_Midpoint_Left, "right": Articular_Process_Midpoint_Right}``. + max_gap_mm: Skip a joint if the two articular surfaces are further + apart than this in millimetres. Defaults to 8.0. + surface_tolerance_mm: Point pairs within ``(min_distance + tolerance)`` + are averaged as the contact surface. Defaults to 1.5. + log: Logger for status messages. + + Returns: + POI: A fresh POI built from ``vert``'s grid containing the facet-joint + midpoints for all successfully processed vertebra pairs. + """ vert_: NII = to_nii(vert, True) subreg_ = to_nii(subreg, True) poi = vert_.make_empty_POI() diff --git a/TPTBox/spine/spinestats/torso_vat_sat.py b/TPTBox/spine/spinestats/torso_vat_sat.py index 2e3e73a5..c165011b 100644 --- a/TPTBox/spine/spinestats/torso_vat_sat.py +++ b/TPTBox/spine/spinestats/torso_vat_sat.py @@ -249,7 +249,7 @@ def body_composition_score( - ``12``: VIBESeg-12 label set. regions : list[tuple[Vertebra_Instance, Vertebra_Instance]], optional Vertebral ranges to analyse. Each tuple specifies the first and last - vertebra (inclusive). Defaults to T12–L1 and L3. + vertebra (inclusive). Defaults to ``[(T12, L1), (L3, L4), (L3, L3)]``. height_m : float, optional Patient height in metres. If provided, the skeletal muscle index @@ -613,11 +613,11 @@ def muscle_fat_infiltration( def torso_vat_sat_muscle_mass( vibe_seg: NII, roi: NII, dataset_id: Literal[100, 12] = 100, roi_ids: tuple[int, ...] = tuple(range(3, 9)), return_nii: bool = False ) -> tuple[dict, NII | None]: - """Compute visceral adipose tissue (VAT), subcutaneous adipose tissue (SAT), and muscle volume in mm from a torso segmentation. + """Compute visceral adipose tissue (VAT), subcutaneous adipose tissue (SAT), and muscle volume (in mm³) from a torso segmentation. The segmentation is optionally restricted to the supplied ROI before calculating tissue volumes. Volumes are reported in physical units - (voxel count × voxel volume). + (voxel count × voxel volume, i.e. mm³). Parameters ---------- diff --git a/TPTBox/stitching/stitching.py b/TPTBox/stitching/stitching.py index affb85d4..04d3c239 100755 --- a/TPTBox/stitching/stitching.py +++ b/TPTBox/stitching/stitching.py @@ -198,24 +198,25 @@ def get_max_affine_and_shape( return nib.Nifti1Image(np.zeros(shape.astype(int), dtype=dtype), affine) # type: ignore -def compute_crop_slice(nii: Nifti1Image, minimum=0, dist=0) -> tuple[slice, slice, slice]: - """Computes the minimum slice that removes unused space from the image and returns the corresponding slice tuple along with the origin shift required for centroids. +def compute_crop_slice(nii: Nifti1Image, minimum: float = 0, dist: int = 0) -> tuple[slice, slice, slice]: + """Compute the tight 3-D crop slice that removes empty space from a NIfTI volume. + + A voxel is considered "filled" when its value is strictly greater than + ``minimum``. The returned slice tuple can be applied via ``nii.slicer`` + to crop the image. Args: - minimum (int): The minimum value of the array (0 for MRI, -1024 for CT). Default value is 0. - dist (int): The amount of padding to be added to the cropped image. Default value is 0. - other_crop (tuple[slice,...], optional): A tuple of slice objects representing the slice of an other image to be combined with the current slice. Default value is None. + nii: Input NIfTI image whose bounding box is computed. + minimum: Background threshold. Voxels above this value delimit the crop + (0 for MRI, -1024 for CT). + dist: Padding in millimetres added on every side of the crop. Converted + to voxels using the image's zooms. Returns: - ex_slice: A tuple of slice objects that need to be applied to crop the image. - origin_shift: A tuple of integers representing the shift required to obtain the centroids of the cropped image. - - Note: - - The computed slice removes the unused space from the image based on the minimum value. - - The padding is added to the computed slice. - - If the computed slice reduces the array size to zero, a ValueError is raised. - - If other_crop is not None, the computed slice is combined with the slice of another image to obtain a common region of interest. - - Only None slice is supported for combining slices. + A 3-tuple of ``slice`` objects to apply along the (X, Y, Z) axes. + + Raises: + ValueError: If no voxels exceed ``minimum`` (crop would be empty). """ shp = nii.shape zms = nii.header.get_zooms() # type: ignore @@ -323,7 +324,7 @@ def from_nibabel(nib_image): Parameters ---------- - img: NiftiImage + nib_image: nibabel Nifti1Image Returns: ------- @@ -493,6 +494,9 @@ def main( # noqa: C901 dtype: Output data type. Accepts a Python type (e.g. ``float``, ``np.uint16``) or a string key from the internal type mapping. save: If True, writes the stitched image to ``output``. + ramp_path: Optional explicit output path for the ramp NIfTI. Only used + when ``store_ramp`` is True; when None the ramp path is derived + from ``output``. Returns: A 2-tuple ``(stitched_nii, ramp_nii)`` where ``ramp_nii`` is None diff --git a/TPTBox/stitching/stitching_tools.py b/TPTBox/stitching/stitching_tools.py index 3febd68c..3795e9cb 100755 --- a/TPTBox/stitching/stitching_tools.py +++ b/TPTBox/stitching/stitching_tools.py @@ -48,6 +48,8 @@ def stitching( dtype: NumPy dtype for the output array. match_histogram: If True, matches histograms between consecutive inputs. store_ramp: If True, also returns the per-volume blending weight array. + ramp_path: Optional explicit output path for the ramp NIfTI; forwarded to + :func:`stitching_raw`. Only used when ``store_ramp`` is True. Returns: A 2-tuple ``(stitched_nii, ramp_nii)`` as returned by diff --git a/docs/api/logger.md b/docs/api/logger.md index dad8b6e2..0da6afa7 100644 --- a/docs/api/logger.md +++ b/docs/api/logger.md @@ -2,6 +2,22 @@ Structured, consistent logging for long-running medical image processing pipelines. +All logger implementations conform to the structural +[`Logger_Interface`][TPTBox.logger.log_file.Logger_Interface] protocol, so +client code can type-hint against the interface and stay decoupled from the +concrete backend: + +- [`Logger`][TPTBox.logger.log_file.Logger] — writes messages to a timestamped + file inside a `logs/` folder next to a dataset root; supports sub-loggers + and accumulated statistics. +- [`No_Logger`][TPTBox.logger.log_file.No_Logger] — verbose-to-terminal fallback + that persists nothing; safe drop-in when a file log is not wanted. +- [`String_Logger`][TPTBox.logger.log_file.String_Logger] — buffers into an + in-memory string, optionally forwarding to a parent logger on flush/close. + +Log entries are classified with [`Log_Type`][TPTBox.logger.log_constants.Log_Type] +which drives both the terminal color and the file-level prefix. + ## Logger ::: TPTBox.logger.log_file.Logger diff --git a/docs/api/mesh3d.md b/docs/api/mesh3d.md index baf72757..98393bff 100644 --- a/docs/api/mesh3d.md +++ b/docs/api/mesh3d.md @@ -1,6 +1,21 @@ # Mesh 3D -3D surface mesh generation from segmentation volumes and snapshot rendering. +Build and visualize 3-D surface meshes derived from segmentation volumes and +Points of Interest. + +The module is split into four cooperating pieces: + +- [`mesh`][TPTBox.mesh3D.mesh] — extracts iso-surfaces from a segmentation + `NII` via marching cubes and wraps them in a `Mesh3D`/`SegmentationMesh` + container backed by a `pyvista.PolyData` object; supports save/load in PLY + format. +- [`snapshot3D`][TPTBox.mesh3D.snapshot3D] — renders a segmentation as one or + more 2-D PNG previews (`R`/`A`/`L`/`P`/`S`/`I` viewpoints) using an off-screen + VTK/Fury pipeline; useful in headless environments via `Xvfb`. +- [`html_preview`][TPTBox.mesh3D.html_preview] — assembles interactive HTML + previews of `NII`/`POI` objects for quick visual inspection. +- [`mesh_colors`][TPTBox.mesh3D.mesh_colors] — colour palette utilities keyed by + vertebra/subregion label so meshes render with a consistent scheme. ## Snapshot 3D diff --git a/docs/api/stitching.md b/docs/api/stitching.md index 0f067087..9f132440 100644 --- a/docs/api/stitching.md +++ b/docs/api/stitching.md @@ -1,7 +1,19 @@ # Stitching -Multi-station image stitching for combining overlapping field-of-view acquisitions into -a single volume. +Combine multiple NIfTI volumes with overlapping field-of-views into a single +volume — typical for whole-body or long-spine multi-station acquisitions where +each station is stored as its own NIfTI file. + +The pipeline resamples every input into a common bounding box, optionally +applies N4 bias-field correction and histogram matching, and blends +overlapping regions using distance-transform-based weight ramps. Both intensity +and segmentation stitching are supported; for segmentations the blending +degenerates to a majority-style selection so labels remain integer-valued. + +Use [`stitching`][TPTBox.stitching.stitching_tools.stitching] as the high-level +entry point (accepts `BIDS_FILE`, `NII`, `str`, or `Path` inputs), or the +lower-level [`stitching_raw`][TPTBox.stitching.stitching.main] when you already +have `Nifti1Image` objects. ## Stitching From 1ebc26fa099091af93c8da381533951e3fd724f0 Mon Sep 17 00:00:00 2001 From: robert-graf <31210726+robert-graf@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:05:49 +0000 Subject: [PATCH 2/2] style fixes by ruff --- TPTBox/core/bids_files.py | 1 + 1 file changed, 1 insertion(+) diff --git a/TPTBox/core/bids_files.py b/TPTBox/core/bids_files.py index c3b1423c..44b536aa 100755 --- a/TPTBox/core/bids_files.py +++ b/TPTBox/core/bids_files.py @@ -1987,6 +1987,7 @@ def loop_dict( key_transform (typing.Callable[[BIDS_FILE], str | None]): provide alternative dict name for certain fils, if default should be used return None key_addendum (list[str] | None, optional): Extra info-key names appended to each family's dict keys to disambiguate otherwise-identical entries. Defaults to None. + Returns: typing.Iterator[typing.Dict[str, BIDS_FILE | list[BIDS_FILE]]] """