4

We are using the registration algorithm of ITK but we only want the affine transformation matrix and not directly apply the registration. In a previous issues we already solved a misunderstanding regarding the image/transform orientation: How to get transformation affine from ITK registration?

We did now run into a sample where the current solution does not properly work. The rotation is good but the result is slightly translated. The image output of ITK is perfect, so we know that the registration worked. That's why we will reduce the problem description below to the affine calculation with the specific matrices.

From the ITK registration we get/read the following parameters:

parameter_map = result_transform_parameters.GetParameterMap(0)

rot00, rot01, rot02, rot10, rot11, rot12, rot20, rot21, rot22 = parameter_map[
    'TransformParameters'][:9]
A = np.array([
    [rot00, rot01, rot02, 0],
    [rot10, rot11, rot12, 0],
    [rot20, rot21, rot22, 0],
    [    0,     0,     0, 1],
], dtype=float)  # yapf: disable

tx, ty, tz = parameter_map['TransformParameters'][9:]
t = np.array([
    [1, 0, 0, tx],
    [0, 1, 0, ty],
    [0, 0, 1, tz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

# In world coordinates
cx, cy, cz = parameter_map['CenterOfRotationPoint']
c = np.array([
    [1, 0, 0, cx],
    [0, 1, 0, cy],
    [0, 0, 1, cz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

ox, oy, oz = parameter_map['Origin']
o = np.array([
    [1, 0, 0, ox],
    [0, 1, 0, oy],
    [0, 0, 1, oz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

moving_ras = moving_image.affine

Where A is the direction/rotation matrix, t the translation matrix, c the center of rotation (CoR), and moving_ras the affine of the moving image in RAS orientation.

The translation and direction matrix can be combined to one transform matrix:

transform = t @ A

We are not sure how to factor in the CenterOfRotationPoint. Based on this, this, and that exchange questions, I thought one might need to do it like that:

transform = c @ transform @ np.linalg.inv(c)

Finally, we need to add the orientation flip between RAS and LPS:

registration = FLIPXY_44 @ transform @ FLIPXY_44

But this does not result in the correct transformation affine.

On the ITK docs and in a GitHub issue we got this formula to apply the above parameters to points:

T(x) = A ( x - c ) + (t + c)

While we can not directly use that since we do not want to directly transform the image but we only want to calculate the correct affine transformation matrix, one can see how the formula is pretty similar to what we are already doing as explained above.

We are again at a dead end with our knowledge.

Things we noticed that might make issues here:

  • Orientation
    • ITK uses LPS orientation for images and transforms
    • Monai/Nibabel uses RAS orientation for images and transforms
  • Center of Rotation
    • ITK provides the used center of rotation
    • Monai implicitly assumes the center of rotation to be the center of the image
  • World space vs. Index space.
    • All transforms and points from ITK are in world space.
    • Monai seems to operate directly on the image.
  • (0, 0, 0) Corner - ITK and Monai seem to use the opposit corner for coordinates - e.g. in a 4x4x4 image, position (0, 0, 0) in ITK is position (3, 3, 3) in Monai.

EDIT: I noticed that my current minimal code example is not quite comprehensive. Therefore here an update. The included affine matrices are taken from the ITK coregistration. The ITK code was omitted for brevity.

Here with new test data (you can view these images via MRIcoGL):

Here a minimal code example:

from pathlib import Path

import nibabel
import numpy as np
from monai.transforms.spatial.array import Affine
from monai.utils.enums import GridSampleMode, GridSamplePadMode
from nibabel import Nifti1Image

np.set_printoptions(suppress=True)  # type: ignore

folder = Path('.')

FLIPXY_44 = np.diag([-1, -1, 1, 1])

# rot00, rot01, rot02, rot10, rot11, rot12, rot20, rot21, rot22 = parameter_map['TransformParameters'][:9]
A = np.array([[ 1.02380734, -0.05137566, -0.00766465,  0.        ],
              [ 0.01916231,  0.93276486, -0.23453097,  0.        ],
              [ 0.01808809,  0.2667324 ,  0.94271694,  0.        ],
              [ 0.        ,  0.        ,  0.        ,  1.        ]]) # yapf: disable

# tx, ty, tz = parameter_map['TransformParameters'][9:]
t = np.array([[ 1.        ,  0.        ,  0.        ,  1.12915465  ],
              [ 0.        ,  1.        ,  0.        , 11.76880151  ],
              [ 0.        ,  0.        ,  1.        , 41.54685788  ],
              [ 0.        ,  0.        ,  0.        ,  1.          ]]) # yapf: disable

# cx, cy, cz = parameter_map['CenterOfRotationPoint']
c = np.array([[ 1.        ,  0.        ,  0.        ,  -0.1015625  ],
              [ 0.        ,  1.        ,  0.        , -24.5521698  ],
              [ 0.        ,  0.        ,  1.        ,   0.1015625  ],
              [ 0.        ,  0.        ,  0.        ,   1.         ]]) # yapf: disable

# Moving image affine
x = np.array([[ 2.        ,  0.        ,  0.        , -125.75732422],
              [ 0.        ,  2.        ,  0.        , -125.23828888],
              [ 0.        ,  0.        ,  2.        ,  -99.86506653],
              [ 0.        ,  0.        ,  0.        ,    1.        ]]) # yapf: disable

o = np.array([
    [1., 0., 0., 126.8984375],
    [0., 1., 0., 102.4478302],
    [0., 0., 1., -126.8984375],
    [0., 0., 0., 1.],
])

moving_ras = x

# Combine the direction and translation
transform = t @ A

# Factor in the center of rotation
# transform = c @ transform @ np.linalg.inv(c)

# Switch from LPS to RAS orientation
registration = FLIPXY_44 @ transform @ FLIPXY_44

y = np.array([[ 2.        ,  0.        ,  0.        , -126.8984375 ],
              [ 0.        ,  2.        ,  0.        , -102.4478302 ],
              [ 0.        ,  0.        ,  2.        , -126.8984375 ],
              [ 0.        ,  0.        ,  0.        ,    1.        ]]) # yapf: disable

fixed_image_affine = y

moving_image_ni: Nifti1Image = nibabel.load(folder / 'real_moving.nii.gz')
moving_image_np: np.ndarray = moving_image_ni.get_fdata()  # type: ignore

affine_transform = Affine(affine=registration,
                          image_only=True,
                          mode=GridSampleMode.NEAREST,
                          padding_mode=GridSamplePadMode.BORDER)
reg_monai = np.squeeze(affine_transform(moving_image_np[np.newaxis, ...]))

out = Nifti1Image(reg_monai, fixed_image_affine)

nibabel.save(out, folder / 'reg_monai.nii.gz')

When you executed this code, the resulting reg_monai.nii.gz should match the real_fixed.nii.gz (in position and outline - not in the actual content).

Currently the result looks like this (viewed via MRIcoGL):

coregistered images do not match

But the result should look like this (this is the direct ITK registration output where the hardcoded affine matrices come from - which should prove that the registration worked and that the parameters generally should be good):

coregistered images do match


For the sake of completeness, here also the code to perform the ITK registration and to get the above affine matrices:

from pathlib import Path

import itk
import numpy as np

np.set_printoptions(suppress=True)  # type: ignore

folder = Path('.')

moving_image = itk.imread(str(folder / 'real_moving.nii.gz'), itk.F)
fixed_image = itk.imread(str(folder / 'real_fixed.nii.gz'), itk.F)

# Import Default Parameter Map
parameter_object = itk.ParameterObject.New()
affine_parameter_map = parameter_object.GetDefaultParameterMap('affine', 4)
affine_parameter_map['FinalBSplineInterpolationOrder'] = ['1']
affine_parameter_map['MaximumNumberOfIterations'] = ['512']
parameter_object.AddParameterMap(affine_parameter_map)

# Call registration function
result_image, result_transform_parameters = itk.elastix_registration_method(  # type: ignore
    fixed_image, moving_image, parameter_object=parameter_object)

itk.imwrite(result_image, str(folder / 'real_reg_itk.nii.gz'), compression=True)

parameter_map = result_transform_parameters.GetParameterMap(0)

rot00, rot01, rot02, rot10, rot11, rot12, rot20, rot21, rot22 = parameter_map['TransformParameters'][:9]
A = np.array([
    [rot00, rot01, rot02, 0],
    [rot10, rot11, rot12, 0],
    [rot20, rot21, rot22, 0],
    [    0,     0,     0, 1],
], dtype=float)  # yapf: disable

tx, ty, tz = parameter_map['TransformParameters'][9:]
t = np.array([
    [1, 0, 0, tx],
    [0, 1, 0, ty],
    [0, 0, 1, tz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

# In world coordinates
cx, cy, cz = parameter_map['CenterOfRotationPoint']
c = np.array([
    [1, 0, 0, cx],
    [0, 1, 0, cy],
    [0, 0, 1, cz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

ox, oy, oz = parameter_map['Origin']
o = np.array([
    [1, 0, 0, ox],
    [0, 1, 0, oy],
    [0, 0, 1, oz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

Package versions:

itk-elastix==0.12.0
monai==0.8.0
nibabel==3.1.1
numpy==1.19.2
Spenhouet
  • 6,556
  • 12
  • 51
  • 76
  • Registration transforms are in world space, at least in ITK. Take a look at this: https://youtu.be/3we-bI5d7CE?t=229 – Dženan Mar 18 '22 at 21:45
  • I'm applying the registration in voxel space and my voxel space is in RAS orientation. – Spenhouet Mar 21 '22 at 08:29
  • No wonder you are having trouble accomplishing even simple things. The reason registration is done in world space (instead of voxel space) is that it is easier to understand. – Dženan Mar 21 '22 at 14:53
  • 2
    Is a bit hard to understand from your minimal example, what are the inputs and what, what do we need to compare and how can we evaluate that a possible output is ok?, output should be the same as fixed_image_affine? – Ziur Olpa Mar 23 '22 at 08:59
  • @PabloRuiz Yes, you are right. It was not clear how to test if it is working properly. I updated the question and redone the minimal code example adding code where the transform is applied as well as images to test this on. I also now added a sentence to explain what the goal is / the expected outcome. I hope these changes make it more clear. – Spenhouet Mar 23 '22 at 18:09
  • I also now added an example image for the expected result and a list of issues we noticed which might be underlying here. – Spenhouet Mar 23 '22 at 18:21
  • For completeness, I also added code for the registration - if it is unclear where the provided matrices come from. – Spenhouet Mar 23 '22 at 18:54
  • Have you tried `inv(c) @ t @ A @ c` instead of `c @ t @ A @ inv(c)`, this will give an image with the same orientation but displaced by `[-2.51637511, -3.2500057 , 13.11302812]` – Bob Mar 23 '22 at 21:54
  • @Bob I'm trying stuff since 10 days. I tried everything one can think of. There must be something we completely miss. I'm sure it is possible. – Spenhouet Mar 24 '22 at 06:29
  • Checking if I am understanding: `fixed_image_affine` is what you expected value of `transform` ? – Bob Mar 24 '22 at 08:23
  • @Bob No, the registration affines (`A`, `t`, `c`) describe a transform to overlay one image onto another (the `moving` image onto the `fixed` image). So `transform` should transform the `moving` image in a way, that it visually overlays the `fixed` image. This should work accross spaces, so after the transform was applied to the data matrix, the transformed `moving` image has the same affine as the `fixed` image to map both images into the world space. – Spenhouet Mar 24 '22 at 08:28
  • This already works via ITK but it does not if I apply the affines through another affine transformation algorithm. So the transform affine needs to be adjusted accordingly. That is what I'm trying to solve. – Spenhouet Mar 24 '22 at 08:30
  • I don't have experience with the ITK, but I do have in geometry and matrix algebra. I am able to run your code and get images. I need to know, what is the target transform relation you expect `afine_transform(moving_image_np) ~ fixed_image`, I will post an answer because I add the plots here. – Bob Mar 24 '22 at 09:00
  • I found the voxel position to be centric for both ITK and Monai. So that is ruled out. But I found something interesting. ITK and Monai seem to use the opposite corner for coordinates - e.g. in a 4x4x4 image, position (0, 0, 0) in ITK is position (3, 3, 3) in Monai. I don't yet understand how I would factor this into the affine and if this is even possible. – Spenhouet Mar 24 '22 at 17:32
  • Have you ever run into this error? `ModuleNotFoundError: No module named 'torch.distributed'` That's what I'm getting. – Red Mar 26 '22 at 00:33
  • @AnnZen No, not yet. Maybe you need to install pytorch? Or you have some miss matching version. You can reinstall the dependencies (especially Monai) with the `--upgrade` argument, this might help. – Spenhouet Mar 28 '22 at 05:07
  • Maybe helpful: code for a similar purpose for Slicer (which uses RAS and 4x4 matrices): https://slicer.readthedocs.io/en/latest/developer_guide/script_repository.html#convert-between-itk-and-slicer-linear-transforms – Dženan Mar 29 '22 at 13:04
  • Please check that you apply everything in the correct order! – Kai Winkler Apr 21 '22 at 08:25
  • If you factor in the center of rotation, it is crucial to first translate the cor to 0,0,0 then rotate, then translate back to your cor and then apply the translation from elastix. I had the same problem where everything was correct but the translation was 3 voxels off. – Kai Winkler Apr 21 '22 at 08:31

2 Answers2

0

I guess this is not the solution, but this easy code/ transformation seems to let the image pointing in the same direction and almost aligned, which makes me question if is really LPS to RAS because this looks a completely different transformation of axis:

transform_matrix= np.array([
    [0, 0, 1, 0],
    [0, 1, 0, 0],
    [1, 0, 0, 0],
    [0, 0, 0,  1]], dtype=float)

to_transform: Nifti1Image = nibabel.load('file/real_moving.nii.gz')
to_transform: np.ndarray = to_transform.get_fdata() 

affine_transform = Affine(affine=transform_matrix, image_only=True,
                          mode=GridSampleMode.NEAREST, padding_mode=GridSamplePadMode.BORDER)
transformed_img = np.squeeze(affine_transform(to_transform[np.newaxis, ...])) 

On the other hand I was not able to find the proper order of the parameters in the parameter_map (on the documentation). Are you sure that A and t are multiplied and not summed (with a zero diagonal on t), maybe you can point me to the documentation where that is written.

On the center of rotation I found this, which as far as I know will mean:

transform = c @ transform @ c_minus

Where c have again no diagonal, whether if this should be applied after or before the t, I have no answer, but for me no option worked for me, as I couldn't even reproduce your images with this data set.

I found some useful information with jupyter examples at the documentation of itk-elastix here

This is the result of the first piece of code, but the images doesn't seem to be the same as yours.

I let you some pictures of how the data appears into my machine with the input, transformed image and reference image at the end.

Before transformation before transformation

Transformed image: transformed image

objective image objective image

I know this is not a final solution, but hope it is still useful.

Ziur Olpa
  • 1,839
  • 1
  • 12
  • 27
  • Hi, did you look at the images via a 3D viewer like MRIcoGL? The transformation matrix you shared does not seem to make any sense to me. Also I'm not sure I get what you mean with respect to LPS and RAS orientation. Changing the orientation will actually result in the correct orientation of the output images. I think you are confusing the * and @ operators. The @ with a translation does actually perform an addition. Also you can't just negate a matrix - that is what the inverse is for. But this code as well as the link you shared was already in my question. – Spenhouet Mar 24 '22 at 06:39
  • Hi! that was matplotlib, but yes i guess that is not the way of visualizing this things. The @ operator in numpy arrays is __matmul__ a matrix multiplication, the multiplication has so many zeros that will look like an addition but is not, the difference will displace your image but not deform it, that may be what are actually facing. Let us look at what parameter_map is, where did you get the order of the arguments of that? – Ziur Olpa Mar 24 '22 at 07:49
  • The inverse of a translation is just the translation in the opposite direction, but yes, I see that using a multiplication is completely right to put the inverse there. – Ziur Olpa Mar 24 '22 at 07:50
  • Reading out the `parameter_map` was pieced together from looking into the ITK docs and code. The first 9 parameters are the rotation matrix in row-major order and the last 3 the translation matrix. I did not mean to state that it is an addition in general but in the case of a translation, it results in the same output. Nonetheless, I do think it is the correct operation here. I'm working with 4x4 affine matrices as shown in my question. The operations are slightly different than what you are proposing with respect to addition, subtraction, multiplaction, ... Are you familiar with that? – Spenhouet Mar 24 '22 at 08:18
  • @Spenhouet, not sure if you are getting what I'm saying, I say that A may be A= np.array([[rot00, rot01, rot02, tx], [rot10, rot11, rot12, ty], [rot20, rot21, rot22, tz],[ 0, 0, 0, 1],], dtype=float) Which is not the same matrix operation and in no case is the same output if you are using @ operator. From the documentation is not clear to me why did you made that, but maybe you can point a documentation source that I don't know. Still i guess it is not the mine issue, as still is needed to factor in the origin/center of rotation... – Ziur Olpa Mar 24 '22 at 13:30
  • From elastix-5.0.0-manual you can check in page 9 how is defined the Affine: AffineTransform parameters and you see a sum not a multiplication, and your multiplication is not a sum and it doesn't have the same effect, at least with the meaning in python. – Ziur Olpa Mar 24 '22 at 13:35
  • But that is actually the same output if your are using the `@` operator. Did you try it? Did you execute the affine calculations and check the intermediary results? If you perform `t @ A` you are actually just adding the x,y,z of t to A. That is due to the identity matrix. Also the formular you are referencing was also already included in my question. – Spenhouet Mar 24 '22 at 16:42
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/243296/discussion-between-pablo-ruiz-and-spenhouet). – Ziur Olpa Mar 24 '22 at 16:48
0

What I see is that the image registration process is not actually working.

def registration_test(moving_image, fixed_image, niter=512):
  # Import Default Parameter Map
  parameter_object = itk.ParameterObject.New()
  affine_parameter_map = parameter_object.GetDefaultParameterMap('affine', 4)
  affine_parameter_map['FinalBSplineInterpolationOrder'] = ['1']
  affine_parameter_map['MaximumNumberOfIterations'] = [str(niter)]
  parameter_object.AddParameterMap(affine_parameter_map)

  # Call registration function
  result_image, result_transform_parameters = itk.elastix_registration_method(  # type: ignore
      fixed_image, moving_image, parameter_object=parameter_object)
  #transform_parameters = parameter_map['TransformParameters']
  #transform_origin = parameter_map['CenterOfRotationPoint']
  transform_parameters = result_transform_parameters.GetParameter(0, 'TransformParameters')
  transform_origin = result_transform_parameters.GetParameter(0, 'CenterOfRotationPoint')
  r = np.asarray(transform_parameters).reshape(4, 3)
  c = np.asarray(transform_origin, dtype=float)
  A = np.eye(4)
  A[:3,3] = r[3]
  A[:3,:3] = r[:3].T
  print(A, c)
  C = np.eye(4)
  C[:3, 3] = c;
  C_inv = np.eye(4)
  C_inv[:3,3] = -c;
  
  affine_transform = Affine(affine=C @ A @ C_inv,
                          image_only=True,
                          mode=GridSampleMode.NEAREST,
                          padding_mode=GridSamplePadMode.BORDER)
  
  moving_image_np = np.asarray(moving_image)
  reg_monoai = affine_transform(moving_image_np[..., np.newaxis])
  obtained = reg_monoai[..., 0]
  print(obtained.shape)
  plt.figure(figsize=(9,9))
  plt.subplot(331)
  plt.imshow(fixed_image[64,:,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.ylabel('fixed_image'); plt.title('plane 0')
  plt.subplot(334)
  plt.imshow(obtained[64,:,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.ylabel('result')
  plt.subplot(337)
  plt.imshow(moving_image[64,:,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.ylabel('moving_image');
  plt.subplot(332)
  plt.imshow(fixed_image[:,64,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.title('plane 1')
  plt.subplot(335)
  plt.imshow(obtained[:,64,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(338)
  plt.imshow(moving_image[:,64,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(333)
  plt.title('plane 2');
  plt.imshow(fixed_image[:,:,64], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(336)
  plt.imshow(obtained[:,:,64], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(339)
  plt.imshow(moving_image[:,:,64], origin='lower'); plt.xticks([]); plt.yticks([])

Then with the pair you sent, that are very close to each other if I run 1000 iterations, here is what I have

%%time
registration_test(moving_image, fixed_image, 1000)
[[ 1.02525991  0.01894165  0.02496272  1.02504064]
 [-0.05196394  0.93458484  0.26571434 11.92591955]
 [-0.00407657 -0.23543312  0.94091849 41.62065545]
 [ 0.          0.          0.          1.        ]] [ -0.1015625 -24.5521698   0.1015625]
(128, 128, 128)
CPU times: user 15.9 s, sys: 654 ms, total: 16.6 s
Wall time: 10.9 s

enter image description here

test with a slight rotation

Using this function to rotate around one axis

def imrot(im, angle, axis=1):
  x,y = [i for i in range(3) if i != axis]
  A = np.eye(4)
  A[x,x] = np.cos(angle);
  A[x,y] = np.sin(angle);
  A[y,x] = -np.sin(angle);
  A[y,y] = np.cos(angle);
  f = Affine(affine=A,
        image_only=True,
        mode=GridSampleMode.NEAREST,
        padding_mode=GridSamplePadMode.BORDER)
  return itk.image_from_array(f(np.asarray(im)[np.newaxis])[0])

I see that over 10 iterations the moving_image is not significantly modified

%%time
e2e_test(moving_image, imrot(fixed_image, 0.5), 10)
[[ 0.9773166  -0.05882861 -0.09435328 -8.29016604]
 [ 0.01960457  1.01097845 -0.06601224 -4.62307826]
 [ 0.09305988  0.07375327  1.06381763  0.74783361]
 [ 0.          0.          0.          1.        ]] [63.5 63.5 63.5]
(128, 128, 128)
CPU times: user 3.57 s, sys: 148 ms, total: 3.71 s
Wall time: 2.24 s

enter image description here

But if I increase the number of iterations to 100, instead of approximating the fixed image as I would expect it seems lost

[[  1.12631932  -0.33513615  -0.70472146 -31.57349579]
 [ -0.07239085   1.08080123  -0.42268541 -28.72943354]
 [ -0.24096706  -0.08024728   0.80870164  -5.86050765]
 [  0.           0.           0.           1.        ]] [63.5 63.5 63.5]

enter image description here

After 1000 iterations

[[  1.28931626  -0.36533121  -0.52561289 -37.00919916]
 [  0.02204954   1.23661994  -0.29418401 -34.36979156]
 [ -0.32713001  -0.13135651   0.96500969   2.75931824]
 [  0.           0.           0.           1.        ]] [63.5 63.5 63.5]

enter image description here

After 10000 iterations

[[  1.46265277   0.02692694   0.14337441 -61.37788428]
 [ -0.15334478   1.37362513   0.16242297 -52.59833838]
 [ -0.53333714  -0.51411401   0.80381994  -4.97349468]
 [  0.           0.           0.           1.        ]] [63.5 63.5 63.5]

enter image description here

Bob
  • 13,867
  • 1
  • 5
  • 27
  • No, they need to absolutely match. See the example image I have in my question. The green image completely overlays the gray image. The gray is the fixed and the green the transformed moving image. – Spenhouet Mar 24 '22 at 15:10
  • Check the results I posted, you get the same behavior on your side? – Bob Mar 25 '22 at 08:04
  • No, I can not confirm that. The registration works just fine for me and it should for you. One thing I noticed (but that is another issue), is that you are reading in the rotation matrix in column-major order and not in row-major order. – Spenhouet Mar 25 '22 at 10:34
  • If you try with different numbers of iteration it gives you approximately the same transfromation? – Bob Mar 28 '22 at 07:13
  • If you apply an affine transformation to `moving_image` before registration, the result image is the same? If that's the case what we can do is to apply a set of affine transforms and build a linear equation system from the received parameters to the expected matrix entries. But you will need to run on your side since it the image registration is not working consistently on my side. – Bob Mar 28 '22 at 07:18