diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2630bdf90fb8990aa2cb351c377482cbc11923ae..09d15c5fca9ef6fac5b8d8dc67705e816e8c8ed1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,25 @@ This document contains the ``fslpy`` release history in reverse chronological order. +3.3.1 (Thursday 8th October 2020) +--------------------------------- + + +Changed +^^^^^^^ + + +* The :func:`.affine.decompose` and :func:`.affine.compose` functions now + have the ability to return/accept shear components. + + +Fixed +^^^^^ + + +* Fixed a bug in the :func:`.affine.decompose` function which was corrupting + the scale estimates when given an affine containing shears. + 3.3.0 (Tuesday 22nd September 2020) ----------------------------------- diff --git a/fsl/transform/affine.py b/fsl/transform/affine.py index c7fb71f2a0571f8a1318a7e7b84749cfa498a9f2..47bdd4b371d0d1a9a26b19177795c0b4290b74c3 100644 --- a/fsl/transform/affine.py +++ b/fsl/transform/affine.py @@ -120,7 +120,7 @@ def scaleOffsetXform(scales, offsets): return xform -def compose(scales, offsets, rotations, origin=None): +def compose(scales, offsets, rotations, origin=None, shears=None): """Compose a transformation matrix out of the given scales, offsets and axis rotations. @@ -133,6 +133,8 @@ def compose(scales, offsets, rotations, origin=None): :arg origin: Origin of rotation - must be scaled by the ``scales``. If not provided, the rotation origin is ``(0, 0, 0)``. + + :arg shears: Sequence of three shear values """ preRotate = np.eye(4) @@ -154,6 +156,7 @@ def compose(scales, offsets, rotations, origin=None): scale = np.eye(4, dtype=np.float64) offset = np.eye(4, dtype=np.float64) rotate = np.eye(4, dtype=np.float64) + shear = np.eye(4, dtype=np.float64) scale[ 0, 0] = scales[ 0] scale[ 1, 1] = scales[ 1] @@ -164,10 +167,15 @@ def compose(scales, offsets, rotations, origin=None): rotate[:3, :3] = rotations - return concat(offset, postRotate, rotate, preRotate, scale) + if shears is not None: + shear[0, 1] = shears[0] + shear[0, 2] = shears[1] + shear[1, 2] = shears[2] + + return concat(offset, postRotate, rotate, preRotate, scale, shear) -def decompose(xform, angles=True): +def decompose(xform, angles=True, shears=False): """Decomposes the given transformation matrix into separate offsets, scales, and rotations, according to the algorithm described in: @@ -175,8 +183,7 @@ def decompose(xform, angles=True): 320-323 in *Graphics Gems II*, James Arvo (editor), Academic Press, 1991, ISBN: 0120644819. - It is assumed that the given transform has no perspective components. Any - shears in the affine are discarded. + It is assumed that the given transform has no perspective components. :arg xform: A ``(3, 3)`` or ``(4, 4)`` affine transformation matrix. @@ -184,6 +191,8 @@ def decompose(xform, angles=True): as axis-angles, in radians. Otherwise, the rotation matrix is returned. + :arg shears: Defaults to ``False``. If ``True``, shears are returned. + :returns: The following: - A sequence of three scales @@ -191,6 +200,7 @@ def decompose(xform, angles=True): was a ``(3, 3)`` matrix) - A sequence of three rotations, in radians. Or, if ``angles is False``, a rotation matrix. + - If ``shears is True``, a sequence of three shears. """ # The inline comments in the code below are taken verbatim from @@ -199,7 +209,7 @@ def decompose(xform, angles=True): # The next step is to extract the translations. This is trivial; # we find t_x = M_{4,1}, t_y = M_{4,2}, and t_z = M_{4,3}. At this # point we are left with a 3*3 matrix M' = M_{1..3,1..3}. - xform = xform.T + xform = np.array(xform).T if xform.shape == (4, 4): translations = xform[ 3, :3] @@ -214,6 +224,7 @@ def decompose(xform, angles=True): # The process of finding the scaling factors and shear parameters # is interleaved. First, find s_x = |M'_1|. sx = np.sqrt(np.dot(M1, M1)) + M1 = M1 / sx # Then, compute an initial value for the xy shear factor, # s_xy = M'_1 * M'_2. (this is too large by the y scaling factor). @@ -230,7 +241,7 @@ def decompose(xform, angles=True): # The second row is normalized, and s_xy is divided by s_y to # get its final value. M2 = M2 / sy - sxy = sxy / sy + sxy = sxy / sx # The xz and yz shear factors are computed as in the preceding, sxz = np.dot(M1, M3) @@ -245,8 +256,8 @@ def decompose(xform, angles=True): # the third row is normalized, and the xz and yz shear factors are # rescaled. M3 = M3 / sz - sxz = sxz / sz - syz = sxz / sz + sxz = sxz / sx + syz = syz / sy # The resulting matrix now is a pure rotation matrix, except that it # might still include a scale factor of -1. If the determinant of the @@ -266,7 +277,13 @@ def decompose(xform, angles=True): if angles: rotations = rotMatToAxisAngles(R.T) else: rotations = R.T - return np.array([sx, sy, sz]), translations, rotations + retval = [np.array([sx, sy, sz]), translations, rotations] + + if shears: + retval.append(np.array((sxy, sxz, syz))) + + return tuple(retval) + def rotMatToAffine(rotmat, origin=None): diff --git a/tests/test_transform/test_affine.py b/tests/test_transform/test_affine.py index 79a17fba6f9a3895b124b06f279be1c9b9f1b0f9..469c6c13d98950e379a9a042467b0a4f4d517dcb 100644 --- a/tests/test_transform/test_affine.py +++ b/tests/test_transform/test_affine.py @@ -226,8 +226,10 @@ def test_compose_and_decompose(): xform = lines[i * 4: i * 4 + 4] xform = np.genfromtxt(xform) - scales, offsets, rotations = affine.decompose(xform) - result = affine.compose(scales, offsets, rotations) + scales, offsets, rotations, shears = affine.decompose( + xform, shears=True) + + result = affine.compose(scales, offsets, rotations, shears=shears) assert np.all(np.isclose(xform, result, atol=1e-5)) @@ -235,8 +237,10 @@ def test_compose_and_decompose(): # different rotation origin, but we test # explicitly passing the origin for # completeness - scales, offsets, rotations = affine.decompose(xform) - result = affine.compose(scales, offsets, rotations, [0, 0, 0]) + scales, offsets, rotations, shears = affine.decompose( + xform, shears=True) + result = affine.compose( + scales, offsets, rotations, origin=[0, 0, 0], shears=shears) assert np.all(np.isclose(xform, result, atol=1e-5)) diff --git a/tests/test_transform/testdata/test_transform_test_compose.txt b/tests/test_transform/testdata/test_transform_test_compose.txt index e61f31bdd1578e526d9c9c78142b0dd31b5a2381..497fba8606f8d4617a9c8998b741eff2ce2504ef 100644 --- a/tests/test_transform/testdata/test_transform_test_compose.txt +++ b/tests/test_transform/testdata/test_transform_test_compose.txt @@ -23,7 +23,7 @@ 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 -1.0 0.0 -0.0 0.0 0.0 1.0 +0.0 0.0 0.0 1.0 0.5 0.0 0.0 0.0 0.0 2.4 0.0 0.0 @@ -35,22 +35,22 @@ 0.0 0.0 1.6 53.0 0.0 0.0 0.0 1.0 -0.5 0.0 0.0 10.0 +0.5 0.0 0.0 10.0 0.0 0.0 -1.6 -6.7877502441 0.0 2.4 0.0 29.7931518555 -0.0 0.0 0.0 1.0 +0.0 0.0 0.0 1.0 0.0 -2.4 -0.0 30.6160888672 0.0 0.0 -1.6 -6.7877502441 0.5 0.0 0.0 50.5961608887 -0.0 0.0 0.0 1.0 +0.0 0.0 0.0 1.0 -0.0 1.3765834472 1.3106432709 0.0736983719 0.0 -1.9659649063 0.9177222982 14.1062102814 0.5 0.0 0.0 50.5961608887 0.0 0.0 0.0 1.0 - 0.453766452 -0.7004337463 0.4832135801 16.789591468 + 0.453766452 -0.7004337463 0.4832135801 16.789591468 -0.0159784155 1.6056689296 1.1880787388 -16.2943532298 -0.2093817023 -1.640493784 0.9565424959 66.1123321137 0.0 0.0 0.0 1.0 @@ -109,3 +109,8 @@ 0.041017 -0.081580 -6.319922 72.090378 -0.000000 0.381914 -1.365036 -41.159451 0.000000 0.000000 0.000000 1.000000 + +1.1441229 0.833406 3.765340 3 +0.8312539 2.459607 -0.804545 4 +1.4142136 1.554275 1.724796 5 +0.0 0.0 0.0 1