knitr::opts_chunk$set( cache = TRUE, collapse = TRUE, comment = "#>", dpi = 72, fig.retina = 1 ) library(tf) oldpar <- par(no.readonly = TRUE) par(las = 1) alpha_palette <- c( "#00000080", "#DF536B80", "#61D04F80", "#2297E680", "#28E2E580", "#CD0BBC80", "#F5C71080", "#9E9E9E80", "#00000080", "#DF536B80" ) have_srvf <- rlang::is_installed("fdasrvf") have_cc <- TRUE
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 shape^[The Karcher mean (also called Frechet 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.] 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:
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).
# Synthetic illustration of different warp types s <- seq(0, 1, length.out = 201) # Template: template_synth <- tfd(exp(-((s - 0.45)^2) / (2 * 0.08^2)) + cos(2*pi*s) + s, arg = s) # Warp 1: affine shift + scale (not domain-preserving -- extends beyond [0,1]) h_shift <- tfd(0.8 * s + 0.1, arg = s) # Warp 2: smooth diffeomorphism (domain-preserving) h_smooth_raw <- s - 0.25 * sin(pi * s) h_smooth <- tfd( (h_smooth_raw - h_smooth_raw[1]) / (h_smooth_raw[length(s)] - h_smooth_raw[1]), arg = s) # Warp 3: wiggly diffeomorphism (domain-preserving) h_wiggly_raw <- .1 * pbeta(s, .1, .1) + .5 * pbeta(s, 25, 25) + .2 * pbeta(s, 4, 20) + .2 * pbeta(s, 100, 10) h_wiggly <- tfd(h_wiggly_raw, arg = s) # Compose template with warps: m(h(s)) warped_shift <- tf_warp(template_synth, h_shift) |> suppressWarnings() warped_smooth <- tf_warp(template_synth, h_smooth) warped_wiggly <- tf_warp(template_synth, h_wiggly) warp_cols <- c("#DF536B", "#2297E6", "#61D04F") layout(matrix(1:9, 3, 3)) par(mar = c(4, 4, 3, 1)) warp_names <- c("Affine (shift & scale)", "Smooth elastic", "Wiggly elastic") warps <- list(h_shift, h_smooth, h_wiggly) warped <- list(warped_shift, warped_smooth, warped_wiggly) # Column 1: blank, template, blank plot.new() plot(template_synth, lwd = 2.5, main = "Template m(s)", xlab = "s", ylab = "m(s)") plot.new() # Column 2: one warp per row for (i in 1:3) { plot(s, s, type = "l", lty = 3, lwd = 1, main = paste0("h(s): ", warp_names[[i]]), xlab = "s", ylab = "h(s)", ylim = c(-0.05, 1.15)) do.call(graphics::lines, list(warps[[i]], col = warp_cols[[i]], lwd = 2.5)) } # Column 3: template (dashed) + warped curve per row for (i in 1:3) { plot(template_synth, lwd = 2, lty = 3, main = paste0("x(t) = m(h(s)): ", warp_names[[i]]), xlab = "t", ylab = "x(t)") do.call(graphics::lines, list(warped[[i]], col = warp_cols[[i]], lwd = 2.5)) }
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" ) reg_shift summary(reg_shift) pinch_inv_warp_shift <- tf_inv_warps(reg_shift) pinch_small_reg <- tf_aligned(reg_shift) pinch_template_shift <- tf_template(reg_shift)
# plot(reg_shift) gives a compact diagnostic; below reuses the extracted # components for a customized version of the same view: layout(t(1:3)) plot( pinch_small, main = "Original Curves", xlab = "Observed Time t [sec]", ylab = "Force [N]", lwd = 1.5, col = alpha_palette ) lines(pinch_template_shift, lwd = 2, lty = 3, col = rgb(0,0,.5)) plot( pinch_inv_warp_shift, points = FALSE, main = "Inverse Warping Functions", xlab = "Observed Time t [sec]", ylab = "Registered Time s [sec]", lwd = 1.5, col = alpha_palette ) abline(0, 1, lty = 3) plot( pinch_small_reg, main = "Registered Curves", xlab = "Registered Time s [sec]", ylab = "Force [N]", lwd = 1.5, col = alpha_palette, points = FALSE ) lines(pinch_template_shift, lwd = 2, lty = 3, col = rgb(0,0,.5))
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(n^2^) 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.6x at 30% contamination vs 2.6-3.7x 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 benchmarks^[For full details on the benchmark design and results, see tidyfun.github.io/sim-registration], 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) reg_median <- tf_register(bumps, method = "affine", type = "shift", template = bumps_median) 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") # summary() gives a quick quantitative overview: summary(reg_aff)
Key things to check in the summary:
plot()# plot() provides a 3-panel diagnostic: plot(reg_aff)
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)) )
# Compare with SRVF (non-linear warps): reg_srvf <- tf_register(x, method = "srvf") # Side-by-side summaries: summary(reg_srvf)
peak_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)) )
layout(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 sphere^[more specifically: the positive orthant of such a sphere, which is the space of SRVFs of warping functions]. 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") 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() # ... 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()) 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() 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) 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) 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) # ... ouch! max slope almost 100 and only 40% amplitude variance reduction .... summary(reg_srvf) summary(reg_cc_pen)
layout(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 girls^[.. because aligning female and male growth curves to the same template does not make sense -- they show different patterns!] 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()
The 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.
# Penalized SRVF on the raw velocity: reg_raw_pen_obj <- tf_register(growth$raw_vel, method = "srvf", lambda = 0.1) inv_warp_raw_pen <- tf_inv_warps(reg_raw_pen_obj) reg_raw_pen <- tf_aligned(reg_raw_pen_obj)
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() # 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...).
par(oldpar)
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.