Mass spectrometry imaging measures analyte response
at each pixel rather than analyte amount directly. Using calibration
standards, quantMSImageR can convert this response into an
estimated amount per pixel (pg/pixel) and, when pixel
dimensions are known, an estimated real amount
(pg/mm²). These estimates are conditional on the calibration design,
internal standard choice and strategy, matrix matching, acquisition
conditions, and validity of the fitted model.
On-slide (external) calibration deposits standards
on the slide adjacent to the tissue. This captures instrumental response
during the same acquisition but does not reproduce tissue-specific
matrix effects. Use cal_type = "cal".
On-slide calibration. Standards are deposited on the slide beside the section, so they share the acquisition but not the tissue matrix. Amount is read straight off the fitted line, whose intercept is instrument background. Schematic.
On-tissue calibration deposits standards onto
tissue, or onto a representative tissue homogenate. Matrix matching is
better, but endogenous analyte signal and spatial heterogeneity still
need careful treatment. Use cal_type = "std_addition".
Standard addition: create_cal_curve() fits
response against deposited amount, takes the x-intercept of that line as
the endogenous amount already present in the tissue, shifts every
deposited amount by it so the axis expresses total amount
(endogenous + added), drops the background level and refits. The
estimate therefore accounts for analyte the tissue already
contained.
On-tissue calibration by standard addition. Standards are deposited onto the section, so they experience the same matrix as the analyte. The response at zero added amount already contains endogenous analyte, so the line is extrapolated back (dashed) to the x-intercept, and that amount is added to every deposited amount. Schematic.
cal_type has no default: the two modes do materially
different things, so it must be chosen explicitly.
Amount estimates are only meaningful where the following hold. Each is the user’s responsibility:
plot_cal_coverage());Keeping the analytical steps distinct from the interpretive one:
int2response());int2snr());int2conc());Steps 1–4 are analytical transformations performed by the package. Step 5 remains conditional on the assumptions above.
When a calibration standards acquisition is available,
quantMSImageR fits a response-versus-amount model per
analyte and converts tissue-pixel responses to pg_pixel and
pg_mm2. The workflow is three steps:
summarise_cal_levels() – average each calibration level
per standard spot,create_cal_curve() – fit the response-vs-amount model
per analyte,int2conc() – apply those models to convert tissue-pixel
responses to estimated amounts,then plot_cal_coverage() to confirm the standards
actually bracket the signal measured in tissue.
Family: calibration.
| Function | Takes | Produces |
|---|---|---|
summarise_cal_levels() |
object with Cal pixels + identifier;
cal_metadata (identifier,
analyte, amount_pg, level) |
fills cal_response_data and the per-level pixel
counts |
create_cal_curve() |
that object; cal_type = "cal" or
"std_addition" |
fills cal_list (one linear model per analyte) and
r2_df |
int2conc() |
object carrying cal_list; pixel_header /
pixels; experimentData$pixelSize for the mm²
layer |
adds pg_pixel and pg_mm2; features without
a curve are dropped |
plot_cal_coverage() |
a calibrated object (after int2conc()) |
a patchwork figure: pixel histogram above the
calibration curve |
This vignette demonstrates the workflow on a small synthetic
calibration dataset bundled with the package. It applies the resulting
models to a study section, and then shows how the same steps are driven
from a study YAML configuration.
run_example(calibrate = TRUE) runs the chain end-to-end.
The core imaging workflow that produces the object these operate on –
SNR filtering, ion images, heatmaps – is covered in the Introduction
to quantMSImageR vignette.
The approach is described and applied in Smith et al. (2024); see References.
The synthetic calibration object (generated by
inst/scripts/generate_cal_data.R) is a
quant_MSImagingExperiment whose pixels are labelled
Cal (standard spots), Tissue or
Background in sample_type, with a unique
identifier per standard spot. Its analytes are named to
match features in the imaging example, so the fitted curves can be
applied to a study section later.
library(quantMSImageR)
library(ggplot2)
cal_dir <- system.file("extdata", "cal_example.raw", package = "quantMSImageR")
cal <- as(readRDS(file.path(cal_dir, "cal_MSI.RDS")),
"quant_MSImagingExperiment")
cal_metadata <- read.csv(file.path(cal_dir, "calibration_metadata.csv"))
pix <- as.data.frame(table(pData(cal)$sample_type))
names(pix) <- c("sample_type", "n_pixels")Table 1. How the calibration acquisition’s pixels
are labelled. Cal are the standard spots used to build the
curves; Tissue and Background are the
surrounding sample and background.
The companion metadata table is the link between a physical standard
spot and what was deposited on it: each identifier maps to
an analyte, the amount spotted (amount_pg) and
which dilution level it belongs to.
Table 2. Calibration metadata: amount of each analyte deposited at every standard spot.
summarise_cal_levels() averages the signal at each
calibration spot, and create_cal_curve() fits a linear
response-vs-amount model per analyte (here cal_type = "cal"
for on-slide standards; use "std_addition" for standard
addition on tissue).
cal <- summarise_cal_levels(cal, cal_metadata, val_slot = "intensity",
cal_label = "Cal", id = "identifier")
cal <- create_cal_curve(cal, cal_type = "cal")Each fit is a straight line,
response = slope x (pg/pixel) + intercept, which
int2conc() later inverts to go from response back to
amount. The slope is the analyte’s sensitivity.
cal_list <- calibrationModels(cal)
r2_df <- calibrationDiagnostics(cal)
eqn <- data.frame(
analyte = names(cal_list),
slope = vapply(cal_list, function(m) unname(coef(m)[2]), numeric(1)),
intercept = vapply(cal_list, function(m) unname(coef(m)[1]), numeric(1)),
row.names = NULL
)
eqn$r2 <- r2_df$r2[match(eqn$analyte, r2_df$feature)]DT::datatable(eqn, rownames = FALSE,
options = list(dom = "t", scrollX = TRUE)) |>
DT::formatSignif(c("slope", "intercept", "r2"), digits = 4)Table 3. Fitted calibration model per analyte: the
slope, intercept and R-squared of the line that int2conc()
inverts.
R² reports only how much of the variance in the standards the straight line accounts for. Treat a low R² as a definite warning and a high R² as necessary but not sufficient.
Before relying on the estimates, examine at minimum:
plot_cal_coverage(), below);create_cal_curve()
currently applies inverse-amount (1/amount) weighting. This
reduces the influence of the highest calibration levels, but its
suitability should be assessed from the residual structure of your
analytical method.Plotting those fits gives the calibration curves. The line must come from the stored model:
cal_data <- calibrationLevels(cal)
models <- calibrationModels(cal)
# Predict from each stored model across the amounts it was fitted to.
cal_lines <- lapply(names(models), function(analyte) {
m <- models[[analyte]]
xx <- seq(min(m$model$pg_perpixel), max(m$model$pg_perpixel),
length.out = 100)
data.frame(analyte = analyte,
pg_perpixel = xx,
response_perpixel = predict(m, newdata = data.frame(pg_perpixel = xx)))
})
cal_lines <- do.call(rbind, cal_lines)
ggplot(cal_data, aes(pg_perpixel, response_perpixel)) +
geom_point() +
geom_line(data = cal_lines, colour = "steelblue", linewidth = 0.8) +
facet_wrap(~ analyte, scales = "free") +
labs(x = "amount (pg / pixel)", y = "response per pixel")Calibration curves. Each point is one standard spot’s summarised response; the line is predicted from the stored, weighted model that int2conc() inverts.
The models are fitted on the standards, then carried across to the study data. Here we load one section of the imaging example (the circular tissue of group A) and attach the fitted models before converting its tissue pixels:
study <- as(readRDS(system.file("extdata", "example.raw", "section01.RDS",
package = "quantMSImageR")),
"quant_MSImagingExperiment")
# carry the fitted curves over, then convert this section's tissue pixels
calibrationData(study) <- calibrationData(cal)
study <- int2conc(study, val_slot = "intensity",
pixel_header = "sample_name", pixels = "tissue_pixels")
analyte <- fData(study)$name[1]
analyte
#> [1] "12-HHTrE"Converting a response into an amount estimate is only trustworthy
where the standards constrain the fit. plot_cal_coverage()
enables this to be evaluated. The lower panel is the calibration itself
– the standard spots (red) and the fitted model (dashed) that
int2conc() inverts. The upper panel is the distribution of
the section’s own pixels on exactly the same x-axis, with the
calibration levels marked in red. When the histogram sits
inside the span of the red lines, every converted pixel
is interpolated between real standards.
Calibration coverage of the measured data. Pixels falling inside the marked range are interpolated between real standards rather than extrapolated.
Called without features it draws one facet per
calibrated analyte, which is the usual way to screen a whole panel at
once for standards that do not bracket the data.
Either the calibration needs re-acquiring such that tissue pixels are
interpolated (recheck with plot_cal_coverage() as above).
Or a small % of extrapolation of extreme pixels is acceptable for your
purpose and is set by max_out_of_range (1
accepts any).
The proportion is recorded per feature, so it can be carried into a report alongside R-squared:
calibrationDiagnostics(study)
#> # A tibble: 3 × 3
#> feature r2 out_of_range
#> <chr> <dbl> <dbl>
#> 1 12-HHTrE 0.999 0
#> 2 12-HOPE 1.000 0
#> 3 9-HOTE 0.999 0Here the synthetic standards span 1.25 to 100 pg/pixel and every
tissue pixel falls inside that, so out_of_range is zero
throughout.
The same section can now be imaged either way. First in raw response, where the colour scale is arbitrary instrument signal:
Study section in raw intensity. The colour scale is arbitrary instrument response and is not comparable to any other acquisition.
Then the same data in calibrated units, where the colour scale is an estimated amount per unit area:
imageR(study, feat_ind = 1, sample_lab = "run",
val_slot = "pg_mm2", value = "pg/mm2", scale = "suppress")The same section after int2conc(). The colour scale is now an estimated amount per unit area rather than instrument response, which is interpretable on its own terms and comparable across acquisitions where standards, matrix and acquisition conditions are matched.
In a full study, calibration is enabled through the
calibration: block of the configuration file
(system.file("config_template.yaml", package = "quantMSImageR")),
positioned just before the ratios: block:
calibration:
enabled: true
cal_acquisition: "cal_run_slide1" # standards .raw acquisition
cal_roi_csv: "cal_ROIs.csv" # pixel -> sample_type = "Cal" + identifier
cal_metadata: "calibration_metadata.csv"
cal_type: "std_addition" # std_addition | cal
val_slot: "intensity"
background_level: "background"
quantify_pixels: ["tissue_pixels"]With enabled: true, the study runner builds the models
from the standards acquisition and writes a
<study>_calibrated.RDS alongside the reports. The
rendered report gains a Quantification section carrying the
fits, the coverage plot and calibrated ion images:
The values produced here are calibrated amount
estimates, not volumetric concentrations and not traceable
reference measurements. Reporting them as pg/pixel or
pg/mm² is a statement about the calibration, not a claim
about the absolute biological content of a voxel.
External or on-slide calibration in particular does not automatically account for:
On-tissue calibration improves matrix matching but does not remove these concerns, and adds the problem of separating added from endogenous analyte.
In practice this means amount estimates are most defensible when used to compare like with like – the same analyte, prepared and acquired the same way, with standards that bracket the observed signal – and progressively weaker as any of those conditions relax. Where a comparison matters, report the calibration diagnostics (range, coverage, R², out-of-range proportion) alongside the values.
Smith MJ, Nie M, Adner M, Säfholm J, Wheelock CE. Development of a Desorption Electrospray Ionization–Multiple-Reaction-Monitoring Mass Spectrometry (DESI-MRM) Workflow for Spatially Mapping Oxylipins in Pulmonary Tissue. Analytical Chemistry (2024). https://doi.org/10.1021/acs.analchem.4c02350
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] quantMSImageR_0.99.0 Cardinal_3.15.0 S4Vectors_0.51.5
#> [4] ProtGenerics_1.45.0 BiocGenerics_0.59.10 generics_0.1.4
#> [7] BiocParallel_1.47.0 ggplot2_4.0.3 BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 circlize_0.4.18 shape_1.4.6.1
#> [4] xfun_0.60 bslib_0.11.0 htmlwidgets_1.6.4
#> [7] GlobalOptions_0.1.4 Biobase_2.73.1 lattice_0.22-9
#> [10] vctrs_0.7.3 tools_4.6.1 crosstalk_1.2.2
#> [13] parallel_4.6.1 tibble_3.3.1 pkgconfig_2.0.3
#> [16] Matrix_1.7-5 RColorBrewer_1.1-3 S7_0.2.2
#> [19] lifecycle_1.0.5 compiler_4.6.1 farver_2.1.2
#> [22] codetools_0.2-20 htmltools_0.5.9 sys_3.4.3
#> [25] buildtools_1.0.0 sass_0.4.10 yaml_2.3.12
#> [28] pillar_1.11.1 jquerylib_0.1.4 tidyr_1.3.2
#> [31] DT_0.34.0 cachem_1.1.0 viridis_0.6.5
#> [34] nlme_3.1-170 tidyselect_1.2.1 digest_0.6.39
#> [37] dplyr_1.2.1 purrr_1.2.2 labeling_0.4.3
#> [40] maketools_1.3.2 fastmap_1.2.0 grid_4.6.1
#> [43] colorspace_2.1-3 cli_3.6.6 magrittr_2.0.5
#> [46] patchwork_1.3.2 utf8_1.2.6 withr_3.0.3
#> [49] matter_2.15.0 scales_1.4.0 chemCal_0.2.3
#> [52] rmarkdown_2.31 otel_0.2.0 gridExtra_2.3.1
#> [55] evaluate_1.0.5 knitr_1.51 irlba_2.3.7
#> [58] viridisLite_0.4.3 CardinalIO_1.11.0 rlang_1.3.0
#> [61] ontologyIndex_2.12 glue_1.8.1 BiocManager_1.30.27
#> [64] jsonlite_2.0.0 R6_2.6.1