diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..075f95b --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,5 @@ +template: | + ## Release Notes + + ## CHANGES + $CHANGES diff --git a/.github/workflows/miss_hit_quality.yml b/.github/workflows/miss_hit_quality.yml index 2112cdf..5f0f9b5 100644 --- a/.github/workflows/miss_hit_quality.yml +++ b/.github/workflows/miss_hit_quality.yml @@ -3,7 +3,9 @@ name: miss_hit_quality on: push: branches: + - main - master + - dev pull_request: branches: ['*'] @@ -14,7 +16,8 @@ jobs: steps: - - uses: actions/checkout@v2 + - name: Install CPP_ROI + uses: actions/checkout@v3 with: submodules: true fetch-depth: 1 @@ -28,8 +31,6 @@ jobs: run: | python -m pip install --upgrade pip setuptools pip3 install -r requirements.txt - cd tests - make data - name: MISS_HIT Metrics run: | diff --git a/.github/workflows/miss_hit_style.yml b/.github/workflows/miss_hit_style.yml index db92e55..d3987fe 100644 --- a/.github/workflows/miss_hit_style.yml +++ b/.github/workflows/miss_hit_style.yml @@ -3,9 +3,11 @@ name: miss_hit_style on: push: branches: + - main - master + - dev pull_request: - branches: '*' + branches: ['*'] jobs: build: @@ -14,7 +16,8 @@ jobs: steps: - - uses: actions/checkout@v2 + - name: Install CPP_ROI + uses: actions/checkout@v3 with: submodules: true fetch-depth: 1 @@ -28,8 +31,6 @@ jobs: run: | python -m pip install --upgrade pip setuptools pip3 install -r requirements.txt - cd tests - make data - name: MISS_HIT Code style run: | diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml deleted file mode 100644 index 3c4c724..0000000 --- a/.github/workflows/run_tests.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: tests and coverage - -# Uses the cron schedule for github actions -# -# https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#scheduled-events -# -# ┌───────────── minute (0 - 59) -# │ ┌───────────── hour (0 - 23) -# │ │ ┌───────────── day of the month (1 - 31) -# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) -# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) -# │ │ │ │ │ -# │ │ │ │ │ -# │ │ │ │ │ -# * * * * * - -on: - push: - branches: - - dev - - main - - master - pull_request: - branches: ['*'] - schedule: - - cron: "1 1 1 * *" - -env: - OCTFLAGS: --no-gui --no-window-system --silent - -jobs: - build: - - runs-on: ubuntu-20.04 - - steps: - - - name: Install dependencies - run: | - sudo apt-get -y -qq update - sudo apt-get -y install octave liboctave-dev - sudo apt-get -y install nodejs npm - - - name: Clone cpp_spm - uses: actions/checkout@v2 - with: - submodules: true - fetch-depth: 2 - - - name: Install SPM - run: | - git clone https://github.com/spm/spm12.git --depth 1 - make -C spm12/src PLATFORM=octave distclean - make -C spm12/src PLATFORM=octave - make -C spm12/src PLATFORM=octave install - octave $OCTFLAGS --eval "addpath(fullfile(pwd, 'spm12')); savepath();" - - - name: Install Moxunit and MOcov - run: | - git clone https://github.com/MOxUnit/MOxUnit.git --depth 1 - make -C MOxUnit install - git clone https://github.com/MOcov/MOcov.git --depth 1 - make -C MOcov install - - - name: Add bids-matlab - run: | - git clone https://github.com/bids-standard/bids-matlab.git --depth 1 - - - name: Update octave path - run: | - octave $OCTFLAGS --eval "initCppRoi; savepath();" - octave $OCTFLAGS --eval "addpath(fullfile(pwd, 'bids-matlab')); savepath();" - - - name: Run tests - run: | - octave $OCTFLAGS --eval "run_tests" - cat test_report.log | grep 0 - bash <(curl -s https://codecov.io/bash) diff --git a/.github/workflows/run_tests_matlab.yml b/.github/workflows/run_tests_matlab.yml new file mode 100644 index 0000000..219533d --- /dev/null +++ b/.github/workflows/run_tests_matlab.yml @@ -0,0 +1,63 @@ +name: tests and coverage with matlab + +on: + push: + branches: + - dev + - main + - master + pull_request: + branches: ['*'] + schedule: + - cron: "1 1 1 * *" + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-20.04 + + steps: + - name: Install MATLAB + uses: matlab-actions/setup-matlab@v1.0.1 + with: + # MATLAB release to set up R2020a + release: R2020a + + - name: Install CPP_ROI + uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 1 + + - name: Install SPM + run: | + git clone https://github.com/spm/spm12.git --depth 1 + + - name: Install Moxunit and MOcov + run: | + git clone https://github.com/MOxUnit/MOxUnit.git --depth 1 + git clone https://github.com/MOcov/MOcov.git --depth 1 + + - name: Add bids-matlab + run: | + git clone https://github.com/bids-standard/bids-matlab.git --depth 1 + + - name: Run commands + uses: matlab-actions/run-command@v1.0.1 + with: + command: cd(fullfile(getenv('GITHUB_WORKSPACE'), '.github', 'workflows')); run tests_matlab; + + - name: Run tests + run: | + cat test_report.log | grep 0 + bash <(curl -s https://codecov.io/bash) + + - name: Code coverage + uses: codecov/codecov-action@v1 + with: + file: coverage.xml # optional + flags: unittests # optional + name: codecov-umbrella # optional + fail_ci_if_error: true # optional (default = false) diff --git a/.github/workflows/tests_matlab.m b/.github/workflows/tests_matlab.m new file mode 100644 index 0000000..45cdc44 --- /dev/null +++ b/.github/workflows/tests_matlab.m @@ -0,0 +1,17 @@ +% +% (C) Copyright 2022 CPP ROI developers + +root_dir = getenv('GITHUB_WORKSPACE'); + +addpath(fullfile(root_dir, 'spm12')); +addpath(fullfile(root_dir, 'bids-matlab')); +addpath(fullfile(root_dir, 'MOcov', 'MOcov')); + +cd(fullfile(root_dir, 'MOxUnit', 'MOxUnit')); +run moxunit_set_path(); + +cd(fullfile(root_dir)); + +initCppRoi(); + +run run_tests(); diff --git a/.gitignore b/.gitignore index 2c3ea86..3588616 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,18 @@ - .DS_Store *.nii - +*.xml +*.log *.asv *.m~ *.mat -<<<<<<< HEAD +*.html + # files in the demo folder related to running the demo analysis # demos/roi -======= + atlas/visual_topography_probability_atlas/ ->>>>>>> 4a2a3830905dfad9b61804bf99e2941a49e4e2fa # test folder and dummy data @@ -28,3 +28,5 @@ envs/* demos/*/*.json demos/*/*/*.json demos/*/derivatives + +atlas/*.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ba41f6..2abe985 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: additional_dependencies: [miss_hit] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/README.md b/README.md index 558fa20..c16bbbf 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ - - -**Documentation** - -**Code quality and style** - -**Unit tests and coverage** - -**How to cite** - -**Contributors** - # CPP ROI -## :warning: :warning: :warning: +--- + +## :warning: -**This code is fairly unstable (:boom:) and might still change a lot.** +**This code is fairly unstable and might still change.** -Also this code currently has 0% test coverage... +So make sure you "pin" which version or commit you are using for a given +project, if you don't want your code to break in the future. --- @@ -57,8 +48,7 @@ as a submodule, and intitialized when running `initCppSpm`. ### Dependencies -======= -TODO +======= TODO | Dependencies | Used version | | ---------------------------------------------------------- | ------------ | @@ -98,8 +88,4 @@ TODO Also includes: - Yeo's 7 networks "atlas" - - add REF and URL - -## Contributing - -## Contributors + diff --git a/src/atlas/returnAtlasDir.m b/atlas/returnAtlasDir.m similarity index 87% rename from src/atlas/returnAtlasDir.m rename to atlas/returnAtlasDir.m index 617a24f..128dfe4 100644 --- a/src/atlas/returnAtlasDir.m +++ b/atlas/returnAtlasDir.m @@ -10,7 +10,7 @@ % % (C) Copyright 2021 CPP ROI developers - atlasDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', 'atlas'); + atlasDir = fileparts(mfilename('fullpath')); if nargin > 0 diff --git a/demos/WIP/get_ROI_Coordinates.m b/demos/WIP/get_ROI_Coordinates.m new file mode 100644 index 0000000..606aba6 --- /dev/null +++ b/demos/WIP/get_ROI_Coordinates.m @@ -0,0 +1,150 @@ +function mni = get_ROI_Coordinates + % This function gets the individual subject coordniates of the highest peak + % within a specified Region of interest (usually coming + % from the group level univariate analysis) + % + % Critical t-value for each experimental condition or mask file + % Critical t-value for visual condition in L-V5 and R-V5 and bilateral PT + % + % (C) Copyright 2021 CPP ROI developers + + CriticalTs = 1; + + %% load the data structure + WD = pwd; + + %% group of subjects to analyze + opt.groups = {'EB', 'SCa', 'SCv'}; % {'EB','SCa','SCv'}; + % suject to run in each group + opt.subjects = {[1:13], [1:10], [1:6]}; % {[1:13],[1:10],[1:6]}; + + %% + smoothing = '6'; + + %% Regions that will be used to extract the highest t-value within that mask + maskFn = { 'face_lFFA.nii' + 'face_rFFA.nii' + }; + + CriticalTs = ones(1, length(maskFn)) * CriticalTs; + + %% Location of the data + data_path = '/Volumes/SanDisk/Oli_Data/Categs_BIDS/derivatives'; + + % create an mni cell with dimensions (1x number of masks) + mni = cell(1, length(maskFn)); + SubNames = {}; + + % for each mask + for iMask = 1:length(maskFn) + + fprintf('Running Mask %.0f \n\n', iMask); + CriticalT = CriticalTs(iMask); % get the critical t + + subCounter = 0; + for iGroup = 1:length(opt.groups) + for iSub = 1:length(opt.subjects{iGroup}) + + fprintf('Running Subject %.0f \n', iSub); + + SubName = ['sub-', opt.groups{iGroup}, ... + sprintf('%02d', opt.subjects{iGroup}(iSub))]; + + fprintf('%s \n', SubName); + subCounter = subCounter + 1; + SubNames{subCounter, iMask} = SubName(5:end); + + %% the first 4 masks are for the FACE condition, the other 4 + % are from the SCENE condition + if iMask <= 2 + result_file = [data_path, '/', ... + SubName, ... + '/stats/ffx_visMotion/ffx_', ... + smoothing, ... + '/spmT_0013.nii']; % HUMAN > BIG_ENV + else + result_file = [data_path, '/', ... + SubName, ... + '/stats/ffx_audMotion/ffx_', ... + smoothing, ... + '/spmT_0014.nii']; % BIG_ENV > HUMAN + end + + %% + mask_path = fullfile(WD, 'Kanwisher_ROIs', maskFn{iMask}); + + r = load_nii(result_file); + m = load_nii(mask_path); + + r.img(~m.img) = nan; + + maxVal = max(r.img(:)); + maxVals(subCounter, iMask) = maxVal; + + if maxVal > CriticalT %|| maxVal < CriticalT + + % Get the location of the higest t-value in slice space + voxel_idx = find(r.img == maxVal); + + disp(maxVal); + [x, y, z] = ind2sub(size(r.img), voxel_idx); + + % convert space from slice number to mni + mni{1, iMask}(subCounter, :) = cor2mni([x y z], mask_path); + + % mni{1,iMask}(iSub,1) = mni{1,iMask}(iSub,1)* -1; + % If masks created from AFNI or FSL, + % the x coordinate could be flipped (multiplied x -1). + % If this is the case, multiply x with -1. + + mni{1, iMask}(subCounter, :); + + else + mni{1, iMask}(subCounter, 1:3) = nan; + end + + end + end + end + + save('mni_coordinates.mat', 'mni', 'maskFn', 'opt', 'maxVals', 'SubNames'); +end + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% cor2mni +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function mni = cor2mni(cor, nifti_image) + % function mni = cor2mni(cor, T) + % convert matrix coordinate to mni coordinate + % + % cor: an Nx3 matrix + % T: (optional) rotation matrix + % mni is the returned coordinate in mni space + % + % caution: if T is not given, the default T is + % T = ... + % [-4 0 0 84;... + % 0 4 0 -116;... + % 0 0 4 -56;... + % 0 0 0 1]; + % + % xu cui + % 2004-8-18 + % last revised: 2005-04-30 + + % if nargin == 1 + % T = ... + % [-4 0 0 84;... + % 0 4 0 -116;... + % 0 0 4 -56;... + % 0 0 0 1]; + % end + + V = spm_vol(nifti_image); + T = V.mat; + + cor = round(cor); + mni = T * [cor(:, 1) cor(:, 2) cor(:, 3) ones(size(cor, 1), 1)]'; + mni = mni'; + mni(:, 4) = []; +end diff --git a/demos/roi/label_and_extract_clusters.m b/demos/roi/label_and_extract_clusters.m index 18d74cd..245ed28 100644 --- a/demos/roi/label_and_extract_clusters.m +++ b/demos/roi/label_and_extract_clusters.m @@ -10,7 +10,7 @@ gunzip(fullfile('inputs', '*.gz')); -zMap = fullfile(pwd, 'inputs', 'visual motion_association-test_z_FDR_0.01.nii'); +zMap = fullfile(pwd, 'inputs', 'visual_motion_association-test_z_FDR_0.01.nii'); zMap = renameNeuroSynth(zMap); peakThreshold = 5; diff --git a/demos/roi/neurosynth_left_right_rois.m b/demos/roi/neurosynth_left_right_rois.m index c681778..e3b328e 100644 --- a/demos/roi/neurosynth_left_right_rois.m +++ b/demos/roi/neurosynth_left_right_rois.m @@ -6,7 +6,7 @@ run ../../initCppRoi; gunzip(fullfile('inputs', '*.gz')); -zMap = fullfile(pwd, 'inputs', 'visual motion_association-test_z_FDR_0.01.nii'); +zMap = fullfile(pwd, 'inputs', 'visual_motion_association-test_z_FDR_0.01.nii'); zMap = renameNeuroSynth(zMap); roiImage = thresholdToMask(zMap, 5); diff --git a/demos/roi/roi_script.m b/demos/roi/roi_script.m index 1a615aa..a31528e 100644 --- a/demos/roi/roi_script.m +++ b/demos/roi/roi_script.m @@ -14,7 +14,7 @@ %% ASSUMPTION % -% This assumes that the 2 immages are in the same space (MNI, individual) +% This assumes that the 2 images are in the same space (MNI, individual) % but they might not necessarily have the same resolution. % % In SPM lingo this means they are coregistered but not necessarily resliced. @@ -29,7 +29,7 @@ % You can use the resliceRoiImages for that. %% -zMap = fullfile(pwd, 'inputs', 'visual motion_association-test_z_FDR_0.01.nii'); +zMap = fullfile(pwd, 'inputs', 'visual_motion_association-test_z_FDR_0.01.nii'); dataImage = fullfile(pwd, 'inputs', 'TStatistic.nii'); opt.unzip.do = true; @@ -44,7 +44,7 @@ % all of these functions can be found below and show you how to create ROIs and % / or ROIs to extract data from an image. % -[roiName, zMap] = preprareDataAndROI(opt, dataImage, zMap); +[roiName, zMap] = prepareDataAndROI(opt, dataImage, zMap); data_mask = getDataFromMask(dataImage, roiName); data_sphere = getDataFromSphere(opt, dataImage); @@ -136,7 +136,7 @@ %% HELPER FUNCTION -function [roiName, zMap] = preprareDataAndROI(opt, dataImage, zMap) +function [roiName, zMap] = prepareDataAndROI(opt, dataImage, zMap) if opt.unzip.do gunzip(fullfile('inputs', '*.gz')); diff --git a/demos/roi/step_7_get_ROI_PSC.m b/demos/roi/step_7_get_ROI_PSC.m deleted file mode 100644 index abf90c6..0000000 --- a/demos/roi/step_7_get_ROI_PSC.m +++ /dev/null @@ -1,189 +0,0 @@ -% This script will run through the ROIs made for each subject fo a ROI -% based anaylisis using MarsBar to get a time course of the activations and -% a percent signal change for each ROI and subject - -clc; - -if ~exist('machine_id', 'var') - machine_id = 2; % 0: container ; 1: Remi ; 2: Beast -end - -% 'MNI' or 'T1w' (native) -if ~exist('space', 'var') - space = 'T1w'; -end - -if ~exist('randomize', 'var') - randomize = 0; -end - -% event specification for getting fitted event time-courses -contrast_idx = 1; -event_session_no = repmat(1:4, 1, 4); -event_type_no = repmat([1:4]', 1, 4)'; -event_spec = [event_session_no; event_type_no(:)']; -event_duration = 16; % default SPM event duration - -% FOR INFO -% contrast_ls = { -% 'Euc-Left + Alm-Left + Euc-Right + Alm-Right > 0' -% 'Euc-Left + Alm-Left + Euc-Right + Alm-Right < 0' -% 'Alm-Left + Alm-Right > 0' -% 'Alm-Left + Alm-Right < 0' -% 'Euc-Left + Euc-Right > 0' -% 'Euc-Left + Euc-Right < 0' -% 'Euc-Right + Alm-Right > 0' -% 'Euc-Right + Alm-Right < 0' -% 'Euc-Left + Alm-Left > 0' -% 'Euc-Left + Alm-Left < 0' -% 'Euc-Left > 0' -% 'Euc-Left < 0' -% 'Alm-Left > 0' -% 'Alm-Left < 0' -% 'Euc-Right > 0' -% 'Euc-Right < 0' -% 'Alm-Right > 0' -% 'Alm-Right < 0' -% 'resp-03 + resp-12 > 0' -% 'resp-03 + resp-12 < 0'}; - -%% -% setting up directories -[data_dir, code_dir, output_dir, fMRIprep_DIR] = set_dir(machine_id); - -% Set up the SPM defaults, just in case -addpath(fullfile(spm('dir'), 'toolbox', 'marsbar')); -% Start marsbar to make sure spm_get works -marsbar('on'); - -% get data info -bids = spm_BIDS(fullfile(data_dir, 'raw')); - -% get subjects -folder_subj = get_subj_list(output_dir); -folder_subj = cellstr(char({folder_subj.name}')); % turn subject folders into a cellstr -[~, ~, folder_subj] = rm_subjects([], [], folder_subj, true); -nb_subjects = numel(folder_subj); -group_id = ~cellfun(@isempty, strfind(folder_subj, 'ctrl')); %#ok<*STRCLFH> - -% see what GLM to run -opt = struct(); -[sets] = get_cfg_GLMS_to_run(); -[opt, all_GLMs] = set_all_GLMS(opt, sets); - -if randomize - shuffle_subjs = randperm(length(group_id)); -end - -%% for each subject - -time_course = {}; -percent_signal_change = {}; - -for i_subj = 1:nb_subjects - - fprintf('running %s\n', folder_subj{i_subj}); - - subj_dir = fullfile(output_dir, [folder_subj{i_subj}]); - - roi_src_folder = fullfile(data_dir, 'derivatives', 'ANTs', folder_subj{i_subj}, 'roi'); - if strcmp(space, 'MNI') - roi_src_folder = fullfile(code_dir, 'inputs'); - end - - roi_tgt_folder = fullfile(subj_dir, 'roi'); - mkdir(roi_tgt_folder); - - marsbar_save_folder = fullfile(output_dir, '..', 'marsbar', folder_subj{i_subj}); - mkdir(marsbar_save_folder); - - % list ROIs - roi_ls = spm_select('FPList', ... - roi_src_folder, ... - ['^ROI-.*_space-' space '.nii$']); - roi_ls = cellstr(roi_ls); - - % go through all the models specified and get for each ROI the percetn - % signal change and time course - fprintf(' running GLMs\n'); - for i_GLM = 1:size(all_GLMs) - - cfg = get_configuration(all_GLMs, opt, i_GLM); - - cfg_list{i_GLM} = cfg; - - % directory for this specific analysis - analysis_dir = name_analysis_dir(cfg, space); - analysis_dir = fullfile ( ... - output_dir, ... - folder_subj{i_subj}, 'stats', analysis_dir); - - SPM = load(fullfile(analysis_dir, 'SPM.mat')); - - for i_roi = 1:size(roi_ls, 1) - - roi = roi_ls{i_roi}; - - [path, file] = spm_fileparts(roi); - - img = spm_read_vols(spm_vol(roi)); - - disp(sum(img(:) > 0)); - - % create ROI object for Marsbar and convert to matrix format to avoid delicacies of image format - roi_obj = maroi_image(struct('vol', spm_vol(roi), 'binarize', 1, ... - 'func', [])); - roi_obj = maroi_matrix(roi_obj); - - % give it a label - label(roi_obj, strrep(file, 'ROI-', '')); - saveroi(roi_obj, fullfile(roi_tgt_folder, [file '_roi.mat'])); - - D = mardo(SPM); - - try - - % Extract data - Y = get_marsy(roi_obj, D, 'mean'); - - % MarsBaR estimation - E = estimate(D, Y); - - % Get, store statistics - stat_struct = compute_contrasts(E, contrast_idx); - - % And fitted time courses - [tc, dt] = event_fitted(E, event_spec, event_duration); - - % Get percent signal change - psc = event_signal(E, event_spec, event_duration, 'abs max'); - - % Make fitted time course into ~% signal change - block_means(E); - tc = tc / mean(block_means(E)) * 100; - - % Store values - time_course{i_roi, i_GLM}(i_subj, :) = tc; %#ok - percent_signal_change{i_GLM}(i_subj, i_roi) = psc; - - % sqve for this subject, ROI, GLM - name_save_file = fullfile(marsbar_save_folder, ... - [file '_' name_analysis_dir(cfg, space) '.mat']); - save(name_save_file, 'tc', 'psc', 'cfg', 'file', 'dt'); - - catch - - warning('\n\nSomething went wrong: %s - %s\n\n', ... - folder_subj{i_subj}, file); - - end - - end - end - - if randomize - time_course{i_roi, i_GLM} = time_course{i_roi, i_GLM}(shuffle_subjs, :); - percent_signal_change{i_GLM} = percent_signal_change{i_GLM}(shuffle_subjs); - end - -end diff --git a/demos/roi/step_8_plot_PSC.m b/demos/roi/step_8_plot_PSC.m deleted file mode 100644 index be9a854..0000000 --- a/demos/roi/step_8_plot_PSC.m +++ /dev/null @@ -1,239 +0,0 @@ -% This script will run through the ROIs made for each subject fo a ROI -% based anaylisis using MarsBar to get a time course of the activations and -% a percent signal change for each ROI and subject - -% TODO -% - refactor -% - what are the units of the time course? -% - they do not match those of the percent signal change !!!! - -clc; -clear; - -if ~exist('machine_id', 'var') - machine_id = 1; % 0: container ; 1: Remi ; 2: Beast -end - -% 'MNI' or 'T1w' (native) -if ~exist('space', 'var') - space = 'T1w'; -end - -if ~exist('randomize', 'var') - randomize = 0; -end - -% FOR INFO -% contrast_ls = { -% 'Euc-Left + Alm-Left + Euc-Right + Alm-Right > 0' -% 'Euc-Left + Alm-Left + Euc-Right + Alm-Right < 0' -% 'Alm-Left + Alm-Right > 0' -% 'Alm-Left + Alm-Right < 0' -% 'Euc-Left + Euc-Right > 0' -% 'Euc-Left + Euc-Right < 0' -% 'Euc-Right + Alm-Right > 0' -% 'Euc-Right + Alm-Right < 0' -% 'Euc-Left + Alm-Left > 0' -% 'Euc-Left + Alm-Left < 0' -% 'Euc-Left > 0' -% 'Euc-Left < 0' -% 'Alm-Left > 0' -% 'Alm-Left < 0' -% 'Euc-Right > 0' -% 'Euc-Right < 0' -% 'Alm-Right > 0' -% 'Alm-Right < 0' -% 'resp-03 + resp-12 > 0' -% 'resp-03 + resp-12 < 0'}; - -%% -% setting up directories -[data_dir, code_dir, output_dir, fMRIprep_DIR] = set_dir(machine_id); - -% get data info -bids = spm_BIDS(fullfile(data_dir, 'raw')); - -% get subjects -marsbar_save_folder = fullfile(output_dir, '..', 'marsbar'); -folder_subj = get_subj_list(marsbar_save_folder); -folder_subj = cellstr(char({folder_subj.name}')); % turn subject folders into a cellstr -[~, ~, folder_subj] = rm_subjects([], [], folder_subj, true); -nb_subjects = numel(folder_subj); -group_id = ~cellfun(@isempty, strfind(folder_subj, 'ctrl')); %#ok<*STRCLFH> - -% see what GLM to run -opt = struct(); -[sets] = get_cfg_GLMS_to_run(); -[opt, all_GLMs] = set_all_GLMS(opt, sets); - -if randomize - shuffle_subjs = randperm(length(group_id)); -end - -roi_ls = { - 'V1' - 'V2' - 'V3d' - 'V3v' - 'V4v' - 'V4d' - 'V5' - 'L-R-Primary-Olf-Cortex' - 'L-R-Secondary-Cortex' - 'L-R-Piri' - 'L-R-Orbitofrontal' - }; - -%% for each subject - -time_course = {}; -percent_signal_change = {}; - -for i_subj = 1:nb_subjects - - fprintf('running %s\n', folder_subj{i_subj}); - - subj_folder = fullfile(output_dir, '..', 'marsbar', folder_subj{i_subj}); - - % go through all the models specified and get for each ROI the percetn - % signal change and time course - fprintf(' running GLMs\n'); - for i_GLM = 1:size(all_GLMs) - - cfg = get_configuration(all_GLMs, opt, i_GLM); - - for i_roi = 1:size(roi_ls, 1) - - roi_file = spm_select('FPList', ... - subj_folder, ... - ['^ROI-' roi_ls{i_roi} ... - '.*_space-' space ... - '.*' name_analysis_dir(cfg, space) ... - '.*.mat$']); - - psc = nan; - tc = nan(855, 1); - - if ~isempty(roi_file) - load(roi_file, 'tc', 'psc'); - end - - time_course{i_roi, i_GLM}(i_subj, :) = tc; %#ok - percent_signal_change{i_GLM}(i_subj, i_roi) = psc; - - clear psc tc; - - end - end - -end - -%% Show fitted event time courses -opt = get_option(opt); -Colors = [ ... - opt.blnd_color / 255; ... - opt.sighted_color / 255]; -Colors_desat = Colors + (1 - Colors) * (1 - .3); - -black = [0 0 0]; - -close all; -figure; -hold on; - -dt = 0.0561; -secs = [0:size(time_course{1}, 2) - 1] * dt; - -for i_roi = 1:size(roi_ls, 1) - - figure('name', roi_ls{i_roi}, 'Position', [100 100 600 600]); - hold on; - - for i_group = 0:1 - - data = time_course{i_roi}(group_id == i_group, :); - - to_plot = nanmean(data); - sem = nanstd(data) / size(data, 1)^.5; - - plot(secs, to_plot, 'color', Colors(i_group + 1, :), 'linewidth', 2); - - end - - plot(secs, zeros(size(secs)), '--k', 'linewidth', 1); - - for i_group = 0:1 - - data = time_course{i_roi}(group_id == i_group, :); - - to_plot = nanmean(data); - sem = nanstd(data) / size(data, 1)^.5; - - % plot(secs, data, 'color', Colors_desat(i_group+1,:), - % 'linewidth', .5); - - shadedErrorBar(secs, to_plot, sem, ... - {'color', Colors(i_group + 1, :), 'linewidth', 2}, 1); - - end - - legend({'blind', 'ctrl'}); - - n_blind = sum(~isnan(percent_signal_change{i_GLM}(group_id == 0, i_roi))); - n_ctrl = sum(~isnan(percent_signal_change{i_GLM}(group_id == 1, i_roi))); - text(30, 0.07, sprintf('blind ; n = %i', n_blind)); - text(30, 0.05, sprintf('ctrl ; n = %i', n_ctrl)); - - limit_axis = axis; - axis([limit_axis(1:2) -0.13 0.13]); - - title(['Time courses for ' roi_ls{i_roi}], 'Interpreter', 'none'); - xlabel('Seconds'); - ylabel('% signal change'); - - ax = gca; - axPos = ax.Position; - axPos(1) = axPos(1) + .5; - axPos(2) = axPos(2) + .1; - axPos(3) = axPos(3) * .3; - axPos(4) = axPos(4) * .3; - axes('Position', axPos); - - for i_group = 0:1 - - to_plot = percent_signal_change{i_GLM}(group_id == i_group, i_roi); - - h = plotSpread(to_plot, 'distributionIdx', ones(size(to_plot)), ... - 'distributionMarkers', {'o'}, ... - 'distributionColors', Colors(i_group + 1, :), ... - 'xValues', i_group + 1, ... - 'showMM', 0, ... - 'binWidth', .1, 'spreadWidth', 1); - - set(h{1}, 'MarkerSize', 5, 'MarkerEdgeColor', 'k', ... - 'MarkerFaceColor', Colors(i_group + 1, :), ... - 'LineWidth', 2); - - h = errorbar(i_group + .5, ... - nanmean(to_plot), ... - nanstd(to_plot) / numel(to_plot)^.5, ... - 'o', ... - 'color', Colors(i_group + 1, :), 'linewidth', 1, ... - 'MarkerSize', 5, ... - 'MarkerEdgeColor', 'k', ... - 'MarkerFaceColor', Colors(i_group + 1, :)); - - end - - plot([0 3], [0 0], 'k'); - - MAX = max(abs(percent_signal_change{i_GLM}(:, i_roi))); - - axis([0.2 2.5 -MAX MAX]); - - set(gca, 'fontsize', 8, ... - 'ytick', -1:0.25:1, 'yticklabel', -1:0.25:1, ... - 'xtick', 1:2, 'xticklabel', {'blind', 'ctrl'}, ... - 'ticklength', [.02 .02], 'tickdir', 'out'); - -end diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 100644 index 712d87b..0000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1,5 +0,0 @@ -# Contributing - -## Unit testing - -## Changelog diff --git a/docs/Makefile b/docs/Makefile index dc98472..d0c3cbf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -18,7 +18,3 @@ help: # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - - -doc: - @$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)" diff --git a/docs/source/conf.py b/docs/source/conf.py index 6d8e931..9191e3f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,7 +23,7 @@ author = "the CPP ROI dev team" # The full version, including alpha/beta/rc tags -release = "v0.1.0" +release = "v0.2.0" # -- General configuration --------------------------------------------------- @@ -31,7 +31,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinxcontrib.matlab", "sphinx.ext.autodoc"] +extensions = ["sphinxcontrib.matlab", "sphinx.ext.autodoc", "sphinx_copybutton"] matlab_src_dir = os.path.dirname(os.path.abspath("../../src")) primary_domain = "mat" diff --git a/initCppRoi.m b/initCppRoi.m index ede9fae..be8b43c 100644 --- a/initCppRoi.m +++ b/initCppRoi.m @@ -3,15 +3,25 @@ function initCppRoi() global CPP_ROI_INITIALIZED + global CPP_ROI_PATHS - if isempty(CPP_ROI_INITIALIZED) + if isempty(CPP_ROI_INITIALIZED) || ~CPP_ROI_INITIALIZED % directory with this script becomes the current directory - WD = fileparts(mfilename('fullpath')); + thisDirectory = fileparts(mfilename('fullpath')); + pathSep = ':'; + if ~isunix + pathSep = ';'; + end % we add all the subfunctions that are in the sub directories - addpath(genpath(fullfile(WD, 'src'))); - addpath(fullfile(WD, 'lib', 'marsbar-0.44')); + CPP_ROI_PATHS = genpath(fullfile(thisDirectory, 'src')); + CPP_ROI_PATHS = cat(2, CPP_ROI_PATHS, pathSep, ... + fullfile(thisDirectory, 'lib', 'marsbar-0.44')); + CPP_ROI_PATHS = cat(2, CPP_ROI_PATHS, pathSep, ... + fullfile(thisDirectory, 'atlas')); + addpath(CPP_ROI_PATHS, '-begin'); + marsbar('on'); try marsbar('splash'); diff --git a/miss_hit.cfg b/miss_hit.cfg index ac150c2..3c512b6 100644 --- a/miss_hit.cfg +++ b/miss_hit.cfg @@ -4,7 +4,7 @@ project_root line_length: 120 regex_function_name: "[a-z]+(_*([a-zA-Z0-9]){1}[A-Za-z]+)*" # almost anything goes in the root folder -regex_parameter_name: "[a-zA-Z0-9]+(_*([a-zA-Z0-9]){1}[A-Za-z]+)*" # +regex_parameter_name: "[a-zA-Z0-9]+(_*([a-zA-Z0-9]){1}[A-Za-z]+)*" # regex_script_name: "[a-z0-9]+(_[a-z0-9]+)*" exclude_dir: "lib" diff --git a/requirements.txt b/requirements.txt index 9d0b0bd..92800bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ Sphinx sphinxcontrib-matlabdomain sphinxcontrib-napoleon sphinx_rtd_theme +sphinx-copybutton miss_hit==0.9.29 diff --git a/run_tests.m b/run_tests.m index a6dc018..4cdf841 100644 --- a/run_tests.m +++ b/run_tests.m @@ -1,11 +1,16 @@ % (C) Copyright 2019 CPP ROI developers +% Does not work due to some relative path of the data when testing + warning('OFF'); spm('defaults', 'fMRI'); -folderToCover = fullfile(pwd, 'src'); -testFolder = fullfile(pwd, 'tests'); +testFolder = fullfile(fileparts(mfilename('fullpath')), 'tests'); + +addpath(fullfile(testFolder, 'utils')); + +folderToCover = fullfile(testFolder, '..', 'src'); success = moxunit_runtests( ... testFolder, ... diff --git a/src/atlas/extractRoiByLabel.m b/src/atlas/extractRoiByLabel.m index 65d10c8..804817c 100644 --- a/src/atlas/extractRoiByLabel.m +++ b/src/atlas/extractRoiByLabel.m @@ -23,12 +23,12 @@ outputVol(vol == labelStruct.label) = true; p = bids.internal.parse_filename(sourceImage); - p.entities.label = labelStruct.ROI; + p.entities.label = bids.internal.camel_case(labelStruct.ROI); p.suffix = 'mask'; - p.use_schema = false; - newName = bids.create_filename(p); - hdr.fname = spm_file(hdr.fname, 'filename', newName); + bidsFile = bids.File(p); + + hdr.fname = spm_file(hdr.fname, 'filename', bidsFile.filename); spm_write_vol(hdr, outputVol); outputImage = hdr.fname; diff --git a/src/atlas/extractRoiFromAtlas.m b/src/atlas/extractRoiFromAtlas.m index 377c4eb..60746d8 100644 --- a/src/atlas/extractRoiFromAtlas.m +++ b/src/atlas/extractRoiFromAtlas.m @@ -59,20 +59,19 @@ roiImage = extractRoiByLabel(atlasFile, labelStruct); % rename file - entities = struct('space', 'MNI', ... - 'hemi', hemisphere, ... + entities = struct('hemi', hemisphere, ... + 'space', 'MNI', ... 'label', roiName, ... 'desc', atlasName); nameStructure = struct('entities', entities, ... 'suffix', 'mask', ... 'ext', '.nii'); - nameStructure.use_schema = false; - newName = bids.create_filename(nameStructure); + bidsFile = bids.File(nameStructure); - movefile(roiImage, fullfile(outputDir, newName)); + movefile(roiImage, fullfile(outputDir, bidsFile.filename)); % create side car json - roiImage = fullfile(outputDir, newName); + roiImage = fullfile(outputDir, bidsFile.filename); json = bids.derivatives_json(roiImage); bids.util.jsonencode(json.filename, json.content); diff --git a/src/atlas/labelClusters.m b/src/atlas/labelClusters.m index 26c7d95..a96b9ea 100644 --- a/src/atlas/labelClusters.m +++ b/src/atlas/labelClusters.m @@ -1,28 +1,61 @@ -function outputImage = labelClusters(sourceImage, peakThreshold, extendThreshold) +function outputImage = labelClusters(varargin) + % + % Returns a binary mask for an image after applying voxel wise threshold and + % an optional cluster size threshold + % + % USAGE:: + % + % outputImage = thresholdToMask(inputImage, peakThreshold, clusterSize) + % + % :param inputImage: + % :type inputImage: path + % :param peakThreshold: + % :type peakThreshold: float + % :param clusterSize: + % :type clusterSize: integer >= 0 (Default: 0) + % + % :returns: - :outputImage: (string) + % + % See also getClusters, sortAndLabelClusters % % Adapted from: % https://en.wikibooks.org/wiki/SPM/How-to#How_to_remove_clusters_under_a_certain_size_in_a_binary_mask? % % (C) Copyright 2021 CPP ROI developers - hdr = spm_vol(sourceImage); + default_clusterSize = 0; + + isFile = @(x) exist(x, 'file') == 2; + isPositive = @(x) isnumeric(x) && x >= 0; + + p = inputParser; + + addRequired(p, 'inputImage', isFile); + addRequired(p, 'peakThreshold', @isnumeric); + addOptional(p, 'clusterSize', default_clusterSize, isPositive); + + parse(p, varargin{:}); - [l2, num] = getClusters(hdr, peakThreshold); + inputImage = p.Results.inputImage; + peakThreshold = p.Results.peakThreshold; + clusterSize = p.Results.clusterSize; + + [l2, num] = getClusters(inputImage, peakThreshold); if ~num warning('No clusters found.'); return end - vol = sortAndLabelClusters(l2, num, extendThreshold); + vol = sortAndLabelClusters(l2, num, clusterSize); % Write new image with cluster laebelled with their voxel size - p = bids.internal.parse_filename(sourceImage); + p = bids.internal.parse_filename(inputImage); p.suffix = 'dseg'; % discrete segmentation - p.use_schema = false; - newName = bids.create_filename(p); - hdr.fname = spm_file(hdr.fname, 'filename', newName); + hdr = spm_vol(inputImage); + bidsFile = bids.File(p); + hdr.fname = spm_file(hdr.fname, 'filename', bidsFile.filename); % Cluster labels as their size. spm_write_vol(hdr, vol); @@ -32,29 +65,3 @@ bids.util.jsonencode(json.filename, json.content); end - -function [l2, num] = getClusters(hdr, peakThreshold) - data = spm_read_vols(hdr); - [l2, num] = spm_bwlabel(double(data > peakThreshold), 26); -end - -function vol = sortAndLabelClusters(l2, num, extendThreshold) - - % Extent threshold, and sort clusters by their extent - % Label corresponds to their rank - [n, ni] = sort(histc(l2(:), 0:num), 1, 'descend'); - vol = zeros(size(l2)); - n = n(2:end); - ni = ni(2:end) - 1; - ni = ni(n >= extendThreshold); - n = n(n >= extendThreshold); - for i = 1:length(n) - vol(l2 == ni(i)) = i; - end - - fprintf('Selected %d clusters (out of %d) in image.\n', length(n), num); - for i = 1:length(n) - fprintf('Cluster label %i ; size: %i voxels\n', i, n(i)); - end - -end diff --git a/src/roi/createRoi.m b/src/roi/createRoi.m index 6721552..5928a49 100644 --- a/src/roi/createRoi.m +++ b/src/roi/createRoi.m @@ -351,9 +351,9 @@ label = p.entities.label; end - p.entities.label = [label ' ' mask.label]; - p.use_schema = false; + p.entities.label = bids.internal.camel_case([label ' ' mask.label]); - roiName = bids.create_filename(p); + bidsFile = bids.File(p); + roiName = bidsFile.filename; end diff --git a/src/roi/keepHemisphere.m b/src/roi/keepHemisphere.m index b4d18c7..5ef2006 100644 --- a/src/roi/keepHemisphere.m +++ b/src/roi/keepHemisphere.m @@ -40,10 +40,9 @@ p = bids.internal.parse_filename(inputImage); p.entities.hemi = hemisphere; - p.use_schema = false; - newName = bids.create_filename(p); + bidsFile = bids.File(p); - hdr.fname = spm_file(inputImage, 'filename', newName); + hdr.fname = spm_file(inputImage, 'filename', bidsFile.filename); spm_write_vol(hdr, vol); diff --git a/src/roi/renameNeuroSynth.m b/src/roi/renameNeuroSynth.m index 0c24f63..477be69 100644 --- a/src/roi/renameNeuroSynth.m +++ b/src/roi/renameNeuroSynth.m @@ -29,13 +29,11 @@ basename = spm_file(inputImage, 'basename'); parts = strsplit(basename, '_'); - p.entities.label = ['neurosynth ' parts{1}]; + p.entities.label = bids.internal.camel_case(['neurosynth ' parts{1}]); - p.use_schema = false; + bidsFile = bids.File(p); - newName = bids.create_filename(p); - - outputImage = spm_file(inputImage, 'filename', newName); + outputImage = spm_file(inputImage, 'filename', bidsFile.filename); movefile(inputImage, outputImage); diff --git a/src/roi/thresholdToMask.m b/src/roi/thresholdToMask.m index d1e9fdd..7767b0d 100644 --- a/src/roi/thresholdToMask.m +++ b/src/roi/thresholdToMask.m @@ -1,26 +1,69 @@ -function outputImage = thresholdToMask(inputImage, threshold) +function outputImage = thresholdToMask(varargin) % - % TODO - % allow threshold to be inferior than, greater than or both + % Returns a binary mask for an image after applying voxel wise threshold and + % an optional cluster size threshold + % + % USAGE:: % + % outputImage = thresholdToMask(inputImage, peakThreshold, clusterSize) % + % :param inputImage: + % :type inputImage: path + % :param peakThreshold: + % :type peakThreshold: float + % :param clusterSize: + % :type clusterSize: integer >= 0 (Default) + % + % :returns: - :outputImage: (string) % % (C) Copyright 2021 CPP ROI developers - hdr = spm_vol(inputImage); - img = spm_read_vols(hdr); + % TODO + % allow threshold to be inferior than, greater than or both + + default_clusterSize = 0; + + isFile = @(x) exist(x, 'file') == 2; + isPositive = @(x) isnumeric(x) && x >= 0; + p = inputParser; + + addRequired(p, 'inputImage', isFile); + addRequired(p, 'peakThreshold', @isnumeric); + addOptional(p, 'clusterSize', default_clusterSize, isPositive); + + parse(p, varargin{:}); + + inputImage = p.Results.inputImage; + peakThreshold = p.Results.peakThreshold; + clusterSize = p.Results.clusterSize; + + [l2, num] = getClusters(inputImage, peakThreshold); + vol = sortAndThresholdClusters(l2, num, clusterSize); + + % create output name p = bids.internal.parse_filename(inputImage); + p.suffix = 'mask'; - p.use_schema = false; - newName = bids.create_filename(p); - hdr.fname = spm_file(hdr.fname, 'filename', newName); - img = img > threshold; - spm_write_vol(hdr, img); + % add peakThreshold and clusterSizeInfo to desc + if ~isfield(p.entities, 'desc') + p.entities.desc = ''; + end + descSuffix = sprintf('p%05.2f', peakThreshold); + if clusterSize > 0 + descSuffix = [descSuffix, sprintf('k%03.0f', clusterSize)]; + end + descSuffix = strrep(descSuffix, '.', 'pt'); + p.entities.desc = [p.entities.desc descSuffix]; + bidsFile = bids.File(p); + hdr = spm_vol(inputImage); + hdr.fname = spm_file(hdr.fname, 'filename', bidsFile.filename); outputImage = hdr.fname; + spm_write_vol(hdr, vol); + json = bids.derivatives_json(outputImage); bids.util.jsonencode(json.filename, json.content); diff --git a/src/utils/cppRoiRoot.m b/src/utils/cppRoiRoot.m new file mode 100644 index 0000000..3f8a82f --- /dev/null +++ b/src/utils/cppRoiRoot.m @@ -0,0 +1,14 @@ +function pth = cppRoiRoot() + % + % + % USAGE:: + % + % pth = cppRoiRoot() + % + % + % (C) Copyright 2022 CPP ROI developers + + pth = fullfile(fileparts(mfilename('fullpath')), '..', '..'); + pth = spm_file(pth, 'cpath'); + +end diff --git a/src/utils/getClusters.m b/src/utils/getClusters.m new file mode 100644 index 0000000..88dcbc4 --- /dev/null +++ b/src/utils/getClusters.m @@ -0,0 +1,21 @@ +function [l2, num] = getClusters(inputImage, peakThreshold) + % + % USAGE:: + % + % [l2, num] = getClusters(inputImage, peakThreshold) + % + % :param sourceImage: string + % :type sourceImage: string + % :param peakThreshold: string + % :type peakThreshold: string + % + % See also labelClusters, sortAndLabelClusters + % + % (C) Copyright 2021 CPP ROI developers + + hdr = spm_vol(inputImage); + vol = spm_read_vols(hdr); + + [l2, num] = spm_bwlabel(double(vol > peakThreshold), 26); + +end diff --git a/src/utils/removeSpmPrefix.m b/src/utils/removeSpmPrefix.m new file mode 100644 index 0000000..3a977fe --- /dev/null +++ b/src/utils/removeSpmPrefix.m @@ -0,0 +1,10 @@ +function image = removeSpmPrefix(image, prefix) + % + % (C) Copyright 2019 CPP ROI developers + + basename = spm_file(image, 'basename'); + tmp = spm_file(image, 'basename', basename(length(prefix) + 1:end)); + movefile(image, tmp); + image = tmp; + +end diff --git a/src/utils/renameFile.m b/src/utils/renameFile.m index 0b3d771..ad476a0 100644 --- a/src/utils/renameFile.m +++ b/src/utils/renameFile.m @@ -1,24 +1,46 @@ function newName = renameFile(inputFile, specification) % % Renames a BIDS valid file into another BIDS valid file given some - % specification. + % specificationification. % % USAGE:: % - % newName = renameFile(inputFile, specification) + % newName = renameFile(inputFile, specificationification) % % :param inputFile: better if fullfile path % :type inputFile: string - % :param specification: structure specifying the details of the new name + % :param specificationification: structure specificationifying the details of the new name % The structure content must resemble that of the % output of bids.internal.parse_filename - % :type specification: structure + % :type specificationification: structure % % (C) Copyright 2021 CPP ROI developers - specification.use_schema = false; - newName = bids.create_filename(specification, inputFile); + bf = bids.File(inputFile, 'use_schema', false); + if isfield(specification, 'prefix') + bf.suffix = specification.prefix; + end + if isfield(specification, 'suffix') + bf.suffix = specification.suffix; + end + if isfield(specification, 'ext') + bf.extension = specification.ext; + end + if isfield(specification, 'entities') + entities = fieldnames(specification.entities); + for i = 1:numel(entities) + bf = bf.set_entity(entities{i}, ... + bids.internal.camel_case(specification.entities.(entities{i}))); + end + end + if isfield(specification, 'entity_order') + bf = bf.reorder_entities(specification.entity_order); + end + + bf = bf.update; + + newName = bf.filename; outputFile = spm_file(inputFile, 'filename', newName); movefile(inputFile, outputFile); diff --git a/src/utils/sortAndLabelClusters.m b/src/utils/sortAndLabelClusters.m new file mode 100644 index 0000000..2b82f7d --- /dev/null +++ b/src/utils/sortAndLabelClusters.m @@ -0,0 +1,34 @@ +function vol = sortAndLabelClusters(l2, num, clusterSize) + % + % Creates a binary image after: + % - Sorting clusters by their extent + % - Removes cluster with smaller than clusterSize + % - Labelling cluster with their rank in the sorting + % + % USAGE:: + % + % vol = sortAndLabelClusters(l2, num, clusterSize) + % + % + % See also: getClusters, labelClusters + % + % (C) Copyright 2021 CPP ROI developers + + % refactor with sortAndThresholdClusters + + [n, ni] = sort(histc(l2(:), 0:num), 1, 'descend'); + vol = zeros(size(l2)); + n = n(2:end); + ni = ni(2:end) - 1; + ni = ni(n >= clusterSize); + n = n(n >= clusterSize); + for i = 1:length(n) + vol(l2 == ni(i)) = i; + end + + fprintf('Selected %d clusters (out of %d) in image.\n', length(n), num); + for i = 1:length(n) + fprintf('Cluster label %i ; size: %i voxels\n', i, n(i)); + end + +end diff --git a/src/utils/sortAndThresholdClusters.m b/src/utils/sortAndThresholdClusters.m new file mode 100644 index 0000000..5c09ddf --- /dev/null +++ b/src/utils/sortAndThresholdClusters.m @@ -0,0 +1,15 @@ +function vol = sortAndThresholdClusters(l2, num, clusterSize) + % + % (C) Copyright 2020 CPP ROI developers + + [n, ni] = sort(histc(l2(:), 0:num), 1, 'descend'); + vol = zeros(size(l2)); + n = n(2:end); + ni = ni(2:end) - 1; + ni = ni(n >= clusterSize); + n = n(n >= clusterSize); + for i = 1:length(n) + vol(l2 == ni(i)) = 1; + end + +end diff --git a/tests/test_extractRoiFromAtlas.m b/tests/test_extractRoiFromAtlas.m index 1120eb9..9442a8d 100644 --- a/tests/test_extractRoiFromAtlas.m +++ b/tests/test_extractRoiFromAtlas.m @@ -12,7 +12,7 @@ function test_extractRoiFromAtlas_wang() roiImage = extractRoiFromAtlas(pwd, 'wang', 'V1v', 'L'); - assertEqual(exist(fullfile(pwd, 'space-MNI_hemi-L_label-V1v_desc-wang_mask.nii'), ... + assertEqual(exist(fullfile(pwd, 'hemi-L_space-MNI_label-V1v_desc-wang_mask.nii'), ... 'file'), ... 2); @@ -29,7 +29,7 @@ function test_extractRoiFromAtlas_neuromorphometrics() roiImage = extractRoiFromAtlas(pwd, 'neuromorphometrics', 'Amygdala', 'L'); assertEqual(exist(fullfile(pwd, ... - 'space-MNI_hemi-L_label-Amygdala_desc-neuromorphometrics_mask.nii'), ... + 'hemi-L_space-MNI_label-Amygdala_desc-neuromorphometrics_mask.nii'), ... 'file'), ... 2); diff --git a/tests/test_getLookUpTable.m b/tests/test_getLookUpTable.m index 0e1032d..ff5a5ac 100644 --- a/tests/test_getLookUpTable.m +++ b/tests/test_getLookUpTable.m @@ -23,10 +23,14 @@ function test_lut_wang() function test_lut_anat_tb() - lut = getLookUpTable('anatomy_toobox'); + if ~isGithubCi - assertEqual(lut.label(1), 7); - assertEqual(lut.ROI{1}, 'Interposed Nucleus'); + lut = getLookUpTable('anatomy_toobox'); + + assertEqual(lut.label(1), 7); + assertEqual(lut.ROI{1}, 'Interposed Nucleus'); + + end end diff --git a/tests/test_thresholdToMask.m b/tests/test_thresholdToMask.m new file mode 100644 index 0000000..cfa84aa --- /dev/null +++ b/tests/test_thresholdToMask.m @@ -0,0 +1,64 @@ +function test_suite = test_thresholdToMask() %#ok<*STOUT> + % + % (C) Copyright 2021 CPP ROI developers + + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +% TODO refactor set up and teardown + +function test_thresholdToMask_default() + + image = 'visual motion_association-test_z_FDR_0.01.nii.gz'; + + inputImage = setUp(image); + + peakThreshold = 5.0; + outputImage = thresholdToMask(inputImage, peakThreshold); + + % check that we have certain number voxels in the mask + vol = spm_read_vols(spm_vol(outputImage)); + assertEqual(sum(vol(:) > 0), 2008); + + teardown(); + +end + +function test_thresholdToMask_cluster() + + image = 'visual motion_association-test_z_FDR_0.01.nii.gz'; + + inputImage = setUp(image); + + peakThreshold = 5; + clusterSize = 10; + outputImage = thresholdToMask(inputImage, peakThreshold, clusterSize); + + % check that we have certain number voxels in the mask + vol = spm_read_vols(spm_vol(outputImage)); + assertEqual(sum(vol(:) > 0), 1926); + + teardown(); + +end + +function inputImage = setUp(image) + + rootDir = fullfile(fileparts(mfilename('fullpath')), '..'); + + gzImage = spm_file(fullfile(rootDir, 'demos', 'roi', 'inputs', image), 'cpath'); + [~, basename] = spm_fileparts(gzImage); + gunzip(gzImage, pwd); + + inputImage = renameNeuroSynth(fullfile(pwd, basename)); + +end + +function teardown() + delete('*.nii'); + delete('*.json'); +end diff --git a/tests/test_unit_returnAtlasDir.m b/tests/test_unit_returnAtlasDir.m index c6685a6..149e292 100644 --- a/tests/test_unit_returnAtlasDir.m +++ b/tests/test_unit_returnAtlasDir.m @@ -12,7 +12,7 @@ function test_returnAtlasDir_default() atlasDir = returnAtlasDir('neuromorphometric'); - assertEqual(atlasDir, baseAtlasDir()); + assertEqual(atlasDir, returnAtlasDir()); end @@ -20,7 +20,7 @@ function test_returnAtlasDir_wang() atlasDir = returnAtlasDir('wang'); - expected = fullfile(baseAtlasDir(), 'visual_topography_probability_atlas'); + expected = fullfile(returnAtlasDir(), 'visual_topography_probability_atlas'); assertEqual(atlasDir, expected); @@ -35,9 +35,3 @@ function test_returnAtlasDir_anatomy_toobox() assertEqual(atlasDir, expected); end - -function baseDir = baseAtlasDir() - - baseDir = spm_file(fullfile(fileparts(mfilename('fullpath')), '..', 'atlas'), 'cpath'); - -end diff --git a/tests/test_unzipAtlas.m b/tests/test_unzipAtlas.m index b81c7b5..f3041bc 100644 --- a/tests/test_unzipAtlas.m +++ b/tests/test_unzipAtlas.m @@ -10,10 +10,15 @@ function test_unzipAtlas_default() + cleanUp(); + unzipAtlas('wang'); assertEqual(exist( ... - fullfile(returnAtlasDir('wang'), 'subj_vol_all', 'space-MNI_hemi-lh_dseg.nii'), 'file'), ... + fullfile(returnAtlasDir('wang'), ... + 'subj_vol_all', ... + 'space-MNI_hemi-lh_dseg.nii'), ... + 'file'), ... 2); cleanUp(); @@ -27,6 +32,10 @@ function cleanUp() if isOctave() confirm_recursive_rmdir (true, 'local'); end - rmdir(returnAtlasDir('wang'), 's'); + + try + rmdir(returnAtlasDir('wang'), 's'); + catch + end end diff --git a/tests/utils/isGithubCi.m b/tests/utils/isGithubCi.m new file mode 100644 index 0000000..6be6dad --- /dev/null +++ b/tests/utils/isGithubCi.m @@ -0,0 +1,18 @@ +function [IS_GITHUB, pth] = isGithubCi() + % + % (C) Copyright 2022 CPP ROI developers + + IS_GITHUB = false; + + GITHUB_WORKSPACE = getenv('HOME'); + + if strcmp(GITHUB_WORKSPACE, '/home/runner') + + fprintf(1, '\n WE ARE RUNNING IN GITHUB CI\n'); + + IS_GITHUB = true; + pth = GITHUB_WORKSPACE; + + end + +end diff --git a/tests/utils/isOctave.m b/tests/utils/isOctave.m new file mode 100644 index 0000000..62402aa --- /dev/null +++ b/tests/utils/isOctave.m @@ -0,0 +1,21 @@ +function retval = isOctave() + % + % Returns true if the environment is Octave. + % Only in the test folder for testing purposes + % + % USAGE:: + % + % retval = isOctave() + % + % :returns: :retval: (boolean) + % + % (C) Copyright 2022 CPP ROI developers + + persistent cacheval % speeds up repeated calls + + if isempty (cacheval) + cacheval = (exist ('OCTAVE_VERSION', 'builtin') > 0); + end + + retval = cacheval; +end diff --git a/tests/rmRetinoAtlas.m b/tests/utils/rmRetinoAtlas.m similarity index 100% rename from tests/rmRetinoAtlas.m rename to tests/utils/rmRetinoAtlas.m diff --git a/uninitCppRoi.m b/uninitCppRoi.m new file mode 100644 index 0000000..c5d7d03 --- /dev/null +++ b/uninitCppRoi.m @@ -0,0 +1,19 @@ +% (C) Copyright 2021 CPP ROI developers + +function uninitCppRoi() + + global CPP_ROI_INITIALIZED + global CPP_ROI_PATHS + + if isempty(CPP_ROI_INITIALIZED) || ~CPP_ROI_INITIALIZED + fprintf('\n\nCPP_ROI not initialized\n\n'); + return + + else + marsbar('off'); + rmpath(CPP_ROI_PATHS); + CPP_ROI_INITIALIZED = false; + + end + +end diff --git a/version.txt b/version.txt index b82608c..1474d00 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v0.1.0 +v0.2.0