test_affine.py 18.2 KB
Newer Older
Paul McCarthy's avatar
Paul McCarthy committed
1
2
3
4
5
6
7
#!/usr/bin/env python
#
# test_transform.py -
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#

8
9
10

from __future__ import division

11
import                 random
Paul McCarthy's avatar
Paul McCarthy committed
12
13
14
15
16
import                 glob
import os.path      as op
import itertools    as it
import numpy        as np
import numpy.linalg as npla
17

18
19
import six

20
import pytest
Paul McCarthy's avatar
Paul McCarthy committed
21

Paul McCarthy's avatar
Paul McCarthy committed
22
import fsl.transform.affine  as affine
Paul McCarthy's avatar
Paul McCarthy committed
23

24

25
datadir = op.join(op.dirname(__file__), 'testdata')
26
27
28
29
30


def readlines(filename):
    with open(filename, 'rt') as f:
        lines = f.readlines()
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        lines = [l.strip()         for l in lines]
        lines = [l                 for l in lines if not l.startswith('#')]
        lines = [l                 for l in lines if l != '']

        # numpy.genfromtxt is busted in python 3.
        # Pass it [str, str, ...], and it complains:
        #
        #   TypeError: must be str or None, not bytes
        #
        # Pass it [bytes, bytes, ...], and it works
        # fine.
        if six.PY3:
            lines = [l.encode('ascii') for l in lines]

45
46
47
48
49
50
51
52
    return lines


def test_invert():

    testfile = op.join(datadir, 'test_transform_test_invert.txt')
    testdata = np.loadtxt(testfile)

53
    nmatrices = testdata.shape[0] // 4
54
55
56
57
58

    for i in range(nmatrices):

        x      = testdata[i * 4:i * 4 + 4, 0:4]
        invx   = testdata[i * 4:i * 4 + 4, 4:8]
Paul McCarthy's avatar
Paul McCarthy committed
59
        result = affine.invert(x)
60
61
62
63
64

        assert np.all(np.isclose(invx, result))


def test_concat():
65

66
67
68
69
    testfile = op.join(datadir, 'test_transform_test_concat.txt')
    lines    = readlines(testfile)


70
    ntests = len(lines) // 4
71
72
73
74
75
    tests  = []

    for i in range(ntests):
        ilines = lines[i * 4:i * 4 + 4]
        data    = np.genfromtxt(ilines)
76
        ninputs = data.shape[1] // 4 - 1
77
78
79
80
81
82
83
84
85
86
87
88

        inputs  = []

        for j in range(ninputs):
            inputs.append(data[:, j * 4:j * 4 + 4])

        output = data[:, -4:]

        tests.append((inputs, output))

    for inputs, expected in tests:

Paul McCarthy's avatar
Paul McCarthy committed
89
        result = affine.concat(*inputs)
90
91
92
93

        assert np.all(np.isclose(result, expected))


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
def test_veclength(seed):

    def l(v):
        v = np.array(v, copy=False).reshape((-1, 3))
        x = v[:, 0]
        y = v[:, 1]
        z = v[:, 2]
        l = x * x + y * y + z * z
        return np.sqrt(l)

    vectors = -100 + 200 * np.random.random((200, 3))

    for v in vectors:

        vtype = random.choice((list, tuple, np.array))
        v     = vtype(v)

Paul McCarthy's avatar
Paul McCarthy committed
111
        assert np.isclose(affine.veclength(v), l(v))
112
113

    # Multiple vectors in parallel
Paul McCarthy's avatar
Paul McCarthy committed
114
    result   = affine.veclength(vectors)
115
116
117
118
119
120
121
122
123
    expected = l(vectors)
    assert np.all(np.isclose(result, expected))


def test_normalise(seed):

    vectors = -100 + 200 * np.random.random((200, 3))

    def parallel(v1, v2):
Paul McCarthy's avatar
Paul McCarthy committed
124
125
        v1 = v1 / affine.veclength(v1)
        v2 = v2 / affine.veclength(v2)
126
127
128
129
130
131
132

        return np.isclose(np.dot(v1, v2), 1)

    for v in vectors:

        vtype = random.choice((list, tuple, np.array))
        v     = vtype(v)
Paul McCarthy's avatar
Paul McCarthy committed
133
134
        vn    = affine.normalise(v)
        vl    = affine.veclength(vn)
135
136
137
138
139
140

        assert np.isclose(vl, 1.0)
        assert parallel(v, vn)

    # normalise should also be able
    # to do multiple vectors at once
Paul McCarthy's avatar
Paul McCarthy committed
141
142
    results = affine.normalise(vectors)
    lengths = affine.veclength(results)
143
144
145
146
147
148
149
150
151
152
153
154
    pars    = np.zeros(200)
    for i in range(200):

        v = vectors[i]
        r = results[i]

        pars[i] = parallel(v, r)

    assert np.all(np.isclose(lengths, 1))
    assert np.all(pars)


Paul McCarthy's avatar
Paul McCarthy committed
155
156
def test_scaleOffsetXform():

157
    # Test numerically
158
159
    testfile = op.join(datadir, 'test_transform_test_scaleoffsetxform.txt')
    lines    = readlines(testfile)
160
    ntests   = len(lines) // 5
Paul McCarthy's avatar
Paul McCarthy committed
161

162
    for i in range(ntests):
163

164
        lineoff         = i * 5
165
        scales, offsets = lines[lineoff].decode('ascii').split(',')
Paul McCarthy's avatar
Paul McCarthy committed
166

167
168
        scales  = [float(s) for s in scales .split()]
        offsets = [float(o) for o in offsets.split()]
Paul McCarthy's avatar
Paul McCarthy committed
169

170
171
172
        expected = lines[lineoff + 1: lineoff + 5]
        expected = [[float(v) for v in l.split()] for l in expected]
        expected = np.array(expected)
Paul McCarthy's avatar
Paul McCarthy committed
173

Paul McCarthy's avatar
Paul McCarthy committed
174
        result = affine.scaleOffsetXform(scales, offsets)
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

        assert np.all(np.isclose(result, expected))

    # Test that different input types work:
    #   - scalars
    #   - lists/tuples of length <= 3
    #   - numpy arrays
    a = np.array
    stests = [
        (5,            [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ([5],          [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ((5,),         [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        (a([5]),       [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ([5, 6],       [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ((5, 6),       [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        (a([5, 6]),    [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ([5, 6, 7],    [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 0, 0, 0, 0, 1]),
        ((5, 6, 7),    [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 0, 0, 0, 0, 1]),
        (a([5, 6, 7]), [5, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 0, 0, 0, 0, 1]),
    ]
    otests = [
        (5,            [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ([5],          [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ((5,),         [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        (a([5]),       [1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
        ([5, 6],       [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 0, 0, 0, 0, 1]),
        ((5, 6),       [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 0, 0, 0, 0, 1]),
        (a([5, 6]),    [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 0, 0, 0, 0, 1]),
        ([5, 6, 7],    [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 7, 0, 0, 0, 1]),
        ((5, 6, 7),    [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 7, 0, 0, 0, 1]),
        (a([5, 6, 7]), [1, 0, 0, 5, 0, 1, 0, 6, 0, 0, 1, 7, 0, 0, 0, 1]),
    ]

    for (scale, expected) in stests:
        expected = np.array(expected).reshape(4, 4)
Paul McCarthy's avatar
Paul McCarthy committed
210
        result   = affine.scaleOffsetXform(scale, 0)
211
212
213
        assert np.all(np.isclose(result, expected))
    for (offset, expected) in otests:
        expected = np.array(expected).reshape(4, 4)
Paul McCarthy's avatar
Paul McCarthy committed
214
        result   = affine.scaleOffsetXform(1, offset)
215
216
217
        assert np.all(np.isclose(result, expected))


218
def test_compose_and_decompose():
Paul McCarthy's avatar
Paul McCarthy committed
219

220
221
    testfile = op.join(datadir, 'test_transform_test_compose.txt')
    lines    = readlines(testfile)
222
    ntests   = len(lines) // 4
Paul McCarthy's avatar
Paul McCarthy committed
223

224
225
226
227
    for i in range(ntests):

        xform                      = lines[i * 4: i * 4 + 4]
        xform                      = np.genfromtxt(xform)
228

229
230
        scales, offsets, rotations, shears = affine.decompose(
            xform, shears=True)
Paul McCarthy's avatar
Paul McCarthy committed
231

232
        result = affine.compose(scales, offsets, rotations, shears=shears)
233

234
        assert np.all(np.isclose(xform, result, atol=1e-5))
235
236
237
238
239

        # The decompose function does not support a
        # different rotation origin, but we test
        # explicitly passing the origin for
        # completeness
Paul McCarthy's avatar
Paul McCarthy committed
240
241
242
243
        scales, offsets, rotations, shears = affine.decompose(
            xform, shears=True)
        result = affine.compose(
            scales, offsets, rotations, origin=[0, 0, 0], shears=shears)
Paul McCarthy's avatar
Paul McCarthy committed
244

245
246
        assert np.all(np.isclose(xform, result, atol=1e-5))

247
248
    # compose should also accept a rotation matrix
    rots = [np.pi / 5, np.pi / 4, np.pi / 3]
Paul McCarthy's avatar
Paul McCarthy committed
249
250
    rmat  = affine.axisAnglesToRotMat(*rots)
    xform = affine.compose([1, 1, 1], [0, 0, 0], rmat)
251

252
253
254
    # And the angles flag should cause decompose
    # to return the rotation matrix, instead of
    # the axis angls
Paul McCarthy's avatar
Paul McCarthy committed
255
256
257
    sc,   of,   rot   = affine.decompose(xform)
    scat, ofat, rotat = affine.decompose(xform, angles=True)
    scaf, ofaf, rotaf = affine.decompose(xform, angles=False)
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273

    sc,   of,   rot   = np.array(sc),   np.array(of),   np.array(rot)
    scat, ofat, rotat = np.array(scat), np.array(ofat), np.array(rotat)
    scaf, ofaf, rotaf = np.array(scaf), np.array(ofaf), np.array(rotaf)

    assert np.all(np.isclose(sc,    [1, 1, 1]))
    assert np.all(np.isclose(of,    [0, 0, 0]))
    assert np.all(np.isclose(scat,  [1, 1, 1]))
    assert np.all(np.isclose(ofat,  [0, 0, 0]))
    assert np.all(np.isclose(scaf,  [1, 1, 1]))
    assert np.all(np.isclose(ofaf,  [0, 0, 0]))

    assert np.all(np.isclose(rot,   rots))
    assert np.all(np.isclose(rotat, rots))
    assert np.all(np.isclose(rotaf, rmat))

274
275
    # decompose should accept a 3x3
    # affine, and return translations of 0
Paul McCarthy's avatar
Paul McCarthy committed
276
277
    affine.decompose(xform[:3, :3])
    sc,   of,   rot   = affine.decompose(xform[:3, :3])
278
279
280
281
282
283
284
    sc,   of,   rot   = np.array(sc), np.array(of), np.array(rot)
    assert np.all(np.isclose(sc,    [1, 1, 1]))
    assert np.all(np.isclose(of,    [0, 0, 0]))
    assert np.all(np.isclose(rot,   rots))



285
286
287
288
289
290
291
292
293
294
295
296

def test_rotMatToAxisAngles(seed):

    pi  = np.pi
    pi2 = pi / 2

    for i in range(100):

        rots = [-pi  + 2 * pi  * np.random.random(),
                -pi2 + 2 * pi2 * np.random.random(),
                -pi  + 2 * pi  * np.random.random()]

Paul McCarthy's avatar
Paul McCarthy committed
297
298
        rmat    = affine.axisAnglesToRotMat(*rots)
        gotrots = affine.rotMatToAxisAngles(rmat)
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316

        assert np.all(np.isclose(rots, gotrots))


def test_rotMatToAffine(seed):

    pi  = np.pi
    pi2 = pi / 2

    for i in range(100):

        rots = [-pi  + 2 * pi  * np.random.random(),
                -pi2 + 2 * pi2 * np.random.random(),
                -pi  + 2 * pi  * np.random.random()]

        if np.random.random() < 0.5: origin = None
        else:                        origin = np.random.random(3)

Paul McCarthy's avatar
Paul McCarthy committed
317
318
319
        rmat   = affine.axisAnglesToRotMat(*rots)
        mataff = affine.rotMatToAffine(rmat, origin)
        rotaff = affine.rotMatToAffine(rots, origin)
320
321
322
323
324
325

        exp         = np.eye(4)
        exp[:3, :3] = rmat
        exp[:3,  3] = origin

        assert np.all(np.isclose(mataff, rotaff))
326

327
328
329
330

def test_axisBounds():
    testfile = op.join(datadir, 'test_transform_test_axisBounds.txt')
    lines    = readlines(testfile)
331
    ntests   = len(lines) // 6
332
333
334

    def readTest(testnum):
        tlines   = lines[testnum * 6: testnum * 6 + 6]
335
        params   = [p.strip() for p in tlines[0].decode('ascii').split(',')]
336
337
338
339
340
341
342
343
        shape    = [int(s) for s in params[0].split()]
        origin   = params[1]
        boundary = None if params[2] == 'None' else params[2]
        xform    = np.genfromtxt(tlines[1:5])
        expected = np.genfromtxt([tlines[5]])
        expected = (expected[:3], expected[3:])

        return shape, origin, boundary, xform, expected
344

345
346
347
348
349
350
351
352
353
354
355
    allAxes  = list(it.chain(
        range(0, 1, 2),
        it.permutations((0, 1, 2), 1),
        it.permutations((0, 1, 2), 2),
        it.permutations((0, 1, 2), 3)))

    for i in range(ntests):

        shape, origin, boundary, xform, expected = readTest(i)

        for axes in allAxes:
Paul McCarthy's avatar
Paul McCarthy committed
356
            result = affine.axisBounds(shape,
357
358
359
360
361
362
363
364
365
366
                                          xform,
                                          axes=axes,
                                          origin=origin,
                                          boundary=boundary)

            exp = expected[0][(axes,)], expected[1][(axes,)]

            assert np.all(np.isclose(exp, result))


367
    # Do some parameter checks on
368
369
370
371
372
373
374
375
376
377
    # the first test in the file
    # which has origin == centre
    for i in range(ntests):
        shape, origin, boundary, xform, expected = readTest(i)
        if origin == 'centre':
            break

    # US-spelling
    assert np.all(np.isclose(
        expected,
Paul McCarthy's avatar
Paul McCarthy committed
378
        affine.axisBounds(
379
380
381
382
            shape, xform, origin='center', boundary=boundary)))

    # Bad origin/boundary values
    with pytest.raises(ValueError):
Paul McCarthy's avatar
Paul McCarthy committed
383
        affine.axisBounds(shape, xform, origin='Blag', boundary=boundary)
384
    with pytest.raises(ValueError):
Paul McCarthy's avatar
Paul McCarthy committed
385
        affine.axisBounds(shape, xform, origin=origin, boundary='Blufu')
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
412


def test_transform():

    def is_orthogonal(xform):
        """Returns ``True`` if the given xform consists
        solely of translations and scales.
        """

        mask = np.array([[1, 0, 0, 1],
                         [0, 1, 0, 1],
                         [0, 0, 1, 1],
                         [0, 0, 0, 1]], dtype=np.bool)

        return np.all((xform != 0) == mask)

    coordfile   = op.join(datadir, 'test_transform_test_transform_coords.txt')
    testcoords  = np.loadtxt(coordfile)

    testpattern = op.join(datadir, 'test_transform_test_transform_??.txt')
    testfiles   = glob.glob(testpattern)

    allAxes  = list(it.chain(
        range(0, 1, 2),
        it.permutations((0, 1, 2), 1),
        it.permutations((0, 1, 2), 2),
        it.permutations((0, 1, 2), 3)))
413

414
    for i, testfile in enumerate(testfiles):
415

416
417
418
        lines    = readlines(testfile)
        xform    = np.genfromtxt(lines[:4])
        expected = np.genfromtxt(lines[ 4:])
Paul McCarthy's avatar
Paul McCarthy committed
419
        result   = affine.transform(testcoords, xform)
420

421
422
423
424
        assert np.all(np.isclose(expected, result))

        if not is_orthogonal(xform):
            continue
425

426
427
428
        for axes in allAxes:
            atestcoords = testcoords[:, axes]
            aexpected   = expected[  :, axes]
Paul McCarthy's avatar
Paul McCarthy committed
429
            aresult     = affine.transform(atestcoords, xform, axes=axes)
430
431
432
433
434
435
436
437
438
439

            assert np.all(np.isclose(aexpected, aresult))

    # Pass in some bad data, expect an error
    xform     = np.eye(4)
    badxform  = np.eye(3)
    badcoords = np.random.randint(1, 10, (10, 4))
    coords    = badcoords[:, :3]

    with pytest.raises(IndexError):
Paul McCarthy's avatar
Paul McCarthy committed
440
        affine.transform(coords, badxform)
441
442

    with pytest.raises(ValueError):
Paul McCarthy's avatar
Paul McCarthy committed
443
        affine.transform(badcoords, xform)
444

445
    with pytest.raises(ValueError):
Paul McCarthy's avatar
Paul McCarthy committed
446
        affine.transform(badcoords.reshape(5, 2, 4), xform)
447
448

    with pytest.raises(ValueError):
Paul McCarthy's avatar
Paul McCarthy committed
449
        affine.transform(badcoords.reshape(5, 2, 4), xform, axes=1)
450
451

    with pytest.raises(ValueError):
Paul McCarthy's avatar
Paul McCarthy committed
452
        affine.transform(badcoords[:, (1, 2, 3)], xform, axes=[1, 2])
453
454


Paul McCarthy's avatar
Paul McCarthy committed
455
456
457
458
def test_transform_vector(seed):

    # Some transform with a
    # translation component
Paul McCarthy's avatar
Paul McCarthy committed
459
    xform = affine.compose([1, 2, 3],
Paul McCarthy's avatar
Paul McCarthy committed
460
461
462
463
464
465
466
467
468
469
                              [5, 10, 15],
                              [np.pi / 2, np.pi / 2, 0])

    vecs = np.random.random((20, 3))

    for v in vecs:

        vecExpected = np.dot(xform, list(v) + [0])[:3]
        ptExpected  = np.dot(xform, list(v) + [1])[:3]

Paul McCarthy's avatar
Paul McCarthy committed
470
471
472
        vecResult   = affine.transform(v, xform,         vector=True)
        vec33Result = affine.transform(v, xform[:3, :3], vector=True)
        ptResult    = affine.transform(v, xform,         vector=False)
Paul McCarthy's avatar
Paul McCarthy committed
473
474

        assert np.all(np.isclose(vecExpected, vecResult))
Paul McCarthy's avatar
Paul McCarthy committed
475
        assert np.all(np.isclose(vecExpected, vec33Result))
Paul McCarthy's avatar
Paul McCarthy committed
476
477
478
        assert np.all(np.isclose(ptExpected,  ptResult))


479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def test_transformNormal(seed):

    normals = -100 + 200 * np.random.random((50, 3))

    def tn(n, xform):

        xform = npla.inv(xform[:3, :3]).T
        return np.dot(xform, n)

    for n in normals:

        scales    = -10    + np.random.random(3) * 10
        offsets   = -100   + np.random.random(3) * 200
        rotations = -np.pi + np.random.random(3) * 2 * np.pi
        origin    = -100   + np.random.random(3) * 200

Paul McCarthy's avatar
Paul McCarthy committed
495
        xform = affine.compose(scales,
496
497
498
499
500
                                  offsets,
                                  rotations,
                                  origin)

        expected = tn(n, xform)
Paul McCarthy's avatar
Paul McCarthy committed
501
        result   = affine.transformNormal(n, xform)
502
503
504
505

        assert np.all(np.isclose(expected, result))


Paul McCarthy's avatar
Paul McCarthy committed
506
507
508
def test_rmsdev():

    t1 = np.eye(4)
Paul McCarthy's avatar
Paul McCarthy committed
509
    t2 = affine.scaleOffsetXform([1, 1, 1], [2, 0, 0])
Paul McCarthy's avatar
Paul McCarthy committed
510

Paul McCarthy's avatar
Paul McCarthy committed
511
512
513
    assert np.isclose(affine.rmsdev(t1, t2), 2)
    assert np.isclose(affine.rmsdev(t1, t2, R=2), 2)
    assert np.isclose(affine.rmsdev(t1, t2, R=2, xc=(1, 1, 1)), 2)
Paul McCarthy's avatar
Paul McCarthy committed
514
515
516
517
518
519

    t1       = np.eye(3)
    lastdist = 0

    for i in range(1, 11):
        rot    = np.pi * i / 10.0
Paul McCarthy's avatar
Paul McCarthy committed
520
521
        t2     = affine.axisAnglesToRotMat(rot, 0, 0)
        result = affine.rmsdev(t1, t2)
Paul McCarthy's avatar
Paul McCarthy committed
522
523
524
525
526
527
528

        assert result > lastdist

        lastdist = result

    for i in range(11, 20):
        rot    = np.pi * i / 10.0
Paul McCarthy's avatar
Paul McCarthy committed
529
530
        t2     = affine.axisAnglesToRotMat(rot, 0, 0)
        result = affine.rmsdev(t1, t2)
Paul McCarthy's avatar
Paul McCarthy committed
531
532
533
534

        assert result < lastdist

        lastdist = result
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579


def test_rescale():

    with pytest.raises(ValueError):
        affine.rescale((5, 5), (10, 10, 10))

    assert np.all(affine.rescale((5, 5),       (5, 5))       == np.eye(3))
    assert np.all(affine.rescale((5, 5, 5),    (5, 5, 5))    == np.eye(4))
    assert np.all(affine.rescale((5, 5, 5, 5), (5, 5, 5, 5)) == np.eye(5))

    # (old shape, new shape, origin, expect)
    tests = [
        ((5, 5), (10, 10), 'centre', np.array([[0.5, 0,    0],
                                               [0,   0.5,  0],
                                               [0,   0,    1]])),
        ((5, 5), (10, 10), 'corner', np.array([[0.5, 0,   -0.25],
                                               [0,   0.5, -0.25],
                                               [0,   0,    1]])),
        ((5, 5, 5), (10, 10, 10), 'centre', np.array([[0.5, 0,    0,   0],
                                                      [0,   0.5,  0,   0],
                                                      [0,   0,    0.5, 0],
                                                      [0,   0,    0,   1]])),
        ((5, 5, 5), (10, 10, 10), 'corner', np.array([[0.5, 0,    0,   -0.25],
                                                      [0,   0.5,  0,   -0.25],
                                                      [0,   0,    0.5, -0.25],
                                                      [0,   0,    0,    1]])),
        ((5, 5, 5, 5), (10, 10, 10, 10), 'centre', np.array([[0.5, 0,    0,   0,   0],
                                                             [0,   0.5,  0,   0,   0],
                                                             [0,   0,    0.5, 0,   0],
                                                             [0,   0,    0,   0.5, 0],
                                                             [0,   0,    0,   0,   1]])),
        ((5, 5, 5, 5), (10, 10, 10, 10), 'corner', np.array([[0.5, 0,    0,   0,   -0.25],
                                                             [0,   0.5,  0,   0,   -0.25],
                                                             [0,   0,    0.5, 0,   -0.25],
                                                             [0,   0,    0,   0.5, -0.25],
                                                             [0,   0,    0,   0,   1]])),
    ]

    for oldshape, newshape, origin, expect in tests:

        got = affine.rescale(oldshape, newshape, origin)
        assert np.all(np.isclose(got, expect))
        got = affine.rescale(newshape, oldshape, origin)
        assert np.all(np.isclose(got, affine.invert(expect)))