Manual landmarks definition using intensity projections and altitude maps

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from timagetk.io import imread
from timagetk.io.image import _image_from_url
from timagetk.algorithms.trsf import apply_trsf
from timagetk.algorithms.trsf import inv_trsf
from timagetk.algorithms.pointmatching import apply_trsf_to_points
from timagetk.algorithms.pointmatching import pointmatching
from timagetk.algorithms.trsf import create_trsf
from timagetk.components.spatial_image import SpatialImage
from timagetk.gui.mpl_image_surface_landmarks import SurfaceLandmarksPlot
/builds/J-gEBwyb/0/mosaic/timagetk/src/timagetk/components/labelled_image.py:31: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from tqdm.autonotebook import tqdm
INFO     [timagetk.visu.util] Custom colormap 'glasbey' is already registered, use `force=True` to register again!
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[2], line 10
      8 from timagetk.algorithms.trsf import create_trsf
      9 from timagetk.components.spatial_image import SpatialImage
---> 10 from timagetk.gui.mpl_image_surface_landmarks import SurfaceLandmarksPlot

ImportError: cannot import name 'SurfaceLandmarksPlot' from 'timagetk.gui.mpl_image_surface_landmarks' (/builds/J-gEBwyb/0/mosaic/timagetk/src/timagetk/gui/mpl_image_surface_landmarks.py)
import plotly.graph_objects as go
from plotly.subplots import make_subplots

def coordinates_tables(tables, names=None, title=None):
    n_tables = len(tables)
    fig = make_subplots(rows=1, cols=n_tables, shared_yaxes=True, horizontal_spacing=0.03, specs=[[{"type": "table"}]*n_tables]    )
    if names is not None:
        try:
            assert len(names) == n_tables
        except AssertionError:
            raise ValueError(f"Not the same number of tables ({n_tables}) and names ({len(names)})!")
        
    for i, pts in enumerate(tables):
        table = go.Table(header={'values': ["X", "Y", "Z"], 'font': {'size': 10}, 'align': "center", 'prefix': names[i]}, 
                         cells={'values': np.round(pts, 3).T, 'align': "left"})
        fig.add_trace(table, row=1, col=i+1)

    fig.update_layout(height=650, showlegend=False, title_text=title)
    fig.show()
import plotly.graph_objects as go

def alti2surface(alti_map, proj=None, z_invert=True):
    import plotly.graph_objects as go
    # Get altitude map and create plotly surface object
    if z_invert:
        alti_map = -alti_map  # revert the map to see it from top
    sh_0, sh_1 = alti_map.shape
    if proj is not None:
        ye, xe = proj.get_extent()
        x, y = np.linspace(0, xe, sh_0), np.linspace(0, ye, sh_1)
        surf_fig = go.Surface(z=alti_map, x=x, y=y, surfacecolor=proj,
                              colorbar={'title': 'Intensity'},
                              colorscale='Greys', reversescale=True)
    else:
        x, y = np.linspace(0, sh_0, sh_0), np.linspace(0, sh_1, sh_1)
        surf_fig = go.Surface(z=alti_map, x=x, y=y,
                              colorbar={'title': 'Altitude'},
                              colorscale='Viridis')
    return surf_fig

def pts2scatter(pts, z_invert=True):
    px, py, pz = np.array(pts).T
    if z_invert:
        pz = -pz
    pts_fig = go.Scatter3d(x=px, y=py, z=pz, mode='markers', marker_symbol='diamond',
                           marker={'size':6, "line": {'width': 2, 'color':'DarkSlateGrey'}})
    return pts_fig

Use known transformation from create_trsf

To test the GUI & the algorithms, we use a known rigid transformation and:

  1. apply it to the reference image to create the floating image

  2. apply the invert of the transformation to the reference points to create floating points as pairs of landmarks

Load the reference image

ref_img = imread(_image_from_url("https://zenodo.org/record/7151866/files/090223-p58-flo-top.lsm",
                                 hash_value="5548917d3d1490200d0d56cbb31c0d35", hash_method='md5'))
ref_pts = np.loadtxt('p58_t0_reference_ldmk-01.txt')

Create the rigid transformation, floating image & points

trsf = create_trsf('random', template_img=ref_img, trsf_type='rigid', seed=12, angle_range=[0.0, 0.1])
flo_img = apply_trsf(ref_img, trsf, template_img=ref_img)
flo_img.filename = 'registered_'+flo_img.filename
flo_pts = apply_trsf_to_points(ref_pts, inv_trsf(trsf, template_img=ref_img))
# print(ref_pts)
# print(flo_pts)
flo_img.filename

Surface landmarks GUI:

gui = SurfaceLandmarksPlot(img_left=ref_img, img_right=flo_img, surface_threshold=np.percentile(ref_img, 60),
                           points={'left': ref_pts, 'right': flo_pts}, orientation={'left': 1, 'right': 1},
                           altimap_smoothing=True)

Access the defined 3D landmarks

From the 2D points, in real coordinates, we return the position in 3D using the corresponding altitude map:

pred_ref_pts = gui.points_3d_coordinates()['left']
pred_flo_pts = gui.points_3d_coordinates()['right']

Access the estimated transformations

Using these 3D points (except the last one from each list), the GUI compute a landmarks based transformation using the pointmatching algorithm. The green channel is the floating (right) image registered by this transformation.

Usually, we you place landmarks, it is to initialize a registration process, often starting with a rigid block-matching registration. To mimmic this behaviour and anticipate the outcome of the next step, the GUI also estimate a rigid transformation with the landmarks transformation as initialisation. The blue channel is the floating (right) image registered by the composotion of manual & rigid transformations.

gui._update_trsf()  # Make sure the transformations are "up-to-date"
# The landmark based rigid transformation
np.round(gui.trsf.get_array(), 3)
# The `blockmatching` rigid transformation initialized with the landmark transformation
np.round(gui.rigid_trsf.get_array(), 3)

Access the projection images & altitudes maps

ref_alti = gui.altitude_maps['left']
ref_proj = gui.projected_images['left']

flo_alti = gui.altitude_maps['right']
flo_proj = gui.projected_images['right']

Show associated altitude maps

import matplotlib.gridspec as gridspec
from matplotlib import colors

fig = plt.figure(figsize=(15, 10), constrained_layout=True)
# Create two panels
gs = gridspec.GridSpec(ncols=2, nrows=1, figure=fig)
ref_ax = fig.add_subplot(gs[0, 0])
flo_ax = fig.add_subplot(gs[0, 1])
# Add the altitude map as images:
images = []
images.append(ref_ax.imshow(ref_alti, cmap='viridis', extent=gui._mpl_panel_extent('left')))
images.append(flo_ax.imshow(flo_alti, cmap='viridis', extent=gui._mpl_panel_extent('right')))
ref_ax.xaxis.tick_top()
flo_ax.xaxis.tick_top()
# Add the landmarks points:
px, py, c = np.array([(p[0], p[1], n) for n, p in enumerate(pred_ref_pts)]).T
ref_ax.scatter(px, py, c=c, cmap='Set3', ec='k', s=100)
px, py, c = np.array([(p[0], p[1], n) for n, p in enumerate(pred_flo_pts)]).T
flo_ax.scatter(px, py, c=c, cmap='Set3', ec='k', s=100)

# Find the min and max of all colors for use in setting the color scale.
vmin = min(image.get_array().min() for image in images)
vmax = max(image.get_array().max() for image in images)
norm = colors.Normalize(vmin=vmin, vmax=vmax)
for im in images:
    im.set_norm(norm)
# Add a colorbar at the bottom
fig.colorbar(images[0], ax=[ref_ax, flo_ax], orientation='horizontal', fraction=.1)

Estimated transformations from landmarks

Ground truth transformation

Using the generated set of landmarks from a random rigid transformation, we can get the “ground truth” transformation:

# To compute a transformation to apply to the floating image using landmarks:
gt_trsf = pointmatching(flo_pts, ref_pts, template_img=ref_img, method='rigid', real=True)
np.round(gt_trsf.get_array(), 3)
# The initial 'random' rigid trsf:
np.round(trsf.get_array(), 3)
# The GT transfo is the invert of the initial 'random' rigid trsf:
np.round(inv_trsf(trsf).get_array(), 3)

Predicted transformation from landmarks point-matching

#pred_ref_pts = gui.points_3d_coordinates()['left']
#pred_flo_pts = gui.points_3d_coordinates()['right']
pred_trsf = pointmatching(pred_flo_pts, pred_ref_pts, template_img=ref_img, method='rigid', real=True)
np.round(pred_trsf.get_array(), 3)

Note that it may be slightly different from the gui.trsf as we use all the 3D points in the previous cell, whereas the GUI do not use the last one (as you may be in the process of editing it).

# Deviation from "ground truth" (GT - PRED)
np.round(gt_trsf.get_array() - pred_trsf.get_array(), 3)

Estimate the deviation from ‘ground truth’ landmarks coordinates:

Knowing the true floating point coordiantes, we can estimate the loss of precision induced by the use of projection and altitude map in the GUI. Only the Z coordinates (last column) will differs as XY remains unchanged by the GUI.

pred_pts = gui.points_3d_coordinates()['right']
# print(pred_pts)
diff = pred_pts - flo_pts
coordinates_tables([flo_pts, pred_pts, diff], ['GT_', 'Pred_', 'Diff_'], "Estimation bias on floating points by GUI")

3D scatter plot of ground truth & gui floating image landmarks

import plotly.express as px
import pandas as pd

pred_pts = gui.points_3d_coordinates()['right']
group_names = ['Ground Truth', 'Prediction']
pts_list = []
for n, pts in enumerate([flo_pts, pred_pts]):
    pts_list.extend([list(p)+[group_names[n]] for p in pts])

df = pd.DataFrame(pts_list, columns=['x', 'y', 'z', 'group'])

fig = px.scatter_3d(df, x='x', y='y', z='z', color='group')
fig.show()

3D rendering of the altitude map & projection

from plotly.subplots import make_subplots

fig = make_subplots(rows=1, cols=2, specs=[[{'type': 'surface'}, {'type': 'surface'}]])
fig.add_trace(alti2surface(ref_alti, ref_proj), col=1, row=1)
fig.add_trace(pts2scatter(ref_pts), col=1, row=1)
fig.add_trace(alti2surface(flo_alti, flo_proj), col=2, row=1)
fig.add_trace(pts2scatter(pred_pts), col=2, row=1)