The goal of SSIMmap is to extend the classical Structural Similarity Index Measure (SSIM) — originally developed for image quality assessment — to maps. The package provides a unified framework for quantifying spatial similarity between two maps of the same area, for both irregular polygon (lattice) data and raster images.
For polygon maps, the geographical SSIM method incorporates geographically weighted summary statistics with an adaptive k-nearest-neighbour Gaussian kernel. For raster images, SSIM is computed using a moving window. In addition to point estimates, SSIMmap also supports permutation tests for both global and local significance, with optional Benjamini–Hochberg FDR correction.
ssim_bandwidth() — bias–variance trade-off plots to help choose
an appropriate bandwidth (number of neighbours k) for polygon maps.ssim_polygon() — global and local SSIM (and components SIM, SIV,
SIP) for polygon maps, with permutation testing and FDR correction.ssim_raster() — global and local SSIM for raster images, with
permutation testing and per-cell p-/q-values."normal_score", "percentile",
"minmax", "none") for comparing variables on different scales.Toronto_SSIM) and British Columbia Fire Weather Index rasters
(fwi_0816_bc, fwi_0818_bc, fwi_1101_bc).Install the released version from CRAN:
install.packages("SSIMmap")
Or the development version from GitHub:
# install.packages("remotes")
remotes::install_github("Hailyee-Ha/SSIMmap", build_vignettes = TRUE)
library(SSIMmap)
library(sf)
library(terra)
library(ggplot2)
library(patchwork)
library(tidyterra)
The package ships with Toronto_SSIM, an sf object covering Toronto
Census Tracts. It includes two area-based deprivation indices (the
Canadian Index of Multiple Deprivation, CIMD, and the Pampalon
index, PP) and a 2016 census variable (Commute).
data("Toronto_SSIM")
p_cimd <- ggplot(Toronto_SSIM) +
geom_sf(aes(fill = CIMD), color = NA) +
scale_fill_viridis_c(option = "magma") +
labs(title = "CIMD") + theme_void()
p_pp <- ggplot(Toronto_SSIM) +
geom_sf(aes(fill = PP), color = NA) +
scale_fill_viridis_c(option = "magma") +
labs(title = "Pampalon") + theme_void()
p_cimd + p_pp

ssim_bandwidth() provides a bias–variance trade-off curve for
selecting k, the number of nearest neighbours used in the adaptive
kernel.
S <- ssim_bandwidth(
shape = Toronto_SSIM,
map1 = "CIMD",
map2 = "PP",
max_bandwidth = 100,
transform = "percentile",
option = "midpoint"
)
S$bandwidth
#> [1] 49
S$plot

S_global <- ssim_polygon(
shape = Toronto_SSIM,
map1 = "CIMD",
map2 = "PP",
global = TRUE,
bandwidth = S$bandwidth,
transform = "percentile",
do_test = TRUE, R = 1000, fdr = TRUE, alpha = 0.05,
seed = 1
)
#>
#>
#> |Statistic | SSIM| SIM| SIV| SIP|
#> |:---------|---------:|---------:|---------:|---------:|
#> |Mean | 0.8250472| 0.9921019| 0.9912113| 0.8392269|
#> |Min | 0.5103267| 0.9505378| 0.9470631| 0.5118904|
#> |Max | 0.9519651| 1.0000000| 0.9999999| 0.9520484|
#> |SD | 0.0931275| 0.0107547| 0.0109567| 0.0953972|
#>
#> Global permutation p-values (two-sided):
#>
#>
#> |Component | Global_Mean| P_value|
#> |:---------|-----------:|-------:|
#> |SSIM | 0.8250| 0.0010|
#> |SIM | 0.9921| 0.3397|
#> |SIV | 0.9912| 0.0090|
#> |SIP | 0.8392| 0.0010|
S_global$p_global
#> Component Global_Mean P_value
#> 1 SSIM 0.8250472 0.000999001
#> 2 SIM 0.9921019 0.339660340
#> 3 SIV 0.9912113 0.008991009
#> 4 SIP 0.8392269 0.000999001
S_local <- ssim_polygon(
shape = Toronto_SSIM,
map1 = "CIMD",
map2 = "PP",
global = FALSE,
bandwidth = S$bandwidth,
transform = "percentile",
do_test = TRUE, R = 1000, fdr = TRUE, alpha = 0.05,
seed = 1
)
ggplot(S_local) +
geom_sf(aes(fill = SSIM), color = NA) +
scale_fill_distiller(palette = "PRGn", limits = c(-1, 1)) +
labs(title = "Local SSIM: CIMD vs. Pampalon") +
theme_void()

The S_local object is an ordinary sf data frame, so you can also map
components (SIM, SIV, SIP) or the FDR-significant cells (sig)
using your favourite mapping package (ggplot2, tmap, etc.).
The package also includes three daily Canadian Fire Weather Index (FWI) maps for British Columbia at a 2 km resolution. Two represent the peak summer fire season (16 and 18 August 2023) and one represents late fall (1 November 2023). We expect the two summer maps to be much more similar to each other than either one is to the fall map.
data("fwi_0816_bc"); fwi_0816_bc <- terra::unwrap(fwi_0816_bc)
data("fwi_0818_bc"); fwi_0818_bc <- terra::unwrap(fwi_0818_bc)
data("fwi_1101_bc"); fwi_1101_bc <- terra::unwrap(fwi_1101_bc)
par(mfrow = c(1, 3))
plot(fwi_0816_bc, main = "FWI – 16 Aug 2023")
plot(fwi_0818_bc, main = "FWI – 18 Aug 2023")
plot(fwi_1101_bc, main = "FWI – 1 Nov 2023")

A quick global comparison (no permutation test, fast):
ssim_raster(fwi_0816_bc, fwi_0818_bc) # summer vs summer
#> $summary
#> metric mean min max sd
#> 1 SSIM 0.5380046 -0.98968521 0.9999790 0.58855978
#> 2 SIM 0.5507247 -0.99427575 1.0000000 0.60271292
#> 3 SIV 0.9822690 0.40999905 1.0000000 0.03216969
#> 4 SIP 0.9924066 0.03063862 0.9999998 0.02029212
#>
#> $settings
#> $settings$global
#> [1] TRUE
#>
#> $settings$w
#> [1] 1
#>
#> $settings$transform
#> [1] "normal_score"
#>
#> $settings$do_test
#> [1] FALSE
#>
#> $settings$local_test
#> [1] FALSE
ssim_raster(fwi_0816_bc, fwi_1101_bc) # summer vs fall
#> $summary
#> metric mean min max sd
#> 1 SSIM 0.2909122 -0.9914894 0.9998444 0.61660743
#> 2 SIM 0.3063775 -0.9964454 1.0000000 0.65287253
#> 3 SIV 0.9561909 0.2553303 1.0000000 0.06145141
#> 4 SIP 0.9845342 -0.1710329 0.9999987 0.04173728
#>
#> $settings
#> $settings$global
#> [1] TRUE
#>
#> $settings$w
#> [1] 1
#>
#> $settings$transform
#> [1] "normal_score"
#>
#> $settings$do_test
#> [1] FALSE
#>
#> $settings$local_test
#> [1] FALSE
As expected, the summer–summer comparison shows much higher similarity than the summer–fall comparison.
For per-cell SSIM surfaces, set global = FALSE. Below we use
tidyterra to faceted-plot the four SSIM components:
p1_l <- ssim_raster(
fwi_0816_bc, fwi_0818_bc,
global = FALSE, w = 1
)
ggplot() +
geom_spatraster(data = p1_l[[c("SSIM", "SIM", "SIV", "SIP")]]) +
facet_wrap(~lyr, ncol = 2) +
scale_fill_viridis_c(limits = c(-1, 1), na.value = "transparent") +
theme_minimal()
Setting do_test = TRUE and local_test = TRUE additionally returns
per-cell permutation p-values (p_SSIM, p_SIM, p_SIV, p_SIP) and
FDR-adjusted q-values (q_SSIM, …).
A more detailed walkthrough — including bandwidth selection,
side-by-side patchwork plots, and tidyterra styling for
publication-ready figures — is available in the package vignette:
vignette("Introduction_to_SSIMmap", package = "SSIMmap")
Or, after installing from GitHub with build_vignettes = TRUE, browse
all vignettes:
browseVignettes("SSIMmap")
If you use SSIMmap in your research, please cite both the package and the original SSIM paper:
citation("SSIMmap")
Bug reports, feature requests, and pull requests are very welcome — please open an issue on GitHub.
MIT © Hui Jeong (Hailyee) Ha and Jed Long.