SNIC Segmentation Pipeline - SpatRaster

Introduction

This vignette revisits the SNIC segmentation pipeline, originally proposed by Achanta and Susstrunk (2017). It works with terra::SpatRaster objects, instead of a small in-memory RGB array. We will use the Sentinel-2 subset (demo-geotiff) that ships with the package. Every step reflects a typical remote-sensing workflow: load raster bands, seed the image, run snic(), and visualize the resulting segments.

Array workflow: If you prefer to start from in-memory arrays, see the SNIC Segmentation Pipeline - Arrays vignette for a compact tutorial that mirrors this tutorial without the geospatial dependencies.

Sentinel-2 preparation with terra

library(terra)

data_dir <- system.file("demo-geotiff", package = "snic", mustWork = TRUE)
band_files <- file.path(
  data_dir,
  c(
    "S2_20LMR_B02_20220630.tif",
    "S2_20LMR_B04_20220630.tif",
    "S2_20LMR_B08_20220630.tif",
    "S2_20LMR_B12_20220630.tif"
  )
)

s2 <- terra::rast(band_files)

names(s2) <- c("B02", "B04", "B08", "B12")
s2
#> class       : SpatRaster 
#> size        : 768, 1024, 4  (nrow, ncol, nlyr)
#> resolution  : 20, 20  (x, y)
#> extent      : 429960, 450440, 9046000, 9061360  (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS 84 / UTM zone 20S (EPSG:32720) 
#> sources     : S2_20LMR_B02_20220630.tif  
#>               S2_20LMR_B04_20220630.tif  
#>               S2_20LMR_B08_20220630.tif  
#>               S2_20LMR_B12_20220630.tif  
#> names       : B02, B04, B08, B12

The four bands cover the blue (B02), red (B04), near-infrared (B08), and short-wave infrared (B12) portions of the spectrum. snic package comes with a helper function to plot false-color composites easily. snic_plot() understands both arrays and SpatRaster inputs.

snic_plot(s2, r = "B08", g = "B04", b = "B02")
Figure 1 - A false-color composite of a Sentinel-2 scene.
Figure 1 - A false-color composite of a Sentinel-2 scene.

Seeds creation

With a SpatRaster input, snic_grid() returns a data frame with latitude/longitude columns that can be inspected or stored for reproducibility.

seeds_rect <- snic_grid(s2, type = "rectangular", spacing = 24, padding = 2)

head(seeds_rect)
#>         lat       lon
#> 1 -8.491482 -63.63589
#> 2 -8.495824 -63.63590
#> 3 -8.500346 -63.63591
#> 4 -8.504688 -63.63592
#> 5 -8.509210 -63.63592
#> 6 -8.513733 -63.63593

spacing and padding parameters control the spacing (in pixels) between seeds and the padding (in pixels) around the image, respectively.

Run SNIC

The number of seeds controls the number of output segments. Seeds outside the image are ignored. The compactness parameter balances spatial proximity and spectral similarity; moderately low values emphasize spectral contrast, which is useful for multispectral imagery with distinct land-cover types. Here we use a compactness of 0.3.

seg_rect <- snic(s2, seeds_rect, compactness = 0.3)
seg_rect
#> SNIC segmentation
#>   Size (rows x cols):  768 x 1024
#> Viewing first rows and columns:
#> class       : SpatRaster 
#> size        : 6, 6, 1  (nrow, ncol, nlyr)
#> resolution  : 20, 20  (x, y)
#> extent      : 429960, 430080, 9061240, 9061360  (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS 84 / UTM zone 20S (EPSG:32720) 
#> source(s)   : memory
#> name        : snic 
#> min value   :    1 
#> max value   :    1

The result is a snic object that contains a SpatRaster with same spatial extent as input but with one layer with integer labels. It also contains a data frame with per-segment feature means and centroids. The SpatRaster can be obtained by calling snic_get_seg(). The labels are 1-based and correspond to the seeds in the order they were provided.

Segments visualization

Here we plot the seed locations and the resulting segments on top of a false-color composite.

snic_plot(
  s2,
  r = "B08", g = "B04", b = "B02",
  stretch = "lin",
  seeds = seeds_rect,
  seeds_plot_args = list(pch = 3, col = "#00FFFF", cex = 0.6),
  seg = seg_rect,
  seg_plot_args = list(border = "#FFD700", col = NA, lwd = 0.5)
)
Figure 2 - The Sentinel-2 false-color composite with superpixel segmentation boundaries overlaid in yellow and seed points marked with cyan crosses.
Figure 2 - The Sentinel-2 false-color composite with superpixel segmentation boundaries overlaid in yellow and seed points marked with cyan crosses.

The segmentation result is a single-band SpatRaster whose values correspond to integer labels. You can extract statistics per segment or convert them to polygons via terra::as.polygons().

Comparative grid types and compactness values

All grid types available through snic_grid() operate on SpatRaster objects as well. Using the same Sentinel-2 subset, we compare rectangular, diamond, hexagonal, and random seeds combined with two compactness settings.

seeds_rect <- snic_grid(s2, type = "rectangular", spacing = 26)
seeds_diam <- snic_grid(s2, type = "diamond", spacing = 26)
seeds_hex  <- snic_grid(s2, type = "hexagonal", spacing = 26)

set.seed(42)
seeds_rand <- snic_grid(s2, type = "random", spacing = 26)

grids <- list(
  seeds_rect, seeds_diam,
  seeds_hex, seeds_rand
)

results <- lapply(grids, function(seeds) {
  list(
    snic(s2, seeds, compactness = 0.1),
    snic(s2, seeds, compactness = 0.4)
  )
})
op <- par(mfrow = c(4, 2), oma = c(2, 2, 2, 0))

for (res in results) {
  snic_plot(
    s2,
    r = "B08", g = "B04", b = "B02",
    seg = res[[1]],
    seg_plot_args = list(border = "#FFFFFF", col = NA, lwd = 0.4),
    mar = c(1, 0, 0, 0)
  )
  snic_plot(
    s2,
    r = "B08", g = "B04", b = "B02",
    stretch = "lin",
    seg = res[[2]],
    seg_plot_args = list(border = "#FFFFFF", col = NA, lwd = 0.4),
    mar = c(1, 0, 0, 0)
  )
}

mtext("Compactness = 0.1", side = 3, outer = TRUE, at = 0.25, line = 0.5)
mtext("Compactness = 0.4", side = 3, outer = TRUE, at = 0.75, line = 0.5)
mtext("Rectangular", side = 2, outer = TRUE, at = 3.5 / 4, line = 0.5, las = 3)
mtext("Diamond", side = 2, outer = TRUE, at = 2.5 / 4, line = 0.5, las = 3)
mtext("Hexagonal", side = 2, outer = TRUE, at = 1.5 / 4, line = 0.5, las = 3)
mtext("Random", side = 2, outer = TRUE, at = 0.5 / 4, line = 0.5, las = 3)
Figure 3 - Superpixel segmentation results on the Sentinel-2 scene. Rows correspond to different seed grid types (rectangular, diamond, hexagonal, random) and columns correspond to different compactness values (0.1 and 0.4).
Figure 3 - Superpixel segmentation results on the Sentinel-2 scene. Rows correspond to different seed grid types (rectangular, diamond, hexagonal, random) and columns correspond to different compactness values (0.1 and 0.4).

par(op)

Rectangular and diamond layouts align segments with cardinal directions, whereas hexagonal and random seeds spread centers more evenly in all directions. Higher compactness induces smoother, more rounded superpixels, so the right column contains fewer jagged boundaries than the left one. Regardless of the seed geometry, the workflow stays the same: read the multi-band raster, place seeds in geographic space, and call snic() with the desired compactness.

Fixed hexagonal seeds across resolutions

Spatial datasets often come in multiple resolutions—for example, you might start from a 10 m grid and also work with 20 m or 40 m aggregates for faster experimentation. Because snic_grid() returns seeds with geographic coordinates, the same seed set can be re-used across rasters that share the same extent and CRS. Below we generate a hexagonal grid once, then aggregate the Sentinel-2 raster to four different resolutions with terra::aggregate() and run snic() on each version without touching the seeds.

seeds_hex <- snic_grid(
  s2,
  type = "hexagonal",
  spacing = 50,
  padding = 5
)

The figure below compares the resulting segments.


op <- par(mfrow = c(2, 2), oma = c(0, 0, 2, 0))

agg_facts <- c(1L, 2L, 4L, 8L)
for (fact in agg_facts) {
  s2_agg <- terra::aggregate(s2, fact = fact, fun = "mean", na.rm = TRUE)
  seg_agg <- snic(s2_agg, seeds_hex, compactness = 0.2)
  snic_plot(
    s2_agg,
    r = "B08", g = "B04", b = "B02",
    seg = seg_agg,
    seg_plot_args = list(border = "white", col = NA, lwd = 0.4),
    main = sprintf("resolution %d m", fact * 20)
  )
}
Figure 4 - Superpixel segmentation results on the Sentinel-2 scene at different spatial resolutions (20 m, 40 m, 80 m, 160 m).
Figure 4 - Superpixel segmentation results on the Sentinel-2 scene at different spatial resolutions (20 m, 40 m, 80 m, 160 m).

par(op)

References

Achanta, R., & Susstrunk, S. (2017). Superpixels and polygons using simple non-iterative clustering. Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR). doi:10.1109/CVPR.2017.520