__init__.py 11.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
#
# __init__.py - fslpy tests
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""Unit tests for ``fslpy``. """


10
import              os
11
import              sys
12
13
import              glob
import              shutil
14
import              fnmatch
15
import              logging
16
import              tempfile
17
import              contextlib
18
19
20
21
import itertools as it
import os.path   as op
import numpy     as np
import nibabel   as nib
22

23
from io import StringIO
24

25
from unittest import mock
26

27
28
29
import fsl.data.image                     as fslimage
from   fsl.utils.tempdir import              tempdir
from   fsl.utils.platform import platform as fslplatform
30

31

32
33
34
logging.getLogger().setLevel(logging.WARNING)


35
@contextlib.contextmanager
36
def mockFSLDIR(**kwargs):
37

38
39
    oldfsldir    = fslplatform.fsldir
    oldfsldevdir = fslplatform.fsldevdir
40

41
42
43
    try:
        with tempdir() as td:
            fsldir = op.join(td, 'fsl')
44
45
            bindir = op.join(fsldir, 'bin')
            os.makedirs(bindir)
46
47
48
49
50
            for subdir, files in kwargs.items():
                subdir = op.join(fsldir, subdir)
                if not op.isdir(subdir):
                    os.makedirs(subdir)
                for fname in files:
51
52
53
54
                    fname = op.join(subdir, fname)
                    touch(fname)
                    if subdir == bindir:
                        os.chmod(fname, 0o755)
55
            fslplatform.fsldir = fsldir
56
            fslplatform.fsldevdir = None
57
58
59
60
61

            path = op.pathsep.join((bindir, os.environ['PATH']))

            with mock.patch.dict(os.environ, {'PATH': path}):
                yield fsldir
62
    finally:
63
64
        fslplatform.fsldir    = oldfsldir
        fslplatform.fsldevdir = oldfsldevdir
65
66


67
68
69
70
71
def touch(fname):
    with open(fname, 'wt') as f:
        pass


72
class CaptureStdout:
73
74
75
76
77
78
79
80
    """Context manager which captures stdout and stderr. """

    def __init__(self):
        self.reset()

    def reset(self):
        self.__mock_stdout = StringIO('')
        self.__mock_stderr = StringIO('')
Paul McCarthy's avatar
Paul McCarthy committed
81
82
83
        self.__mock_stdout.mode = 'w'
        self.__mock_stderr.mode = 'w'
        return self
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

    def __enter__(self):
        self.__real_stdout = sys.stdout
        self.__real_stderr = sys.stderr

        sys.stdout = self.__mock_stdout
        sys.stderr = self.__mock_stderr


    def __exit__(self, *args, **kwargs):
        sys.stdout = self.__real_stdout
        sys.stderr = self.__real_stderr

        if args[0] is not None:
            print('Error')
            print('stdout:')
            print(self.stdout)
            print('stderr:')
            print(self.stderr)

        return False

    @property
    def stdout(self):
        self.__mock_stdout.seek(0)
        return self.__mock_stdout.read()

    @property
    def stderr(self):
        self.__mock_stderr.seek(0)
        return self.__mock_stderr.read()



118
def testdir(contents=None, suffix=""):
119
120
    """Returnsa context manager which creates, changes to, and returns a
    temporary directory, and then deletes it on exit.
121
    """
122
123
124

    if contents is not None:
        contents = [op.join(*c.split('/')) for c in contents]
125

126
    class ctx(object):
127
128
129

        def __init__(self, contents):
            self.contents = contents
130

131
        def __enter__(self):
132

133
            self.testdir = tempfile.mkdtemp(suffix=suffix)
134
135
136
            self.prevdir = os.getcwd()

            os.chdir(self.testdir)
137
138
139
140
141

            if self.contents is not None:
                contents = [op.join(self.testdir, c) for c in self.contents]
                make_dummy_files(contents)

142
143
144
            return self.testdir

        def __exit__(self, *a, **kwa):
145
            os.chdir(self.prevdir)
146
147
            shutil.rmtree(self.testdir)

148
149
150
151
152
153
    return ctx(contents)

def make_dummy_files(paths):
    """Creates dummy files for all of the given paths. """
    for p in paths:
        make_dummy_file(p)
154
155


156
def make_dummy_file(path, contents=None):
157
    """Makes a plain text file. Returns a hash of the file contents. """
158
    dirname = op.dirname(path)
159

160
161
162
163
164
    if not op.exists(dirname):
        os.makedirs(dirname)

    if contents is None:
        contents = '{}\n'.format(op.basename(path))
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
    with open(path, 'wt') as f:
        f.write(contents)

    return hash(contents)


def looks_like_image(path):
    """Returns True if the given path looks like a NIFTI/ANALYZE image.
    """
    return any((path.endswith('.nii'),
                path.endswith('.nii.gz'),
                path.endswith('.img'),
                path.endswith('.hdr'),
                path.endswith('.img.gz'),
                path.endswith('.hdr.gz')))


def make_dummy_image_file(path):
    """Makes some plain files with NIFTI/ANALYZE file extensions.
    """

    if   path.endswith('.nii'):    paths = [path]
    elif path.endswith('.nii.gz'): paths = [path]
    elif path.endswith('.img'):    paths = [path, path[:-4] + '.hdr']
    elif path.endswith('.hdr'):    paths = [path, path[:-4] + '.img']
    elif path.endswith('.img.gz'): paths = [path, path[:-7] + '.hdr.gz']
    elif path.endswith('.hdr.gz'): paths = [path, path[:-7] + '.img.gz']
    else: raise RuntimeError()

    for path in paths:
        make_dummy_file(path)


198
def cleardir(dir, pat=None):
199
200
201
202
    """Deletes everything in the given directory, but not the directory
    itself.
    """
    for f in os.listdir(dir):
203
204
205
206

        if pat is not None and not fnmatch.fnmatch(f, pat):
            continue

207
        f = op.join(dir, f)
208

209
210
211
212
        if   op.isfile(f): os.remove(f)
        elif op.isdir(f):  shutil.rmtree(f)


213
214
215
216
217
def checkdir(dir, *expfiles):
    for f in expfiles:
        assert op.exists(op.join(dir, f))


218
def random_voxels(shape, nvoxels=1):
219
220
    randVoxels = np.vstack(
        [np.random.randint(0, s, nvoxels) for s in shape[:3]]).T
221
222
223
224
225
226
227

    if nvoxels == 1:
        return randVoxels[0]
    else:
        return randVoxels


228
229
230
231
232
233
234
235
236
237
238
239
def make_random_image(filename=None,
                      dims=(10, 10, 10),
                      xform=None,
                      imgtype=1,
                      pixdims=None,
                      dtype=np.float32):
    """Convenience function which makes an image containing random data.
    Saves and returns the nibabel object.

    imgtype == 0: ANALYZE
    imgtype == 1: NIFTI1
    imgtype == 2: NIFTI2
240
241
    """

242
243
244
245
246
247
248
249
250
251
252
253
254
255
    if   imgtype == 0: hdr = nib.AnalyzeHeader()
    elif imgtype == 1: hdr = nib.Nifti1Header()
    elif imgtype == 2: hdr = nib.Nifti2Header()

    if pixdims is None:
        pixdims = [1] * len(dims)

    pixdims = pixdims[:len(dims)]
    zooms   = [abs(p) for p in pixdims]

    hdr.set_data_dtype(dtype)
    hdr.set_data_shape(dims)
    hdr.set_zooms(zooms)

256
257
    if xform is None:
        xform = np.eye(4)
258
259
        for i, p in enumerate(pixdims[:3]):
            xform[i, i] = p
260

261
    data  = np.array(np.random.random(dims) * 100, dtype=dtype)
262

263
264
265
266
267
268
269
270
271
272
273
    if   imgtype == 0: img = nib.AnalyzeImage(data, xform, hdr)
    elif imgtype == 1: img = nib.Nifti1Image( data, xform, hdr)
    elif imgtype == 2: img = nib.Nifti2Image( data, xform, hdr)

    if filename is not None:

        if op.splitext(filename)[1] == '':
            if imgtype == 0: filename = '{}.img'.format(filename)
            else:            filename = '{}.nii'.format(filename)

        nib.save(img, filename)
274

275
    return img
276

277

278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def make_mock_feat_analysis(featdir,
                            testdir,
                            shape4D,
                            xform=None,
                            indata=True,
                            voxEVs=True,
                            pes=True,
                            copes=True,
                            zstats=True,
                            residuals=True,
                            clustMasks=True):

    if xform is None:
        xform = np.eye(4)

    timepoints = shape4D[ 3]
    shape      = shape4D[:3]

    src     = featdir
    dest    = op.join(testdir, op.basename(featdir))
298
299
    featdir = dest

300
301
302
303
    shutil.copytree(src, dest)

    if indata:
        filtfunc = op.join(featdir, 'filtered_func_data.nii.gz')
304
305
        img = make_random_image(filtfunc, shape4D, xform)
        del img
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321

    # and some dummy voxelwise EV files
    if voxEVs:
        voxFiles = list(it.chain(
            glob.glob(op.join(featdir, 'designVoxelwiseEV*nii.gz')),
            glob.glob(op.join(featdir, 'InputConfoundEV*nii.gz'))))

        for i, vf in enumerate(voxFiles):

            # Each voxel contains range(i, i + timepoints),
            # offset by the flattened voxel index
            data = np.meshgrid(*[range(s) for s in shape], indexing='ij')
            data = np.ravel_multi_index(data, shape)
            data = data.reshape(list(shape) + [1]).repeat(timepoints, axis=3)
            data[..., :] += range(i, i + timepoints)

322
323
324
325
            img = nib.nifti1.Nifti1Image(data, xform)

            nib.save(img, vf)
            del img
326
327
328
329
330
331
332
333
334
335
336
337

    otherFiles  = []
    otherShapes = []

    if pes:
        files = glob.glob(op.join(featdir, 'stats', 'pe*nii.gz'))
        otherFiles .extend(files)
        otherShapes.extend([shape] * len(files))

    if copes:
        files = glob.glob(op.join(featdir, 'stats', 'cope*nii.gz'))
        otherFiles .extend(files)
338
        otherShapes.extend([shape] * len(files))
339
340
341
342

    if zstats:
        files = glob.glob(op.join(featdir, 'stats', 'zstat*nii.gz'))
        otherFiles .extend(files)
343
344
        otherShapes.extend([shape] * len(files))

345
346
347
    if residuals:
        files = glob.glob(op.join(featdir, 'stats', 'res4d.nii.gz'))
        otherFiles .extend(files)
348
349
        otherShapes.extend([shape4D])

350
351
352
353
354
355
    if clustMasks:
        files = glob.glob(op.join(featdir, 'cluster_mask*nii.gz'))
        otherFiles .extend(files)
        otherShapes.extend([shape] * len(files))

    for f, s in zip(otherFiles, otherShapes):
356
357
        img = make_random_image(f, s, xform)
        del img
358
359

    return featdir
360
361


Paul McCarthy's avatar
Paul McCarthy committed
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def make_mock_melodic_analysis(basedir, shape4D, ntimepoints, xform=None):

    if xform is None:
        xform = np.eye(4)

    ncomps = shape4D[-1]
    halftime = int(np.floor(ntimepoints / 2))

    os.makedirs(basedir)

    make_random_image(op.join(basedir, 'melodic_IC.nii.gz'),
                      dims=shape4D,
                      xform=xform)

    mix   = np.random.randint(1, 255, (ntimepoints, ncomps))
    ftmix = np.random.randint(1, 255, (halftime,    ncomps))

    np.savetxt(op.join(basedir, 'melodic_mix'),   mix)
    np.savetxt(op.join(basedir, 'melodic_FTmix'), ftmix)


def make_mock_dtifit_analysis(basedir, shape3D, basename='dti', xform=None, tensor=False):

    if xform is None:
        xform = np.eye(4)

    os.makedirs(basedir)

    shape4D = tuple(shape3D) + (3,)

    def mk(ident, shp):
        make_random_image(
            op.join(basedir, '{}_{}.nii.gz'.format(basename, ident)),
            shp,
            xform)

    mk('V1', shape4D)
    mk('V2', shape4D)
    mk('V3', shape4D)
    mk('L1', shape3D)
    mk('L2', shape3D)
    mk('L3', shape3D)
    mk('S0', shape3D)
    mk('MD', shape3D)
    mk('MO', shape3D)
    mk('FA', shape3D)

    if tensor:
        mk('tensor', tuple(shape3D) + (6,))

412

Paul McCarthy's avatar
Paul McCarthy committed
413
def make_random_mask(filename, shape, xform, premask=None, minones=1):
414
415
416
417
    """Make a random binary mask image. """

    mask = np.zeros(shape, dtype=np.uint8)

Paul McCarthy's avatar
Paul McCarthy committed
418
    numones = np.random.randint(minones, np.prod(shape) / 100)
419
420
421
422
423
424
425
426
    xc      = np.random.randint(0, shape[0], numones)
    yc      = np.random.randint(0, shape[1], numones)
    zc      = np.random.randint(0, shape[2], numones)

    mask[xc, yc, zc] = 1

    if premask is not None:
        mask[premask == 0] = 0
427
428
429
430
431

    img = fslimage.Image(mask, xform=xform)
    img.save(filename)

    return img