Siamese Change Detector
1. Introduction
The Siamese Change Detector is a bi-temporal architecture: given two co-registered images of the same area at two different dates, it outputs a per-pixel change probability map. It follows the general design of:
Daudt, R. C., Le Saux, B., & Boulch, A. (2018). Fully convolutional siamese networks for change detection. 2018 25th IEEE International Conference on Image Processing (ICIP) (pp. 4063–4067). https://doi.org/10.1109/ICIP.2018.8451652
Unlike LightTAE, TempCNN, and UTAE — which were ported layer-for-layer and rigorously cross-validated against reference implementations (sits or the official UTAE repo) — cdts.ai.SiameseChangeDetector is a compact, independent implementation of the general Siamese/twin-encoder change-detection pattern, not a line-for-line port of a specific published codebase. Treat it as a solid, ready-to-train baseline architecture for two-date change detection rather than a bit-exact reproduction of any one paper's exact numbers.
2. How It Works
The core idea of a Siamese network is weight sharing: the same encoder is applied independently to both input images, so that the two resulting feature maps live in a comparable representation space. SiameseChangeDetector's forward pass:
- Twin encoding (
forward_once): each input image (x_t0, the "before" image, andx_t1, the "after" image) is passed independently through the same two-stage convolutional encoder (ConvBlockat 64 channels, max-pooled, then a secondConvBlockat 128 channels) — the weights are shared, so a difference in the two output feature maps reflects a genuine change in content, not a difference in how the two images were processed. - Difference: the absolute difference between the two encoded feature maps (
|feat_t0 - feat_t1|) is computed — large values indicate the encoder detected substantially different content at that spatial location between the two dates. - Decoder: the difference map is passed through a decoding
ConvBlock, upsampled back to the input resolution (bilinear upsampling), and a final1x1convolution (classifier) produces per-pixel class logits (by defaultnum_classes=2: "no change" vs. "change").
3. When to Use It
Use the Siamese Change Detector for classic bi-temporal change detection: you have exactly two dates (before/after an event — a wildfire, deforestation, a flood, construction) and want a change map between them. If you have a longer time series and want to classify or segment based on the whole trajectory rather than just two snapshots, use LightTAE (per-pixel) or UTAE (whole-patch segmentation) instead.
4. Preparing Your Data
The model expects two separate image tensors of identical shape, (Batch, Channels, Height, Width) — one per acquisition date — plus a binary (or multi-class) change label map of shape (Batch, Height, Width).
import numpy as np
import torch
# x_t0, x_t1: (n_samples, n_bands, patch_h, patch_w) - "before" and "after" patches
x_t0_train = torch.tensor(np.load("images_before.npy"), dtype=torch.float32)
x_t1_train = torch.tensor(np.load("images_after.npy"), dtype=torch.float32)
# y: (n_samples, patch_h, patch_w) - 0 = no change, 1 = change
y_train = torch.tensor(np.load("change_masks.npy"), dtype=torch.long)
x_t0 and x_t1 must have the same spatial dimensions and be co-registered — the model assumes pixel (i, j) in both images corresponds to the same location on the ground.
5. Instantiating the Model
from cdts.ai import SiameseChangeDetector
model = SiameseChangeDetector(
in_channels=6, # spectral bands per image
num_classes=2, # 2 for binary change/no-change; more for multi-class change type
)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
6. Loss Functions
Two natural options from cdts.ai.losses, depending on how you want to train:
FocalLoss/TverskyLoss: apply directly to the classifier's(B, num_classes, H, W)output logits vs. the(B, H, W)label map — the standard approach if you're training the full pipeline (encoders + decoder + classifier) end-to-end as a segmentation problem. Both handle the severe class imbalance typical of change detection (changed pixels are usually a small minority).ContrastiveSiameseLoss: operates directly on the two encoder feature maps (feat_t0,feat_t1fromforward_once) rather than the final classifier output — it pulls feature vectors together for unchanged pixels and pushes them apart (up to a margin) for changed pixels. Use this if you want to train the twin encoder as a metric-learning problem (e.g. for downstream thresholding or few-shot change detection), separately from or in addition to the classifier head.
from cdts.ai.losses import FocalLoss, ContrastiveSiameseLoss
criterion = FocalLoss(alpha=0.25, gamma=2.0)
# or, to also supervise the encoder features directly:
contrastive_criterion = ContrastiveSiameseLoss(margin=2.0)
7. Training Loop
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
train_loader = DataLoader(
TensorDataset(x_t0_train, x_t1_train, y_train), batch_size=16, shuffle=True
)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = FocalLoss(alpha=0.25, gamma=2.0)
num_epochs = 20
for epoch in range(num_epochs):
model.train()
epoch_loss = 0.0
for x_t0, x_t1, labels in train_loader:
x_t0, x_t1, labels = x_t0.to(device), x_t1.to(device), labels.to(device)
optimizer.zero_grad()
logits = model(x_t0, x_t1) # (batch, num_classes, H, W)
loss = criterion(logits, labels)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {epoch_loss / len(train_loader):.4f}")
8. Inference
model.eval()
with torch.no_grad():
new_t0 = torch.rand(1, 6, 256, 256).to(device)
new_t1 = torch.rand(1, 6, 256, 256).to(device)
logits = model(new_t0, new_t1)
change_map = torch.argmax(logits, dim=1) # (1, H, W), 0 = no change, 1 = change
print(f"Change map shape: {change_map.shape}")
Pro Tip: because the decoder only upsamples once (matching the single
MaxPool2d(2)in the encoder), the output resolution matches the input resolution exactly — no separate resizing step is needed before comparing the predicted change map to your reference mask.
References
- Daudt, R. C., Le Saux, B., & Boulch, A. (2018). Fully convolutional siamese networks for change detection. In 2018 25th IEEE International Conference on Image Processing (ICIP) (pp. 4063–4067). https://doi.org/10.1109/ICIP.2018.8451652