Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KIRBY21 BIDS converter #1298

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions clinica/iotools/bids_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class StudyName(str, Enum):
OASIS3 = "OASIS3"
UKB = "UKB"
IXI = "IXI"
KIRBY = "KIRBY"


BIDS_VALIDATOR_CONFIG = {
Expand Down Expand Up @@ -93,6 +94,8 @@ def bids_id_factory(study: StudyName) -> Type[BIDSSubjectID]:
return HABSBIDSSubjectID
if study == StudyName.IXI:
return IXIBIDSSubjectID
if study == StudyName.KIRBY:
return KIRBYBIDSSubjectID


class ADNIBIDSSubjectID(BIDSSubjectID):
Expand Down Expand Up @@ -319,6 +322,30 @@ def from_original_study_id(cls, study_id: str) -> str:
def to_original_study_id(self) -> str:
return str(self.replace("sub-", ""))

class KIRBYBIDSSubjectID(BIDSSubjectID):
"""Implementation for KIRBY of the BIDSSubjectIDClass, allowing to go from the source id KIRBY###
to a bids id sub-KKI### and reciprocally."""

def validate(self, value: str) -> str:
if re.fullmatch(r"sub-KKI\d{3}", value):
return value
raise ValueError(
f"BIDS KKI subject ID {value} is not properly formatted. "
"Expecting a 'sub-KKI' format."
)

@classmethod
def from_original_study_id(cls, study_id: str) -> str:
if re.fullmatch(r"KKI\d{3}", study_id):
return f"sub-{study_id}"
raise ValueError(
f"Raw KIRBY subject ID {study_id} is not properly formatted. "
"Expecting a 'Y' format."
)

def to_original_study_id(self) -> str:
return str(self.replace("sub-", ""))


# -- Methods for the clinical data --
def create_participants_df(
Expand Down
2 changes: 2 additions & 0 deletions clinica/iotools/converters/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .oasis3_to_bids import oasis3_to_bids_cli
from .oasis_to_bids import oasis_to_bids_cli
from .ukb_to_bids import ukb_to_bids_cli
from .kirby_to_bids import kirby_to_bids_cli


@click.group("convert")
Expand All @@ -26,6 +27,7 @@ def cli() -> None:
cli.add_command(ukb_to_bids_cli.cli)
cli.add_command(genfi_to_bids_cli.cli)
cli.add_command(ixi_to_bids_cli.cli)
cli.add_command(kirby_to_bids_cli.cli)

if __name__ == "__main__":
cli()
2 changes: 2 additions & 0 deletions clinica/iotools/converters/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,6 @@ def converter_factory(study: Union[str, StudyName]) -> Callable:
from .ukb_to_bids import convert
if study == StudyName.IXI:
from .ixi_to_bids import convert
if study == StudyName.KIRBY:
from .kirby_to_bids import convert
return convert
3 changes: 3 additions & 0 deletions clinica/iotools/converters/kirby_to_bids/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .kirby_to_bids import convert

__all__ = ["convert"]
121 changes: 121 additions & 0 deletions clinica/iotools/converters/kirby_to_bids/kirby_to_bids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Convert KIRBY dataset (https://brain-development.org/ixi-dataset/) to BIDS."""

from pathlib import Path
from typing import Optional

import nibabel as nb
import numpy as np
import csv

from clinica.iotools.bids_utils import write_modality_agnostic_files
from clinica.iotools.converters.kirby_to_bids.kirby_to_bids_utils import (
create_bids_structure,
convert_to_nii_gz,
find_nii_files,
normalize_dashes,
replace_dashes_with_underscore,
)
from clinica.utils.filemanip import UserProvidedPath

__all__ = ["convert"]

import os
import pandas as pd

def convert(
path_to_dataset: str,
bids_dir: str,
path_to_clinical: str,
subjects: Optional[str] = None,
n_procs: Optional[int] = 1,
**kwargs,
):
"""Convert KIRBY data to BIDS format from Excel clinical data."""
clinical_data_file = None
for file in os.listdir(path_to_clinical):
if file.endswith('.xlsx'):
clinical_data_file = os.path.join(path_to_clinical, file)
break

if not clinical_data_file:
raise FileNotFoundError(f"No clinical data Excel file found in {path_to_clinical}")

clinical_data = pd.read_excel(clinical_data_file)
clinical_data_filtered = clinical_data[['MPRAGE', 'Age', 'Sex', 'Fiducial', 'Subject ID', 'Visit ID']]
clinical_data_filtered['MPRAGE'] = clinical_data_filtered['MPRAGE'].apply(replace_dashes_with_underscore).str.strip()
clinical_data_filtered.reset_index(drop=True, inplace=True)

participants_data = {}
session_count = {}

# Get all .nii files (directly or within subfolders)
nii_files = find_nii_files(path_to_dataset)

# Traverse found nii files
for file_path in nii_files:
file = os.path.basename(file_path).replace('.nii', '').strip() # Remove file extension and strip whitespaces
file_normalized = replace_dashes_with_underscore(file)

if 'MPRAGE' in file_normalized:
# Find the exact match in the clinical data's MPRAGE column
clinical_row = clinical_data_filtered[clinical_data_filtered['MPRAGE'] == file_normalized]

# Check if any matching rows were found
if clinical_row.empty:
print(f"No matching clinical data found for file: {file_normalized}")
continue

# Extract the first matching row (in case multiple matches are found)
clinical_row = clinical_row.iloc[0]

# Extract relevant clinical information
subject_id = clinical_row['Subject ID']
session_id = clinical_row['Visit ID']
age = clinical_row['Age']
sex = clinical_row['Sex']
handedness = clinical_row['Fiducial']

# Keep track of how many sessions have been processed for this subject
if subject_id not in session_count:
session_count[subject_id] = 1
else:
session_count[subject_id] += 1

# Assign session as "ses-01", "ses-02", etc.
session_label = f"{session_count[subject_id]:02d}"

# Create BIDS structure and move the file
create_bids_structure(subject_id, session_label, file_path, bids_dir)

# Store participant data (with baseline age if needed)
if subject_id not in participants_data:
participants_data[subject_id] = {'participant_id': f"sub-KKI{subject_id}",
'sex': sex,
'age': age,
'handedness': handedness}

# Write participants.csv
participants_csv = os.path.join(bids_dir, 'participants.csv')
with open(participants_csv, 'w', newline='') as participants_file:
participants_writer = csv.writer(participants_file)
participants_writer.writerow(['participant_id', 'sex', 'age', 'handedness'])

for participant_info in participants_data.values():
participants_writer.writerow([participant_info['participant_id'],
participant_info['sex'],
participant_info['age'],
participant_info['handedness']])

# Write sessions.tsv for each subject
for subject_id, session_count in session_count.items():
sessions_file = os.path.join(bids_dir, f"sub-KKI{subject_id}", 'sessions.tsv')
os.makedirs(os.path.dirname(sessions_file), exist_ok=True)

with open(sessions_file, 'w', newline='') as session_file:
session_writer = csv.writer(session_file, delimiter='\t')
session_writer.writerow(['session_id', 'age'])

for session_num in range(1, session_count + 1):
session_writer.writerow([f"ses-{session_num:02d}", participants_data[subject_id]['age']])

print(f"KIRBY BIDS conversion completed using the clinical data from {clinical_data_file}.")
27 changes: 27 additions & 0 deletions clinica/iotools/converters/kirby_to_bids/kirby_to_bids_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from os import PathLike
from typing import Optional

import click

from clinica.iotools.converters import cli_param


@click.command(name="kirby-to-bids")
@cli_param.dataset_directory
@cli_param.bids_directory
@cli_param.clinical_data_directory
@cli_param.subjects_list
def cli(
dataset_directory: PathLike,
bids_directory: PathLike,
clinical_data_directory: PathLike,
subjects_list: Optional[PathLike] = None,
) -> None:
"""KIRBY to BIDS converter."""
from .kirby_to_bids import convert

convert(dataset_directory, bids_directory, clinical_data_directory, subjects_list)


if __name__ == "__main__":
cli()
72 changes: 72 additions & 0 deletions clinica/iotools/converters/kirby_to_bids/kirby_to_bids_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import json
import re
import shutil
from enum import Enum
from pathlib import Path
from typing import List, Optional, Union
import os

import nibabel as nib
import pandas as pd
from nilearn.image import concat_imgs

from clinica.iotools.bids_utils import StudyName, bids_id_factory
from clinica.utils.stream import cprint, log_and_raise

__all__ = [
"read_clinical_data",
"define_participants",
"write_subject_data",
"write_sessions",
"write_scans",
"write_participants",
"check_modalities",
]


# Function to recursively find all files with .nii extension in input directory
def find_nii_files(directory):
nii_files = []
for root, _, files in os.walk(directory):
for file in files:
if file.endswith('.nii') and 'MPRAGE' in file:
nii_files.append(os.path.join(root, file))
return nii_files

# Function to normalize dashes (replace any type of dash with a standard hyphen '-')
def normalize_dashes(text):
if isinstance(text, str):
return re.sub(r'[\u2013\u2014\u2212]', '-', text) # Replaces en dash, em dash, and other similar symbols
return text

def convert_to_nii_gz(input_file):
"""Convert a .nii file to .nii.gz format without deleting the original .nii file."""
if input_file.endswith(".nii.gz"):
return input_file
img = nib.load(input_file)
output_file = input_file.replace(".nii", ".nii.gz")
nib.save(img, output_file)
return output_file

def replace_dashes_with_underscore(string):
"""Replace dashes with underscores for standardized comparison."""
return string.replace("-", "_")

# Helper function to create BIDS folders and move files
def create_bids_structure(subject_id, session, input_file, output_dir):
"""Create BIDS folder structure and move files into it."""
sub_id = f"sub-KKI{subject_id}"
ses_id = f"ses-{session}"

# Create output directory for this subject/session
anat_dir = os.path.join(output_dir, sub_id, ses_id, 'anat')
os.makedirs(anat_dir, exist_ok=True)

# Convert the input file to .nii.gz if necessary
input_file_gz = convert_to_nii_gz(input_file)

# Destination filename in BIDS format
bids_filename = f"{sub_id}_{ses_id}_T1w.nii.gz"

# Copy and rename the file to BIDS format
shutil.copy(input_file_gz, os.path.join(anat_dir, bids_filename))
Loading