1
2from os.path import splitext
3import re
4import warnings
5import numpy as np
6from nibabel.tmpdirs import InTemporaryDirectory
7
8
9def read_bvals_bvecs(fbvals, fbvecs):
10    """Read b-values and b-vectors from disk.
11
12    Parameters
13    ----------
14    fbvals : str
15       Full path to file with b-values. None to not read bvals.
16    fbvecs : str
17       Full path of file with b-vectors. None to not read bvecs.
18
19    Returns
20    -------
21    bvals : array, (N,) or None
22    bvecs : array, (N, 3) or None
23
24    Notes
25    -----
26    Files can be either '.bvals'/'.bvecs' or '.txt' or '.npy' (containing
27    arrays stored with the appropriate values).
28
29    """
30    # Loop over the provided inputs, reading each one in turn and adding them
31    # to this list:
32    vals = []
33    for this_fname in [fbvals, fbvecs]:
34        # If the input was None or empty string, we don't read anything and
35        # move on:
36        if this_fname is None or not this_fname:
37            vals.append(None)
38            continue
39
40        if not isinstance(this_fname, str):
41            raise ValueError('String with full path to file is required')
42
43        base, ext = splitext(this_fname)
44        if ext in ['.bvals', '.bval', '.bvecs', '.bvec', '.txt',
45                   '.eddy_rotated_bvecs', '']:
46            with open(this_fname, 'r') as f:
47                content = f.read()
48            # We replace coma and tab delimiter by space
49            with InTemporaryDirectory():
50                tmp_fname = "tmp_bvals_bvecs.txt"
51                with open(tmp_fname, 'w') as f:
52                    f.write(re.sub(r'(\t|,)', ' ', content))
53                vals.append(np.squeeze(np.loadtxt(tmp_fname)))
54        elif ext == '.npy':
55            vals.append(np.squeeze(np.load(this_fname)))
56        else:
57            e_s = "File type %s is not recognized" % ext
58            raise ValueError(e_s)
59
60    # Once out of the loop, unpack them:
61    bvals, bvecs = vals[0], vals[1]
62
63    # If bvecs is None, you can just return now w/o making more checks:
64    if bvecs is None:
65        return bvals, bvecs
66
67    if 3 not in bvecs.shape:
68        raise IOError('bvec file should have three rows')
69    if bvecs.ndim != 2:
70        bvecs = bvecs[None, ...]
71        bvals = bvals[None, ...]
72        msg = "Detected only 1 direction on your bvec file. For diffusion "
73        msg += "dataset, it is recommended to have at least 3 directions."
74        msg += "You may have problems during the reconstruction step."
75        warnings.warn(msg)
76    if bvecs.shape[1] != 3 and bvecs.shape[1] > bvecs.shape[0]:
77        bvecs = bvecs.T
78
79    # If bvals is None, you don't need to check that they have the same shape:
80    if bvals is None:
81        return bvals, bvecs
82
83    if len(bvals.shape) > 1:
84        raise IOError('bval file should have one row')
85
86    if bvals.shape[0] != bvecs.shape[0]:
87        raise IOError('b-values and b-vectors shapes do not correspond')
88
89    return bvals, bvecs
90