Skip to content

Commit a714220

Browse files
authored
Implement SMACC algorithm (#112)
1 parent 1a6fec8 commit a714220

File tree

3 files changed

+178
-1
lines changed

3 files changed

+178
-1
lines changed

spectral/algorithms/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
linear_discriminant, create_training_classes, ndvi,
55
orthogonalize, transform_image, unmix, spectral_angles,
66
calc_stats, cov_avg, msam, noise_from_diffs, mnf,
7-
GaussianStats, ppi)
7+
GaussianStats, ppi, smacc)
88
from .classifiers import *
99
from .clustering import L1, L2, kmeans
1010
from .resampling import BandResampler

spectral/algorithms/algorithms.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,3 +1826,126 @@ def ppi(X, niters, threshold=0, centered=False, start=None, display=0,
18261826
spy._status.end_percentage()
18271827

18281828
return counts.reshape(shape[:2])
1829+
1830+
1831+
def smacc(spectra, min_endmembers=None, max_residual_norm=float('Inf')):
1832+
'''Returns SMACC decomposition (H = F * S + R) matrices for an image or
1833+
array of spectra.
1834+
1835+
Let `H` be matrix of shape NxB, where B is number of bands, and N number of
1836+
spectra, then if `spectra` is of the same shape, `H` will be equal to `spectra`.
1837+
Otherwise, `spectra` is assumed to be 3D spectral image, and it is reshaped
1838+
to match shape of `H`.
1839+
1840+
Arguments:
1841+
1842+
`spectra` (ndarray):
1843+
1844+
Image data for which to calculate SMACC decomposition matrices.
1845+
1846+
`min_endmembers` (int):
1847+
1848+
Minimal number of endmembers to find. Defaults to rank of `H`,
1849+
computed numerically with `numpy.linalg.matrix_rank`.
1850+
1851+
`max_residual_norm`:
1852+
1853+
Maximum value of residual vectors' norms. Algorithm will keep finding
1854+
new endmembers until max value of residual norms is less than this
1855+
argument. Defaults to float('Inf')
1856+
1857+
Returns:
1858+
3 matrices, S, F and R, such that H = F * S + R (but it might not always hold).
1859+
F is matrix of expansion coefficients of shape N x num_endmembers.
1860+
All values of F are equal to, or greater than zero.
1861+
S is matrix of endmember spectra, extreme vectors, of shape num_endmembers x B.
1862+
R is matrix of residuals of same shape as H (N x B).
1863+
1864+
If values of H are large (few tousands), H = F * S + R, might not hold,
1865+
because of numeric errors. It is advisable to scale numbers, by dividing
1866+
by 10000, for example. Depending on how accurate you want it to be,
1867+
you can check if H is really strictly equal to F * S + R,
1868+
and adjust R: R = H - np.matmul(F, S).
1869+
1870+
References:
1871+
1872+
John H. Gruninger, Anthony J. Ratkowski, and Michael L. Hoke "The sequential
1873+
maximum angle convex cone (SMACC) endmember model", Proc. SPIE 5425, Algorithms
1874+
and Technologies for Multispectral, Hyperspectral, and Ultraspectral Imagery X,
1875+
(12 August 2004); https://doi.org/10.1117/12.543794
1876+
'''
1877+
# Indices of vectors in S.
1878+
q = []
1879+
1880+
H = spectra if len(spectra.shape) == 2 else spectra.reshape(
1881+
(spectra.shape[0] * spectra.shape[1], spectra.shape[2]))
1882+
R = H
1883+
Fs = []
1884+
1885+
F = None
1886+
S = None
1887+
1888+
if min_endmembers is None:
1889+
min_endmembers = np.linalg.matrix_rank(H)
1890+
1891+
# Add the longest vector to q.
1892+
residual_norms = np.sqrt(np.einsum('ij,ij->i', H, H))
1893+
current_max_residual_norm = np.max(residual_norms)
1894+
1895+
if max_residual_norm is None:
1896+
max_residual_norm = current_max_residual_norm / min_endmembers
1897+
1898+
while len(q) < min_endmembers or current_max_residual_norm > max_residual_norm:
1899+
q.append(np.argmax(residual_norms))
1900+
n = len(q) - 1
1901+
# Current basis vector.
1902+
w = R[q[n]]
1903+
# Temporary to be used for projection calculation.
1904+
wt = w / (np.dot(w, w))
1905+
# Calculate projection coefficients.
1906+
On = np.dot(R, wt)
1907+
alpha = np.ones(On.shape, dtype=np.float64)
1908+
# Make corrections to satisfy convex cone conditions.
1909+
# First correct alphas for oblique projection when needed.
1910+
for k in range(len(Fs)):
1911+
t = On * Fs[k][q[n]]
1912+
# This is not so important for the algorithm itself.
1913+
# These values correpond to values where On == 0.0, and these
1914+
# will be zeroed out below. But to avoid divide-by-zero warning
1915+
# we set small values instead of zero.
1916+
t[t == 0.0] = 1e-10
1917+
np.minimum(Fs[k]/t, alpha, out=alpha)
1918+
# Clip negative projection coefficients.
1919+
alpha[On <= 0.0] = 0.0
1920+
# Current extreme vector should always be removed completely.
1921+
alpha[q[n]] = 1.0
1922+
# Calculate oblique projection coefficients.
1923+
Fn = alpha * On
1924+
# Correction for numerical stability.
1925+
Fn[Fn <= 0.0] = 0.0
1926+
# Remove projection to current basis from R.
1927+
R = R - np.outer(Fn, w)
1928+
# Update projection coefficients.
1929+
for k in range(len(Fs)):
1930+
Fs[k] -= Fs[k][q[n]] * Fn
1931+
# Correction because of numerical problems.
1932+
Fs[k][Fs[k] <= 0.0] = 0.0
1933+
1934+
# Add new Fn.
1935+
Fs.append(Fn)
1936+
1937+
residual_norms[:] = np.sqrt(np.einsum('ij,ij->i', R, R))
1938+
current_max_residual_norm = np.max(residual_norms)
1939+
print('Found {0} endmembers, current max residual norm is {1:.4f}\r'
1940+
.format(len(q), current_max_residual_norm), end='')
1941+
1942+
# Correction as suggested in the SMACC paper.
1943+
for k, s in enumerate(q):
1944+
Fs[k][q] = 0.0
1945+
Fs[k][s] = 1.0
1946+
1947+
F = np.array(Fs).T
1948+
S = H[q]
1949+
1950+
# H = F * S + R
1951+
return S, F, R

spectral/tests/dimensionality.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,60 @@ def test_ppi_centered(self):
6161
p2 = spy.ppi(data_centered, 4)
6262
assert(np.all(p == p2))
6363

64+
def test_smacc_minimal(self):
65+
'''Tests smacc correcntess on minimal example.'''
66+
H = np.array([
67+
[1.0, 0.0, 0.0],
68+
[0.0, 1.0, 0.0],
69+
[1.0, 1.0, 0.0],
70+
[0.0, 1.0, 1.0]
71+
])
72+
S, F, R = spy.smacc(H)
73+
assert_allclose(np.matmul(F, S) + R, H)
74+
assert(np.min(F) == 0.0)
75+
expected_S = np.array([
76+
# First two longer ones.
77+
[1., 1., 0.],
78+
[0., 1., 1.],
79+
# First of the two shorted ones. Other can be expressed other 3.
80+
[1., 0., 0.],
81+
])
82+
assert(np.array_equal(S, expected_S))
83+
84+
def test_smacc_runs(self):
85+
'''Tests that smacc runs without additional arguments.'''
86+
# Without scaling numeric errors accumulate.
87+
scaled_data = self.data / 10000
88+
S, F, R = spy.smacc(scaled_data)
89+
data_shape = scaled_data.shape
90+
H = scaled_data.reshape(data_shape[0] * data_shape[1], data_shape[2])
91+
assert_allclose(np.matmul(F, S) + R, H)
92+
assert(np.min(F) == 0.0)
93+
assert(len(S.shape) == 2 and S.shape[0] == 9 and S.shape[1] == 220)
94+
95+
def test_smacc_min_endmembers(self):
96+
'''Tests that smacc runs with min_endmember argument.'''
97+
# Without scaling numeric errors accumulate.
98+
scaled_data = self.data / 10000
99+
S, F, R = spy.smacc(scaled_data, 10)
100+
data_shape = scaled_data.shape
101+
H = scaled_data.reshape(data_shape[0] * data_shape[1], data_shape[2])
102+
assert_allclose(np.matmul(F, S) + R, H)
103+
assert(np.min(F) == 0.0)
104+
assert(len(S.shape) == 2 and S.shape[0] == 10 and S.shape[1] == 220)
105+
106+
def test_smacc_max_residual_norm(self):
107+
'''Tests that smacc runs with max_residual_norm argument.'''
108+
# Without scaling numeric errors accumulate.
109+
scaled_data = self.data / 10000
110+
S, F, R = spy.smacc(scaled_data, 9, 0.8)
111+
data_shape = scaled_data.shape
112+
H = scaled_data.reshape(data_shape[0] * data_shape[1], data_shape[2])
113+
assert_allclose(np.matmul(F, S) + R, H)
114+
assert(np.min(F) == 0.0)
115+
residual_norms = np.einsum('ij,ij->i', R, R)
116+
assert(np.max(residual_norms) <= 0.8)
117+
64118
def test_pca_runs(self):
65119
'''Should be able to compute PCs and transform data.'''
66120
data = self.data

0 commit comments

Comments
 (0)