Skip to content
Merged
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
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ BIDS app for decoding gaze position from the eyeball MR-signal using
([1](https://doi.org/10.1038/s41593-021-00947-w)).

To be used on preprocessed BIDS derivatives (e.g.
[fMRIprep](https://github.com/nipreps/fmriprep) outputs). No eye-tracking data
required.
[fMRIprep](https://github.com/nipreps/fmriprep) outputs).
No eye-tracking data required.

By default, bidsMReye uses a [pre-trained version](https://osf.io/23t5v) of
By default, bidsMReye uses a [pre-trained version](https://osf.io/mrhk9/) of
[deepMReye](https://github.com/DeepMReye/DeepMReye) trained on 5 datasets incl.
guided fixations ([2](https://doi.org/10.1038/sdata.2017.181)), smooth pursuit
([3](https://doi.org/10.1016/j.neuroimage.2018.04.012),[4](https://doi.org/10.1101/2021.08.03.454928),[5](https://doi.org/10.1038/s41593-017-0050-8))
and free viewing ([6](https://doi.org/10.1038/s41593-017-0049-1)). Other
pretrained versions are optional. Dedicated model training is recommended.

The pipeline automatically extracts the eyeball voxels and saves them as a
python pickle file. This can be used also for other multivariate pattern
analyses in the absence of eye-tracking data. Decoded gaze positions allow
computing eye movements.
The pipeline automatically extracts the eyeball voxels.
This can be used also for other multivariate pattern
analyses in the absence of eye-tracking data.
Decoded gaze positions allow computing eye movements.

For more information, see the
[User Recommendations](https://deepmreye.slite.com/p/channel/MUgmvViEbaATSrqt3susLZ/notes/kKdOXmLqe).
Expand All @@ -39,8 +39,8 @@ At the moment bidsmreye only supports python 3.8 and 3.9.

## Install

Better to use the docker image as there are known install issues of deepmreye
on Apple M1 for example.
Better to use the docker image as there are known install issues
of deepmreye on Apple M1 for example.

### Docker

Expand Down Expand Up @@ -75,6 +75,7 @@ To encapsulate bidsMReye in a virtual environment install with the following com
```bash
conda create --name bidsmreye python=3.9
conda activate bidsmreye
conda install pip
pip install bidsmreye
```

Expand Down
17 changes: 5 additions & 12 deletions bidsmreye/bidsmreye.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
from bidsmreye.download import download
from bidsmreye.generalize import generalize
from bidsmreye.prepare_data import prepare_data
from bidsmreye.utils import available_models
from bidsmreye.utils import bidsmreye_log
from bidsmreye.utils import Config
from bidsmreye.utils import default_log_level
from bidsmreye.utils import default_model
from bidsmreye.utils import log_levels

__version__ = _version.get_versions()["version"]
Expand Down Expand Up @@ -66,7 +68,7 @@ def cli(argv: Any = sys.argv) -> None:
log.debug(f"args:\n{args}")
log.debug(f"Configuration:\n{cfg}")

if args.action in ["all", "generalize"]:
if args.action in ["all", "generalize"] and isinstance(cfg.model_weights_file, str):
cfg.model_weights_file = download(cfg.model_weights_file)

if args.analysis_level == "participant":
Expand Down Expand Up @@ -207,17 +209,8 @@ def common_parser() -> MuhParser:
gen.add_argument(
"--model",
help="model to use",
choices=[
"1_guided_fixations",
"2_pursuit",
"3_openclosed",
"3_pursuit",
"4_pursuit",
"5_free_viewing",
"6_1-to-5",
"7_1-to-6",
],
default="7_1-to-6",
choices=available_models(),
default=default_model(),
)

return parser
68 changes: 27 additions & 41 deletions bidsmreye/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import argparse
import sys
import warnings
from pathlib import Path
from typing import Any
from typing import IO
Expand All @@ -11,8 +12,9 @@
import pooch
import rich

from bidsmreye.utils import available_models
from bidsmreye.utils import bidsmreye_log
from bidsmreye.utils import move_file
from bidsmreye.utils import default_model

log = bidsmreye_log(name="bidsmreye")

Expand All @@ -38,16 +40,8 @@ def download_parser() -> MuhParser:
help="""
Model to download.
""",
choices=[
"1_guided_fixations",
"2_pursuit",
"3_openclosed",
"3_pursuit",
"4_pursuit",
"5_free_viewing",
"all",
],
default="1_guided_fixations",
choices=available_models(),
default=default_model(),
)
parser.add_argument(
"--output_dir",
Expand All @@ -73,43 +67,32 @@ def cli(argv: Any = sys.argv) -> None:


def download(
model_name: str | Path | None = None, output_dir: Path | None = None
) -> Path:
model_name: str | Path | None = None, output_dir: Path | str | None = None
) -> Path | None:
"""Download the models from OSF.

:param model_name: _description_, defaults to None
:param model_name: Model to download. defaults to None
:type model_name: str, optional

:param output_dir: _description_, defaults to None
:param output_dir: Path where to save the model. Defaults to None.
:type output_dir: Path, optional

:return: _description_
:return: Path to the downloaded model.
:rtype: Path
"""
if not model_name:
model_name = "7_1-to-6"

if not output_dir:
model_name = default_model()
if isinstance(model_name, Path):
assert model_name.is_file()
return model_name.resolve()
if model_name not in available_models():
warnings.warn(f"{model_name} is not a valid model name.")
return None

if output_dir is None:
output_dir = Path.cwd().joinpath("models")

OSF_ID = {
"1_guided_fixations": "cqf74",
"2_pursuit": "4f6m7",
"3_openclosed": "8cr2j",
"3_pursuit": "e89wp",
"4_pursuit": "96nyp",
"5_free_viewing": "89nky",
"6_1-to-5": "datasets_1to5.h5",
"7_1-to-6": "datasets_1to6.h5",
}

if model_name == "all":
for key in list(OSF_ID):
output_file = download(model_name=key, output_dir=output_dir)
return output_file

if model_name not in OSF_ID:
raise ValueError(f"{model_name} is not a valid model name.")
if isinstance(output_dir, str):
output_dir = Path(output_dir)

POOCH = pooch.create(
path=output_dir,
Expand All @@ -119,12 +102,15 @@ def download(
registry_file = pkg_resources.resource_stream("bidsmreye", "models/registry.txt")
POOCH.load_registry(registry_file)

output_file = output_dir.joinpath(f"dataset{model_name}.h5")
output_file = output_dir.joinpath(f"dataset_{model_name}")

if not output_file.is_file():

fname = POOCH.fetch(OSF_ID[model_name], progressbar=True) # type: ignore
move_file(Path(fname), output_file)
file_idx = available_models().index(model_name)
filename = f"dataset_{available_models()[file_idx]}.h5"
output_file = POOCH.fetch(filename, progressbar=True)
if isinstance(output_file, str):
output_file = Path(output_file)

else:
log.info(f"{output_file} already exists.")
Expand Down
16 changes: 8 additions & 8 deletions bidsmreye/models/registry.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
cqf74 e5e3f4aa68af141129a95a7c40bd37981e54505c14ee7f3f81cdb57adce87683
4f6m7 4a103e5773609f3f19ed566ce3221d4c8851a6c73983b4af881d6d68ca3dc299
8cr2j a681d986efab63db63f4f6c50f098b2b036173926286e17a3e464c74cd586c6a
e89wp a4514e1b689b58b7856b2c2bb9b793c0638a1e1cdab3d6d485fd2a01f75c2520
96nyp 2bb80462e5e62e5f7a408144e770440f5156f5ddbb964e3f60c96a66ba70e1a0
89nky d2f7f9e0f42ba2fd63790d0eb2dd0b45dfde4f33da540a317ac862ff2ed85ee2
datasets_1to5.h5 19a5b85a1e435100c1c9d9f342ae371b14ae44d668c4889b2ebaba9c24a95c2c https://osf.io/download/23t5v/
datasets_1to6.h5 625ad7caa0c8151f751e87001a407135275d7aebf0928ffbfd3d327c5e135e0e https://osf.io/download/mr87v/
dataset_1_guided_fixations.h5 e5e3f4aa68af141129a95a7c40bd37981e54505c14ee7f3f81cdb57adce87683 https://osf.io/download/cqf74/
dataset_2_pursuit.h5 4a103e5773609f3f19ed566ce3221d4c8851a6c73983b4af881d6d68ca3dc299 https://osf.io/download/4f6m7/
dataset_3_openclosed.h5 a681d986efab63db63f4f6c50f098b2b036173926286e17a3e464c74cd586c6a https://osf.io/download/8cr2j/
dataset_3_pursuit.h5 a4514e1b689b58b7856b2c2bb9b793c0638a1e1cdab3d6d485fd2a01f75c2520 https://osf.io/download/e89wp/
dataset_4_pursuit.h5 2bb80462e5e62e5f7a408144e770440f5156f5ddbb964e3f60c96a66ba70e1a0 https://osf.io/download/96nyp/
dataset_5_free_viewing.h5 d2f7f9e0f42ba2fd63790d0eb2dd0b45dfde4f33da540a317ac862ff2ed85ee2 https://osf.io/download/89nky/
dataset_1to5.h5 19a5b85a1e435100c1c9d9f342ae371b14ae44d668c4889b2ebaba9c24a95c2c https://osf.io/download/23t5v/
dataset_1to6.h5 625ad7caa0c8151f751e87001a407135275d7aebf0928ffbfd3d327c5e135e0e https://osf.io/download/mr87v/
10 changes: 6 additions & 4 deletions bidsmreye/templates/CITATION.mustache
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
## eyemotion decoding

Results included in this manuscript come from preprocessing performed using *bidsMReye* {{version}},
Eyetracking results included in this manuscript
come from preprocessing
performed using [*bidsMReye*](https://github.com/cpp-lln-lab/bidsMReye) (version {{version}}),
a BIDS app relying on [deepMReye](https://github.com/DeepMReye/DeepMReye) (@deepmreye)
to decode eye motion for fMRI time series data.
to decode eye motion from fMRI time series data.

### data extraction

All imaging data underwent co-registration conducted
using Advanced Normalization Tools (ANTs, RRID:SCR_004757) within Python (ANTsPy).
First, each participant's mean EPI was non-linearly co-registered
to an average template.
Second, we co-registered all voxels within a bounding box that included the eyes
to a preselected bounding box in our group template to further improve the fit.
Second, all voxels within a bounding box that included the eyes
were co-registered to a preselected bounding box in our group template to further improve the fit.

Each voxel within those bounding box underwent two normalization steps.
First, the across-run median signal intensity was subtracted
Expand Down
19 changes: 19 additions & 0 deletions bidsmreye/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ def log_levels() -> list[str]:
return ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]


def default_model() -> str:
"""Return default model."""
return "1to6"


def available_models() -> list[str]:
"""Return a list of available models."""
return [
"1_guided_fixations",
"2_pursuit",
"3_openclosed",
"3_pursuit",
"4_pursuit",
"5_free_viewing",
"1to5",
"1to6",
]


def bidsmreye_log(name: str | None = None) -> logging.Logger:
"""Create log.

Expand Down
40 changes: 39 additions & 1 deletion tests/test_download.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
from __future__ import annotations

import shutil
from pathlib import Path

import pytest

from bidsmreye.download import download
from bidsmreye.download import download_parser


def test_download_parser():
"""Test parser."""

parser = download_parser()

assert parser.description == "Download deepmreye pretrained model from OSF."

args, unknowns = parser.parse_known_args(
Expand All @@ -18,3 +25,34 @@ def test_download_parser():
)

assert args.output_dir == "/home/bob/models"


def test_download():

output_dir = Path().joinpath("tmp")

download(model_name="1_guided_fixations", output_dir=str(output_dir))

assert output_dir.is_dir()
assert output_dir.joinpath("dataset_1_guided_fixations.h5").is_file()

shutil.rmtree(output_dir)


def test_download_basic():

download()
output_file = download()

output_dir = Path.cwd().joinpath("models")
print(output_file)
assert output_dir.is_dir()
assert output_dir.joinpath("dataset_1to6.h5").is_file()

shutil.rmtree(output_dir)


def test_download_unknown_model():

with pytest.warns(UserWarning):
download(model_name="foo")
3 changes: 2 additions & 1 deletion tests/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def test_methods():
output_dir = Path.cwd().joinpath("temp")
output_file = methods(output_dir)

assert output_dir.is_dir()
assert output_file.is_file()

shutil.rmtree(output_dir)
# shutil.rmtree(output_dir)