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

MIRIAD converter to BIDS #1290

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 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: 26 additions & 1 deletion 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"
MIRIAD = "MIRIAD"


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

if study == StudyName.MIRIAD:
return MIRIADBIDSSubjectID

class ADNIBIDSSubjectID(BIDSSubjectID):
"""Implementation for ADNI of the BIDSSubjectIDClass, allowing to go from the source id XXX_S_XXXX
Expand Down Expand Up @@ -319,6 +321,29 @@ def from_original_study_id(cls, study_id: str) -> str:
def to_original_study_id(self) -> str:
return str(self.replace("sub-", ""))

class MIRIADBIDSSubjectID(BIDSSubjectID):
"""Implementation for MIRIAD of the BIDSSubjectIDClass, allowing to go from the source id MIRIAD###
to a bids id sub-MIRAD### and reciprocally."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to a bids id sub-MIRAD### and reciprocally."""
to a bids id sub-MIRIAD### and reciprocally."""


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Expecting a 'sub-MIRIAD' format."
"Expecting a 'sub-MIRIADXXX' format."

)

@classmethod
def from_original_study_id(cls, study_id: str) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written you should have ids of 3 digits always (so there would need to be a padding if it can be 1 or 2). Though I don't think you are using that class for now

if re.fullmatch(r"MIRIAD\d{3}", study_id):
return f"sub-{study_id}"
raise ValueError(
f"Raw MIRIAD subject ID {study_id} is not properly formatted. "
"Expecting a 'Y' format."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Expecting a 'Y' format."
"Expecting a 'MIRIADXXX' 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 .miriad_to_bids import miriad_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(miriad_to_bids_cli.cli)

if __name__ == "__main__":
cli()
4 changes: 4 additions & 0 deletions clinica/iotools/converters/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def get_converter_name(study: Union[str, StudyName]) -> str:
return "UkbToBids"
if study == StudyName.IXI:
return "IxiToBids"
if study == StudyName.MIRIAD:
return "MiriadToBids"


def converter_factory(study: Union[str, StudyName]) -> Callable:
Expand All @@ -62,4 +64,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.MIRIAD:
from .miriad_to_bids import convert
return convert
3 changes: 3 additions & 0 deletions clinica/iotools/converters/miriad_to_bids/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .miriad_to_bids import convert

__all__ = ["convert"]
64 changes: 64 additions & 0 deletions clinica/iotools/converters/miriad_to_bids/miriad_to_bids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Convert MIRIAD dataset to BIDS."""

from pathlib import Path
from typing import Optional

import os
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import os
import re

Having consistent Path objects (see below !) allows to use only path lib, so we don't need os anymore

import shutil
import csv
from clinica.utils.filemanip import UserProvidedPath

# Paths
input_dir = 'your_input_directory' # Where the original data is located
output_dir = 'your_output_directory' # Where the BIDS data will be written
csv_file = 'metadata.csv' # Metadata CSV file to store extracted information
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
input_dir = 'your_input_directory' # Where the original data is located
output_dir = 'your_output_directory' # Where the BIDS data will be written
csv_file = 'metadata.csv' # Metadata CSV file to store extracted information

You can directly use the paths given to the convert function :

  • input_dir < > path_to_dataset
  • output_dir < > bids_dir

And if you use 'metadata.csv' only once I would not define a variable specifically for this, you can use it as is inside convert


def convert(
path_to_dataset: UserProvidedPath,
bids_dir: UserProvidedPath,
path_to_clinical: UserProvidedPath,
subjects: Optional[UserProvidedPath] = None,
n_procs: Optional[int] = 1,
**kwargs,
):
"""_summary_

Args:
path_to_dataset (UserProvidedPath): _description_
bids_dir (UserProvidedPath): _description_
path_to_clinical (UserProvidedPath): _description_
subjects (Optional[UserProvidedPath], optional): _description_. Defaults to None.
n_procs (Optional[int], optional): _description_. Defaults to 1.
"""
from clinica.iotools.converters.miriad_to_bids.miriad_to_bids_utils import create_bids_structure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from clinica.iotools.converters.miriad_to_bids.miriad_to_bids_utils import create_bids_structure
from clinica.iotools.converters.miriad_to_bids.miriad_to_bids_utils import create_bids_structure
from ..utils import validate_input_path
path_to_dataset = validate_input_path(path_to_dataset)
bids_dir = validate_input_path(bids_dir, check_exist=False)
if n_procs != 1:
cprint(
f"{get_converter_name(StudyName.MIRIAD)} converter does not support multiprocessing yet. n_procs set to 1.",
lvl="warning",
)
if not subjects:
#todo

First step there would be to check the inputs of the convert function :

  • your paths : the function given here verify existence go the path if needed and return a Path object, so we are sure we get always the same type of objects when it comes to user-given paths.
  • optional parameters : n_procs is not implemented (and I doubt you want to) so we just warn the user if she/he tries to use it. Should be the same for subjects 😁

Makes me wonder though, you do not plan on using any clinical data as an input ?

# Prepare CSV
with open(csv_file, 'w', newline='') as csvfile:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with open(csv_file, 'w', newline='') as csvfile:
with open(bids_dir/'metadata.csv', 'w', newline='') as csvfile:

There I am assuming you want to write the .csv at the root of the new bids directory but that might not be what you want. Maybe you could use path_to_clinical here

csvwriter = csv.writer(csvfile)
csvwriter.writerow(['cohort', 'subject_id', 'diagnosis', 'gender', 'session', 'input_file', 'output_file'])

# Traverse the input directory
for root, dirs, files in os.walk(path_to_dataset):
for file in files:
if file.endswith('.nii'):
# Example: miriad_215_AD_M_01_MR_1.nii
parts = file.split('_')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for root, dirs, files in os.walk(path_to_dataset):
for file in files:
if file.endswith('.nii'):
# Example: miriad_215_AD_M_01_MR_1.nii
parts = file.split('_')
file_paths = list(
path for path in path_to_dataset.rglob(pattern="*.nii")
if re.match(r"miriad(_\w*){4,}.nii", path.name)
)
# there I am assuming the file names always start by 'miriad' ; it makes sure your filename has at least 5 components separated by a '_'
for file_path in file_paths:
parts = file_path.name.split('_')


# Extract information from filename
cohort = parts[0] # miriad
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cohort = parts[0] # miriad

If it is always the same as I assumed above in my regex you do not need to retrieve the info anymore

subject_id = parts[1] # 215
diagnosis = parts[2] # AD (Alzheimer's) or HC (Healthy Control)
gender = parts[3] # M or F
session = parts[4] # Session number

# Full path of input file
input_file = os.path.join(root, file)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
input_file = os.path.join(root, file)

You can use file_path directly


# Create BIDS structure and move the file
create_bids_structure(subject_id, session, cohort, diagnosis, gender, input_file, path_to_dataset, bids_dir, path_to_clinical)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
create_bids_structure(subject_id, session, cohort, diagnosis, gender, input_file, path_to_dataset, bids_dir, path_to_clinical)
create_bids_structure(subject_id, session, file_path, bids_dir)


# Write the extracted information to CSV
bids_filename = f"sub-{subject_id}_ses-{session}_T1w.nii.gz"
output_file = os.path.join(f"sub-{subject_id}", f"ses-{session}", 'anat', bids_filename)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bids_filename = f"sub-{subject_id}_ses-{session}_T1w.nii.gz"
output_file = os.path.join(f"sub-{subject_id}", f"ses-{session}", 'anat', bids_filename)
output_file = f"sub-MIRIAD{subject_id}/ses-{session}/anat/sub-MIRIAD{subject_id}_ses-{session}_T1w.nii.gz"

csvwriter.writerow([cohort, subject_id, diagnosis, gender, session, input_file, output_file])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want the "subject_id" to be MIRIADXXX or XXX ?


print("Conversion to BIDS format and metadata extraction completed.")
27 changes: 27 additions & 0 deletions clinica/iotools/converters/miriad_to_bids/miriad_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="miriad-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:
"""MIRIAD to BIDS converter."""
from .miriad_to_bids import convert

convert(dataset_directory, bids_directory, clinical_data_directory, subjects_list)


if __name__ == "__main__":
cli()
30 changes: 30 additions & 0 deletions clinica/iotools/converters/miriad_to_bids/miriad_to_bids_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
import shutil

# Helper function to create BIDS folders and move files
def create_bids_structure(subject_id, session, cohort, diagnosis, gender, input_file, path_to_dataset, output_dir, path_to_clinical
):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def create_bids_structure(subject_id, session, cohort, diagnosis, gender, input_file, path_to_dataset, output_dir, path_to_clinical
):
def create_bids_structure(
subject_id : str,
session : str,
input_file : Path,
output_dir : Path,
) -> None:


"""_summary_

Args:
session (_type_): _description_
cohort (_type_): _description_
diagnosis (_type_): _description_
gender (_type_): _description_
input_file (_type_): _description_
output_dir (_type_): _description_
path_to_dataset (_type_, optional): _description_. Defaults to None, n_procs: Optional[int] = 1, **kwargs, ):#subject_id.
"""
sub_id = f"sub-MIRIAD{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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
anat_dir = os.path.join(output_dir, sub_id, ses_id, 'anat')
anat_dir = output_dir / sub_id / ses_id / 'anat'

os.makedirs(anat_dir, exist_ok=True)

# 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, os.path.join(anat_dir, bids_filename))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
shutil.copy(input_file, os.path.join(anat_dir, bids_filename))
shutil.copy(input_file, anat_dir / bids_filename)

Loading