From c60cda177848414e9d2324ddedf635b991aceb15 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Sun, 12 Jun 2016 20:26:24 +0100 Subject: [PATCH] My first set of unit tests for fslpy - tests functions in the imagewrapper module. --- test/test_imagewrapper.py | 320 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 test/test_imagewrapper.py diff --git a/test/test_imagewrapper.py b/test/test_imagewrapper.py new file mode 100644 index 000000000..4c38ff297 --- /dev/null +++ b/test/test_imagewrapper.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +# +# test_imagewrapper.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> + + + +import collections +import random +import itertools as it +import numpy as np + + +import fsl.data.image as image +import fsl.data.imagewrapper as imagewrap + + +def setup_module(): + pass + +def teardown_module(): + pass + + +def random_coverage(shape): + + ndims = len(shape) - 1 + nvols = shape[-1] + + print '{}D (shape: {}, {} vectors/slices/volumes)'.format(ndims, shape, nvols) + + # Generate a random coverage. + # We use the same coverage for + # each vector/slice/volume, so + # are not fully testing the function. + coverage = np.zeros((2, ndims, nvols), dtype=np.uint32) + + for dim in range(ndims): + dsize = shape[dim] + + # Random low/high indices for each dimension. + low = np.random.randint(0, dsize) + + # We have to make sure that the coverage is not + # complete, as some of the tests will fail if + # the coverage is complete. + if low == 0: high = np.random.randint(low + 1, dsize) + else: high = np.random.randint(low + 1, dsize + 1) + + coverage[0, dim, :] = low + coverage[1, dim, :] = high + + return coverage + + +def random_slices(coverage, shape, mode): + + ndims = len(shape) - 1 + nvols = shape[-1] + + slices = np.zeros((2, len(shape))) + + for dim, size in enumerate(shape): + + # Volumes + if dim == ndims: + lowCover = np.random.randint(0, nvols) + highCover = np.random.randint(lowCover + 1, nvols + 1) + + slices[:, dim] = lowCover, highCover + continue + + # Assuming that coverage is same for each volume + lowCover = coverage[0, dim, 0] + highCover = coverage[1, dim, 0] + + # Generate some slices that will + # be contained within the coverage + if mode == 'in': + lowSlice = np.random.randint(lowCover, highCover) + highSlice = np.random.randint(lowSlice + 1, highCover + 1) + + # Generate some indices which will + # randomly overlap with the coverage + # (if it is possible to do so) + elif mode == 'overlap': + + if highCover == size: lowSlice = np.random.randint(0, lowCover) + else: lowSlice = np.random.randint(0, highCover) + + if lowSlice < lowCover: highSlice = np.random.randint(lowCover + 1, size + 1) + else: highSlice = np.random.randint(highCover + 1, size + 1) + + elif mode == 'out': + + # The coverage is full, so we can't + # generate an outside range + if lowCover == 0 and highCover == size: + lowSlice = np.random.randint(lowCover, highCover) + highSlice = np.random.randint(lowSlice + 1, highCover + 1) + + # If low coverage is 0, the slice + # must be above the coverage + elif lowCover == 0: + lowSlice = np.random.randint(highCover, size) + highSlice = np.random.randint(lowSlice + 1, size + 1) + + # If high coverage is size, the + # slice must be below the coverage + elif highCover == size: + lowSlice = np.random.randint(0, lowCover) + highSlice = np.random.randint(lowSlice + 1, lowCover + 1) + + # Otherwise the slice could be + # below or above the coverage + else: + lowSlice = random.choice((np.random.randint(0, lowCover), + np.random.randint(highCover, size))) + + if lowSlice < lowCover: highSlice = np.random.randint(lowSlice + 1, lowCover + 1) + else: highSlice = np.random.randint(lowSlice + 1, size + 1) + + + slices[0, dim] = lowSlice + slices[1, dim] = highSlice + + slices = [tuple(map(int, pair)) for pair in slices.T] + return slices + + +def test_sliceObjToSliceTuple(): + + func = imagewrap.sliceObjToSliceTuple + shape = (10, 10, 10) + + + assert func( 2, shape) == ((2, 3), (0, 10), (0, 10)) + assert func( slice(None), shape) == ((0, 10), (0, 10), (0, 10)) + assert func((slice(None), slice(None), slice(None)), shape) == ((0, 10), (0, 10), (0, 10)) + assert func((9, slice(None), slice(None)), shape) == ((9, 10), (0, 10), (0, 10)) + assert func((slice(None), 5, slice(None)), shape) == ((0, 10), (5, 6), (0, 10)) + assert func((slice(None), slice(None), 3), shape) == ((0, 10), (0, 10), (3, 4)) + assert func((slice(4, 6), slice(None), slice(None)), shape) == ((4, 6), (0, 10), (0, 10)) + assert func((8, slice(1, 10), slice(None)), shape) == ((8, 9), (1, 10), (0, 10)) + + + +def test_sliceTupleToSliceObj(): + + func = imagewrap.sliceTupleToSliceObj + shape = (10, 10, 10) + + for x1, y1, z1 in it.product(*[range(d - 1) for d in shape]): + + for x2, y2, z2 in it.product(*[range(s + 1, d) for s, d in zip((x1, y1, z1), shape)]): + + slices = [[x1, x2], [y1, y2], [z1, z2]] + sliceobj = (slice(x1, x2, 1), slice(y1, y2, 1), slice(z1, z2, 1)) + + assert func(slices) == sliceobj + + +def test_adjustCoverage(): + + # TODO Randomise + + # Each test is a tuple of (coverage, expansion, expectedResult) + tests = [([[3, 5], [2, 6]], [[6, 7], [8, 10]], [[3, 7], [2, 10]]), + ([[0, 0], [0, 0]], [[1, 2], [3, 5]], [[0, 2], [0, 5]]), + ([[2, 3], [0, 6]], [[1, 5], [4, 10]], [[1, 5], [0, 10]]), + ([[0, 1], [0, 1]], [[0, 7], [19, 25], [0, 1]], [[0, 7], [0, 25]]), + ] + + for coverage, expansion, result in tests: + + result = np.array(result) .T + coverage = np.array(coverage).T + + assert np.all(imagewrap.adjustCoverage(coverage, expansion) == result) + + +def test_sliceCovered(): + + # A bunch of random coverages + for i in range(500): + + # 2D, 3D or 4D? + # ndims is the number of dimensions + # in one vector/slice/volume + ndims = random.choice((2, 3, 4)) - 1 + + # Shape of one vector[2D]/slice[3D]/volume[4D] + shape = np.random.randint(5, 100, size=ndims + 1) + + # Number of vectors/slices/volumes + nvols = shape[-1] + + coverage = random_coverage(shape) + + # Generate some slices that should + # be contained within the coverage + for j in range(500): + slices = random_slices(coverage, shape, 'in') + assert imagewrap.sliceCovered(slices, coverage, shape) + + # Generate some slices that should + # overlap with the coverage + for j in range(500): + slices = random_slices(coverage, shape, 'overlap') + assert not imagewrap.sliceCovered(slices, coverage, shape) + + # Generate some slices that should + # be outside of the coverage + for j in range(500): + slices = random_slices(coverage, shape, 'out') + assert not imagewrap.sliceCovered(slices, coverage, shape) + + + +# The sum of the coverage ranges + the +# expansion ranges should be equal to +# the coverage, expanded to include the +# original slices (or the expansions +# - should be equivalent). Note that +# if imagewrapper.adjustCoverage is +# broken, this validation will also be +# broken. +def _test_expansion(coverage, slices, volumes, expansions): + ndims = coverage.shape[1] + + print + print 'Slice: "{}"'.format(" ".join(["{:2d} {:2d}".format(l, h) for l, h in slices])) + + # Bin the expansions by volume + expsByVol = collections.defaultdict(list) + for vol, exp in zip(volumes, expansions): + print ' {:3d}: "{}"'.format(vol, " ".join(["{:2d} {:2d}".format(l, h) for l, h in exp])) + expsByVol[vol].append(exp) + + for vol, exps in expsByVol.items(): + + # Figure out what the adjusted + # coverage should look like (assumes + # that adjustCoverage is working). + oldCoverage = coverage[..., vol] + newCoverage = imagewrap.adjustCoverage(oldCoverage, slices) + + nc = newCoverage + + dimranges = [] + for d in range(ndims): + dimranges.append(np.arange(nc[0, d], nc[1, d])) + + points = it.product(*dimranges) + + for point in points: + + # Is this point in the old coverage? + covered = True + for dim in range(ndims): + covLow, covHigh = oldCoverage[:, dim] + + if point[dim] < covLow or point[dim] > covHigh: + covered = False + break + + if covered: + break + + # Is this point in any of the expansions + covered = [False] * len(exps) + for i, exp in enumerate(exps): + + covered[i] = True + + for dim in range(ndims): + + expLow, expHigh = exp[dim] + if point[dim] < expLow or point[dim] > expHigh: + covered[i] = False + break + + if not any(covered): + raise AssertionError(point) + + +def test_calcSliceExpansion(): + + for i in range(500): + + ndims = 3 # random.choice((2, 3, 4)) - 1 + shape = np.random.randint(5, 100, size=ndims + 1) + coverage = random_coverage(shape) + + cov = [(lo, hi) for lo, hi in coverage[:, :, 0].T] + + print 'Shape: {}'.format(shape) + print 'Coverage: {}'.format(cov) + + print + print '-- In --' + for j in range(250): + slices = random_slices(coverage, shape, 'in') + vols, exps = imagewrap.calcExpansion(slices, coverage) + _test_expansion(coverage, slices, vols, exps) + + print + print '-- Overlap --' + for j in range(250): + slices = random_slices(coverage, shape, 'overlap') + vols, exps = imagewrap.calcExpansion(slices, coverage) + _test_expansion(coverage, slices, vols, exps) + + print + print '-- Out --' + for j in range(250): + slices = random_slices(coverage, shape, 'out') + vols, exps = imagewrap.calcExpansion(slices, coverage) + _test_expansion(coverage, slices, vols, exps) -- GitLab