Skip to content

dicom2nifti

tit.pre.dicom2nifti

Source-image ingestion with BIDS-compliant naming.

Converts each modality found under sourcedata/sub-{id}/ into a BIDS-named NIfTI (sub-{id}_{suffix}.nii.gz). DICOM series are converted with dcm2niix; a modality folder that already holds a NIfTI is copied into place instead.

Public API

run_dicom_to_nifti Ingest every supported modality for a subject. MODALITIES Supported modalities and their BIDS datatype directories. bids_outputs The files a conversion of one modality wrote. pe_direction_label The BIDS dir- label of a phase-encoding direction in a NIfTI.

See Also

tit.pre.structural.run_pipeline : Full preprocessing pipeline.

bids_outputs

bids_outputs(output_dir: Path, subject_id: str, modality: str) -> list[Path]

Files a conversion of modality wrote into output_dir.

sub-{id}_{suffix} and sub-{id}_dir-{label}_{suffix} with their sidecars, NIfTIs first.

Source code in tit/pre/dicom2nifti.py
def bids_outputs(output_dir: Path, subject_id: str, modality: str) -> list[Path]:
    """Files a conversion of *modality* wrote into *output_dir*.

    ``sub-{id}_{suffix}`` and ``sub-{id}_dir-{label}_{suffix}`` with their
    sidecars, NIfTIs first.
    """
    suffix = _BIDS_SUFFIX.get(modality, modality)
    stems = (f"sub-{subject_id}_{suffix}", f"sub-{subject_id}_dir-*_{suffix}")
    return [
        path
        for extension in _SERIES_SUFFIXES
        for stem in stems
        for path in sorted(Path(output_dir).glob(f"{stem}{extension}"))
    ]

pe_direction_label

pe_direction_label(nifti: Path, phase_encoding_direction) -> str | None

BIDS dir- label (AP, PA, LR, RL, SI, IS) for a phase-encoding direction.

PhaseEncodingDirection names a voxel axis of nifti and travels from index 0 to the last index unless it ends in -. The label is the world direction of that travel, taken from the NIfTI's sform and rounded to the closest cardinal axis, as BIDS asks of AP/PA-style labels. None when the direction or the sform is missing -- a label is never guessed.

Source code in tit/pre/dicom2nifti.py
def pe_direction_label(nifti: Path, phase_encoding_direction) -> str | None:
    """BIDS ``dir-`` label (AP, PA, LR, RL, SI, IS) for a phase-encoding direction.

    ``PhaseEncodingDirection`` names a voxel axis of *nifti* and travels from index
    0 to the last index unless it ends in ``-``. The label is the world direction
    of that travel, taken from the NIfTI's sform and rounded to the closest
    cardinal axis, as BIDS asks of ``AP``/``PA``-style labels. ``None`` when the
    direction or the sform is missing -- a label is never guessed.
    """
    if phase_encoding_direction not in ("i", "i-", "j", "j-", "k", "k-"):
        return None
    column = _sform_column(nifti, "ijk".index(phase_encoding_direction[0]))
    if column is None or not any(column):
        return None
    sign = -1.0 if phase_encoding_direction.endswith("-") else 1.0
    world = [sign * value for value in column]
    dominant = max(range(3), key=lambda index: abs(world[index]))
    return _DIR_LABELS[dominant][world[dominant] > 0]

converted_nifti

converted_nifti(output_dir: Path, subject_id: str, modality: str) -> Path | None

The NIfTI an earlier ingestion of modality left in output_dir, or None.

Both extensions count: we always write .nii.gz, but a hand-placed .nii is the same BIDS entity. dir- variants count as well.

Source code in tit/pre/dicom2nifti.py
def converted_nifti(output_dir: Path, subject_id: str, modality: str) -> Path | None:
    """The NIfTI an earlier ingestion of *modality* left in *output_dir*, or ``None``.

    Both extensions count: we always write ``.nii.gz``, but a hand-placed
    ``.nii`` is the same BIDS entity. ``dir-`` variants count as well.
    """
    return next(
        (
            path
            for path in bids_outputs(output_dir, subject_id, modality)
            if _has_suffix(path.name, _NIFTI_SUFFIXES)
        ),
        None,
    )

run_dicom_to_nifti

run_dicom_to_nifti(project_dir: str, subject_id: str, *, logger, runner: CommandRunner | None = None, skip_existing: bool = False) -> None

Ingest a subject's source images into BIDS-named NIfTI files.

Looks for a T1w, T2w, ct, dwi and fmap folder under sourcedata/sub-{subject_id}/ (folder names are matched case-insensitively) and ingests each one that exists. Anatomical images and CT go to the subject's anat/ folder, diffusion images to dwi/ (with their .bval/.bvec sidecars), and the DWI's reverse phase-encoding series to fmap/sub-{id}_dir-{label}_epi.nii.gz with an IntendedFor naming the DWI. Two diffusion series in dwi with different phase encoding become sub-{id}_dir-{label}_dwi. The label (AP, PA, ...) comes from :func:pe_direction_label.

Each modality folder is searched recursively. Supported archives (.zip, .tar, .tar.gz, .tgz) are safely extracted to extracted_archives/ first. DICOM files (.dcm/.dicom, or any file carrying the DICOM Part 10 DICM prefix, such as .IMA or extensionless exports) are then converted with dcm2niix; if the folder holds no DICOMs but does hold a NIfTI, that file is copied into place instead (compressing a bare .nii on the way). Dotfiles are ignored throughout.

CT is written as anat/sub-{id}_ct.nii.gz. This is a local convention, not BIDS -- see :data:MODALITIES.

Parameters

project_dir : str BIDS project root directory. subject_id : str Subject identifier without the sub- prefix. logger : logging.Logger Logger for progress messages. runner : CommandRunner or None, optional Subprocess runner for streaming output. skip_existing : bool, optional Leave a modality whose NIfTI already exists as it is and ingest the others, instead of raising.

Raises

PreprocessError If an output NIfTI already exists for a modality (unless skip_existing), or if modality folders hold files and nothing at all could be ingested from them.

See Also

run_pipeline : Full preprocessing pipeline. MODALITIES : Supported modalities and their BIDS datatype directories.

Source code in tit/pre/dicom2nifti.py
def run_dicom_to_nifti(
    project_dir: str,
    subject_id: str,
    *,
    logger,
    runner: CommandRunner | None = None,
    skip_existing: bool = False,
) -> None:
    """Ingest a subject's source images into BIDS-named NIfTI files.

    Looks for a ``T1w``, ``T2w``, ``ct``, ``dwi`` and ``fmap`` folder under
    ``sourcedata/sub-{subject_id}/`` (folder names are matched
    case-insensitively) and ingests each one that exists. Anatomical images
    and CT go to the subject's ``anat/`` folder, diffusion images to ``dwi/``
    (with their ``.bval``/``.bvec`` sidecars), and the DWI's reverse
    phase-encoding series to ``fmap/sub-{id}_dir-{label}_epi.nii.gz`` with an
    ``IntendedFor`` naming the DWI. Two diffusion series in ``dwi`` with
    different phase encoding become ``sub-{id}_dir-{label}_dwi``. The label
    (``AP``, ``PA``, ...) comes from :func:`pe_direction_label`.

    Each modality folder is searched recursively. Supported archives
    (``.zip``, ``.tar``, ``.tar.gz``, ``.tgz``) are safely extracted to
    ``extracted_archives/`` first. DICOM files (``.dcm``/``.dicom``, or any
    file carrying the DICOM Part 10 ``DICM`` prefix, such as ``.IMA`` or
    extensionless exports) are then converted with ``dcm2niix``; if the folder
    holds no DICOMs but does hold a NIfTI, that file is copied into place
    instead (compressing a bare ``.nii`` on the way). Dotfiles are ignored
    throughout.

    CT is written as ``anat/sub-{id}_ct.nii.gz``. This is a local convention,
    not BIDS -- see :data:`MODALITIES`.

    Parameters
    ----------
    project_dir : str
        BIDS project root directory.
    subject_id : str
        Subject identifier without the ``sub-`` prefix.
    logger : logging.Logger
        Logger for progress messages.
    runner : CommandRunner or None, optional
        Subprocess runner for streaming output.
    skip_existing : bool, optional
        Leave a modality whose NIfTI already exists as it is and ingest the
        others, instead of raising.

    Raises
    ------
    PreprocessError
        If an output NIfTI already exists for a modality (unless
        *skip_existing*), or if modality folders hold files and nothing at all
        could be ingested from them.

    See Also
    --------
    run_pipeline : Full preprocessing pipeline.
    MODALITIES : Supported modalities and their BIDS datatype directories.
    """
    from tit.telemetry import track_operation
    from tit import constants as _const

    with track_operation(_const.TELEMETRY_OP_PRE_DICOM):
        pm = get_path_manager(project_dir)
        sourcedata_dir = Path(pm.sourcedata_subject(subject_id))

        converted = skipped = False
        unconverted: list[str] = []
        for modality, datatype in MODALITIES:
            modality_dir = _modality_source_dir(sourcedata_dir, modality)
            if not modality_dir.exists():
                continue
            output_dir = Path(pm.bids_datatype(subject_id, datatype))
            existing = converted_nifti(output_dir, subject_id, modality)
            if skip_existing and existing is not None:
                logger.info(f"{modality}: skipping because {existing} already exists")
                skipped = True
                continue
            if _ingest_modality(
                modality_dir, output_dir, subject_id, modality, logger, runner
            ):
                converted = True
                continue
            count = sum(1 for _ in _iter_files(modality_dir))
            if count:
                unconverted.append(f"{modality_dir} holds {count} file(s)")

        if not converted and not skipped and unconverted:
            # Returning here would hand the next stage nothing, and it would fail
            # telling the user to run the conversion they just ran.
            raise PreprocessError(
                f"DICOM conversion produced nothing for sub-{subject_id}: "
                f"{'; '.join(unconverted)}, but none could be converted. A DICOM "
                "needs a .dcm/.dicom extension or the DICM prefix of a DICOM file, "
                "a NIfTI a .nii/.nii.gz extension, and dcm2niix must succeed "
                "(see the warnings above)."
            )
        if not converted and not skipped:
            logger.warning("No DICOM or NIfTI files found or converted")