Images alignment on S3/S2#

This notebook presents some examples of imagery alignment techniques introduced in the previous notebook on the real-life example of aligning Sentinel-2 and Sentinel-3 OLCI imagery.

Load in data needed#

import numpy as np
import matplotlib.pyplot as plt

save_path = '/content/drive/MyDrive/GEOL0069/2425/Week 5/Regression_application'
s2_data = np.load(save_path+'/s2_zoomed_data.npz')
s2_x = s2_data['x']
s2_y = s2_data['y']
s2_band_data = s2_data['band_data']

s3_data = np.load(save_path+'/s3_zoomed_data.npz')
s3_x = s3_data['x']
s3_y = s3_data['y']
s3_reflectance = s3_data['reflectance']

You don’t need to run this cell. This cell mainly interpolates the loaded S3/S2 scene to a common grid for alignment.

import numpy as np
from scipy.interpolate import griddata

def process_and_save_data(
    x_s2, y_s2, s2_band_data, s2_band_index,
    x_s3, y_s3, s3_reflectance, s3_band_index,
    ngrid=400, out_file='interpolated_data.npz'
):
    # Pick single band
    s2_vals = s2_band_data[:, s2_band_index]
    s3_vals = s3_reflectance[:, s3_band_index]

    x_min = min(x_s3.min(), x_s2.min())
    x_max = max(x_s3.max(), x_s2.max())
    y_min = min(y_s3.min(), y_s2.min())
    y_max = max(y_s3.max(), y_s2.max())

    x_grid = np.linspace(x_min, x_max, ngrid)
    y_grid = np.linspace(y_min, y_max, ngrid)
    xg, yg = np.meshgrid(x_grid, y_grid)

    z_s2 = griddata((x_s2, y_s2), s2_vals, (xg, yg), method='cubic')
    z_s3 = griddata((x_s3, y_s3), s3_vals, (xg, yg), method='cubic')

    np.savez_compressed(out_file, xg=xg, yg=yg, z_s2=z_s2, z_s3=z_s3)
    print(f"Saved precomputed arrays to '{out_file}'")

process_and_save_data(
    s2_x, s2_y, s2_band_data, 0,
    s3_x, s3_y, s3_reflectance, 0,
    ngrid=400,
    out_file='/content/drive/MyDrive/GEOL0069/2324/Week 6 2025/interpolated_data.npz'
)

def load_precomputed_data(file='interpolated_data.npz'):
    data = np.load(file)
    xg = data['xg']
    yg = data['yg']
    z_s2 = data['z_s2']
    z_s3 = data['z_s3']
    print(f"Loaded arrays from '{file}' (shapes: {z_s2.shape}, {z_s3.shape})")
    return xg, yg, z_s2, z_s3


xg, yg, z_s2, z_s3 = load_precomputed_data('/content/drive/MyDrive/GEOL0069/2324/Week 6 2025/interpolated_data.npz')
Loaded arrays from '/content/drive/MyDrive/GEOL0069/2324/Week 6 2025/interpolated_data.npz' (shapes: (400, 400), (400, 400))

Auto-correlation#

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrow
from scipy.signal import correlate2d

def auto_corr_demo_with_arrows(
    z_s2, z_s3,
    manual_dx=15,
    manual_dy=-7,
    step=20
):
    A = np.nan_to_num(z_s2, nan=np.nanmean(z_s2))
    B = np.nan_to_num(z_s3, nan=np.nanmean(z_s3))

    B_misaligned = np.roll(np.roll(B, manual_dy, axis=0), manual_dx, axis=1)

    corr = correlate2d(A, B_misaligned, mode='same', boundary='wrap')
    peak_y, peak_x = np.unravel_index(np.argmax(corr), corr.shape)

    shift_x_btoa = peak_x - B_misaligned.shape[1]//2
    shift_y_btoa = peak_y - B_misaligned.shape[0]//2

    dx_est = -shift_x_btoa
    dy_est = -shift_y_btoa

    print(f"[AutoCorr] Manual shift=({manual_dx},{manual_dy}), Recovered=({dx_est},{dy_est})")

    B_realigned = np.roll(np.roll(B_misaligned, shift_y_btoa, axis=0),
                          shift_x_btoa, axis=1)

    fig, (axA, axB, axC) = plt.subplots(1, 3, figsize=(15, 5))

    axA.imshow(A, cmap='gray')
    axA.set_title("S2 with Motion Arrows")

    h, w = A.shape
    rows = np.arange(0, h, step)
    cols = np.arange(0, w, step)

    for r in rows:
        for c in cols:
            axA.add_patch(FancyArrow(c, r, manual_dx, manual_dy, color="yellow",
                                     width=0.5, head_width=2, head_length=2, alpha=0.8))

    axB.imshow(B_misaligned, cmap='gray')
    axB.set_title("S3 Misaligned")

    axC.imshow(B_realigned, cmap='gray')
    axC.set_title("S3 Re-aligned (AutoCorr)")

    plt.tight_layout()
    plt.show()

# Example usage:
auto_corr_demo_with_arrows(z_s2, z_s3, manual_dx=10, manual_dy=-5, step=20)
[AutoCorr] Manual shift=(10,-5), Recovered=(12,-7)
_images/449f887988bc09ad1bc01633e812108f2629f0ed8fff449e2e7f592661f92054.png

Phase correlation#

from skimage.registration import phase_cross_correlation


def phase_corr_demo_with_arrows(
    z_s2, z_s3,
    manual_dx=15,
    manual_dy=-7,
    step=20
):
    A = np.nan_to_num(z_s2, nan=np.nanmean(z_s2))
    B = np.nan_to_num(z_s3, nan=np.nanmean(z_s3))

    B_misaligned = np.roll(np.roll(B, manual_dy, axis=0), manual_dx, axis=1)

    shift, error, diffphase = phase_cross_correlation(A, B_misaligned)
    dy_est, dx_est = shift
    dx_est, dy_est = -dx_est, -dy_est
    print(f"[PhaseCorr] Manual shift=({manual_dx},{manual_dy}), Recovered=({dx_est:.2f},{dy_est:.2f})")

    B_realigned = np.roll(np.roll(B_misaligned, int(dy_est), axis=0),
                          int(dx_est), axis=1)

    fig, (axA, axB, axC) = plt.subplots(1,3, figsize=(15,5))

    axA.imshow(A, cmap='gray')
    axA.set_title(f"S2 (Arrows showing shift)")
    h, w = A.shape
    rows = np.arange(0, h, step)
    cols = np.arange(0, w, step)
    for r in rows:
        for c in cols:
            axA.add_patch(FancyArrow(c, r, manual_dx, manual_dy, color="yellow",
                                     width=0.5, head_width=2, head_length=2, alpha=0.8))

    axB.imshow(B_misaligned, cmap='gray')
    axB.set_title("S3 Misaligned (Phase Corr)")

    axC.imshow(B_realigned, cmap='gray')
    axC.set_title("S3 Re-aligned (Phase Corr)")

    plt.tight_layout()
    plt.show()

phase_corr_demo_with_arrows(z_s2, z_s3, manual_dx=10, manual_dy=-5, step=20)
[PhaseCorr] Manual shift=(10,-5), Recovered=(11.00,-7.00)
_images/3798c3f48e6eb9492d523012f7238609d117dbb3e5620abf9d823ce7b5764e8e.png

ECC (Enhanced Correlation Coefficient) Alignment#


import cv2


def ecc_demo_with_arrows(
    z_s2, z_s3,
    manual_dx=15,
    manual_dy=-7,
    step=20,
    iterations=200,
    eps=1e-6
):
    A = np.nan_to_num(z_s2, nan=np.nanmean(z_s2)).astype(np.float32)
    B = np.nan_to_num(z_s3, nan=np.nanmean(z_s3)).astype(np.float32)

    B_misaligned = np.roll(np.roll(B, manual_dy, axis=0), manual_dx, axis=1)

    warp_matrix = np.eye(2, 3, dtype=np.float32)
    criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, iterations, eps)

    try:
        cc, warp_matrix = cv2.findTransformECC(A, B_misaligned, warp_matrix, motionType=cv2.MOTION_TRANSLATION, criteria=criteria)
        dx_est, dy_est = warp_matrix[0,2], warp_matrix[1,2]
        print(f"[ECC] Manual shift=({manual_dx},{manual_dy}), Recovered=({dx_est:.2f},{dy_est:.2f}), CC={cc:.4f}")
    except cv2.error as e:
        print("[ECC Failed]", e)
        dx_est, dy_est = None, None

    B_realigned = np.roll(np.roll(B_misaligned, int(dy_est), axis=0),
                          int(dx_est), axis=1)

    fig, (axA, axB, axC) = plt.subplots(1,3, figsize=(15,5))

    axA.imshow(A, cmap='gray')
    axA.set_title(f"S2 (Arrows showing shift)")
    h, w = A.shape
    rows = np.arange(0, h, step)
    cols = np.arange(0, w, step)
    for r in rows:
        for c in cols:
            axA.add_patch(FancyArrow(c, r, manual_dx, manual_dy, color="yellow",
                                     width=0.5, head_width=2, head_length=2, alpha=0.8))

    axB.imshow(B_misaligned, cmap='gray')
    axB.set_title("S3 Misaligned (ECC)")

    axC.imshow(B_realigned, cmap='gray')
    axC.set_title("S3 Re-aligned (ECC)")

    plt.tight_layout()
    plt.show()

ecc_demo_with_arrows(z_s2, z_s3, manual_dx=10, manual_dy=-5, step=20)
[ECC] Manual shift=(10,-5), Recovered=(10.94,-7.87), CC=0.7336
_images/d28b14a3c661ee3d4f2ee8167276e092824cc07fc79342bdd3a6eb9f93552e49.png

Sea ice drift algorithm#

Nansen Centre has developed a sea ice drift tracking algorithm based on ‘Feature tracking’ for Sentinel-1 SAR images. For more information, please have a look at this link.