<jupyter_start><jupyter_text>Registration Errors, Terminology and Interpretation Registration is defined as the estimation of a geometric transformation aligning objects such that the distance between corresponding points on these objects is minimized, bringing the objects into alignment.When working with point-based registration algorithms we have three types of errors associated with our points (originally defined in [1]):  **Fiducial Localization Error (FLE)**: The error in determining the location of a point which is used to estimate the transformation. The most widely used FLE model is that of a zero mean Gaussian with independent, identically distributed errors. The figure below illustrates the various possible fiducial localization errors:**Fiducial Registration Error (FRE)**: The error of the fiducial markers following registration, $\|T(\mathbf{p_f}) - \mathbf{p_m}\|$ where $T$ is the estimated transformation and the points $\mathbf{p_f},\;\mathbf{p_m}$ were used to estimate $T$. **Target Registration Error (TRE)**: The error of the target fiducial markers following registration,$\|T(\mathbf{p_f}) - \mathbf{p_m}\|$ where $T$ is the estimated transformation and the points $\mathbf{p_f},\;\mathbf{p_m}$ were **not** used to estimate $T$. Things to remember:1. TRE is the only quantity of interest, but in most cases we can only estimate its distribution.2. FRE should never be used as a surrogate for TRE as the TRE for a specific registration is uncorrelated with its FRE [2]. 3. TRE is spatially varying.4. A good TRE is dependent on using a good fiducial configuration [3].5. The least squares solution to paired-point registration is sensitive to outliers.[1] "[Registration of Head Volume Images Using Implantable Fiducial Markers](https://www.ncbi.nlm.nih.gov/pubmed/9263002)", <NAME>. et al., *IEEE Trans Med Imaging*, 16(4):447-462, 1997.[2] "[Fiducial registration error and target registration error are uncorrelated](http://spie.org/Publications/Proceedings/Paper/10.1117/12.813601)", <NAME>, *SPIE Medical Imaging: Visualization, Image-Guided Procedures, and Modeling*, 7261:1–12, 2009.[3] "[Fiducial point placement and the accuracy of point-based, rigid body registration](https://www.ncbi.nlm.nih.gov/pubmed/11322441)", <NAME> al., *Neurosurgery*, 48(4):810-816, 2001.<jupyter_code>import SimpleITK as sitk
import numpy as np
import copy
%matplotlib nbagg
from gui import PairedPointDataManipulation, display_errors
import matplotlib.pyplot as plt
from registration_utilities import registration_errors<jupyter_output><empty_output><jupyter_text>FLE, FRE, TRE empirical experimentationIn the following cell you will use a user interface to experiment with the various concepts associated with FLE, FRE, and TRE.  Interacting with the GUI1. Change the mode and interact directly with the figure:    * Edit: Add pairs of fiducial markers with a left click and pairs of target markers with a right click.        * Markers can only be added prior to any manipulation (translation/rotation/noise/bias...)    * Translate: right-mouse button down + drag, anywhere in the figure.    * Rotate: right-mouse button down + drag, anywhere in the figure, will rotate the fiducials around their centroid        (marked by a blue dot).2. Buttons:    * Clear Fiducials/Targets: Removes all fiducial/target marker pairs.    * Reset: Moves all marker pairs to the original locations.    * Noise: Add noise to the fiducial markers.    * Bias (FRE<TRE): Add bias to all fiducial markers so that FRE<TRE (move all markers in the same direction).    * Bias (FRE>TRE): Add bias to all fiducial markers so that FRE>TRE (move half of the markers in one direction and        the other half in the opposite direction).    * Register: Align the two point sets using the paired fiducials. Marker glyphs:* Light red plus: moving fiducials* Dark red x: fixed fiducials* Light green circle: moving targets* Dark green square: fixed fiducials<jupyter_code>manipulation_interface = PairedPointDataManipulation(sitk.Euler2DTransform())<jupyter_output><empty_output><jupyter_text>Sensitivity to outliersThe least-squares solutions to the paired-point registration task used by the SimpleITK ``LandmarkBasedTransformInitializer`` method are optimal under the assumption of isotropic homogeneous zero mean Gaussian noise. They will readily fail in the presence of outliers (breakdown point is 0, a single outlier can cause arbitrarily large errors). The GUI above allows you to observe this qualitatively. In the following code cells we illustrate this quantitatively.<jupyter_code>ideal_fixed_fiducials = [[23.768817532447077, 60.082971482049849], [29.736559467930949, 68.740980140058511],
                   [37.639785274382561, 68.524529923608299], [41.994623984059984, 59.000720399798773]]
ideal_fixed_targets = [[32.317204629221266, 60.732322131400501], [29.413978822769653, 56.403317802396167]]
ideal_moving_fiducials = [[76.77857043206542, 30.557710579173616], [86.1401622129338, 25.76859196933914],
                    [86.95501792478755, 17.904506579872375], [78.07960498849866, 12.346214284259808]]
ideal_moving_targets = [[78.53588814928511, 22.166738486331596], [73.86559697098288, 24.481339720595585]]
# Registration with perfect data (no noise or outliers) 
fixed_fiducials = copy.deepcopy(ideal_fixed_fiducials)
fixed_targets = copy.deepcopy(ideal_fixed_targets)
moving_fiducials = copy.deepcopy(ideal_moving_fiducials)
moving_targets = copy.deepcopy(ideal_moving_targets)

# Flatten the point lists, SimpleITK expects a single list/tuple with coordinates (x1,y1,...xn,yn)
fixed_fiducials_flat = [c for p in fixed_fiducials for c in p]
moving_fiducials_flat = [c for p in moving_fiducials for c in p]

transform = sitk.LandmarkBasedTransformInitializer(sitk.Euler2DTransform(), fixed_fiducials_flat, moving_fiducials_flat)

FRE_information = registration_errors(transform, fixed_fiducials, moving_fiducials)
TRE_information = registration_errors(transform, fixed_targets, moving_targets)
FLE_values = [0.0]*len(moving_fiducials)
FLE_information =  (np.mean(FLE_values), np.std(FLE_values), np.min(FLE_values), np.max(FLE_values), FLE_values) 
display_errors(fixed_fiducials, fixed_targets, FLE_information, FRE_information, TRE_information, title="Ideal Input")

# Change fourth fiducial to an outlier and register
outlier_fiducial = [88.07960498849866, 22.34621428425981]
FLE_values[3] = np.sqrt((outlier_fiducial[0] - moving_fiducials[3][0])**2 + 
                        (outlier_fiducial[1] - moving_fiducials[3][1])**2)
moving_fiducials[3][0] = 88.07960498849866
moving_fiducials[3][1] = 22.34621428425981

moving_fiducials_flat = [c for p in moving_fiducials for c in p]

transform = sitk.LandmarkBasedTransformInitializer(sitk.Euler2DTransform(), fixed_fiducials_flat, moving_fiducials_flat)

FRE_information = registration_errors(transform, fixed_fiducials, moving_fiducials)
TRE_information = registration_errors(transform, fixed_targets, moving_targets)
FLE_information =  (np.mean(FLE_values), np.std(FLE_values), np.min(FLE_values), np.max(FLE_values), FLE_values) 
display_errors(fixed_fiducials, fixed_targets, FLE_information, FRE_information, TRE_information, title="Single Outlier")<jupyter_output><empty_output><jupyter_text>FRE is not a surrogate for TREIn the next code cell we illustrate that FRE and TRE are not correlated. We first add the same fixed bias to all of the moving fiducials. This results in a large TRE, but the FRE remains zero. We then add a fixed bias to half of the moving fiducials and the negative of that bias to the other half. This results in a small TRE, but the FRE is large.<jupyter_code># Registration with same bias added to all points
fixed_fiducials = copy.deepcopy(ideal_fixed_fiducials)
fixed_targets = copy.deepcopy(ideal_fixed_targets)
moving_fiducials = copy.deepcopy(ideal_moving_fiducials)
bias_vector = [4.5, 4.5]
bias_fle = np.sqrt(bias_vector[0]**2 + bias_vector[1]**2)
for fiducial in moving_fiducials:
    fiducial[0] +=bias_vector[0]
    fiducial[1] +=bias_vector[1]
FLE_values = [bias_fle]*len(moving_fiducials)
moving_targets = copy.deepcopy(ideal_moving_targets)

# Flatten the point lists, SimpleITK expects a single list/tuple with coordinates (x1,y1,...xn,yn)
fixed_fiducials_flat = [c for p in fixed_fiducials for c in p]
moving_fiducials_flat = [c for p in moving_fiducials for c in p]

transform = sitk.LandmarkBasedTransformInitializer(sitk.Euler2DTransform(), fixed_fiducials_flat, moving_fiducials_flat)

FRE_information = registration_errors(transform, fixed_fiducials, moving_fiducials)
TRE_information = registration_errors(transform, fixed_targets, moving_targets)
FLE_information =  (np.mean(FLE_values), np.std(FLE_values), np.min(FLE_values), np.max(FLE_values), FLE_values) 
display_errors(fixed_fiducials, fixed_targets, FLE_information, FRE_information, TRE_information, title="FRE<TRE")

# Registration with bias in one direction for half the fiducials and in the opposite direction for the other half
moving_fiducials = copy.deepcopy(ideal_moving_fiducials)
pol = 1
for fiducial in moving_fiducials:
    fiducial[0] +=bias_vector[0]*pol
    fiducial[1] +=bias_vector[1]*pol
    pol*=-1.0
FLE_values = [bias_fle]*len(moving_fiducials)
moving_targets = copy.deepcopy(ideal_moving_targets)

# Flatten the point lists, SimpleITK expects a single list/tuple with coordinates (x1,y1,...xn,yn)
fixed_fiducials_flat = [c for p in fixed_fiducials for c in p]
moving_fiducials_flat = [c for p in moving_fiducials for c in p]

transform = sitk.LandmarkBasedTransformInitializer(sitk.Euler2DTransform(), fixed_fiducials_flat, moving_fiducials_flat)

FRE_information = registration_errors(transform, fixed_fiducials, moving_fiducials)
TRE_information = registration_errors(transform, fixed_targets, moving_targets)
FLE_information =  (np.mean(FLE_values), np.std(FLE_values), np.min(FLE_values), np.max(FLE_values), FLE_values) 
display_errors(fixed_fiducials, fixed_targets, FLE_information, FRE_information, TRE_information, title="FRE>TRE")<jupyter_output><empty_output><jupyter_text>Fiducial ConfigurationEven when our model of the world is correct, no outliers and FLE is isotropic and homogeneous, the fiducial configurationhas a significant effect on the TRE. Ideally you want the targets to be at the centroid of your fiducial configuration.This is illustrated in the code cell below. Translate, rotate and add noise to the fiducials, then register. The targets that are near the fiducials should have a better alignment than those far from the fiducials. Now, reset the setup. Where would you add two fiducials to improve the overall TRE? Experiment with various fiducial configurations.<jupyter_code>fiducials = [[31.026882048576109, 65.696247315510021], [34.252688500189009, 70.674602293864993], 
             [41.349462693737394, 71.756853376116084], [47.801075596963202, 68.510100129362826], 
             [52.47849495180192, 63.315294934557635]]
targets = [[38.123656242124497, 64.397546016808718], [43.768817532447073, 63.748195367458059], 
           [26.833333661479333, 8.7698403891030861], [33.768817532447073, 8.120489739752438]]
manipulation_interface = PairedPointDataManipulation(sitk.Euler2DTransform())
manipulation_interface.set_fiducials(fiducials)
manipulation_interface.set_targets(targets)<jupyter_output><empty_output><jupyter_text>FRE-TRE, and Occam's razorWhen we perform registration, our goal is to minimize the Target Registration Error. In practice it needs to be below a problem specific threshold for the registration to be useful.The target point(s) can be a single point or a region in space, and we want to minimize our registration error for this target. We go about this task by minimizing another quantity, in paired-point registration this is the FRE, in the case of intensity based registration we minimize an appropriate similarity metric. In both cases we expect that TRE is minimized indirectly.This can easily lead us astray, down the path of **overfitting**. In our 2D case, instead of using a rigid transformation with **three degrees of freedom** we may be tempted to use an affine transformation with **six degrees of freedom**. By introducing these additional degrees of freedom we will likely improve the FRE, but what about TRE?   In the cell below  you can qualitatively evaluate the effects of overfitting. Start by adding noise with no rotation or translation and then register. Switch to an affine transformation model and see how registration effects the fiducials and targets. You can then repeat this qualitative evaluation incorporating translation/rotation and noise. In this notebook we are working in an ideal setting, we know the appropriate transformation model is rigid. Unfortunately, this is often not the case. So which transformation model should you use? When presented with multiple competing hypotheses we select one using the principle of parsimony, often referred to as [Occam's razor](https://en.wikipedia.org/wiki/Occam%27s_razor). Our choice is to select the simplest model that can explain our observations. **In the case of registration, the transformation model with the least degrees of freedom that reduces the TRE below the problem specific threshold.**<jupyter_code>fiducials = [[31.026882048576109, 65.696247315510021], 
             [41.349462693737394, 71.756853376116084], 
             [52.47849495180192, 63.315294934557635]]

targets = [[38.123656242124497, 64.397546016808718], [43.768817532447073, 63.748195367458059]]
manipulation_interface = PairedPointDataManipulation(sitk.Euler2DTransform())
#manipulation_interface = PairedPointDataManipulation(sitk.AffineTransform(2))
manipulation_interface.set_fiducials(fiducials)
manipulation_interface.set_targets(targets)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07737490935460478, 'fraction_numerical': 0.07744742567077592, 'mean_word_length': 5.9969558599695585, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 31, 'https://': 3, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '98984', 'license': 'Apache-2.0', 'n_tokens_mistral': 4964, 'n_tokens_neox': 4058, 'n_words': 1409, 'path': 'Python/68_Registration_Errors.ipynb', 'repo_name': 'Seojin24/SimpleITK-Cardiac-', 'size': '13822'}