Skip to content

dti_extractor

tit.pre.qsi.dti_extractor

DTI tensor extraction for SimNIBS integration.

This module extracts DTI tensors from QSIRecon outputs and converts them to the format expected by SimNIBS for anisotropic conductivity simulations.

SimNIBS expects a 4D NIfTI file with the diffusion tensor stored as a 6-component upper triangular representation: [Dxx, Dxy, Dxz, Dyy, Dyz, Dzz].

The tensor must be coregistered to the SimNIBS T1 space.

extract_dti_tensor

extract_dti_tensor(project_dir: str, subject_id: str, *, logger: Logger, source: str = 'qsirecon', skip_registration: bool = False) -> Path

Extract DTI tensor from QSIRecon output for SimNIBS.

This function finds the DTI tensor in QSIRecon outputs, converts it to SimNIBS format, and saves it to the m2m directory.

Parameters

project_dir : str Path to the BIDS project root directory. subject_id : str Subject identifier (without 'sub-' prefix). logger : logging.Logger Logger for status messages. source : str, optional Source of DTI tensor. Currently only 'qsirecon' is supported. Default: 'qsirecon'. skip_registration : bool, optional If True, skip registration to SimNIBS T1 space. Use this if the tensor is already in the correct space. Default: False.

Returns

Path Path to the extracted DTI tensor file in m2m directory.

Raises

PreprocessError If tensor extraction fails.

Source code in tit/pre/qsi/dti_extractor.py
def extract_dti_tensor(
    project_dir: str,
    subject_id: str,
    *,
    logger: logging.Logger,
    source: str = "qsirecon",
    skip_registration: bool = False,
) -> Path:
    """
    Extract DTI tensor from QSIRecon output for SimNIBS.

    This function finds the DTI tensor in QSIRecon outputs, converts it
    to SimNIBS format, and saves it to the m2m directory.

    Parameters
    ----------
    project_dir : str
        Path to the BIDS project root directory.
    subject_id : str
        Subject identifier (without 'sub-' prefix).
    logger : logging.Logger
        Logger for status messages.
    source : str, optional
        Source of DTI tensor. Currently only 'qsirecon' is supported.
        Default: 'qsirecon'.
    skip_registration : bool, optional
        If True, skip registration to SimNIBS T1 space. Use this if the
        tensor is already in the correct space. Default: False.

    Returns
    -------
    Path
        Path to the extracted DTI tensor file in m2m directory.

    Raises
    ------
    PreprocessError
        If tensor extraction fails.
    """
    # Delayed import to avoid circular dependencies
    import nibabel as nib

    logger.info(f"Extracting DTI tensor for subject {subject_id}")

    # Get paths
    pm = get_path_manager(project_dir)

    m2m_dir = pm.m2m(subject_id)
    if not os.path.isdir(m2m_dir):
        raise PreprocessError(
            f"m2m directory not found for subject {subject_id}. "
            "Run SimNIBS charm first."
        )

    output_path = Path(m2m_dir) / const.FILE_DTI_TENSOR

    # Check for existing output
    if output_path.exists():
        raise PreprocessError(
            f"DTI tensor already exists at {output_path}. "
            "Remove the file manually before rerunning."
        )

    # Find source tensor
    if source != "qsirecon":
        raise PreprocessError(f"Unknown source: {source}")

    qsirecon_dir = Path(project_dir) / "derivatives" / "qsirecon"

    # Try to find DSI Studio tensor components first (most common)
    dsistudio_components = _find_dsistudio_tensor_components(
        qsirecon_dir, subject_id, logger
    )

    if dsistudio_components:
        logger.info("Using DSI Studio tensor components")
        tensor_data, affine, header = _combine_dsistudio_tensor_components(
            dsistudio_components, logger
        )
        # Already in the correct format [Dxx, Dxy, Dxz, Dyy, Dyz, Dzz]
        simnibs_tensor = tensor_data
    else:
        # Try to find DKI tensor
        dt_file, _ = _find_dki_tensor_files(qsirecon_dir, subject_id, logger)

        if dt_file is not None:
            source_file = dt_file
            logger.info(f"Using DKI diffusion tensor: {dt_file}")
        else:
            # Fall back to general DTI tensor search
            source_file = _find_dti_tensor_file(qsirecon_dir, subject_id, logger)

        if source_file is None:
            raise PreprocessError(
                f"No DTI tensor found for subject {subject_id} in QSIRecon output. "
                "Ensure QSIRecon was run with DSI Studio (dsi_studio_gqi) or "
                "another DTI-producing spec like dipy_dki."
            )

        logger.info(f"Source tensor file: {source_file}")

        # Load and convert tensor
        try:
            tensor_img = nib.load(str(source_file))
            tensor_data = tensor_img.get_fdata(dtype=np.float32)
            affine = tensor_img.affine
            header = tensor_img.header
        except (OSError, ValueError) as e:
            raise PreprocessError(f"Failed to load tensor file: {e}")

        # Convert to SimNIBS format
        try:
            simnibs_tensor = _convert_tensor_to_simnibs_format(tensor_data, logger)
        except ValueError as e:
            raise PreprocessError(f"Failed to convert tensor: {e}")

    _validate_tensor(simnibs_tensor, logger)

    # Save intermediate tensor (before registration)
    intermediate_path = Path(m2m_dir) / "DTI_ACPC_tensor.nii.gz"
    try:
        intermediate_img = nib.Nifti1Image(simnibs_tensor, affine, header)
        nib.save(intermediate_img, str(intermediate_path))
        logger.info(f"Saved intermediate tensor to: {intermediate_path}")
    except (OSError, ValueError, TypeError) as e:
        raise PreprocessError(f"Failed to save intermediate tensor: {e}")

    # Register to SimNIBS T1 space
    if not skip_registration:
        simnibs_t1_path = Path(m2m_dir) / const.FILE_T1
        if not simnibs_t1_path.exists():
            raise PreprocessError(
                f"SimNIBS T1 not found at {simnibs_t1_path}. "
                "Run SimNIBS charm first."
            )

        qsiprep_t1_path = _find_qsiprep_t1(Path(project_dir), subject_id)
        if qsiprep_t1_path is None:
            logger.warning(
                "qsiprep T1 not found. Using simple resampling instead of "
                "proper registration."
            )
            _resample_tensor_to_target(
                intermediate_path, simnibs_t1_path, output_path, logger
            )
        else:
            try:
                _register_tensor_to_simnibs_t1(
                    intermediate_path,
                    qsiprep_t1_path,
                    simnibs_t1_path,
                    output_path,
                    logger,
                )
            except PreprocessError as e:
                logger.warning(
                    f"Registration failed: {e}. Falling back to simple resampling."
                )
                _resample_tensor_to_target(
                    intermediate_path, simnibs_t1_path, output_path, logger
                )
    else:
        # Just copy the intermediate file to output
        shutil.copy2(intermediate_path, output_path)
        logger.info(f"Copied tensor to output (skip_registration=True)")

    logger.info(f"Saved DTI tensor to: {output_path}")
    return output_path

check_dti_tensor_exists

check_dti_tensor_exists(project_dir: str, subject_id: str) -> bool

Check if a DTI tensor file exists for a subject.

Parameters

project_dir : str Path to the project directory. subject_id : str Subject identifier.

Returns

bool True if the DTI tensor file exists in the m2m directory.

Source code in tit/pre/qsi/dti_extractor.py
def check_dti_tensor_exists(project_dir: str, subject_id: str) -> bool:
    """
    Check if a DTI tensor file exists for a subject.

    Parameters
    ----------
    project_dir : str
        Path to the project directory.
    subject_id : str
        Subject identifier.

    Returns
    -------
    bool
        True if the DTI tensor file exists in the m2m directory.
    """
    pm = get_path_manager(project_dir)

    m2m_dir = pm.m2m(subject_id)
    if not os.path.isdir(m2m_dir):
        return False

    tensor_path = Path(m2m_dir) / const.FILE_DTI_TENSOR
    return tensor_path.exists()