Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.

Commit 1de1958

Browse files
committed
Merge branch 'dev'
* dev: interface complete Update retroicor.py Update README.md draft some utility functions draft the CLI
2 parents 60ca28d + c70070e commit 1de1958

File tree

10 files changed

+320
-121
lines changed

10 files changed

+320
-121
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
# PyRETROICOR
22
Python implementation of RETROICOR
33

4-
## Dependency
5-
Python 3
6-
numpy >= 1.18
4+
## Dependencies
5+
**[Python 3](https://www.python.org/)**
6+
| Package | Tested version |
7+
|------------------------------------------------------------|----------------|
8+
| [NumPy](http://www.numpy.org/) | 1.18.4 |
9+
| [Nibabel](https://nipy.org/nibabel/) | 3.1.0 |
710

811
## Reference
912
Glover, G. H., Li, T. Q., & Ress, D. (2000). Image‐based method for retrospective correction of physiological motion effects in fMRI: RETROICOR. Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine, 44(1), 162-167.
1013

1114
MATLAB implementation from: https://github.com/mkassinopoulos/PRF_estimation
15+
16+
## Support
17+
Please use [GitHub issues](https://github.com/htwangtw/pyretroicor/issues) for questions, bug reports or feature requests.
18+
19+
## License
20+
This project is licensed under [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause).

pyretroicor/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import pkg_resources
2+
3+
__version__ = pkg_resources.require(
4+
"pyretroicor")[0].version

pyretroicor/__main__.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
Main entry point
3+
"""
4+
from pathlib import Path
5+
import argparse
6+
import json
7+
import gzip
8+
9+
import numpy as np
10+
import nibabel as nb
11+
12+
from pyretroicor.retroicor import retroicor_cardiac, retroicor_respiratory
13+
from pyretroicor.prepro import process_resp, process_cardiac
14+
15+
def main():
16+
parser = argparse.ArgumentParser()
17+
18+
parser.add_argument(
19+
'filename', metavar='path',
20+
help="Path to fMRI nifti files."
21+
)
22+
parser.add_argument(
23+
'physio', metavar='physio',
24+
help="Path to physiology files in BIDS standard. \
25+
Accept cardiac and/or respiratory recording."
26+
)
27+
parser.add_argument(
28+
'output', metavar='output',
29+
help="Path to save calculated derivatives."
30+
)
31+
32+
args = parser.parse_args()
33+
NII_FILE = args.filename
34+
PHYSIO_FILE = args.physio
35+
OUTDIR = args.output
36+
37+
# check all files neesed exist: nifti, physio, accompanied jsons
38+
physio_json = PHYSIO_FILE.split(".tsv.gz")[0] + ".json"
39+
func_json = NII_FILE.split(".nii.gz")[0] + ".json"
40+
with open(func_json) as f:
41+
func_meta = json.load(f)
42+
43+
if not Path(physio_json).is_file():
44+
raise ValueError("Missing json file for physiology data. Is data in BIDS format?")
45+
else:
46+
with open(physio_json) as f:
47+
physio_meta = json.load(f)
48+
49+
sampling_freq = physio_meta["SamplingFrequency"]
50+
start_time = physio_meta["StartTime"]
51+
columns = physio_meta["Columns"]
52+
53+
54+
func = nb.load(NII_FILE) # nibabel should do the test itself
55+
physio = np.loadtxt(PHYSIO_FILE)
56+
# create time
57+
time = np.arange(0, physio.shape[0]) * (1 / sampling_freq) + start_time
58+
if start_time < 0:
59+
time_zero = np.where(time < 0)[0][-1] + 1
60+
time = time[time_zero:]
61+
physio = physio[time_zero:, :]
62+
63+
cardiac = physio[:, columns.index("cardiac")]
64+
respiratory = physio[:, columns.index("respiratory")]
65+
66+
# TR labels
67+
n_vol = func.shape[0]
68+
tr = func_meta["RepetitionTime"]
69+
tr_time = np.arange(1, n_vol + 1) * tr
70+
tr_idx = []
71+
for t in tr_time:
72+
idx = np.where(time < t)[0][-1] + 1
73+
tr_idx.append(idx)
74+
75+
# peak detection and cleaning
76+
r_peak = process_cardiac(cardiac, sampling_freq) # r peak index
77+
r_peak = time[r_peak] # r peak time point
78+
retro_card = retroicor_cardiac(time, r_peak)
79+
80+
# run retroicor
81+
respiratory = process_resp(respiratory, sampling_freq)
82+
retro_resp = retroicor_respiratory(respiratory)
83+
# rextract value per TR
84+
retroicor_regressors = np.hstack((retro_card, retro_resp))[tr_idx, :]
85+
86+
# create output dir
87+
nii_file = Path(NII_FILE)
88+
subject = nii_file.name.split("_")[0].split("sub-")[-1]
89+
filename_root = nii_file.name.split("_bold")[0]
90+
if "ses" in NII_FILE:
91+
session = nii_file.name.split("_")[1].split("ses-")[-1]
92+
filepath = Path(OUTDIR) / "pyretroicor" / f"sub-{subject}" / f"ses-{session}"
93+
out_name = f"{filename_root}_desc-retroicor_regressors.tsv"
94+
else:
95+
filepath = Path(OUTDIR) / "pyretroicor" / f"sub-{subject}"
96+
out_name = f"{filename_root}_desc-retroicor_regressors.tsv"
97+
98+
filepath.mkdir(parents=True, exist_ok=True)
99+
100+
# save results as tsv
101+
header = [f"retroicor_cardiac_{i + 1}" for i in range(6)] \
102+
+ [f"retroicor_respiration_{i + 1}" for i in range(6)]
103+
104+
np.savetxt(filepath / out_name, retroicor_regressors,
105+
delimiter="\t", header="\t".join(header),
106+
comments="")
107+
108+
if __name__ == "__main__":
109+
main()

pyretroicor/prepro.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""physio data preprocessing"""
2+
3+
import numpy as np
4+
5+
def process_cardiac(cardiac, fs):
6+
'''
7+
preprocess cardiac signal and get R peaks times
8+
'''
9+
# clean outliers
10+
cardiac = smooth(cardiac, fs)
11+
outliers = rolling_mad(cardiac, int(0.5 * fs))
12+
time = np.arange(0, cardiac.shape[0])
13+
clean = np.interp(time, time[~outliers], cardiac[~outliers])
14+
# get peaks
15+
peak_idx = detect_peaks(clean, mpd=int(0.5 * fs))
16+
return peak_idx
17+
18+
def zscore(x):
19+
m = np.mean(x)
20+
sd = np.std(x)
21+
return (x - m) / sd
22+
23+
def detect_peaks(x, mpd):
24+
# standardlize signal
25+
x = zscore(x)
26+
dx = np.diff(x)
27+
# find the edges of raises (proximate peaks)
28+
idx = np.where((np.hstack((dx, 0)) <= 0) & (np.hstack((0, dx)) > 0))[0]
29+
if idx[0] == 0: # first and least location must not be peak
30+
idx = idx[1:]
31+
if idx[-1] == (x.size - 1):
32+
idx = idx[:-1]
33+
34+
# remove peak smaller than average signal
35+
idx = idx[x[idx] >= 0]
36+
37+
# remove peaks closer than mpd
38+
idx = idx[np.argsort(x[idx])][::-1] # sort idx by peak height
39+
idel = np.zeros(idx.size, dtype=bool)
40+
for i in range(idx.size):
41+
if not idel[i]:
42+
idel = idel | (idx >= idx[i] - mpd) & (idx <= idx[i] + mpd)
43+
idel[i] = 0 # Keep current peak
44+
# remove the small peaks and sort back the indices by their occurrence
45+
idx = np.sort(idx[~idel])
46+
return idx
47+
48+
49+
def process_resp(resp, fs):
50+
'''
51+
process respiratory data
52+
'''
53+
resp_f = smooth(resp, fs)
54+
resp_der = np.diff(resp_f)
55+
resp_der = np.append(resp_der, resp_der[-1])
56+
time = np.arange(0, resp_der.shape[0])
57+
outliers = rolling_mad(resp_der, int(0.5 * fs))
58+
clean = np.interp(time, time[~outliers], resp_der[~outliers])
59+
return clean
60+
61+
def mad(arr):
62+
""" Median Absolute Deviation: a "Robust" version of standard deviation.
63+
Indices variabililty of the sample.
64+
https://en.wikipedia.org/wiki/Median_absolute_deviation
65+
"""
66+
med = np.median(arr)
67+
return med, np.median(np.abs(arr - med))
68+
69+
def rolling_mad(a, win):
70+
'''
71+
Rolling MAD outlier detection
72+
'''
73+
outliers = []
74+
for i in range(win, len(a)):
75+
cur = a[(i - win):i]
76+
med, cur_mad = mad(cur)
77+
cur_out = cur > (med + cur_mad * 3)
78+
idx = list(np.arange((i - win), i)[cur_out])
79+
outliers += idx
80+
outliers = list(set(outliers))
81+
82+
# turn index into boolean
83+
bool_outliers = np.zeros(a.shape[0], dtype=bool)
84+
bool_outliers[outliers] = True
85+
return bool_outliers
86+
87+
88+
def smooth(a, winsize):
89+
'''
90+
a: NumPy 1-D array containing the data to be smoothed
91+
winsize: smoothing window size needs, which must be odd number,
92+
as in the original MATLAB implementation
93+
'''
94+
winsize = int(winsize)
95+
if winsize % 2 != 1:
96+
winsize += 1
97+
out0 = np.convolve(a, np.ones(winsize, dtype=int),'valid') / winsize
98+
r = np.arange(1, winsize - 1, 2)
99+
start = np.cumsum(a[: winsize - 1])[::2] / r
100+
stop = (np.cumsum(a[: -winsize:-1])[::2] / r)[::-1]
101+
return np.concatenate((start, out0, stop))

pyretroicor/retroicor.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import numpy as np
2+
3+
def retroicor_cardiac(time, r_peak, M=3):
4+
'''
5+
we need documentations
6+
translated from matlab code
7+
8+
r_peak - time of the r peaks
9+
time - tr time stamp?
10+
M - ??????
11+
'''
12+
13+
# the algorithm
14+
num_time = len(time)
15+
cardiac_phase = np.zeros(num_time)
16+
for i, t in enumerate(time):
17+
closest_peak = np.abs(r_peak - t)
18+
peak_idx = np.where(closest_peak == np.amin(closest_peak))[0][0]
19+
min_left = (t - r_peak[peak_idx]) > 0 # check if the peak is the first or last
20+
21+
if peak_idx == 0 and not min_left:
22+
t2 = r_peak[peak_idx]
23+
t1 = t2 - 1
24+
elif peak_idx == (len(r_peak) - 1) and min_left:
25+
t1 = r_peak[peak_idx]
26+
t2 = t1 + 1
27+
elif min_left:
28+
t1 = r_peak[peak_idx]
29+
t2 = r_peak[peak_idx + 1]
30+
else:
31+
t1 = r_peak[peak_idx - 1]
32+
t2 = r_peak[peak_idx]
33+
cardiac_phase[i] = 2 * np.pi * (t - t1) / (t2 - t1) # Eq 2
34+
35+
regr = np.zeros((num_time, M * 2))
36+
for i in range(M):
37+
regr[:,i * 2] = np.cos(i * cardiac_phase)
38+
regr[:,(i * 2) + 1] = np.sin(i * cardiac_phase)
39+
40+
return regr
41+
42+
43+
def retroicor_respiratory(resp, M=3):
44+
'''
45+
need doc
46+
'''
47+
num_time = len(resp)
48+
resp_phase = np.zeros(num_time)
49+
50+
# the algorithm
51+
nb = 500
52+
val, edges = np.histogram(resp, bins=nb)
53+
54+
for i in range(num_time):
55+
v = resp[i]
56+
closest_peak = np.abs(v - edges)
57+
edge = np.where(closest_peak == np.amin(closest_peak))[0][0]
58+
if (edge - v) > 0:
59+
edge = edge - 1
60+
area = np.sum(val[0: edge])
61+
sign_resp = np.sign(resp[i])
62+
if sign_resp == 0:
63+
sign_resp = 1
64+
resp_phase[i] = np.pi * area * sign_resp / num_time
65+
66+
regr = np.zeros((num_time, M * 2))
67+
for i in range(M):
68+
regr[:,i * 2] = np.cos(i * resp_phase)
69+
regr[:,(i * 2) + 1] = np.sin(i * resp_phase)
70+
71+
return regr

pyretroicor/util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""I/O and file path senity check"""

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
nibabel>=2.5
2+
numpy>=1.18

0 commit comments

Comments
 (0)