Partitions a point set into compact, feature-homogeneous patches by the SLIC relaxation (Achanta et al. 2012) run on an arbitrary neighbour graph rather than a raster. Starting from initial labels, every boundary point is moved, all points at once, to the adjacent patch whose feature mean is closest, with a spatial term that keeps patches compact:
Usage
shoal_slic(
x,
xy,
nb,
init,
lambda = 1,
lambda_scale = NULL,
s_nom = 1,
n_iter = 25L,
adaptive = c("none", "mean"),
alpha = NULL,
min_patch = 1L,
tol = 0
)Arguments
- x
A numeric matrix or data frame of features, one row per point and any number of columns, one included. Data frames are coerced to a matrix using their numeric columns. Rows with missing or non-finite values are an error.
- xy
A numeric matrix with two columns of coordinates, one row per point, in the units
s_nomis expressed in.- nb
An integer matrix with one row per point whose columns hold the row indices of that point's neighbours,
NAfor an unused slot, asshoal_knn()$idordbscan::kNN()$idgive after distant neighbours are set toNA. Need not be symmetric.- init
An integer vector of initial patch labels, one per point. Any labelling works; a regular tiling at the intended patch scale is the usual choice.
- lambda
Spatial weight at
s_nom. Ignored whenlambda_scaleis given oradaptiveis not"none".- lambda_scale
Optional. Sets the spatial weight to this multiple of the median of
init_stat, so one setting carries across data of different feature scales. Ignored whenadaptiveis not"none".- s_nom
Nominal radius, in the units of
xy. Default1.- n_iter
Iterations. Default
25L. Themovescomponent shows whether that was enough: the count typically decays geometrically.- adaptive
"none"(default) for one globallambda, or"mean"for per-patch weights ofalphatimes the patch's mean squared feature distance.- alpha
Multiplier for
adaptive = "mean"; required then, ignored otherwise.- min_patch
Smallest patch kept after relaxation, in points. Default
1Lkeeps every component.- tol
Stop early once fewer than this share of points move in an iteration. Default
0runs alln_iter.
Value
An object of class c("shoal_slic", "shoal_clustering"): a list
with components cluster (integer patch labels), n_clusters,
n_noise (always 0), data, algorithm, params (with lambda
the weight actually used, resolved from lambda_scale where that was
given), moves (points
moved in each iteration run), init_stat (mean squared feature
distance of each initial patch's points to its mean, named by the
values of init) and lambda_median (median per-patch weight over
non-empty patches in each iteration run).
Details
$$\|x_i - m_p\|^2 + \lambda_p \, \|xy_i - c_p\|^2 / s_{nom}^2$$
where \(m_p\) and \(c_p\) are the patch's feature and coordinate means at the start of the iteration. Because the adjacency and the seeding are inputs, the same routine serves a raster, a hexagonal or pentagonal discrete global grid, or any point cloud with a neighbour structure.
Compactness
lambda weights the spatial term at the nominal radius s_nom, in the
units of xy: a point s_nom from its patch centre pays lambda in the
squared-feature-distance currency of x. The scale of that currency is
data dependent, so init_stat in the result gives the mean squared
feature distance of each initial patch's points to its mean, the local
variability statistic of supercells::sc_tune_compactness(). Expressing
lambda as a multiple of its median carries a tuned setting to other
data, and lambda_scale does exactly that: the weight used is
lambda_scale times the median of init_stat, and is reported as
lambda in the result. Balancing the two terms one-for-one
(lambda_scale = 1) is a common default that over-regularises
high-dimensional embeddings; on those, values of a tenth or less are what
track the data.
adaptive = "mean" rescales the spatial term per patch to alpha times
that patch's own mean squared feature distance at the start of each
iteration, in the manner of ASLIC: the balance is then relative to local
contrast, so homogeneous and heterogeneous regions are regularised alike
in relative terms. The per-patch statistic is floored at 1e-4 so a
perfectly uniform patch keeps a spatial term.
Post-processing
After the iterations each patch is split into its connected components
over nb, and patches of fewer than min_patch points are absorbed into
the neighbouring patch they share the most edges with, until none remain.
Labels are then renumbered 1..n_clusters.
Every step is deterministic. Ties in the move decision go to the point's
own patch, then to the earlier column of nb; component ids follow first
appearance in row order.
References
Achanta R, Shaji A, Smith K, Lucchi A, Fua P, Süsstrunk S (2012). SLIC superpixels compared to state-of-the-art superpixel methods. IEEE Transactions on Pattern Analysis and Machine Intelligence 34(11), 2274-2282.
Examples
# A 20 x 20 grid with two feature regions and a 4-neighbour graph.
side <- 20L
g <- expand.grid(c = seq_len(side), r = seq_len(side))
x <- cbind(as.numeric(g$c > side / 2), 0.1 * g$r / side) + rnorm(2 * side^2, sd = 0.02)
xy <- cbind(g$c, g$r) * 10
nn <- shoal_knn(xy, k = 4L)
nb <- nn$id
nb[nn$dist > 10] <- NA
init <- as.integer(factor(paste((g$r - 1) %/% 4, (g$c - 1) %/% 4)))
sp <- shoal_slic(x, xy, nb, init, lambda = 0.01, s_nom = 20, n_iter = 10L)
sp
#>
#> ── Graph SLIC Clustering
#> Parameters: lambda = 0.01, lambda_scale = NA, s_nom = 20, n_iter = 10, adaptive
#> = none, alpha = NA, min_patch = 1
#> Clusters: 20, Noise points: 0
#> Iterations run: 10; points moved: first 40, last 0
#> Initial-patch variability (median): 0.0008034
table(sp$cluster, x[, 1] > 0.5)[1:5, ]
#>
#> FALSE TRUE
#> 1 20 0
#> 2 20 0
#> 3 0 20
#> 4 0 20
#> 5 20 0
# The same weight expressed relative to the data's own variability.
sp2 <- shoal_slic(x, xy, nb, init, lambda_scale = 0.1, s_nom = 20, n_iter = 10L)
sp2$params$lambda
#> [1] 8.034354e-05
