def test_mi_gradient_dense(): # Test the gradient of mutual information h = 1e-5 for ttype in factors: transform = regtransforms[ttype] dim = ttype[1] if dim == 2: nslices = 1 warp_method = vf.warp_2d_affine else: nslices = 45 warp_method = vf.warp_3d_affine # Get data (pair of images related to each other by an known transform) factor = factors[ttype] static, moving, static_g2w, moving_g2w, smask, mmask, M = \ setup_random_transform(transform, factor, nslices, 5.0) smask = None mmask = None # Prepare a MattesBase instance # The computation of the metric is done in 3 steps: # 1.Compute the joint distribution # 2.Compute the gradient of the joint distribution # 3.Compute the metric's value and gradient using results from 1 and 2 metric = MattesBase(32) metric.setup(static, moving, smask, mmask) # 1. Update the joint distribution metric.update_pdfs_dense(static.astype(np.float64), moving.astype(np.float64)) # 2. Update the joint distribution gradient (the derivative of each # histogram cell w.r.t. the transform parameters). This requires # among other things, the spatial gradient of the moving image. theta = transform.get_identity_parameters().copy() grid_to_space = np.eye(dim + 1) spacing = np.ones(dim, dtype=np.float64) shape = np.array(static.shape, dtype=np.int32) mgrad, inside = vf.gradient(moving.astype(np.float32), moving_g2w, spacing, shape, grid_to_space) metric.update_gradient_dense(theta, transform, static.astype(np.float64), moving.astype(np.float64), grid_to_space, mgrad, smask, mmask) # 3. Update the metric (in this case, the Mutual Information) and its # gradient, which is computed from the joint density and its gradient metric.update_mi_metric(update_gradient=True) # Now we can extract the value and gradient of the metric # This is the gradient according to the implementation under test val0 = metric.metric_val actual = np.copy(metric.metric_grad) # Compute the gradient using finite-diferences n = transform.get_number_of_parameters() expected = np.empty_like(actual) for i in range(n): dtheta = theta.copy() dtheta[i] += h M = transform.param_to_matrix(dtheta) shape = np.array(static.shape, dtype=np.int32) warped = np.array(warp_method(moving.astype(np.float32), shape, M)) metric.update_pdfs_dense(static.astype(np.float64), warped.astype(np.float64)) metric.update_mi_metric(update_gradient=False) val1 = metric.metric_val expected[i] = (val1 - val0) / h dp = expected.dot(actual) enorm = np.linalg.norm(expected) anorm = np.linalg.norm(actual) nprod = dp / (enorm * anorm) assert(nprod >= 0.999)
def test_joint_pdf_gradients_dense(): # Compare the analytical and numerical (finite differences) gradient of # the joint distribution (i.e. derivatives of each histogram cell) w.r.t. # the transform parameters. Since the histograms are discrete partitions # of the image intensities, the finite difference approximation is # normally not very close to the analytical derivatives. Other sources of # error are the interpolation used when transforming the images and the # boundary intensities introduced when interpolating outside of the image # (i.e. some "zeros" are introduced at the boundary which affect the # numerical derivatives but is not taken into account by the analytical # derivatives). Thus, we need to relax the verification. Instead of # looking for the analytical and numerical gradients to be very close to # each other, we will verify that they approximately point in the same # direction by testing if the angle they form is close to zero. h = 1e-4 # Make sure dictionary entries are processed in the same order regardless # of the platform. Otherwise any random numbers drawn within the loop # would make the test non-deterministic even if we fix the seed before # the loop. Right now, this test does not draw any samples, but we still # sort the entries to prevent future related failures. for ttype in sorted(factors): dim = ttype[1] if dim == 2: nslices = 1 transform_method = vf.transform_2d_affine else: nslices = 45 transform_method = vf.transform_3d_affine transform = regtransforms[ttype] factor = factors[ttype] theta = transform.get_identity_parameters() static, moving, static_g2w, moving_g2w, smask, mmask, M = \ setup_random_transform(transform, factor, nslices, 5.0) parzen_hist = ParzenJointHistogram(32) parzen_hist.setup(static, moving, smask, mmask) # Compute the gradient at theta with the implementation under test M = transform.param_to_matrix(theta) shape = np.array(static.shape, dtype=np.int32) moved = transform_method(moving.astype(np.float32), shape, M) moved = np.array(moved) parzen_hist.update_pdfs_dense(static.astype(np.float64), moved.astype(np.float64)) # Get the joint distribution evaluated at theta J0 = np.copy(parzen_hist.joint) grid_to_space = np.eye(dim + 1) spacing = np.ones(dim, dtype=np.float64) mgrad, inside = vf.gradient(moving.astype(np.float32), moving_g2w, spacing, shape, grid_to_space) id = transform.get_identity_parameters() parzen_hist.update_gradient_dense(id, transform, static.astype(np.float64), moved.astype(np.float64), grid_to_space, mgrad, smask, mmask) actual = np.copy(parzen_hist.joint_grad) # Now we have the gradient of the joint distribution w.r.t. the # transform parameters # Compute the gradient using finite-diferences n = transform.get_number_of_parameters() expected = np.empty_like(actual) for i in range(n): dtheta = theta.copy() dtheta[i] += h # Update the joint distribution with the transformed moving image M = transform.param_to_matrix(dtheta) shape = np.array(static.shape, dtype=np.int32) moved = transform_method(moving.astype(np.float32), shape, M) moved = np.array(moved) parzen_hist.update_pdfs_dense(static.astype(np.float64), moved.astype(np.float64)) J1 = np.copy(parzen_hist.joint) expected[..., i] = (J1 - J0) / h # Dot product and norms of gradients of each joint histogram cell # i.e. the derivatives of each cell w.r.t. all parameters P = (expected * actual).sum(2) enorms = np.sqrt((expected**2).sum(2)) anorms = np.sqrt((actual**2).sum(2)) prodnorms = enorms * anorms # Cosine of angle between the expected and actual gradients. # Exclude very small gradients P[prodnorms > 1e-6] /= (prodnorms[prodnorms > 1e-6]) P[prodnorms <= 1e-6] = 0 # Verify that a large proportion of the gradients point almost in # the same direction. Disregard very small gradients mean_cosine = P[P != 0].mean() std_cosine = P[P != 0].std() assert (mean_cosine > 0.9) assert (std_cosine < 0.25)
def test_joint_pdf_gradients_dense(): # Compare the analytical and numerical (finite differences) gradient of the # joint distribution (i.e. derivatives of each histogram cell) w.r.t. the # transform parameters. Since the histograms are discrete partitions of the # image intensities, the finite difference approximation is normally not # very close to the analytical derivatives. Other sources of error are the # interpolation used when warping the images and the boundary intensities # introduced when interpolating outside of the image (i.e. some "zeros" are # introduced at the boundary which affect the numerical derivatives but is # not taken into account by the analytical derivatives). Thus, we need to # relax the verification. Instead of looking for the analytical and # numerical gradients to be very close to each other, we will verify that # they approximately point in the same direction by testing if the angle # they form is close to zero. h = 1e-4 for ttype in factors: dim = ttype[1] if dim == 2: nslices = 1 warp_method = vf.warp_2d_affine else: nslices = 45 warp_method = vf.warp_3d_affine transform = regtransforms[ttype] factor = factors[ttype] theta = transform.get_identity_parameters() static, moving, static_g2w, moving_g2w, smask, mmask, M = \ setup_random_transform(transform, factor, nslices, 5.0) metric = MattesBase(32) metric.setup(static, moving, smask, mmask) # Compute the gradient at theta with the implementation under test M = transform.param_to_matrix(theta) shape = np.array(static.shape, dtype=np.int32) warped = warp_method(moving.astype(np.float32), shape, M) warped = np.array(warped) metric.update_pdfs_dense(static.astype(np.float64), warped.astype(np.float64)) # Get the joint distribution evaluated at theta J0 = np.copy(metric.joint) grid_to_space = np.eye(dim + 1) spacing = np.ones(dim, dtype=np.float64) mgrad, inside = vf.gradient(moving.astype(np.float32), moving_g2w, spacing, shape, grid_to_space) id = transform.get_identity_parameters() metric.update_gradient_dense(id, transform, static.astype(np.float64), warped.astype(np.float64), grid_to_space, mgrad, smask, mmask) actual = np.copy(metric.joint_grad) # Now we have the gradient of the joint distribution w.r.t. the # transform parameters # Compute the gradient using finite-diferences n = transform.get_number_of_parameters() expected = np.empty_like(actual) for i in range(n): dtheta = theta.copy() dtheta[i] += h # Update the joint distribution with the warped moving image M = transform.param_to_matrix(dtheta) shape = np.array(static.shape, dtype=np.int32) warped = warp_method(moving.astype(np.float32), shape, M) warped = np.array(warped) metric.update_pdfs_dense(static.astype(np.float64), warped.astype(np.float64)) J1 = np.copy(metric.joint) expected[..., i] = (J1 - J0) / h # Dot product and norms of gradients of each joint histogram cell # i.e. the derivatives of each cell w.r.t. all parameters P = (expected * actual).sum(2) enorms = np.sqrt((expected ** 2).sum(2)) anorms = np.sqrt((actual ** 2).sum(2)) prodnorms = enorms * anorms # Cosine of angle between the expected and actual gradients. # Exclude very small gradients P[prodnorms > 1e-6] /= (prodnorms[prodnorms > 1e-6]) P[prodnorms <= 1e-6] = 0 # Verify that a large proportion of the gradients point almost in # the same direction. Disregard very small gradients mean_cosine = P[P != 0].mean() std_cosine = P[P != 0].std() assert(mean_cosine > 0.9) assert(std_cosine < 0.25)
def test_gradient_3d(): np.random.seed(3921116) shape = (25, 32, 15) # Create grid coordinates x_0 = np.asarray(range(shape[0])) x_1 = np.asarray(range(shape[1])) x_2 = np.asarray(range(shape[2])) X = np.zeros(shape + (4, ), dtype=np.float64) O = np.ones(shape) X[..., 0] = x_0[:, None, None] * O X[..., 1] = x_1[None, :, None] * O X[..., 2] = x_2[None, None, :] * O X[..., 3] = 1 transform = regtransforms[("RIGID", 3)] theta = np.array([0.1, 0.05, 0.12, -12.0, -15.5, -7.2]) T = transform.param_to_matrix(theta) TX = X.dot(T.T) # Eval an arbitrary (known) function at TX # f(x, y, z) = ax^2 + by^2 + cz^2 + dxy + exz + fyz # df/dx = 2ax + dy + ez # df/dy = 2by + dx + fz # df/dz = 2cz + ex + fy a, b, c = 2e-3, 3e-3, 1e-3 d, e, f = 1e-3, 2e-3, 3e-3 img = (a * TX[..., 0]**2 + b * TX[..., 1]**2 + c * TX[..., 2]**2 + d * TX[..., 0] * TX[..., 1] + e * TX[..., 0] * TX[..., 2] + f * TX[..., 1] * TX[..., 2]) img = img.astype(floating) # Test sparse gradient: choose some sample points (in space) sample = sample_domain_regular(100, np.array(shape, dtype=np.int32), T) sample = np.array(sample) # Compute the analytical gradient at all points expected = np.empty((sample.shape[0], 3), dtype=floating) expected[..., 0] = (2 * a * sample[:, 0] + d * sample[:, 1] + e * sample[:, 2]) expected[..., 1] = (2 * b * sample[:, 1] + d * sample[:, 0] + f * sample[:, 2]) expected[..., 2] = (2 * c * sample[:, 2] + e * sample[:, 0] + f * sample[:, 1]) # Get the numerical gradient with the implementation under test sp_to_grid = np.linalg.inv(T) img_spacing = np.ones(3) actual, inside = vfu.sparse_gradient(img, sp_to_grid, img_spacing, sample) img_d = cupy.asarray(img) img_spacing_d = cupy.asarray(img_spacing) sp_to_grid_d = cupy.asarray(sp_to_grid) sample_d = cupy.asarray(sample) actual_gpu, inside_gpu = sparse_gradient(img_d, sp_to_grid_d, img_spacing_d, sample_d.T) atol = rtol = 1e-5 cupy.testing.assert_allclose( actual * inside[..., np.newaxis], actual_gpu * inside_gpu[..., np.newaxis], atol=atol, rtol=rtol, ) cupy.testing.assert_array_equal(inside, inside_gpu) # TODO: test invalid inputs # # Verify exception is raised when passing invalid affine or spacings # invalid_affine = np.eye(3) # invalid_spacings = np.ones(2) # assert_raises(ValueError, vfu.sparse_gradient, img, invalid_affine, # img_spacing, sample) # assert_raises(ValueError, vfu.sparse_gradient, img, sp_to_grid, # invalid_spacings, sample) # Test dense gradient # Compute the analytical gradient at all points expected = np.empty(shape + (3, ), dtype=floating) expected[..., 0] = 2 * a * TX[..., 0] + d * TX[..., 1] + e * TX[..., 2] expected[..., 1] = 2 * b * TX[..., 1] + d * TX[..., 0] + f * TX[..., 2] expected[..., 2] = 2 * c * TX[..., 2] + e * TX[..., 0] + f * TX[..., 1] # Get the numerical gradient with the implementation under test sp_to_grid = np.linalg.inv(T) img_spacing = np.ones(3) actual, inside = vfu.gradient(img, sp_to_grid, img_spacing, shape, T) sp_to_grid_d = cupy.asarray(sp_to_grid) img_spacing_d = cupy.asarray(img_spacing) T_d = cupy.asarray(T) actual_gpu, inside_gpu = gradient(img_d, sp_to_grid_d, img_spacing_d, shape, T_d) atol = rtol = 1e-5 cupy.testing.assert_allclose( actual * inside[..., np.newaxis], actual_gpu * inside_gpu[..., np.newaxis], atol=atol, rtol=rtol, ) cupy.testing.assert_array_equal(inside, inside_gpu)
def test_gradient_2d(): np.random.seed(3921116) sh = (25, 32) # Create grid coordinates x_0 = np.arange(sh[0]) x_1 = np.arange(sh[1]) X = np.empty(sh + (3, ), dtype=np.float64) O = np.ones(sh) X[..., 0] = x_0[:, None] * O X[..., 1] = x_1[None, :] * O X[..., 2] = 1 transform = regtransforms[("RIGID", 2)] theta = np.array([0.1, 5.0, 2.5]) T = transform.param_to_matrix(theta) TX = X.dot(T.T) # Eval an arbitrary (known) function at TX # f(x, y) = ax^2 + bxy + cy^{2} # df/dx = 2ax + by # df/dy = 2cy + bx a = 2e-3 b = 5e-3 c = 7e-3 img = (a * TX[..., 0]**2 + b * TX[..., 0] * TX[..., 1] + c * TX[..., 1]**2) img = img.astype(floating) # img is an image sampled at X with grid-to-space transform T # Test sparse gradient: choose some sample points (in space) sample = sample_domain_regular(20, np.array(sh, dtype=np.int32), T) sample = np.array(sample) # Compute the analytical gradient at all points expected = np.empty((sample.shape[0], 2), dtype=floating) expected[..., 0] = 2 * a * sample[:, 0] + b * sample[:, 1] expected[..., 1] = 2 * c * sample[:, 1] + b * sample[:, 0] # Get the numerical gradient with the implementation under test sp_to_grid = np.linalg.inv(T) img_spacing = np.ones(2) img_d = cupy.asarray(img) img_spacing_d = cupy.asarray(img_spacing) sp_to_grid_d = cupy.asarray(sp_to_grid) sample_d = cupy.asarray(sample) actual, inside = vfu.sparse_gradient(img, sp_to_grid, img_spacing, sample) actual_gpu, inside_gpu = sparse_gradient(img_d, sp_to_grid_d, img_spacing_d, sample_d.T) atol = rtol = 1e-5 cupy.testing.assert_allclose( actual * inside[..., np.newaxis], actual_gpu * inside_gpu[..., np.newaxis], atol=atol, rtol=rtol, ) cupy.testing.assert_array_equal(inside, inside_gpu) # TODO: verify exceptions # # Verify exception is raised when passing invalid affine or spacings # invalid_affine = np.eye(2) # invalid_spacings = np.ones(1) # assert_raises(ValueError, vfu.sparse_gradient, img, invalid_affine, # img_spacing, sample) # assert_raises(ValueError, vfu.sparse_gradient, img, sp_to_grid, # invalid_spacings, sample) # Test dense gradient # Compute the analytical gradient at all points expected = np.empty(sh + (2, ), dtype=floating) expected[..., 0] = 2 * a * TX[..., 0] + b * TX[..., 1] expected[..., 1] = 2 * c * TX[..., 1] + b * TX[..., 0] # Get the numerical gradient with the implementation under test sp_to_grid = np.linalg.inv(T) img_spacing = np.ones(2) actual, inside = vfu.gradient(img, sp_to_grid, img_spacing, sh, T) sp_to_grid_d = cupy.asarray(sp_to_grid) img_spacing_d = cupy.asarray(img_spacing) T_d = cupy.asarray(T) actual_gpu, inside_gpu = gradient(img_d, sp_to_grid_d, img_spacing_d, sh, T_d) atol = rtol = 1e-5 cupy.testing.assert_allclose( actual * inside[..., np.newaxis], actual_gpu * inside_gpu[..., np.newaxis], atol=atol, rtol=rtol, ) cupy.testing.assert_array_equal(inside, inside_gpu)
static = np.array(static).astype(np.float64) moving = np.array(moving).astype(np.float64) import numpy.linalg as npl moving_world2grid = npl.inv(moving_grid2world) from dipy.align.imwarp import get_direction_and_spacings dim = len(static.shape) _, moving_spacing = get_direction_and_spacings(moving_grid2world, dim) from dipy.align.vector_fields import gradient mgrad, _ = gradient(moving, moving_world2grid, moving_spacing, static.shape, static_grid2world) np.save('sl_aff_par_grad.npy', mgrad) # out = np.asarray(out, dtype=np.float64) # out = 255 * (out - out.min()) / (out.max() - out.min()) # slice_indices = np.array(out.shape) // 2 # axial = np.asarray(out[:, :, slice_indices[2]]).astype(np.uint8) # coronal = np.asarray(out[:, slice_indices[1], :]).astype(np.uint8) # sagittal = np.asarray(out[slice_indices[0], :, :]).astype(np.uint8) # import matplotlib.pyplot as plt # from xvfbwrapper import Xvfb # with Xvfb() as xvfb: # fig, ax = plt.subplots(3, 3)
def test_mi_gradient_dense(): # Test the gradient of mutual information h = 1e-5 for ttype in factors: transform = regtransforms[ttype] dim = ttype[1] if dim == 2: nslices = 1 warp_method = vf.warp_2d_affine else: nslices = 45 warp_method = vf.warp_3d_affine # Get data (pair of images related to each other by an known transform) factor = factors[ttype] static, moving, static_g2w, moving_g2w, smask, mmask, M = \ setup_random_transform(transform, factor, nslices, 5.0) smask = None mmask = None # Prepare a MattesBase instance # The computation of the metric is done in 3 steps: # 1.Compute the joint distribution # 2.Compute the gradient of the joint distribution # 3.Compute the metric's value and gradient using results from 1 and 2 metric = MattesBase(32) metric.setup(static, moving, smask, mmask) # 1. Update the joint distribution metric.update_pdfs_dense(static.astype(np.float64), moving.astype(np.float64)) # 2. Update the joint distribution gradient (the derivative of each # histogram cell w.r.t. the transform parameters). This requires # among other things, the spatial gradient of the moving image. theta = transform.get_identity_parameters().copy() grid_to_space = np.eye(dim + 1) spacing = np.ones(dim, dtype=np.float64) shape = np.array(static.shape, dtype=np.int32) mgrad, inside = vf.gradient(moving.astype(np.float32), moving_g2w, spacing, shape, grid_to_space) metric.update_gradient_dense(theta, transform, static.astype(np.float64), moving.astype(np.float64), grid_to_space, mgrad, smask, mmask) # 3. Update the metric (in this case, the Mutual Information) and its # gradient, which is computed from the joint density and its gradient metric.update_mi_metric(update_gradient=True) # Now we can extract the value and gradient of the metric # This is the gradient according to the implementation under test val0 = metric.metric_val actual = np.copy(metric.metric_grad) # Compute the gradient using finite-diferences n = transform.get_number_of_parameters() expected = np.empty_like(actual) for i in range(n): dtheta = theta.copy() dtheta[i] += h M = transform.param_to_matrix(dtheta) shape = np.array(static.shape, dtype=np.int32) warped = np.array(warp_method(moving.astype(np.float32), shape, M)) metric.update_pdfs_dense(static.astype(np.float64), warped.astype(np.float64)) metric.update_mi_metric(update_gradient=False) val1 = metric.metric_val expected[i] = (val1 - val0) / h dp = expected.dot(actual) enorm = np.linalg.norm(expected) anorm = np.linalg.norm(actual) nprod = dp / (enorm * anorm) assert (nprod >= 0.999)