Skip to content

Commit 6bae5c1

Browse files
authored
EHN detact different affine matrix from input (#19)
* EHN detact different affine matrix from input * add gitattribute * ADD codecov.yml
1 parent 8d5a294 commit 6bae5c1

File tree

7 files changed

+192
-12
lines changed

7 files changed

+192
-12
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.git_archival.txt export-subst

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2-
[![codecov](https://codecov.io/gh/SIMEXP/giga_connectome/branch/main/graph/badge.svg?token=P4EGV7NKZ8)](https://codecov.io/gh/SIMEXP/giga_connectome)
2+
[![codecov](https://codecov.io/github/SIMEXP/giga_connectome/branch/main/graph/badge.svg?token=TYE4UURNTQ)](https://codecov.io/github/SIMEXP/giga_auto_qc)
3+
34
# giga_connectome
45

56
BIDS App to generate connectome from fMRIPrep outputs.

codecov.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
# similar to scikit-learn .codecov.yml
3+
# coverage:
4+
# status:
5+
# project:
6+
# default:
7+
# # Commits pushed to main should not make the overall
8+
# # project coverage decrease by more than 1%:
9+
# target: auto
10+
# threshold: 1%
11+
# patch:
12+
# default:
13+
# # Be tolerant on slight code coverage diff on PRs to limit
14+
# # noisy red coverage status on github PRs.
15+
# target: auto
16+
# threshold: 1%
17+
comment: # this is a top-level key
18+
layout: reach, diff, flags, files
19+
behavior: default
20+
require_changes: false # if true: only post the comment if coverage changes
21+
require_base: no # [yes :: must have a base report to post]
22+
require_head: yes # [yes :: must have a head report to post]
23+
24+
ignore:
25+
- '*/tests/' # ignore folders related to testing
26+
- '*/data/'

giga_connectome/mask.py

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import os
22
import re
33
import json
4-
from typing import Optional, Union, List
4+
from typing import Optional, Union, List, Tuple
55

66
from pathlib import Path
77
from tqdm import tqdm
88
import nibabel as nib
9-
109
from nilearn.masking import compute_multi_epi_mask
11-
from nilearn.image import resample_to_img, new_img_like, get_data, math_img
10+
from nilearn.image import (
11+
resample_to_img,
12+
new_img_like,
13+
get_data,
14+
math_img,
15+
load_img,
16+
)
1217
from nibabel import Nifti1Image
18+
import numpy as np
1319
from scipy.ndimage import binary_closing
1420

1521
from pkg_resources import resource_filename
@@ -74,6 +80,7 @@ def generate_group_mask(
7480
template: str = "MNI152NLin2009cAsym",
7581
templateflow_dir: Optional[Path] = None,
7682
n_iter: int = 2,
83+
verbose: int = 1,
7784
) -> Nifti1Image:
7885
"""
7986
Generate a group EPI grey matter mask, and overlaid with a MNI grey
@@ -98,6 +105,9 @@ def generate_group_mask(
98105
Number of repetitions of dilation and erosion steps performed in
99106
scipy.ndimage.binary_closing function.
100107
108+
verbose :
109+
Level of verbosity.
110+
101111
Keyword Arguments
102112
-----------------
103113
Used to filter the cirret
@@ -109,7 +119,12 @@ def generate_group_mask(
109119
nibabel.nifti1.Nifti1Image
110120
EPI (grey matter) mask for the current group of subjects.
111121
"""
112-
# TODO: subject native space grey matter mask???
122+
if verbose > 1:
123+
print(f"Found {len(imgs)} masks")
124+
if exclude := _check_mask_affine(imgs, verbose):
125+
imgs, __annotations__ = _get_consistent_masks(imgs, exclude)
126+
if verbose > 1:
127+
print(f"Remaining: {len(imgs)} masks")
113128

114129
# templateflow environment setting to get around network issue
115130
if templateflow_dir and templateflow_dir.exists():
@@ -258,3 +273,107 @@ def _load_config(atlas: Union[str, Path, dict]) -> dict:
258273
else:
259274
raise ValueError(f"Invalid input: {atlas}")
260275
return atlas_config
276+
277+
278+
def _get_consistent_masks(
279+
mask_imgs: List[Union[Path, str, Nifti1Image]], exclude: List[int]
280+
) -> Tuple[List[int], List[str]]:
281+
"""Create a list of masks that has the same affine.
282+
283+
Parameters
284+
----------
285+
286+
mask_imgs :
287+
The original list of functional masks
288+
289+
exclude :
290+
List of index to exclude.
291+
292+
Returns
293+
-------
294+
List of str
295+
Functional masks with the same affine.
296+
297+
List of str
298+
Identidiers of scans with a different affine.
299+
"""
300+
weird_mask_identifiers = []
301+
odd_masks = np.array(mask_imgs)[np.array(exclude)]
302+
odd_masks = odd_masks.tolist()
303+
for odd_file in odd_masks:
304+
identifier = Path(odd_file).name.split("_space")[0]
305+
weird_mask_identifiers.append(identifier)
306+
cleaned_func_masks = set(mask_imgs) - set(odd_masks)
307+
cleaned_func_masks = list(cleaned_func_masks)
308+
return cleaned_func_masks, weird_mask_identifiers
309+
310+
311+
def _check_mask_affine(
312+
mask_imgs: List[Union[Path, str, Nifti1Image]], verbose: int = 1
313+
) -> Union[list, None]:
314+
"""Given a list of input mask images, show the most common affine matrix
315+
and subjects with different values.
316+
317+
Parameters
318+
----------
319+
mask_imgs : :obj:`list` of Niimg-like objects
320+
See :ref:`extracting_data`.
321+
3D or 4D EPI image with same affine.
322+
323+
verbose :
324+
Level of verbosity.
325+
326+
Returns
327+
-------
328+
329+
List or None
330+
Index of masks with odd affine matrix. Return None when all masks have
331+
the same affine matrix.
332+
"""
333+
# save all header and affine info in hashable type...
334+
header_info = {"affine": []}
335+
key_to_header = {}
336+
for this_mask in mask_imgs:
337+
img = load_img(this_mask)
338+
affine = img.affine
339+
affine_hashable = str(affine)
340+
header_info["affine"].append(affine_hashable)
341+
if affine_hashable not in key_to_header:
342+
key_to_header[affine_hashable] = affine
343+
344+
if isinstance(mask_imgs[0], Nifti1Image):
345+
mask_imgs = np.arange(len(mask_imgs))
346+
else:
347+
mask_imgs = np.array(mask_imgs)
348+
# get most common values
349+
common_affine = max(
350+
set(header_info["affine"]), key=header_info["affine"].count
351+
)
352+
if verbose > 0:
353+
print(
354+
f"We found {len(set(header_info['affine']))} unique affine "
355+
f"matrices. The most common one is "
356+
f"{key_to_header[common_affine]}"
357+
)
358+
odd_balls = set(header_info["affine"]) - {common_affine}
359+
if not odd_balls:
360+
return None
361+
362+
exclude = []
363+
for ob in odd_balls:
364+
ob_index = [
365+
i for i, aff in enumerate(header_info["affine"]) if aff == ob
366+
]
367+
if verbose > 1:
368+
print(
369+
"The following subjects has a different affine matrix "
370+
f"({key_to_header[ob]}) comparing to the most common value: "
371+
f"{mask_imgs[ob_index]}."
372+
)
373+
exclude += ob_index
374+
if verbose > 0:
375+
print(
376+
f"{len(exclude)} out of {len(mask_imgs)} has "
377+
"different affine matrix. Ignore when creating group mask."
378+
)
379+
return sorted(exclude)

giga_connectome/tests/test_mask.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,55 @@
11
import pytest
22
import numpy as np
3-
from giga_connectome.mask import generate_group_mask
3+
from giga_connectome import mask
44
from nilearn import datasets
5+
from nibabel import Nifti1Image
56

67

78
def test_generate_group_mask():
89
"""Generate group epi grey matter mask and resample atlas."""
910
data = datasets.fetch_development_fmri(n_subjects=3)
1011
imgs = data.func
1112

12-
group_epi_mask = generate_group_mask(imgs)
13+
group_epi_mask = mask.generate_group_mask(imgs)
1314
# match the post processing details: https://osf.io/wjtyq
1415
assert group_epi_mask.shape == (50, 59, 50)
15-
diff_tpl = generate_group_mask(imgs, template="MNI152NLin2009aAsym")
16+
diff_tpl = mask.generate_group_mask(imgs, template="MNI152NLin2009aAsym")
1617
assert diff_tpl.shape == (50, 59, 50)
1718

1819
# test bad inputs
1920
with pytest.raises(
2021
ValueError, match="TemplateFlow does not supply template blah"
2122
):
22-
generate_group_mask(imgs, template="blah")
23+
mask.generate_group_mask(imgs, template="blah")
24+
25+
26+
def test_check_mask_affine():
27+
"""Check odd affine detection."""
28+
29+
img_base = np.zeros([5, 5, 6])
30+
processed_vol = img_base.copy()
31+
processed_vol[2:4, 2:4, 2:4] += 1
32+
processed = Nifti1Image(processed_vol, np.eye(4))
33+
weird = Nifti1Image(processed_vol, np.eye(4) * np.array([1, 1, 1.5, 1]).T)
34+
weird2 = Nifti1Image(processed_vol, np.eye(4) * np.array([1, 1, 1.6, 1]).T)
35+
exclude = mask._check_mask_affine(
36+
[processed, processed, processed, processed, weird, weird, weird2],
37+
verbose=2,
38+
)
39+
assert len(exclude) == 3
40+
assert exclude == [4, 5, 6]
41+
42+
43+
def test_get_consistent_masks():
44+
"""Check odd affine detection."""
45+
mask_imgs = [
46+
f"sub-{i + 1:2d}_task-rest_space-MNI_desc-brain_mask.nii.gz"
47+
for i in range(10)
48+
]
49+
exclude = [1, 2, 5]
50+
(
51+
cleaned_func_masks,
52+
weird_mask_identifiers,
53+
) = mask._get_consistent_masks(mask_imgs, exclude)
54+
assert len(cleaned_func_masks) == 7
55+
assert len(weird_mask_identifiers) == 3

giga_connectome/tests/test_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
@pytest.mark.skipif(
13-
IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions."
13+
IN_GITHUB_ACTIONS, reason="Test data is not set up on Github Actions."
1414
)
1515
def test_get_metadata():
1616
"""Check the function can load from different fMRIPrep dataset."""

giga_connectome/workflow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def workflow(args):
2525
output_dir = args.output_dir
2626
working_dir = args.work_dir
2727
analysis_level = args.analysis_level
28-
28+
2929
subjects = utils.get_subject_lists(args.participant_label, bids_dir)
3030
strategy = get_denoise_strategy_parameters(args.denoise_strategy)
3131
atlas = load_atlas_setting(args.atlas)
@@ -43,7 +43,7 @@ def workflow(args):
4343
# https://github.com/nipreps/fmriprep/blob/689ad26811cfb18771fdb8d7dc208fe24d27e65c/fmriprep/cli/parser.py#L72
4444
fmriprep_bids_layout = BIDSLayout(
4545
root=bids_dir,
46-
database_path=working_dir,
46+
database_path=bids_dir,
4747
validate=False,
4848
derivatives=True,
4949
)

0 commit comments

Comments
 (0)