Skip to content

Commit aa74fbb

Browse files
authored
Fix SAVI formula: (1+L) was in denominator instead of numerator (#1094) (#1095)
* Fix SAVI formula and EBBI type inconsistency (#1094) SAVI: The (1+L) factor was in the denominator instead of the numerator. The standard formula (Huete 1988) is ((NIR-Red)/(NIR+Red+L))*(1+L). The code computed (NIR-Red)/((NIR+Red+L)*(1+L)), making all results too small by (1+L)^2 = 2.25 with default L=0.5. EBBI GPU: Changed nb.int64(10) to 10.0 to match CPU path's type behavior. * Fix SAVI formula and add regression test (#1094) SAVI had (1+L) in the denominator instead of the numerator. The Huete (1988) formula is ((NIR-Red)/(NIR+Red+L))*(1+L). The code computed (NIR-Red)/((NIR+Red+L)*(1+L)), making results too small by (1+L)^2. Updated the qgis_savi fixture and uint dtype fixture with correct reference values. Added test_savi_formula_1094 which checks all 4 backends against the formula directly. Also fixed EBBI GPU: nb.int64(10) -> 10.0 for type consistency.
1 parent 1437c61 commit aa74fbb

File tree

2 files changed

+61
-19
lines changed

2 files changed

+61
-19
lines changed

xrspatial/multispectral.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,9 +1062,8 @@ def _savi_cpu(nir_data, red_data, soil_factor):
10621062
red = red_data[y, x]
10631063
numerator = nir - red
10641064
soma = nir + red + soil_factor
1065-
denominator = soma * (1.0 + soil_factor)
1066-
if denominator != 0.0:
1067-
out[y, x] = numerator / denominator
1065+
if soma != 0.0:
1066+
out[y, x] = (numerator / soma) * (1.0 + soil_factor)
10681067

10691068
return out
10701069

@@ -1077,9 +1076,8 @@ def _savi_gpu(nir_data, red_data, soil_factor, out):
10771076
red = red_data[y, x]
10781077
numerator = nir - red
10791078
soma = nir + red + soil_factor
1080-
denominator = soma * (nb.float32(1.0) + soil_factor)
1081-
if denominator != 0.0:
1082-
out[y, x] = numerator / denominator
1079+
if soma != 0.0:
1080+
out[y, x] = (numerator / soma) * (1.0 + soil_factor)
10831081

10841082

10851083
def _savi_dask(nir_data, red_data, soil_factor):
@@ -1367,7 +1365,7 @@ def _ebbi_gpu(red_data, swir_data, tir_data, out):
13671365
swir = swir_data[y, x]
13681366
tir = tir_data[y, x]
13691367
numerator = swir - red
1370-
denominator = nb.int64(10) * sqrt(swir + tir)
1368+
denominator = 10.0 * sqrt(swir + tir)
13711369
if denominator != 0.0:
13721370
out[y, x] = numerator / denominator
13731371

xrspatial/tests/test_multispectral.py

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -215,19 +215,18 @@ def qgis_ndmi():
215215

216216
@pytest.fixture
217217
def qgis_savi():
218-
# this result is obtained by using NIR, and red band data
219-
# running through QGIS Raster Calculator with formula:
220-
# savi = (nir - red) / ((nir + red + soil_factor) * (1 + soil_factor))
218+
# Correct SAVI (Huete 1988):
219+
# savi = ((nir - red) / (nir + red + L)) * (1 + L)
221220
# with default value of soil_factor=1
222221
result = np.array([
223-
[0., 0.10726268, 0.10682587, 0.09168259],
224-
[0.10089815, 0.10729991, 0.10749393, np.nan],
225-
[0.11363809, 0.11995638, 0.11994251, 0.10915995],
226-
[0.10226355, 0.11864913, 0.12966092, 0.11774762],
227-
[0.09810041, 0.10675804, 0.12213238, 0.11514599],
228-
[0.09377059, 0.10416108, 0.11123802, 0.10735555],
229-
[0.09097284, 0.0988547, 0.10404798, 0.10413785],
230-
[0.0870268, 0.09878284, 0.105046, 0.10514525]], dtype=np.float32)
222+
[0., 0.4290507, 0.4273035, 0.3667304],
223+
[0.4035926, 0.4291996, 0.4299757, np.nan],
224+
[0.4545524, 0.4798255, 0.4797700, 0.4366398],
225+
[0.4090542, 0.4745965, 0.5186437, 0.4709905],
226+
[0.3924016, 0.4270322, 0.4885295, 0.4605840],
227+
[0.3750824, 0.4166443, 0.4449521, 0.4294222],
228+
[0.3638914, 0.3954188, 0.4161919, 0.4165514],
229+
[0.3481072, 0.3951314, 0.4201840, 0.4205810]], dtype=np.float32)
231230
return result
232231

233232

@@ -314,7 +313,8 @@ def data_uint_dtype_evi(dtype):
314313
def data_uint_dtype_savi(dtype):
315314
nir = xr.DataArray(np.array([[1, 1], [1, 1]], dtype=dtype))
316315
red = xr.DataArray(np.array([[0, 1], [0, 2]], dtype=dtype))
317-
result = np.array([[0.25, 0.], [0.25, -0.125]], dtype=np.float32)
316+
# Correct SAVI with L=1: ((nir-red)/(nir+red+1)) * 2
317+
result = np.array([[1.0, 0.], [1.0, -0.5]], dtype=np.float32)
318318
return nir, red, result
319319

320320

@@ -424,6 +424,50 @@ def test_savi_gpu(nir_data, red_data, qgis_savi):
424424
general_output_checks(nir_data, result, qgis_savi)
425425

426426

427+
@pytest.mark.parametrize("backend", ["numpy", "cupy", "dask+numpy", "dask+cupy"])
428+
def test_savi_formula_1094(backend):
429+
"""Verify SAVI against the Huete (1988) formula directly.
430+
431+
Regression test for #1094: (1+L) was in the denominator instead
432+
of the numerator.
433+
"""
434+
from xrspatial.tests.general_checks import has_cuda_and_cupy, dask_array_available as _dask_avail
435+
if 'cupy' in backend and not has_cuda_and_cupy():
436+
pytest.skip("Requires CUDA and CuPy")
437+
try:
438+
import dask.array
439+
except ImportError:
440+
if 'dask' in backend:
441+
pytest.skip("Requires Dask")
442+
443+
nir_arr = np.array([[0.8, 0.6], [0.4, 0.0]])
444+
red_arr = np.array([[0.2, 0.3], [0.4, 0.0]])
445+
446+
nir_agg = create_test_raster(nir_arr, backend=backend, chunks=(2, 2))
447+
red_agg = create_test_raster(red_arr, backend=backend, chunks=(2, 2))
448+
449+
L = 0.5
450+
result = savi(nir_agg, red_agg, soil_factor=L)
451+
data = result.data
452+
if hasattr(data, 'compute'):
453+
data = data.compute()
454+
if hasattr(data, 'get'):
455+
data = data.get()
456+
data = np.asarray(data)
457+
458+
# Correct: ((NIR - Red) / (NIR + Red + L)) * (1 + L)
459+
expected = np.where(
460+
(nir_arr + red_arr + L) != 0,
461+
((nir_arr - red_arr) / (nir_arr + red_arr + L)) * (1 + L),
462+
np.nan,
463+
).astype(np.float32)
464+
465+
np.testing.assert_allclose(data, expected, rtol=1e-5, equal_nan=True)
466+
467+
# Spot check: NIR=0.8, Red=0.2, L=0.5 -> (0.6/1.5)*1.5 = 0.6
468+
assert abs(float(data[0, 0]) - 0.6) < 1e-5
469+
470+
427471
# arvi -------------
428472
@pytest.mark.parametrize("backend", ["numpy", "dask+numpy"])
429473
def test_arvi_cpu_against_qgis(nir_data, red_data, blue_data, qgis_arvi):

0 commit comments

Comments
 (0)