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 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_mattes_densities(): # Test the computation of the joint intensity distribution # using a dense and a sparse set of values seed = 1246592 nbins = 32 nr = 30 nc = 35 ns = 20 nvals = 50 for dim in [2, 3]: if dim == 2: shape = (nr, nc) static, moving = create_random_image_pair(shape, nvals, seed) else: shape = (ns, nr, nc) static, moving = create_random_image_pair(shape, nvals, seed) # Initialize mbase = MattesBase(nbins) mbase.setup(static, moving) # Get distributions computed by dense sampling mbase.update_pdfs_dense(static, moving) actual_joint_dense = mbase.joint actual_mmarginal_dense = mbase.mmarginal actual_smarginal_dense = mbase.smarginal # Get distributions computed by sparse sampling sval = static.reshape(-1) mval = moving.reshape(-1) mbase.update_pdfs_sparse(sval, mval) actual_joint_sparse = mbase.joint actual_mmarginal_sparse = mbase.mmarginal actual_smarginal_sparse = mbase.smarginal # Compute the expected joint distribution with dense sampling expected_joint_dense = np.zeros(shape=(nbins, nbins)) for index in ndindex(shape): sv = mbase.bin_normalize_static(static[index]) mv = mbase.bin_normalize_moving(moving[index]) sbin = mbase.bin_index(sv) # The spline is centered at mv, will evaluate for all row spline_arg = np.array([i - mv for i in range(nbins)]) contribution = cubic_spline(spline_arg) expected_joint_dense[sbin, :] += contribution # Compute the expected joint distribution with sparse sampling expected_joint_sparse = np.zeros(shape=(nbins, nbins)) for index in range(sval.shape[0]): sv = mbase.bin_normalize_static(sval[index]) mv = mbase.bin_normalize_moving(mval[index]) sbin = mbase.bin_index(sv) # The spline is centered at mv, will evaluate for all row spline_arg = np.array([i - mv for i in range(nbins)]) contribution = cubic_spline(spline_arg) expected_joint_sparse[sbin, :] += contribution # Verify joint distributions expected_joint_dense /= expected_joint_dense.sum() expected_joint_sparse /= expected_joint_sparse.sum() assert_array_almost_equal(actual_joint_dense, expected_joint_dense) assert_array_almost_equal(actual_joint_sparse, expected_joint_sparse) # Verify moving marginals expected_mmarginal_dense = expected_joint_dense.sum(0) expected_mmarginal_dense /= expected_mmarginal_dense.sum() expected_mmarginal_sparse = expected_joint_sparse.sum(0) expected_mmarginal_sparse /= expected_mmarginal_sparse.sum() assert_array_almost_equal(actual_mmarginal_dense, expected_mmarginal_dense) assert_array_almost_equal(actual_mmarginal_sparse, expected_mmarginal_sparse) # Verify static marginals expected_smarginal_dense = expected_joint_dense.sum(1) expected_smarginal_dense /= expected_smarginal_dense.sum() expected_smarginal_sparse = expected_joint_sparse.sum(1) expected_smarginal_sparse /= expected_smarginal_sparse.sum() assert_array_almost_equal(actual_smarginal_dense, expected_smarginal_dense) assert_array_almost_equal(actual_smarginal_sparse, expected_smarginal_sparse)
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 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)