This vignette provides access to the registration workflows from
tf and places them in the
tidyfun site for convenience. It is aimed
at users who need to understand templates, warps, and diagnostics rather
than at casual end users exploring the core wrangling and plotting
features.
Registration aligns functions in time by reducing phase variation (timing differences - “horizontal” variability) while preserving amplitude variation (shape/height differences - “vertical” variability).
Use registration if:
Do not use registration as a (default) preprocessing step if:
Core problem:
Registration models observed curves \(x_i(t)\) as time-deformed versions of a template \(m(s)\), where the time deformation is given by a monotone warping function \(h_i(s)\):
\[ x_i(t) \approx m(h_i^{-1}(t)) \]
Key interpretation:
Core workflow in tf:
# One-shot registration (returns tf_registration object):
reg <- tf_register(x, method = "...")
tf_aligned(reg) # registered/aligned curves
tf_inv_warps(reg) # estimated inverse warping functions (observed → aligned time)
tf_template(reg) # template used
summary(reg) # alignment diagnostics
plot(reg) # 3-panel diagnostic plot
# Or step by step:
warps <- tf_estimate_warps(x, method = "...")
x_registered <- tf_align(x, warps)Template choice is the most important modeling choice in registration: every estimated warp is relative to that template.
Default template behavior in tf_register() /
tf_estimate_warps():
| Method | Default template behavior | How to override |
|---|---|---|
srvf |
Karcher-type mean shape1 estimated iteratively
by fdasrvf |
pass template = ... |
cc |
arithmetic mean curve (estimated iteratively) | pass template = ... |
affine |
arithmetic mean curve | pass template = ... |
landmark |
column-wise mean of landmark locations | pass template_landmarks = ... |
Practical rules:
If you need interpretable warpings relative to a known reference, supply an explicit template.
Template and data need to be on the same domain and grid for
srvf and cc.
Always visualize raw curves and the template together before trusting warps.
Under strong phase variation, the pointwise mean can be a poor template — see Unsuitable template in the Pitfalls section for an example and remedy.
tf_estimate_warps() returns forward warps \(h_i\) (aligned → observed time), while
tf_inv_warps(reg) returns the inverse warps \(h_i^{-1}\) (observed → aligned time) that
are directly used for alignment. These are the natural functions to
inspect and plot — they show how each curve’s observed timepoints map to
aligned “system” time.
This distinction is fundamental for interpretation and edge behavior:
srvf, cc,
and landmark methods in tf is
domain preserving.affine method in tf.Implications:
NAs
after tf_align() because parts of the aligned timeline have
no observed support.The figure below illustrates how warping functions compose with a template to produce shifted/deformed curves. Note that this shows the forward simulation view, \(m(h(s))\): applying a warp to generate an observed curve from the template. In practice, registration works in the inverse direction — estimating \(h_i^{-1}\) to map an observed curve back to system time (i.e., to align it to a template).
This minimal example uses simple shift registration, i.e. the warping model assumes \(h_i(s) = s + b_i\) (no elastic deformation of the domain, simply shifts the functions’ arguments). We pass an explicit template to make the target shape transparent:
pinch_small <- pinch[1:10]
template_affine <- pinch[7] |> tf_smooth(f= .2)
reg_shift <- tf_register(
pinch_small,
method = "affine",
template = template_affine,
type = "shift"
)
#> Warning: ℹ 128 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
#> ℹ 128 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
reg_shift
#> <tf_registration>
#> Call: tf_register(x = pinch_small, type = "shift", template = template_affine,
#> method = "affine")
#> 10 curves on [0, 0.3]
#> Components: aligned, inv_warps, template, original data
summary(reg_shift)
#> tf_register(x = pinch_small, type = "shift", template = template_affine,
#> method = "affine")
#>
#> 10 curve(s) on [0, 0.3]
#>
#> Amplitude variance reduction: 97.1%
#>
#> Inverse warp deviations from identity (relative to domain length):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.0002 0.1027 0.1350 0.1875 0.2027 0.2090 0.2469
#>
#> Inverse warp slopes (1 = identity):
#> overall range: [1, 1]
#> per-curve slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 1 1 1 1 1 1 1
#>
#> Domain coverage loss after alignment (fraction of original range):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.0067 0.0547 0.0733 0.0967 0.1067 0.1087 0.1267
pinch_inv_warp_shift <- tf_inv_warps(reg_shift)
pinch_small_reg <- tf_aligned(reg_shift)
pinch_template_shift <- tf_template(reg_shift)Quick success checks:
NAs are limited and scientifically
acceptable.srvf (default) |
cc |
affine |
landmark |
|
|---|---|---|---|---|
| Use when | Smooth, non-linear timing differences with shared shape family | You want continuous-criterion alignment and can tune criterion | Mostly shift/scale timing variability; need interpretable warps | Reliable, repeated landmarks are available across curves |
| Avoid when | Very noisy data, sparse grids, highly heterogeneous shapes | Complex phase-amplitude interactions or severe irregularity | Strong non-linear timing deformation | Ambiguous/noisy landmarks or mismatched counts |
| Template handling | Implicit Karcher-type mean unless template
supplied |
Arithmetic mean unless template
supplied |
Arithmetic mean unless template
supplied |
Uses template_landmarks (default: column
means) |
| Key arguments | method = "srvf", lambda |
method = "cc", crit = 1/2,
nbasis, lambda, conv,
iterlim |
method = "affine", type,
bounds |
method = "landmark",
landmarks, template_landmarks |
| Input grid | regular only | regular only | regular + irregular | regular + irregular |
| Noise robustness | Low (pre-smooth if noisy) | High (most stable) | Moderate | Moderate; outlier-robust |
| Typical speed | Fast (but O(n2) in grid) | Moderate | Fast | Very fast |
| Typical failure signs | Unstable warps, over-warping, inconsistent reruns | Sensitive to criterion choice, weak alignment gains | Residual misalignment of local features, boundary
NAs under stronger shifts/scales |
Forced/broken alignments from bad landmarks |
| First fallback | Pre-smooth inputs; try affine or
landmark |
Change criterion, basis dimension and/or amount of
penalization; try srvf or affine |
Set stricter/looser bounds; upgrade to
srvf |
Check appropriateness of landmarks; use
srvf or affine |
The srvf and cc methods require regular
grids (tfd_reg or tfb). The
affine and landmark methods also accept
irregular grids (tfd_irreg).
Based on benchmarks across 15 data-generating processes, 3 noise levels, and 5 methods:
landmark. Fastest, simplest warps, and most robust to
outlier contamination (warp MISE degrades only ~1.6× at 30%
contamination vs 2.6–3.7× for iterative methods).affine. Fast, interpretable, but produces boundary
NAs.cc (criterion 2) as
a stable default — the most noise-robust method in our benchmarks. Or
use srvf after pre-smoothing:
tfb(x, k = 25) |> tf_register(method = "srvf").landmark if possible. Otherwise, remove outliers before
registration.srvf. Best overall warp recovery on clean data with
domain-preserving warps, but rankings shift with noise level and
template shape.See Theoretical Background below for method-specific mathematical details.
Issue: Registered curves still form distinct shape clusters.
Remedies: Stratify first; register within clusters; compare cluster-specific results.
Registration assumes a single shared template. If curves come from different shape families, a single registration will try to compromise and may distort curves from both groups.
Issue: Warps become jagged, wiggly or highly variable across near-identical curves.
Remedies: Smooth inputs (more) first; reduce basis flexibility; compare before/after sensitivity.
In our benchmarks2, CC methods (method = "cc")
were the most noise-robust, while SRVF degraded most sharply under noise
— because SRSFs involve numerical derivatives that amplify observation
noise. Pre-smoothing SRVF inputs with tfb(x, k = 25) before
registration reduced warp error (measured as warp MISE) by 50–70% under
moderate noise. Recipe:
x_smooth <- tfb(x, k = 25); reg <- tf_register(x_smooth, method = "srvf").
Issue: Registration quality may be affected strongly by interpolation grid choices.
Remedies: Re-evaluate on a fine, common grid; run sensitivity checks on grid density.
The srvf and cc methods require regular
grids (tfd_reg); interpolate sparse or irregular data to a
common regular grid first. The affine and
landmark methods also accept irregular grids
(tfd_irreg). Grid density can affect results — try at least
two grid resolutions to check stability. SRVF performance varies
substantially with grid resolution due to numerical differentiation:
grid sizes around 100 points are a robust default. Finer grids (>200
points) can degrade SRVF warp recovery under noise by amplifying
derivative artifacts (warp MISE roughly doubles at grid=201 vs 101 under
moderate noise). CC, affine, and landmark methods are largely
grid-insensitive.
Issue: Large NA regions after
unwarping, especially for aggressive affine shifts/scales.
Remedies: Inspect boundary behavior explicitly;
narrow affine bounds via
shift_range/scale_range.
This issue is specific to the affine method, which is
the only non-domain-preserving method in tf. The
srvf, cc, and landmark methods
preserve domain endpoints by construction and do not produce boundary
NAs.
Issue: Extreme timing distortions with little gain in feature alignment.
Remedies: Prefer simpler methods
(affine), reduce flexibility, or avoid registration for
that subset.
This is primarily a risk with flexible methods (srvf,
cc). Affine warps are constrained by design and rarely
over-warp. Comparing results across methods can help detect
over-warping: if a flexible method’s warps are much more variable than
an affine baseline without a clear improvement in alignment,
over-warping may be occurring.
Issue: You want to constrain warp flexibility, but
are unsure which lambda value to use.
Remedies: For cc, lambda
in range 1e-4 to 0.01 is a reasonable starting
point; higher values pull warps toward the identity, reducing
over-warping at the cost of alignment precision. For srvf,
lambda penalization has inconsistent effects across DGPs and noise
levels in our benchmarks — prefer pre-smoothing inputs (e.g.,
tfb(x, k = 25)) over lambda tuning. Note that optimal
lambda values are problem-specific; the ranges above are derived from
oracle (ex-post) analysis and have not been validated via
cross-validation.
Issue: Registration produces poor alignment or increases variability because the default template does not represent the common curve shape.
Remedies: Inspect the template; supply a robust alternative such as the MBD median.
The default template for affine and cc
registration is the pointwise arithmetic mean (re-estimated
iteratively). When phase variation is large — curve features are spread
far apart in time — the pointwise mean gets smeared out and no longer
resembles any individual curve. A robust alternative is to use the most
central observed curve, e.g. the curve with the highest
modified band depth (MBD, see tf_depth()).
# Gaussian bumps with large shifts:
s <- seq(-4, 6, length.out = 201)
mus <- c(-2, -1, 0, 1, 2)
bumps <- tfd(t(sapply(mus, \(mu) dnorm(s, mu, sd = 0.5))), arg = s)
# Pointwise mean is smeared — not a good template:
bumps_mean <- mean(bumps)
# MBD median picks the most central observed curve:
bumps_median <- median(bumps, depth = "MBD")
# Register with default (mean) vs MBD median template:
reg_mean <- tf_register(bumps, method = "affine", type = "shift",
template = bumps_mean)
#> Warning: ℹ 126 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
#> ℹ 126 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
reg_median <- tf_register(bumps, method = "affine", type = "shift",
template = bumps_median)
#> Warning: ℹ 120 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
#> Warning: ℹ 120 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
layout(matrix(1:6, 2, 3, byrow = TRUE))
plot(bumps, col = alpha_palette, lwd = 1.5,
main = "Original + mean template")
lines(bumps_mean, lwd = 3, lty = 2)
plot(tf_inv_warps(reg_mean), col = alpha_palette, lwd = 1.5, points = FALSE,
main = "Inverse warps (mean template)",
xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_mean), col = alpha_palette, lwd = 1.5, points = FALSE,
main = "Aligned to mean")
lines(bumps_mean, lwd = 3, lty = 2)
plot(bumps, col = alpha_palette, lwd = 1.5,
main = "Original + MBD median template")
lines(bumps_median, lwd = 3, lty = 2, col = "red3")
plot(tf_inv_warps(reg_median), col = alpha_palette, lwd = 1.5, points = FALSE,
main = "Inverse warps (median template)",
xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_median), col = alpha_palette, lwd = 1.5, points = FALSE,
main = "Aligned to MBD median")
lines(bumps_median, lwd = 3, lty = 2, col = "red3")The pointwise mean (top row, dashed) is smeared and not
representative of any individual curve’s shape. Registration toward it
produces poor alignment. The MBD median (bottom row, dashed red) is the
most central observed curve — it has the correct shape, and registration
aligns the peaks well. When you expect strong phase variation,
inspect the default template and consider supplying a
suitable custom template like template = median(x).
After any registration run, use summary() and
plot() for a quick assessment, then dig deeper as
needed:
summary(reg): Check amplitude variance
reduction (should be positive), aggregated warp deviations (how far from
identity?), min and max warp slopes (any extreme local distortions?),
and domain coverage loss (for affine warps).plot(reg): Three-panel view of
original curves + template, inverse warping functions + identity line,
and aligned curves + template. Check that features are more aligned and
warps are plausible.We demonstrate this workflow below using the pinch
data.
x <- pinch[1:10]
# Register with affine shift warps:
reg_aff <- tf_register(x, method = "affine", type = "shift_scale")
#> Warning: ℹ 48 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
#> Warning: ℹ 46 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
#> ℹ 46 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
# summary() gives a quick quantitative overview:
summary(reg_aff)
#> tf_register(x = x, type = "shift_scale", method = "affine")
#>
#> 10 curve(s) on [0, 0.3]
#>
#> Amplitude variance reduction: 97.9%
#>
#> Inverse warp deviations from identity (relative to domain length):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.0483 0.0989 0.1222 0.1478 0.1725 0.1855 0.2374
#>
#> Inverse warp slopes (1 = identity):
#> overall range: [1.088, 1.287]
#> per-curve slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 1.088 1.099 1.116 1.135 1.227 1.265 1.287
#>
#> Domain coverage loss after alignment (fraction of original range):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.0000 0.0000 0.0000 0.0200 0.0567 0.0640 0.1000Key things to check in the summary:
plot()Check that (1) the template (dashed) looks representative of the original curve shapes, (2) inverse warping functions are not too far from the identity and have no extremely flat or steep segments, and (3) aligned curves show better feature alignment to the template.
For this example:
# Are global peak locations more concentrated after alignment?
peak_before <- tf_where(x, value == max(value)) |> as.numeric()
peak_after_aff <- tf_where(tf_aligned(reg_aff), value == max(value)) |> as.numeric()
data.frame(
metric = c("sd_peak_before", "sd_peak_after_affine"),
value = c(sd(peak_before), sd(peak_after_aff))
)
#> metric value
#> 1 sd_peak_before 0.028031728
#> 2 sd_peak_after_affine 0.002796824# Compare with SRVF (non-linear warps):
reg_srvf <- tf_register(x, method = "srvf")
# Side-by-side summaries:
summary(reg_srvf)
#> tf_register(x = x, method = "srvf")
#>
#> 10 curve(s) on [0, 0.3]
#>
#> Amplitude variance reduction: 97.2%
#>
#> Inverse warp deviations from identity (relative to domain length):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.0427 0.0767 0.0839 0.1141 0.1279 0.1431 0.2048
#>
#> Inverse warp slopes (1 = identity):
#> overall range: [0.122, 7.461]
#> per-curve min slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.122 0.150 0.180 0.208 0.244 0.252 0.278
#> per-curve max slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 3.509 4.226 5.262 5.513 6.237 7.205 7.461peak_after_srvf <- tf_where(tf_aligned(reg_srvf), value == max(value)) |> as.numeric()
data.frame(
metric = c("sd_peak_before", "sd_peak_after_affine", "sd_peak_after_srvf"),
value = c(sd(peak_before), sd(peak_after_aff), sd(peak_after_srvf))
)
#> metric value
#> 1 sd_peak_before 0.028031728
#> 2 sd_peak_after_affine 0.002796824
#> 3 sd_peak_after_srvf 0.001751190layout(matrix(1:6, 2, 3, byrow = TRUE))
# Affine:
plot(x, main = "Original", col = alpha_palette, lwd = 1.5)
lines(tf_template(reg_aff), lwd = 2, lty = 2)
plot(tf_inv_warps(reg_aff), main = "Affine Inverse Warps",
col = alpha_palette, lwd = 1.5, points = FALSE,
xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_aff), main = "Affine Aligned",
col = alpha_palette, lwd = 1.5, points = FALSE)
lines(tf_template(reg_aff), lwd = 2, lty = 2)
# SRVF:
plot(x, main = "Original", col = alpha_palette, lwd = 1.5)
lines(tf_template(reg_srvf), lwd = 2, lty = 2)
plot(tf_inv_warps(reg_srvf), main = "SRVF Inverse Warps",
col = alpha_palette, lwd = 1.5, points = FALSE,
xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_srvf), main = "SRVF Aligned",
col = alpha_palette, lwd = 1.5, points = FALSE)
lines(tf_template(reg_srvf), lwd = 2, lty = 2)If different methods disagree strongly and diagnostics are unstable, treat conclusions as low confidence until you resolve data representation and model-choice sensitivity.
Problem is to find an optimal deformation of the function’s domain:
\[ \text{Find } h_i(s) \text{ so that } d\left(x_i(h_i(s)), m(s)\right) \to \min \]
tf_estimate_warps() (called
internally by tf_register()), and registration methods
differ in the representations and constraints for these (see
below).tf_align() to align observed curves, can be
inspected/extracted via
tf_inv_warps(<tf_registration>).Key references: Marron et al. (2015) provide an overview of amplitude and phase variation in FDA. Ramsay & Silverman (2005, Functional Data Analysis, Ch. 7–8) cover landmark and continuous registration. Srivastava & Klassen (2016, Functional and Shape Data Analysis) develop the elastic/SRVF framework in depth.
Represents each curve via its square root velocity function
(SRVF), \(q(t) = \dot{x}(t) /
\sqrt{|\dot{x}(t)|}\), and aligns curves by minimizing \(L_2\) distances between (aligned) SRVFs.
This corresponds to minimizing an elastic distance metric
between functions modulo reparameterization (i.e. under “warping”) and
transforms the non-linear alignment problem into a simpler optimization
on a Hilbert sphere3. See Srivastava et
al. (2011) and Tucker et
al. (2013) for the fdasrvf implementation.
fdasrvf).method = "srvf". Pass
template to override the Karcher mean. Control warp
flexibility via lambda (default is 0 for
unrestricted/unpenalized warps) and penalty_method
("roughness" (default), "geodesic", or
"norm").tfb(x, k = 25)) before
registration reduces warp error (measured as warp MISE) by 50–70% under
moderate noise. Also grid-sensitive: avoid grids >200 points on noisy
data (see Sparse or irregular
grids). Shows the most variable performance under template
estimation compared to other methods.Estimates smooth monotone warping functions by maximizing the
integrated cross-correlation between the derivative of each registered
curve and the first eigenfunction of the derivatives’ covariance, which
represents the dominant common shape of the sample (criterion
crit=2, the default) or by minimizing the squared
differences between aligned function and template (criterion
crit=1, not recommended). In current tf, this
is implemented as a tf-native dense-grid optimizer with monotone spline
warps rather than the older fda::register.fd() backend. See
Ramsay & Li (1998,
JRSS-B) and Ramsay & Silverman (2005, Functional Data
Analysis, Ch. 8).
max_iter iterations (default 3), or user-supplied.method = "cc". Use
crit = 2 (default) to maximize the proportion of variance
explained by the first eigenfunction of the registered sample, or
crit = 1 to minimize integrated squared differences.
Control warp flexibility via B-spline basis dimension of the warping
functions nbasis (default is 6), their
wiggliness via penalty parameter lambda (default is
0 for no penalization), and optimizer tolerances via
conv and iterlim. In our experience,
crit = 1 without penalization tends to be considerably less
reliable than crit = 2 or penalized variants — the
unpenalized L2 criterion can produce strongly distorted warps.lambda > 0) in most conditions.Models warps as linear functions \(h(s) = a \cdot s + b\), with three sub-types:
type = "shift" (default): \(a
= 1\), only horizontal translation (\(b\) free).type = "scale": only uniform time scaling (\(a\) free, \(b\) derived from centering).type = "shift_scale": both parameters free.Each curve is aligned independently via bounded L-BFGS-B optimization of the L2 distance to the template. See Ramsay & Silverman (2005, Ch. 7) and Wang & Gasser (1997) for context on shift/scale alignment models.
NAs after tf_align().tfd_reg and tfd_irreg).method = "affine",
type, shift_range and scale_range
to set upper and lower limits for time shifts \(b_i\) and time scales \(a_i\).Constructs piecewise linear warps by mapping user-specified landmark positions to target positions. No continuous optimization is performed — the warp is fully determined by the landmark correspondence. See Kneip & Gasser (1992, Annals of Statistics; pdf) and Ramsay & Silverman (2005, Ch. 7).
template_landmarks.tfd_reg and tfd_irreg).method = "landmark",
landmarks (required: \(n \times
k\) matrix of landmark positions),
template_landmarks (optional target positions).tf_landmarks_extrema() can automatically detect local
maxima, minima, or zero crossings and cluster them across curves -
usually better to pre-smooth noisy inputs with tf_smooth()
or tfb before calling this function. Results will typically
be better for user-defined landmarks that are based on domain
knowledge.pinch_small <- pinch[1:10]
reg_aff <- tf_register(pinch_small, method = "affine", type = "shift_scale")
#> Warning: ℹ 48 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
#> Warning: ℹ 46 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
#> ℹ 46 evaluations were `NA`
#> ✖ Returning irregular <tfd>.
inv_warp_aff <- tf_inv_warps(reg_aff)
# tf_landmarks_extrema needs smoothed inputs,
# otherwise it tends to detect lots of spurious features:
pinch_small |> tf_landmarks_extrema(which = "max") |> head()
#> [,1] [,2] [,3] [,4]
#> [1,] 0.028 0.160 0.222 0.284
#> [2,] 0.040 0.136 0.226 0.284
#> [3,] 0.036 0.160 0.226 0.282
#> [4,] 0.044 0.144 0.236 0.284
#> [5,] 0.040 0.126 0.228 0.284
#> [6,] 0.038 0.164 0.224 0.284
# ... so in this case, we simply use the global maximum for each curve:
(peak_locs <- pinch_small |> tf_where(value == max(value)) |> unlist() |> as.matrix())
#> [,1]
#> [1,] 0.082
#> [2,] 0.144
#> [3,] 0.082
#> [4,] 0.144
#> [5,] 0.090
#> [6,] 0.080
#> [7,] 0.114
#> [8,] 0.134
#> [9,] 0.140
#> [10,] 0.090
reg_lm <- tf_register(pinch_small, method = "landmark", landmarks = peak_locs)
inv_warp_lm <- tf_inv_warps(reg_lm)
# ... but using more than one peak location as the only landmark will get us
# better alignment here - use times where curve first & last exceeds 3 as well:
pinch_landmarks <- cbind(
start = pinch_small |> tf_where(value > 3, return = "first") |> unlist(),
peak = peak_locs,
end = pinch_small |> tf_where(value > 3, return = "last") |> unlist()
)
pinch_landmarks |> head()
#> start end
#> [1,] 0.042 0.082 0.148
#> [2,] 0.102 0.144 0.206
#> [3,] 0.046 0.082 0.160
#> [4,] 0.112 0.144 0.214
#> [5,] 0.056 0.090 0.154
#> [6,] 0.046 0.080 0.148
reg_lm2 <- tf_register(pinch_small, method = "landmark", landmarks = pinch_landmarks)
inv_warp_lm2 <- tf_inv_warps(reg_lm2)layout(t(matrix(1:12, 4, 3)))
par(cex.main = 0.8)
plot.new()
plot(inv_warp_aff, main = "Affine Inverse Warps", ylab = "",
points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_aff), main = "Affine Registered",
col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_aff), main = "Affine Registered",
type = "lasagna", col = hcl.colors(12, rev = TRUE))
plot(pinch_small, main = "Original",
col = alpha_palette, lwd = 1.5)
plot(inv_warp_lm, main = "Landmark Inverse Warps \n (Peak only)", ylab = "",
points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_lm), main = "Landmark Registered\n (Peak only)", col = alpha_palette, lwd = 1.5)
plot(tf_aligned(reg_lm), main = "Landmark Registered\n (Peak only)", type = "lasagna", col = hcl.colors(12, rev = TRUE))
plot.new()
plot(inv_warp_lm2, main = "Landmark Inverse Warps \n (Start + Peak + End)", ylab = "",
points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_lm2), main = "Landmark Registered\n (Start + Peak + End)", col = alpha_palette, lwd = 1.5)
plot(tf_aligned(reg_lm2), main = "Landmark Registered\n (Start + Peak + End)", type = "lasagna", col = hcl.colors(12, rev = TRUE))Note that affine registration with shift+scale warps
produces some boundary NAs because the warps are not
domain-preserving, but it does a good job aligning the peaks. Landmark
registration with only the peak locations does a decent job aligning the
peaks without producing NAs, but it does not align the
start and end of the curves well. Landmark registration with start +
peak + end landmarks does a good job aligning all three features without
producing NAs.
reg_srvf <- tf_register(pinch_small, method = "srvf")
inv_warp_srvf <- tf_inv_warps(reg_srvf)
reg_cc_unpen <- tf_register(pinch_small, method = "cc", max_iter = 10, nbasis = 10, crit = 1)
#> Iterative registration stopped after 4 of 10 iterations: alignment worsened
#> (objective 0.5535 > 0.538).
inv_warp_cc_unpen <- tf_inv_warps(reg_cc_unpen)
reg_cc_pen <- tf_register(pinch_small, method = "cc", lambda = 1e-4, max_iter = 20)
#> Iterative registration stopped after 17 of 20 iterations: alignment worsened
#> (objective 0.4379 > 0.437).
inv_warp_cc_pen <- tf_inv_warps(reg_cc_pen)SRVF and CC both use non-linear warps and tend to be able to align
more complex phase variation than the affine method, but
their warps are more complex and potentially harder to interpret, and
registration can go off the rails more easily (as we can see here with
the unpenalized cc registration plotted below, check the
huge maximal warp slope and its poor amplitude variance reduction…) –
using summary(<tf_registration>) for quick
quantitative diagnostics:
summary(reg_cc_unpen)
#> tf_register(x = pinch_small, nbasis = 10, crit = 1, method = "cc",
#> max_iter = 10)
#>
#> 10 curve(s) on [0, 0.3]
#>
#> Amplitude variance reduction: 98.7%
#>
#> Inverse warp deviations from identity (relative to domain length):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.1147 0.1393 0.1532 0.1667 0.1905 0.1958 0.1971
#>
#> Inverse warp slopes (1 = identity):
#> overall range: [0.285, 3.841]
#> per-curve min slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.285 0.327 0.357 0.437 0.456 0.467 0.482
#> per-curve max slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 2.265 2.314 2.489 2.870 3.230 3.728 3.841
# ... ouch! max slope almost 100 and only 40% amplitude variance reduction ....
summary(reg_srvf)
#> tf_register(x = pinch_small, method = "srvf")
#>
#> 10 curve(s) on [0, 0.3]
#>
#> Amplitude variance reduction: 97.2%
#>
#> Inverse warp deviations from identity (relative to domain length):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.0427 0.0767 0.0839 0.1141 0.1279 0.1431 0.2048
#>
#> Inverse warp slopes (1 = identity):
#> overall range: [0.122, 7.461]
#> per-curve min slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.122 0.150 0.180 0.208 0.244 0.252 0.278
#> per-curve max slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 3.509 4.226 5.262 5.513 6.237 7.205 7.461
summary(reg_cc_pen)
#> tf_register(x = pinch_small, lambda = 1e-04, method = "cc", max_iter = 20)
#>
#> 10 curve(s) on [0, 0.3]
#>
#> Amplitude variance reduction: 97.1%
#>
#> Inverse warp deviations from identity (relative to domain length):
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.0187 0.0592 0.0817 0.1150 0.1532 0.1707 0.1940
#>
#> Inverse warp slopes (1 = identity):
#> overall range: [0.507, 1.863]
#> per-curve min slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 0.507 0.549 0.592 0.776 0.819 0.854 0.875
#> per-curve max slopes:
#> 0% 10% 25% 50% 75% 90% 100%
#> 1.054 1.200 1.227 1.570 1.666 1.696 1.863layout(t(matrix(1:12, 4, 3)))
par(cex.main = 0.8)
plot.new()
plot(inv_warp_srvf, main = "SRVF Inverse Warps", ylab = "",
points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_srvf), main = "SRVF Registered",
col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_srvf), main = "SRVF Registered",
type = "lasagna", col = hcl.colors(12, rev = TRUE))
plot(pinch_small, main = "Original",
col = alpha_palette, lwd = 1.5)
plot(inv_warp_cc_unpen, main = "CC Inverse Warps (unpen.)", ylab = "",
points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_cc_unpen), main = "CC Registered (unpen.)",
col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_cc_unpen), main = "CC Registered (unpen.)",
type = "lasagna", col = hcl.colors(12, rev = TRUE))
plot.new()
plot(inv_warp_cc_pen, main = expression("CC Inverse Warps (" * lambda * " = 1e-4)"),
ylab = "", points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_cc_pen), main = expression("CC Registered (" * lambda * " = 1e-4)"),
col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_cc_pen), main = expression("CC Registered (" * lambda * " = 1e-4)"),
type = "lasagna", col = hcl.colors(12, rev = TRUE))The Berkeley growth data contains height measurements for 39 boys and 54 girls aged 1–18. Growth velocity curves (first derivatives of height) show a prominent pubertal growth spurt whose timing varies substantially between individuals — a natural target for registration. We use the subset of girls5 from this dataset to illustrate how data representation, penalization, and landmark choice affect registration quality.
growth <- tf::growth |> dplyr::filter(gender == "female")
# Raw velocity via finite differences — noisy, only 30 midpoints from 31 measurements:
growth$raw_vel <- tf_derive(growth$height)
# Smooth velocity via spline representation on a much denser grid, then derive analytically:
growth$smooth_vel <- tfb(
growth$height,
k = 15,
bs = "tp",
arg = seq(1, 18, l = 80),
global = TRUE
# family = gaussian(link = "log") # ensures positivity of velocity
) |>
tf_derive()
#> Using global smoothing parameter `sp = 0.008` estimated on subsample of curves.
#> Percentage of input data variability preserved in basis representation
#> (per functional observation, approximate):
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 100 100 100 100 100 100The raw finite-difference velocity estimates are jagged (30 irregularly spaced points from the original measurement grid). SRVF registration on such noisy inputs tends to produce wiggly warps that chase noise rather than genuine phase variation. Converting to a smooth representation first gives SRVF cleaner input and much better results.
# SRVF on raw (noisy) velocity:
reg_raw_obj <- tf_register(growth$raw_vel, method = "srvf")
inv_warp_raw <- tf_inv_warps(reg_raw_obj)
reg_raw <- tf_aligned(reg_raw_obj)
# SRVF on smooth velocity:
reg_smooth_obj <- tf_register(growth$smooth_vel, method = "srvf")
inv_warp_smooth <- tf_inv_warps(reg_smooth_obj)
reg_smooth <- tf_aligned(reg_smooth_obj)reg_brks <- range(c(tf_evaluations(reg_raw), tf_evaluations(reg_smooth))) |>
(\(x) seq(x[1], x[2], l = 13))()
layout(t(matrix(1:8, 4, 2)))
par(cex.main = 0.8)
plot(growth$raw_vel, main = "Raw Velocity (finite diff.)",
xlab = "Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(inv_warp_raw, points = FALSE, main = "SRVF Inverse Warps (raw)",
xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_raw, main = "SRVF Registered (raw)",
xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(reg_raw, main = "SRVF Registered (raw)",
type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)
plot(growth$smooth_vel, main = "Smooth Velocity (tfb deriv.)",
xlab = "Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(inv_warp_smooth, points = FALSE, main = "SRVF Inverse Warps (smooth)",
xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_smooth, main = "SRVF Registered (smooth)",
xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(reg_smooth, main = "SRVF Registered (smooth)",
type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)The top row shows extreme, biologically implausible warps (i.e. mapping chronological ages 5-8 to “registered” ages < 2) with local wiggles that are more likely to reflect noise in the raw velocity estimates rather than genuine timing differences. The bottom row, using smoother velocity curves, produces simpler and less extreme warps that capture the pubertal timing variation — the main feature of interest.
Even on noisy inputs, SRVF penalization (lambda > 0)
can help suppress over-warping. Higher lambda pulls warps
toward the identity (no warping), trading less strict alignment for
smoother and more subtle warps.
layout(t(matrix(1:4, 4, 1)))
par(cex.main = 0.8)
plot(growth$raw_vel, main = "Raw Velocity",
xlab = "Age [years]", ylab = "cm/year", lwd = 0.8)
plot(inv_warp_raw_pen, points = FALSE,
main = expression("Penalized Warps (raw, " * lambda * " = 0.1)"),
xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_raw_pen,
main = expression("Penalized Registered (raw, " * lambda * " = 0.1)"),
xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8)
plot(reg_raw_pen, main = "Penalized Registered (raw)",
type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)Penalization smooths out the worst warp artifacts, and the result
here looks more reasonable than the result for the (oversmoothed)
tfb-inputs above. That said, in most cases the best
strategy is likely to be to (carefully!) pre-smooth, and then to use
penalization only if the warps still look too flexible.
Landmark registration avoids continuous optimization entirely — it constructs piecewise linear warps from a (user-specified) correspondence of curve features. For growth velocity, natural landmarks are the end of rapid infant growth (velocity drops a lot below initial velocity), pre-pubertal trough (minimum velocity before the puberty growth spurt), the pubertal peak (maximum velocity during puberty). We identify these by searching for the minimum in age 5–(peak-1) and the maximum in age 8–17 on each smooth velocity curve.
# End of rapid infant growth:
# less than 2/3 of max early childhood growth (1-3) velocity before age 5
growth_slows <- growth$smooth_vel |> tf_zoom(begin = 1, end = 5) |>
tf_where(value < 0.66 * max(value[1:10]), return = "first") |> unlist()
# Pubertal peak: maximum velocity in age 8--17
growth_peaks <- growth$smooth_vel |> tf_zoom(begin = 8, end = 17) |>
tf_where(value == max(value)) |> unlist()
# Pre-pubertal trough: minimum velocity between age 5 and 1 year before the peak
growth_troughs <- growth$smooth_vel |> tf_zoom(begin = 5, end = growth_peaks - 1) |>
tf_where(value == min(value)) |> unlist()
#> ✖ `tf_zoom()` was called with varying start or end points -- converting to
#> <tfd>.
# Build landmark matrix and register:
growth_lm <- cbind(slowdown = growth_slows, trough = growth_troughs, peak = growth_peaks)
reg_lm_obj <- tf_register(growth$smooth_vel, method = "landmark", landmarks = growth_lm)
inv_warp_lm <- tf_inv_warps(reg_lm_obj)
reg_lm <- tf_aligned(reg_lm_obj)if (!exists("reg_brks")) {
reg_brks <- range(c(tf_evaluations(growth$smooth_vel), tf_evaluations(reg_lm))) |>
(\(x) seq(x[1], x[2], l = 13))()
}
layout(t(1:4))
plot(growth$smooth_vel, main = "Smooth Velocity",
xlab = "Age [years]", ylab = "cm/year", lwd = 0.8)
plot(inv_warp_lm, points = FALSE, main = "Landmark Inverse Warps",
xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_lm, main = "Landmark Registered",
xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8)
plot(reg_lm, main = "Landmark Registered",
type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)The piecewise linear warps align the pubertal peaks well, with the pubertal trough and infant growth landmarks anchoring the earlier phases. Compared to SRVF, the warps are simpler and fully interpretable — each segment directly corresponds to a physiological period (early childhood, late childhood, pubertal acceleration, post-peak deceleration). The trade-off is that landmark registration cannot adapt between landmarks (alignment between successive landmarks depends entirely on those two anchor points) and that landmark definition is often somewhat arbitrary (c.f. definition of “end of rapid infant growth” above…).
The Karcher mean (also called Fréchet mean) generalizes arithmetic means to general spaces. In this case, it is a centroid in the amplitude quotient space (functions modulo reparameterization), computed iteratively using the elastic (Fisher-Rao) distance rather than pointwise averaging.↩︎
For full details on the benchmark design and results, see tidyfun.github.io/sim-registration↩︎
more specifically: the positive orthant of such a sphere, which is the space of SRVFs of warping functions↩︎
smooth, monotone, one-to-one functions, basically.↩︎
.. because aligning female and male growth curves to the same template does not make sense – they show different patterns!↩︎