Skip to content

Commit

Permalink
[ENH] include model name in ouput file (#232)
Browse files Browse the repository at this point in the history
* rename output of the prepare step

* update naming and metadata for generalize step

* fix tests

* fix

* ref

* fix

* change suffix to timeseries
  • Loading branch information
Remi-Gau authored Aug 12, 2024
1 parent 72d0e81 commit 391274b
Show file tree
Hide file tree
Showing 31 changed files with 271 additions and 142 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ generalize: ## demo: predicts labels of MOAE dataset
generalize \
--model 1_guided_fixations \
-vv
bidsmreye $$PWD/tests/data/moae_fmriprep \
$$PWD/outputs/moae_fmriprep/derivatives \
participant \
generalize \
-vv

# run demo via boutiques
demo_boutiques: tests/data/moae_fmriprep
Expand Down
45 changes: 38 additions & 7 deletions bidsmreye/bids_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -88,7 +89,10 @@ def check_layout(cfg: Config, layout: BIDSLayout, for_file: str = "bold") -> Non


def create_bidsname(
layout: BIDSLayout, filename: dict[str, str] | str | Path, filetype: str
layout: BIDSLayout,
filename: dict[str, str] | str | Path,
filetype: str,
extra_entities: dict[str, str] | None = None,
) -> Path:
"""Return a BIDS valid filename for layout and a filename or a dict of BIDS entities.
Expand Down Expand Up @@ -117,8 +121,9 @@ def create_bidsname(
padding = len(x.split("-")[1])
entities["run"] = f"{entities['run']:0{padding}d}"

else:
raise TypeError(f"filename must be a dict or a Path, not {type(filename)}")
if extra_entities is not None:
for key in extra_entities:
entities[key] = extra_entities[key]

bids_name_config = get_bidsname_config()

Expand All @@ -128,6 +133,32 @@ def create_bidsname(
return output_file.absolute()


def return_desc_entity(model_filename: Path):
model_name = sanitize_filename(model_filename).replace("Dataset", "")
if model_name in ["1GuidedFixations", "5FreeViewing"]:
return model_name[1:]
elif model_name == "3Openclosed":
return "OpenClosed"
elif model_name in ["4Pursuit", "2Pursuit", "3Pursuit"]:
return model_name[1:] + model_name[0]
elif model_name in ["1to5", "1to6"]:
return model_name
else:
return sanitize_filename(model_filename)


def sanitize_filename(filename: Path):
"""Turn filename stem into its alphanumeric CamelCase equivalent.
To use as a BIDS entity label.
"""
# Remove non-alphanumeric characters and split into words
words = re.sub(r"[^a-zA-Z0-9]", " ", filename.stem).split()
# Capitalize the first letter of each word and join them
camelcase_name = "".join(word.capitalize() for word in words)
return camelcase_name


def create_sidecar(
layout: BIDSLayout,
filename: str,
Expand All @@ -142,9 +173,9 @@ def create_sidecar(
}
if source is not None:
content["Sources"] = [source] # type: ignore
sidecar_name = create_bidsname(layout, filename, "confounds_json")
sidecar_name = create_bidsname(layout, filename, "no_label_json")
json.dump(content, open(sidecar_name, "w"), indent=4)
log.debug(f"sidecar saved to {sidecar_name}")
log.debug(f"Sidecar saved to {sidecar_name}")


def save_sampling_frequency_to_json(
Expand Down Expand Up @@ -191,7 +222,7 @@ def get_dataset_layout(
if config is None:
pybids_config = get_pybids_config()

log.info(f"indexing {dataset_path}")
log.info(f"Indexing {dataset_path}")

if not use_database:
return BIDSLayout(
Expand Down Expand Up @@ -263,7 +294,7 @@ def list_subjects(cfg: Config, layout: BIDSLayout) -> list[str]:
subjects = [subjects[0]]
log.debug("Running first subject only.")

log.info(f"processing subjects: {subjects}")
log.info(f"Processing subjects: {subjects}")

return subjects

Expand Down
13 changes: 7 additions & 6 deletions bidsmreye/config/config_bidsname.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"mask": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_mask.p",
"report": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_report.html",
"no_label": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-nolabel_bidsmreye.npz",
"confounds_tsv": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.tsv",
"confounds_json": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.json",
"confounds_svg": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.svg",
"confounds_html": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.html",
"confounds_numpy": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}_space-{space}_desc-bidsmreye_confounds.npy"
"no_label_bold": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_timeseries.npz",
"no_label_json": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_timeseries.json",
"confounds_tsv": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.tsv",
"confounds_json": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.json",
"confounds_html": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.html",
"confounds_svg": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.svg",
"confounds_numpy": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}_space-{space}_desc-{desc}_confounds.npy"
}
11 changes: 8 additions & 3 deletions bidsmreye/config/default_filter_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
"suffix": "mask",
"extension": "p"
},
"no_label": {
"desc": "nolabel",
"suffix": "^bidsmreye$$",
"no_label_bold": {
"desc": "eye",
"suffix": "^timeseries$$",
"extension": "npz"
},
"no_label_bold_json": {
"desc": "eye",
"suffix": "^timeseries$$",
"extension": "json"
},
"eyetrack": {
"suffix": "^eyetrack$$",
"extension": "tsv"
Expand Down
80 changes: 67 additions & 13 deletions bidsmreye/generalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

import json
import logging
import os
import shutil
import warnings
from pathlib import Path
from typing import Any
Expand All @@ -20,12 +22,13 @@
create_bidsname,
get_dataset_layout,
list_subjects,
return_desc_entity,
)
from bidsmreye.configuration import Config
from bidsmreye.logger import bidsmreye_log
from bidsmreye.quality_control import quality_control_output
from bidsmreye.utils import (
add_sidecar_in_root,
add_timestamps_to_dataframe,
check_if_file_found,
create_dir_for_file,
move_file,
Expand Down Expand Up @@ -70,7 +73,9 @@ def create_and_save_figure(
fig.write_image(confound_svg)


def convert_confounds(layout_out: BIDSLayout, file: str | Path) -> Path:
def convert_confounds(
layout_out: BIDSLayout, file: str | Path, extra_entities: dict[str, str] | None = None
) -> Path:
"""Convert numpy output to TSV.
:param layout_out: pybids layout to of the dataset to act on.
Expand All @@ -86,7 +91,43 @@ def convert_confounds(layout_out: BIDSLayout, file: str | Path) -> Path:
but should still be able to unpack the results from a numpy file
with results from multiple files.
"""
confound_numpy = create_bidsname(layout_out, file, "confounds_numpy")
COLUMNS = ["timestamp", "x_coordinate", "y_coordinate"]

bold_json = Path(file).with_suffix(".json")
confounds_json = create_bidsname(
layout_out, file, "confounds_json", extra_entities=extra_entities
)
shutil.copyfile(bold_json, confounds_json)
with open(confounds_json) as f:
metadata = json.load(f)
metadata["StartTime"] = 0.0
metadata["Columns"] = COLUMNS
metadata["PhysioType"] = "eyetrack"
metadata["EnvironmentCoordinates"] = "center"
metadata["RecordedEye"] = "cyclopean"
metadata["timestamp"] = {
"Description": (
"Timestamp indexing the continuous recordings "
"corresponding to the sampled eye."
),
"Units": "seconds",
}
metadata["x_coordinate"] = {
"Description": ("Gaze position x-coordinate of the recorded eye."),
"Units": "degrees",
}
metadata["y_coordinate"] = {
"Description": ("Gaze position y-coordinate of the recorded eye."),
"Units": "degrees",
}
with open(confounds_json, "w") as f:
metadata = {key: metadata[key] for key in sorted(metadata)}
json.dump(metadata, f, indent=4)
log.debug(f"Sidecar saved to {confounds_json}")

confound_numpy = create_bidsname(
layout_out, file, "confounds_numpy", extra_entities=extra_entities
)

content = np.load(
file=confound_numpy,
Expand All @@ -100,14 +141,19 @@ def convert_confounds(layout_out: BIDSLayout, file: str | Path) -> Path:

this_pred = np.nanmedian(item["pred_y"], axis=1)

confound_name = create_bidsname(layout_out, Path(key + "p"), "confounds_tsv")
confound_name = create_bidsname(
layout_out, Path(key + "p"), "confounds_tsv", extra_entities=extra_entities
)

log.info(f"Saving eye gaze data to {confound_name.relative_to(layout_out.root)}")

pd.DataFrame(this_pred).to_csv(
df = pd.DataFrame(this_pred)
df = add_timestamps_to_dataframe(df, metadata["SamplingFrequency"])

df.to_csv(
confound_name,
sep="\t",
header=["eye1_x_coordinate", "eye1_y_coordinate"],
header=COLUMNS,
index=None,
)

Expand All @@ -117,7 +163,12 @@ def convert_confounds(layout_out: BIDSLayout, file: str | Path) -> Path:
return confound_name


def create_confounds_tsv(layout_out: BIDSLayout, file: str, subject_label: str) -> None:
def create_confounds_tsv(
layout_out: BIDSLayout,
file: str,
subject_label: str,
extra_entities: dict[str, str] | None = None,
) -> None:
"""Generate a TSV file for the eye motion timeseries.
:param layout_out:
Expand All @@ -129,7 +180,9 @@ def create_confounds_tsv(layout_out: BIDSLayout, file: str, subject_label: str)
:param subject_label:
:type subject_label: str
"""
confound_numpy = create_bidsname(layout_out, file, "confounds_numpy")
confound_numpy = create_bidsname(
layout_out, file, "confounds_numpy", extra_entities=extra_entities
)

source_file = Path(layout_out.root) / f"sub-{subject_label}" / "results_tmp.npy"

Expand All @@ -138,7 +191,7 @@ def create_confounds_tsv(layout_out: BIDSLayout, file: str, subject_label: str)
confound_numpy,
)

convert_confounds(layout_out, file)
convert_confounds(layout_out, file, extra_entities=extra_entities)


def process_subject(cfg: Config, layout_out: BIDSLayout, subject_label: str) -> None:
Expand All @@ -155,7 +208,7 @@ def process_subject(cfg: Config, layout_out: BIDSLayout, subject_label: str) ->
"""
log.info(f"Running subject: {subject_label}")

this_filter = set_this_filter(cfg, subject_label, "no_label")
this_filter = set_this_filter(cfg, subject_label, "no_label_bold")

bf = layout_out.get(
regex_search=True,
Expand Down Expand Up @@ -199,7 +252,10 @@ def process_subject(cfg: Config, layout_out: BIDSLayout, subject_label: str) ->
percentile_cut=80,
)

create_confounds_tsv(layout_out, file.path, subject_label)
extra_entities = None
if cfg.model_weights_file is not None:
extra_entities = {"desc": return_desc_entity(Path(cfg.model_weights_file))}
create_confounds_tsv(layout_out, file.path, subject_label, extra_entities)


def generalize(cfg: Config) -> None:
Expand All @@ -211,8 +267,6 @@ def generalize(cfg: Config) -> None:
layout_out = get_dataset_layout(cfg.output_dir)
check_layout(cfg, layout_out)

add_sidecar_in_root(layout_out)

subjects = list_subjects(cfg, layout_out)

text = "GENERALIZING"
Expand Down
4 changes: 2 additions & 2 deletions bidsmreye/prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def combine_data_with_empty_labels(layout_out: BIDSLayout, img: Path, i: int = 1
subj["labels"].append(labels)
subj["ids"].append(([entities["subject"]] * labels.shape[0], [i] * labels.shape[0]))

output_file = create_bidsname(layout_out, Path(img), "no_label")
output_file = create_bidsname(layout_out, Path(img), "no_label_bold")
file_to_move = Path(layout_out.root) / ".." / "bidsmreye" / output_file.name

preprocess.save_data(
Expand Down Expand Up @@ -151,7 +151,7 @@ def prepapre_image(

report_name = create_bidsname(layout_out, filename=img_path, filetype="report")
mask_name = create_bidsname(layout_out, filename=img_path, filetype="mask")
output_file = create_bidsname(layout_out, Path(img_path), "no_label")
output_file = create_bidsname(layout_out, Path(img_path), "no_label_bold")

if (
not cfg.force
Expand Down
Loading

0 comments on commit 391274b

Please sign in to comment.