diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ab0ddf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +--- +# Documentation +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: +- package-ecosystem: github-actions + directory: / + schedule: + interval: monthly diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..8ab0795 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,35 @@ + +name: validate + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [master] + pull_request: + branches: ['*'] + +jobs: + + validate: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install flake8 + + - name: run flake8 + run: flake8 atlasreader diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bb1a3f0..6ff5b73 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] runs-on: ubuntu-latest @@ -35,16 +35,20 @@ jobs: - name: Install dependencies run: | sudo apt-get update - python -m pip install --upgrade pip pytest jupyter nbval + python -m pip install --upgrade pip setuptools pytest pytest-cov - name: Install run: pip install . - name: Test - run: pytest --cov-report term-missing --cov=atlasreader + run: python -m pytest -vvv --pyargs atlasreader --cov=atlasreader + + - name: Coveralls + uses: coverallsapp/github-action@v2 - name: Test notebooks run: | + pip install nbval for n in `ls notebooks/*ipynb` do pytest --nbval-lax -v -s ${n}; diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e25820d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +--- +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +repos: + +- repo: https://github.com/pyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 797778a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,47 +0,0 @@ -language: python -sudo: false -dist: xenial - -python: - - 3.6 - - 3.7 - -env: - - STYLE=1 - - COVERAGE=1 - - NOTEBOOKS=1 - -before_install: - - python -m pip install --upgrade pip - - pip install --upgrade virtualenv - - if [ "${STYLE}" == "1" ]; then - pip install flake8; - fi - - if [ "${COVERAGE}" == "1" ]; then - pip install coverage coveralls codecov pytest==4.1 pytest-cov; - fi - - if [ "${NOTEBOOKS}" == "1" ]; then - pip install pytest jupyter nbval; - fi - -install: - - python setup.py install - -script: - - | - if [ "${STYLE}" == "1" ]; then - flake8 atlasreader - elif [ "${COVERAGE}" == "1" ]; then - TEST_ARGS="--cov-report term-missing --cov=atlasreader"; - py.test ${TEST_ARGS} atlasreader; - elif [ "${NOTEBOOKS}" == "1" ]; then - for n in `ls notebooks/*ipynb` - do - pytest --nbval-lax -v -s ${n}; - done - fi - -after_success: - - if [ "${COVERAGE}" == "1" ]; then - coveralls; codecov; - fi diff --git a/README.md b/README.md index ed4adf3..c718f16 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ [![codecov](https://codecov.io/gh/miykael/atlasreader/branch/master/graph/badge.svg)](https://codecov.io/gh/miykael/atlasreader) +[![Coverage Status](https://coveralls.io/repos/github/miykael/atlasreader/badge.svg?branch=master)](https://coveralls.io/github/miykael/atlasreader?branch=master) [![Build Status](https://travis-ci.org/miykael/atlasreader.svg?branch=master)](https://travis-ci.org/miykael/atlasreader) [![GitHub issues](https://img.shields.io/github/issues/miykael/atlasreader.svg)](https://github.com/miykael/atlasreader/issues/) [![GitHub pull-requests](https://img.shields.io/github/issues-pr/miykael/atlasreader.svg)](https://github.com/miykael/atlasreader/pulls/) [![GitHub contributors](https://img.shields.io/github/contributors/miykael/atlasreader.svg)](https://GitHub.com/miykael/atlasreader/graphs/contributors/) -[![GitHub Commits](https://github-basic-badges.herokuapp.com/commits/miykael/atlasreader.svg)](https://github.com/miykael/atlasreader/commits/master) -[![GitHub size](https://github-size-badge.herokuapp.com/miykael/atlasreader.svg)](https://github.com/miykael/atlasreader/archive/master.zip) +![GitHub repo size](https://img.shields.io/github/repo-size/miykael/atlasreader) # AtlasReader @@ -15,10 +15,10 @@ to localize and extract relevant peak and cluster information and create informative and nice looking overview figures. Please check out our interactive notebook on mybinder.org to see `atlasreader` -in action: +in action: [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/miykael/atlasreader/master?filepath=notebooks%2Fatlasreader.ipynb) -If you are using `atlasreader` in your publication, please cite the following paper: +If you are using `atlasreader` in your publication, please cite the following paper: [![DOI](http://joss.theoj.org/papers/10.21105/joss.01257/status.svg)](https://doi.org/10.21105/joss.01257) Notter M. P., Gale D., Herholz P., Markello R. D., Notter-Bielser M.-L., & Whitaker K. (2019). AtlasReader: A Python package to generate coordinate tables, region labels, and informative figures from statistical MRI images. *Journal of Open Source Software, 4(34), 1257*, [https://doi.org/10.21105/joss.01257](https://doi.org/10.21105/joss.01257). @@ -33,7 +33,7 @@ installing `atlasreader` is as simple as this: pip install atlasreader ``` -If you want to build `atlasreader` directly from source code, use the +If you want to build `atlasreader` directly from source code, use the following code: ```bash @@ -67,25 +67,25 @@ atlasreader file_name 5 After executing AtlasReader on a given image, four kinds of outputs are generated: -1. An **overview figure** that shows the results within the whole brain at once +1. An **overview figure** that shows the results within the whole brain at once ![Overview Figure](paper/fig_overview_figure.png) 2. **For each cluster**, an **informative figure** showing the sagittal, coronal - and transversal plane centered on the main peak of the cluster + and transversal plane centered on the main peak of the cluster ![Cluster Figure](paper/fig_cluster_figure.png) 3. A **csv file** containing relevant information about the **peak** of each cluster. This table contains the cluster association and location of each peak, its signal value at this location, the cluster extent (in mm, not in number of voxels), as well as the membership of each peak, given a - particular atlas. + particular atlas. ![Table Peak](paper/table_peak.png) 4. A **csv** file containing relevant information about each **cluster**. Table showing relevant information for the cluster extent of each ROI. This table contains the cluster association and location of each peak, the mean value within the cluster, the cluster extent (in mm, not in number of voxels), as - well as the membership of each cluster, given a particular atlas. + well as the membership of each cluster, given a particular atlas. ![Table Cluster](paper/table_cluster.png) @@ -94,34 +94,34 @@ After executing AtlasReader on a given image, four kinds of outputs are generate `atlasreader.create_output` has many additional parameters that allow you to change the way the clusters are generated and what kind of outputs are generated: -- **filename**: Niimg_like +- **filename**: Niimg_like A 3D statistical image. -- **cluster_extent**: int +- **cluster_extent**: int Minimum number of contiguous voxels required to consider a cluster in `filename` -- **atlas**: str or list, optional +- **atlas**: str or list, optional Name of atlas(es) to consider for cluster analysis. ***Default***: `'default'` - **voxel_thresh**: float, optional - Threshold to apply to `stat_img`. Use `direction` to specify the + Threshold to apply to `stat_img`. Use `direction` to specify the directionality of the threshold. If a negative number is provided a percentile threshold is used instead, where the percentile is determined by the equation `100 - voxel_thresh`. ***Default***: `1.96` - **direction**: str, optional Specifies the direction in which `voxel_thresh` should be applied. Possible values are `'both'`, `'pos'` or `'neg'`. ***Default***: `'both'` -- **prob_thresh**: int, optional +- **prob_thresh**: int, optional Probability (percentage) threshold to apply to `atlas`, if it is probabilistic. ***Default***: `5` -- **min_distance**: float, optional +- **min_distance**: float, optional Specifies the minimum distance (in mm) required between sub-peaks in a cluster. If None, sub-peaks will not be examined and only the primary cluster peak will be reported. ***Default***: `None` -- **outdir**: str or None, optional +- **outdir**: str or None, optional Path to desired output directory. If None, generated files will be saved to the same folder as `filename`. ***Default***: `None` -- **glass_plot_kws**: dict or None, optional +- **glass_plot_kws**: dict or None, optional Additional keyword arguments to pass to `nilearn.plotting.plot_glass_brain`. ***Default***: `None` -- **stat_plot_kws**: dict or None, optional +- **stat_plot_kws**: dict or None, optional Additional keyword arguments to pass to `nilearn.plotting.plot_stat_map`. ***Default***: `None` @@ -148,7 +148,7 @@ about it! ## Licence -AtlasReader is licensed under the BSD-3 license; however, the atlases it uses +AtlasReader is licensed under the BSD-3 license; however, the atlases it uses are separately licensed under more restrictive frameworks. By using AtlasReader, you agree to abide by the license terms of the individual atlases. Information on these terms can be found online at: diff --git a/atlasreader/atlasreader.py b/atlasreader/atlasreader.py index e567629..a0046e7 100644 --- a/atlasreader/atlasreader.py +++ b/atlasreader/atlasreader.py @@ -256,10 +256,15 @@ def get_subpeak_coords(clust_img, min_distance=20): data = check_niimg(clust_img).get_fdata() # find local maxima, excluding peaks that are on the border of the cluster - local_max = peak_local_max(data, exclude_border=1, indices=False) + local_max = peak_local_max(data, exclude_border=1) # make new clusters to check for "flat" peaks + find CoM of those clusters labels, nl = label(local_max) + labels_img = np.zeros_like(data) + for ldx in range(len(labels)): + labels_img[tuple(local_max[ldx])] = np.mean(labels[ldx]) + labels = labels_img.astype('int') + ijk = center_of_mass(data, labels=labels, index=range(1, nl + 1)) ijk = np.round(ijk).astype(int) @@ -341,7 +346,7 @@ def read_atlas_peak(atlastype, coordinate, prob_thresh=5): # get atlas data checked_atlastype = check_atlases(atlastype) - if type(checked_atlastype) == list: + if isinstance(checked_atlastype, list): if not len(checked_atlastype) == 1: raise ValueError( '\'{}\' is not a string or a single atlas. \'all\' ' @@ -407,7 +412,7 @@ def read_atlas_cluster(atlastype, cluster, affine, prob_thresh=5): # get atlas data checked_atlastype = check_atlases(atlastype) - if type(checked_atlastype) == list: + if isinstance(checked_atlastype, list): if not len(checked_atlastype) == 1: raise ValueError( '\'{}\' is not a string or a single atlas. \'all\' ' @@ -572,8 +577,9 @@ def get_peak_data(clust_img, atlas='default', prob_thresh=5, segment = read_atlas_peak(atype, coord, prob_thresh) coord_info.append([atype.atlas, segment]) - peak_info += [[peak if type(peak) != list else - '; '.join(['{}% {}'.format(*e) for e in peak]) + peak_info += [['; '.join(['{}% {}'.format(*e) for e in peak]) + if isinstance(peak, list) + else peak for (_, peak) in coord_info]] return np.column_stack([coords, peak_values, cluster_volume, peak_info]) diff --git a/atlasreader/info.py b/atlasreader/info.py index 0eb531a..94fb328 100644 --- a/atlasreader/info.py +++ b/atlasreader/info.py @@ -25,7 +25,8 @@ TESTS_REQUIRE = [ 'pytest', - 'pytest-cov' + 'pytest-cov', + 'nbval' ] PACKAGE_DATA = { diff --git a/atlasreader/tests/conftest.py b/atlasreader/tests/conftest.py new file mode 100644 index 0000000..e94ed72 --- /dev/null +++ b/atlasreader/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest +from pathlib import Path + + +@pytest.fixture +def data_dir(): + return Path(__file__).parent / 'data' + + +@pytest.fixture +def stat_img(data_dir): + return data_dir / 'collection_658' / "image_10426.nii.gz" diff --git a/atlasreader/tests/data/collection_658/collection_metadata.json b/atlasreader/tests/data/collection_658/collection_metadata.json new file mode 100644 index 0000000..a081c0c --- /dev/null +++ b/atlasreader/tests/data/collection_658/collection_metadata.json @@ -0,0 +1 @@ +{"id": 658, "url": "http://neurovault.org/collections/658/", "download_url": "http://neurovault.org/collections/658/download", "owner": 470, "contributors": "bthirion, gael.varoquaux", "owner_name": "schwarty", "number_of_images": 65, "name": "Principal Component Regression predicts functional responses across individuals", "DOI": null, "authors": null, "paper_url": null, "journal_name": null, "description": "Inter-subject variability is a major hurdle for neuroimaging group-level inference, as it creates complex image patterns that are not captured by standard analysis models and jeopardizes the sensitivity of statistical procedures. A solution to this problem is to model random subjects effects by using the redundant information conveyed by multiple imaging contrasts. In this paper, we introduce a novel analysis framework, where we estimate the amount of variance that is fit by a random effects subspace learned on other images; we show that a principal component regression estimator outperforms other regression models and that it fits a significant proportion (10% to 25%) of the between-subject variability. This proves for the first time that the accumulation of contrasts in each individual can provide the basis for more sensitive neuroimaging group analyzes.", "full_dataset_url": null, "private": false, "add_date": "2015-06-22T13:54:57.479256+02:00", "modify_date": "2016-01-27T23:00:42.540459+01:00", "doi_add_date": null, "type_of_design": null, "number_of_imaging_runs": null, "number_of_experimental_units": null, "length_of_runs": null, "length_of_blocks": null, "length_of_trials": null, "optimization": null, "optimization_method": null, "subject_age_mean": null, "subject_age_min": null, "subject_age_max": null, "handedness": null, "proportion_male_subjects": null, "inclusion_exclusion_criteria": null, "number_of_rejected_subjects": null, "group_comparison": null, "group_description": null, "scanner_make": null, "scanner_model": null, "field_strength": null, "pulse_sequence": null, "parallel_imaging": null, "field_of_view": null, "matrix_size": null, "slice_thickness": null, "skip_distance": null, "acquisition_orientation": null, "order_of_acquisition": null, "repetition_time": null, "echo_time": null, "flip_angle": null, "software_package": null, "software_version": null, "order_of_preprocessing_operations": null, "quality_control": null, "used_b0_unwarping": null, "b0_unwarping_software": null, "used_slice_timing_correction": null, "slice_timing_correction_software": null, "used_motion_correction": null, "motion_correction_software": null, "motion_correction_reference": null, "motion_correction_metric": null, "motion_correction_interpolation": null, "used_motion_susceptibiity_correction": null, "used_intersubject_registration": null, "intersubject_registration_software": null, "intersubject_transformation_type": null, "nonlinear_transform_type": null, "transform_similarity_metric": null, "interpolation_method": null, "object_image_type": null, "functional_coregistered_to_structural": null, "functional_coregistration_method": null, "coordinate_space": null, "target_template_image": null, "target_resolution": null, "used_smoothing": null, "smoothing_type": null, "smoothing_fwhm": null, "resampled_voxel_size": null, "intrasubject_model_type": null, "intrasubject_estimation_type": null, "intrasubject_modeling_software": null, "hemodynamic_response_function": null, "used_temporal_derivatives": null, "used_dispersion_derivatives": null, "used_motion_regressors": null, "used_reaction_time_regressor": null, "used_orthogonalization": null, "orthogonalization_description": null, "used_high_pass_filter": null, "high_pass_filter_method": null, "autocorrelation_model": null, "group_model_type": null, "group_estimation_type": null, "group_modeling_software": null, "group_inference_type": null, "group_model_multilevel": null, "group_repeated_measures": null, "group_repeated_measures_method": null, "nutbrain_hunger_state": null, "nutbrain_food_viewing_conditions": null, "nutbrain_food_choice_type": null, "nutbrain_taste_conditions": null, "nutbrain_odor_conditions": null, "communities": [], "relative_path": "collection_658"} \ No newline at end of file diff --git a/atlasreader/tests/data/collection_658/image_10426.nii.gz b/atlasreader/tests/data/collection_658/image_10426.nii.gz new file mode 100644 index 0000000..6585e3d Binary files /dev/null and b/atlasreader/tests/data/collection_658/image_10426.nii.gz differ diff --git a/atlasreader/tests/data/collection_658/image_10426_metadata.json b/atlasreader/tests/data/collection_658/image_10426_metadata.json new file mode 100644 index 0000000..5e42dde --- /dev/null +++ b/atlasreader/tests/data/collection_658/image_10426_metadata.json @@ -0,0 +1 @@ +{"url": "http://neurovault.org/images/10426/", "id": 10426, "file": "http://neurovault.org/media/images/658/task001_left_vs_right_motor.nii.gz", "collection": "http://neurovault.org/collections/658/", "collection_id": 658, "file_size": 186649, "cognitive_paradigm_cogatlas": null, "cognitive_paradigm_cogatlas_id": null, "cognitive_contrast_cogatlas": null, "cognitive_contrast_cogatlas_id": null, "map_type": "other", "analysis_level": null, "name": "task001 left vs right motor", "description": null, "add_date": "2016-01-21T18:22:58.807468+01:00", "modify_date": "2016-01-27T22:59:40.180124+01:00", "is_valid": false, "surface_left_file": null, "surface_right_file": null, "data_origin": "volume", "target_template_image": "GenericMNI", "subject_species": "homo sapiens", "figure": null, "handedness": null, "age": null, "gender": null, "race": null, "ethnicity": null, "BMI": null, "fat_percentage": null, "waist_hip_ratio": null, "mean_PDS_score": null, "tanner_stage": null, "days_since_menstruation": null, "hours_since_last_meal": null, "bis_bas_score": null, "spsrq_score": null, "bis11_score": null, "thumbnail": "http://neurovault.org/media/images/658/glass_brain_10426.jpg", "reduced_representation": "http://neurovault.org/media/images/658/transform_4mm_10426.npy", "is_thresholded": false, "perc_bad_voxels": 70.4103024857742, "not_mni": false, "brain_coverage": 68.1115393334845, "perc_voxels_outside": 0.840521035029044, "number_of_subjects": null, "modality": null, "statistic_parameters": null, "smoothness_fwhm": null, "contrast_definition": null, "contrast_definition_cogatlas": null, "cognitive_paradigm_description_url": null, "image_type": "statistic_map", "relative_path": "collection_658/image_10426.nii.gz", "resampled_relative_path": "collection_658/image_10426_resampled.nii.gz"} \ No newline at end of file diff --git a/atlasreader/tests/test_atlasreader.py b/atlasreader/tests/test_atlasreader.py index 501d1ab..332a8da 100644 --- a/atlasreader/tests/test_atlasreader.py +++ b/atlasreader/tests/test_atlasreader.py @@ -1,12 +1,9 @@ -import os import numpy as np from atlasreader import atlasreader import nibabel as nb -from nilearn.datasets import fetch_neurovault_motor_task import pytest import pandas as pd -STAT_IMG = fetch_neurovault_motor_task().images[0] EXAMPLE_COORDS = dict( affine=np.array([[1, 0, 0, -90], [0, 1, 0, -150], @@ -57,6 +54,11 @@ ) +@pytest.mark.parametrize('atlas', atlasreader._ATLASES) +def test_check_atlases_each(atlas): + atlasreader.check_atlases(atlas) + + def test_get_atlases(): for atlas in atlasreader._ATLASES: a = atlasreader.get_atlas(atlas, cache=False) @@ -65,11 +67,17 @@ def test_get_atlases(): atlasreader.get_atlas('not_an_atlas') -def test_check_atlases(): +def test_check_atlases_all(): atlases = atlasreader.check_atlases('all') assert len(atlases) == len(atlasreader._ATLASES) + + +def test_check_atlases_default(): atlases = atlasreader.check_atlases('default') assert len(atlases) == len(atlasreader._DEFAULT) + + +def test_check_atlases_as_list(): atlases = atlasreader.check_atlases(['aal', 'destrieux']) assert atlasreader.check_atlases(atlases) == atlases assert atlasreader.check_atlases(atlases[0]) == atlases[0] @@ -94,18 +102,20 @@ def test_bounding_box_check(): assert np.all(ijk_out == coords['ijk_out']) -def test_get_statmap_info(): +@pytest.mark.parametrize('min_distance', [None, 20]) +def test_get_statmap_info(stat_img, min_distance): # general integration test to check that min_distance works # this will take a little while since it's running it twice - stat_img = nb.load(STAT_IMG) - for min_distance in [None, 20]: - cdf, pdf = atlasreader.get_statmap_info(stat_img, - cluster_extent=20, - atlas=['Harvard_Oxford', - 'AAL'], - min_distance=min_distance) + stat_img = nb.load(stat_img) + atlasreader.get_statmap_info(stat_img, + cluster_extent=20, + atlas=['Harvard_Oxford', 'AAL'], + min_distance=min_distance) + +def test_get_statmap_info_empty_image(stat_img): # test that empty image return empty dataframes + stat_img = nb.load(stat_img) zero_img = nb.Nifti1Image(np.zeros(stat_img.shape), stat_img.affine, header=stat_img.header) cdf, pdf = atlasreader.get_statmap_info(zero_img, @@ -114,19 +124,19 @@ def test_get_statmap_info(): assert len(pdf) == 0 -def test_read_atlas_preaks(): +def test_read_atlas_peaks(): # Load a correct atlas - atlasreader.read_atlas_peak('aal', [10, 10, 10]) + atlasreader.read_atlas_peak('aicha', [10, 10, 10]) + + +def test_read_atlas_peaks_error_type(): # Load a list of atlases with pytest.raises(ValueError): - atlasreader.read_atlas_peak(2*['aal'], [10, 10, 10]) - # Load 'all' atlas - with pytest.raises(ValueError): - atlasreader.read_atlas_peak('all', [10, 10, 10]) + atlasreader.read_atlas_peak(2*['aicha'], [10, 10, 10]) -def test_process_image(): - stat_img = nb.load(STAT_IMG) +def test_process_image(stat_img): + stat_img = nb.load(stat_img) # check that defaults for processing image work img = atlasreader.process_img(stat_img, cluster_extent=20) assert isinstance(img, nb.Nifti1Image) @@ -148,33 +158,33 @@ def test_process_image(): assert np.allclose(img.get_fdata(), 0) -def test_create_output(tmpdir): - # create mock data - stat_img_name = os.path.basename(STAT_IMG)[:-7] +def test_create_output(tmp_path, stat_img): - # temporary output - output_dir = tmpdir.mkdir('mni_test') - atlasreader.create_output(STAT_IMG, cluster_extent=20, + output_dir = tmp_path / 'mni_test' + output_dir.mkdir() + + atlasreader.create_output(str(stat_img), cluster_extent=20, voxel_thresh=7, atlas=['Harvard_Oxford'], outdir=output_dir) # test if output exists and if the key .csv and .png files were created assert output_dir.exists() - assert len(output_dir.listdir()) > 0 - assert output_dir.join('{}_clusters.csv'.format(stat_img_name)).isfile() - assert output_dir.join('{}_peaks.csv'.format(stat_img_name)).isfile() - assert output_dir.join('{}.png'.format(stat_img_name)).isfile() + assert len([x for x in output_dir.iterdir()]) > 0 + stat_img_name = stat_img.stem[:11] + for ending in ['_clusters.csv', '_peaks.csv', '.png']: + assert (output_dir / f'{stat_img_name}{ending}').exists() -def test_plotting(tmpdir): + +def test_plotting(tmpdir, stat_img): """Test functionality of kwarg implementation""" # temporary output output_dir = tmpdir.mkdir('mni_test') # overwrite some default params - atlasreader.create_output(STAT_IMG, cluster_extent=20, + atlasreader.create_output(stat_img, cluster_extent=20, voxel_thresh=7, atlas=['Harvard_Oxford'], outdir=output_dir, @@ -182,25 +192,33 @@ def test_plotting(tmpdir): stat_plot_kws={'black_bg': False}) # add new parameter not already set by default - atlasreader.create_output(STAT_IMG, cluster_extent=20, + atlasreader.create_output(stat_img, cluster_extent=20, voxel_thresh=7, atlas=['Harvard_Oxford'], outdir=output_dir, glass_plot_kws={'alpha': .4}) -def test_table_output(tmpdir): - # create mock data - stat_img_name = os.path.basename(STAT_IMG)[:-7] +def test_table_output(tmp_path, stat_img): - # temporary output - output_dir = tmpdir.mkdir('mni_test') - atlasreader.create_output(STAT_IMG, cluster_extent=20, + output_dir = tmp_path / 'mni_test' + output_dir.mkdir() + + atlasreader.create_output(str(stat_img), cluster_extent=20, voxel_thresh=4, atlas='default', outdir=output_dir) # test if output tables contain expected output - df = pd.read_csv(output_dir.join('{}_clusters.csv'.format(stat_img_name))) + stat_img_name = stat_img.stem[:11] + + df = pd.read_csv(output_dir / f'{stat_img_name}_clusters.csv') assert np.allclose(df[df.keys()[1:6]].values, EXPECTED_TABLES['cluster']) - df = pd.read_csv(output_dir.join('{}_peaks.csv'.format(stat_img_name))) + + df = pd.read_csv(output_dir / f'{stat_img_name}_peaks.csv') assert np.allclose(df[df.keys()[1:6]].values, EXPECTED_TABLES['peak']) + + +def test_read_atlas_peaks_error_all(): + # Load 'all' atlas + with pytest.raises(ValueError): + atlasreader.read_atlas_peak('all', [10, 10, 10]) diff --git a/atlasreader/tests/test_cli.py b/atlasreader/tests/test_cli.py index bce44ed..11e56bd 100644 --- a/atlasreader/tests/test_cli.py +++ b/atlasreader/tests/test_cli.py @@ -1,27 +1,24 @@ -import os -from nilearn.datasets import fetch_neurovault_motor_task import subprocess -STAT_IMG = fetch_neurovault_motor_task().images[0] +def test_cli(tmp_path, stat_img): -def test_cli(tmpdir): - stat_img_name = os.path.basename(STAT_IMG)[:-7] + output_dir = tmp_path / 'mni_test' + output_dir.mkdir() - # temporary output - output_dir = tmpdir.mkdir('mni_test') ret = subprocess.run(['atlasreader', '--atlas', 'harvard_oxford', 'aal', '--threshold', '7.0', '--probability', '5', '--mindist', '20', '--outdir', output_dir, - STAT_IMG, '20']) + stat_img, '20']) assert ret.returncode == 0 # test if output exists and if the key .csv and .png files were created assert output_dir.exists() - assert len(output_dir.listdir()) > 0 - assert output_dir.join('{}_clusters.csv'.format(stat_img_name)).isfile() - assert output_dir.join('{}_peaks.csv'.format(stat_img_name)).isfile() - assert output_dir.join('{}.png'.format(stat_img_name)).isfile() + assert len([x for x in output_dir.iterdir()]) > 0 + + stat_img_name = stat_img.stem[:11] + for ending in ['_clusters.csv', '_peaks.csv', '.png']: + assert (output_dir / f'{stat_img_name}{ending}').exists() diff --git a/notebooks/.gitignore b/notebooks/.gitignore new file mode 100644 index 0000000..1e058e0 --- /dev/null +++ b/notebooks/.gitignore @@ -0,0 +1,3 @@ +*.png +*.csv +*.nii.gz \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c6c9b06..be97ad3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -matplotlib -nibabel -nilearn -numpy -pandas -scipy -scikit-image -scikit-learn +matplotlib>=3.7 +nibabel>=5.0 +nilearn>=0.10 +numpy>=1.22.0 +pandas>=2.0 +scikit-image>=0.21.0 +scikit-learn>=1.0 +scipy>=1.10