Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement non-linear scaling for NDVI hybrid green correction #2554

Merged
53 changes: 47 additions & 6 deletions satpy/composites/spectral.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@

This green band correction follows the same approach as the HybridGreen compositor, but with a dynamic blend
factor `f` that depends on the pixel-level Normalized Differece Vegetation Index (NDVI). The higher the NDVI, the
smaller the contribution from the nir channel will be, following a liner relationship between the two ranges
`[ndvi_min, ndvi_max]` and `limits`.
smaller the contribution from the nir channel will be, following a liner (default) or non-linear relationship
between the two ranges `[ndvi_min, ndvi_max]` and `limits`.

As an example, a new green channel using e.g. FCI data and the NDVIHybridGreen compositor can be defined like::

Expand All @@ -124,6 +124,7 @@
ndvi_min: 0.0
ndvi_max: 1.0
limits: [0.15, 0.05]
strength: 1.0
prerequisites:
- name: vis_05
modifiers: [sunz_corrected, rayleigh_corrected]
Expand All @@ -138,30 +139,70 @@
pixels with NDVI=1.0 will be a weighted average with 5% contribution from the near-infrared
vis_08 channel and the remaining 95% from the native green vis_05 channel. For other values of
NDVI a linear interpolation between these values will be performed.

A strength larger or smaller than 1.0 will introduce a non-linear relationship between the two ranges
`[ndvi_min, ndvi_max]` and `limits`. Hence, a higher strength (> 1.0) will result in a slower transition
to higher/lower fractions at the NDVI extremes. Similarly, a lower strength (< 1.0) will result in a
faster transition to higher/lower fractions at the NDVI extremes.
"""

def __init__(self, *args, ndvi_min=0.0, ndvi_max=1.0, limits=(0.15, 0.05), **kwargs):
"""Initialize class and set the NDVI limits and the corresponding blending fraction limits."""
def __init__(self, *args, ndvi_min=0.0, ndvi_max=1.0, limits=(0.15, 0.05), strength=1.0, **kwargs):

Check warning on line 149 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L149

Added line #L149 was not covered by tests
"""Initialize class and set the NDVI limits, blending fraction limits and strength."""
if strength <= 0.0:
raise ValueError(f"Expected stength greater than 0.0, got {strength}.")

Check warning on line 152 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L151-L152

Added lines #L151 - L152 were not covered by tests

self.ndvi_min = ndvi_min
self.ndvi_max = ndvi_max
self.limits = limits
self.strength = strength

Check notice on line 157 in satpy/composites/spectral.py

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

ℹ Getting worse: Excess Number of Function Arguments

NDVIHybridGreen.__init__ increases from 5 to 6 arguments, threshold = 4. This function has too many arguments, indicating a lack of encapsulation. Avoid adding more arguments.

Check warning on line 157 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L157

Added line #L157 was not covered by tests
super().__init__(*args, **kwargs)

def __call__(self, projectables, optional_datasets=None, **attrs):
"""Construct the hybrid green channel weighted by NDVI."""
LOG.info(f"Applying NDVI-weighted hybrid-green correction with limits [{self.limits[0]}, "

Check warning on line 162 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L162

Added line #L162 was not covered by tests
f"{self.limits[1]}] and strength {self.strength}.")

ndvi_input = self.match_data_arrays([projectables[1], projectables[2]])

ndvi = (ndvi_input[1] - ndvi_input[0]) / (ndvi_input[1] + ndvi_input[0])

ndvi.data = da.where(ndvi > self.ndvi_min, ndvi, self.ndvi_min)
ndvi.data = da.where(ndvi < self.ndvi_max, ndvi, self.ndvi_max)

fraction = (ndvi - self.ndvi_min) / (self.ndvi_max - self.ndvi_min) * (self.limits[1] - self.limits[0]) \
+ self.limits[0]
# Introduce non-linearity to ndvi for non-linear scaling to NIR blend fraction
if self.strength != 1.0: # self._apply_strength() has no effect if strength = 1.0 -> no non-linear behaviour
ndvi = self._apply_strength(ndvi)

Check warning on line 174 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L173-L174

Added lines #L173 - L174 were not covered by tests

# Compute pixel-level NIR blend fractions from ndvi
fraction = self._compute_blend_fraction(ndvi)

Check warning on line 177 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L177

Added line #L177 was not covered by tests

# Prepare input as required by parent class (SpectralBlender)
self.fractions = (1 - fraction, fraction)

return super().__call__([projectables[0], projectables[2]], **attrs)

def _apply_strength(self, ndvi):

Check warning on line 184 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L184

Added line #L184 was not covered by tests
"""Introduce non-linearity by applying strength factor.

The method introduces non-linearity to the ndvi for a non-linear scaling from ndvi to blend fraction in
`_compute_blend_fraction`. This can be used for a slower or faster transision to higher/lower fractions
at the ndvi extremes. If strength equals 1.0, this operation has no effect on the ndvi.
"""
ndvi = ndvi ** self.strength / (ndvi ** self.strength + (1 - ndvi) ** self.strength)

Check warning on line 191 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L191

Added line #L191 was not covered by tests
mraspaud marked this conversation as resolved.
Show resolved Hide resolved

return ndvi

Check warning on line 193 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L193

Added line #L193 was not covered by tests

def _compute_blend_fraction(self, ndvi):

Check warning on line 195 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L195

Added line #L195 was not covered by tests
"""Compute pixel-level fraction of NIR signal to blend with native green signal.

This method linearly scales the input ndvi values to pixel-level blend fractions within the range
`[limits[0], limits[1]]` following this implementation <https://stats.stackexchange.com/a/281164>.
"""
fraction = (ndvi - self.ndvi_min) / (self.ndvi_max - self.ndvi_min) * (self.limits[1] - self.limits[0]) \

Check warning on line 201 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L201

Added line #L201 was not covered by tests
+ self.limits[0]

return fraction

Check warning on line 204 in satpy/composites/spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/composites/spectral.py#L204

Added line #L204 was not covered by tests


class GreenCorrector(SpectralBlender):
"""Previous class used to blend channels for green band corrections.
Expand Down
2 changes: 2 additions & 0 deletions satpy/etc/composites/ahi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ composites:

ndvi_hybrid_green:
compositor: !!python/name:satpy.composites.spectral.NDVIHybridGreen
limits: [0.15, 0.05]
strength: 3.0
prerequisites:
- name: B02
modifiers: [sunz_corrected, rayleigh_corrected]
Expand Down
2 changes: 2 additions & 0 deletions satpy/etc/composites/ami.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ composites:
currently implemented are experimental and may change in future versions of Satpy.
compositor: !!python/name:satpy.composites.spectral.NDVIHybridGreen
limits: [0.15, 0.05]
strength: 3.0
prerequisites:
- name: VI005
modifiers: [sunz_corrected, rayleigh_corrected]
Expand All @@ -83,6 +84,7 @@ composites:
Alternative to ndvi_hybrid_green, but without solar zenith or rayleigh correction.
compositor: !!python/name:satpy.composites.spectral.NDVIHybridGreen
limits: [0.15, 0.05]
strength: 3.0
prerequisites:
- name: VI005
- name: VI006
Expand Down
4 changes: 3 additions & 1 deletion satpy/etc/composites/fci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ composites:
The FCI green band at 0.51 µm deliberately misses the chlorophyll band, such that
the signal comes from aerosols and ash rather than vegetation. An effect
is that vegetation in a true colour RGB looks rather brown than green and barren rather red. Mixing in
some part of the NIR 0.8 channel reduced this effect. Note that the fractions
some part of the NIR 0.8 channel reduced this effect. Note that the fractions and non-linear strength
currently implemented are experimental and may change in future versions of Satpy.
compositor: !!python/name:satpy.composites.spectral.NDVIHybridGreen
limits: [0.15, 0.05]
strength: 3.0
prerequisites:
- name: vis_05
modifiers: [sunz_corrected, rayleigh_corrected]
Expand All @@ -25,6 +26,7 @@ composites:
Alternative to ndvi_hybrid_green, but without solar zenith or rayleigh correction.
compositor: !!python/name:satpy.composites.spectral.NDVIHybridGreen
limits: [0.15, 0.05]
strength: 3.0
prerequisites:
- name: vis_05
- name: vis_06
Expand Down
50 changes: 34 additions & 16 deletions satpy/tests/compositor_tests/test_spectral.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
# You should have received a copy of the GNU General Public License along with
# satpy. If not, see <http://www.gnu.org/licenses/>.
"""Tests for spectral correction compositors."""
import warnings

import dask.array as da
import numpy as np
Expand Down Expand Up @@ -66,18 +65,37 @@
data = res.compute()
np.testing.assert_allclose(data, 0.23)

def test_ndvi_hybrid_green(self):
"""Test NDVI-scaled hybrid green correction of 'green' band."""
def test_green_corrector(self):

Check warning on line 68 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L68

Added line #L68 was not covered by tests
"""Test the deprecated class for green corrections."""
comp = GreenCorrector('blended_channel', fractions=(0.85, 0.15), prerequisites=(0.51, 0.85),

Check warning on line 70 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L70

Added line #L70 was not covered by tests
standard_name='toa_bidirectional_reflectance')
res = comp((self.c01, self.c03))
assert isinstance(res, xr.DataArray)
assert isinstance(res.data, da.Array)
assert res.attrs['name'] == 'blended_channel'
assert res.attrs['standard_name'] == 'toa_bidirectional_reflectance'
data = res.compute()
np.testing.assert_allclose(data, 0.23)

Check warning on line 78 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Code Duplication

The module contains 3 functions with similar structure: TestSpectralComposites.test_green_corrector,TestSpectralComposites.test_hybrid_green,TestSpectralComposites.test_spectral_blender. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

Check warning on line 78 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L72-L78

Added lines #L72 - L78 were not covered by tests


class TestNdviHybridGreenCompositor:

Check warning on line 81 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L81

Added line #L81 was not covered by tests
"""Test NDVI-weighted hybrid green correction of green band."""

def setup_method(self):

Check warning on line 84 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L84

Added line #L84 was not covered by tests
"""Initialize channels."""
self.c01 = xr.DataArray(da.from_array([[0.25, 0.30], [0.20, 0.30]], chunks=25),
dims=('y', 'x'), attrs={'name': 'C02'})
self.c02 = xr.DataArray(da.from_array([[0.25, 0.30], [0.25, 0.35]], chunks=25),
dims=('y', 'x'), attrs={'name': 'C03'})
self.c03 = xr.DataArray(da.from_array([[0.35, 0.35], [0.28, 0.65]], chunks=25),
dims=('y', 'x'), attrs={'name': 'C04'})

def test_ndvi_hybrid_green(self):

Check warning on line 93 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L93

Added line #L93 was not covered by tests
"""Test General functionality with linear scaling from ndvi to blend fraction."""
comp = NDVIHybridGreen('ndvi_hybrid_green', limits=(0.15, 0.05), prerequisites=(0.51, 0.65, 0.85),
standard_name='toa_bidirectional_reflectance')

# Test General functionality with linear strength (=1.0)
res = comp((self.c01, self.c02, self.c03))
assert isinstance(res, xr.DataArray)
assert isinstance(res.data, da.Array)
Expand All @@ -86,16 +104,16 @@
data = res.values
np.testing.assert_array_almost_equal(data, np.array([[0.2633, 0.3071], [0.2115, 0.3420]]), decimal=4)

def test_green_corrector(self):
"""Test the deprecated class for green corrections."""
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning, message=r'.*deprecated.*')
comp = GreenCorrector('blended_channel', fractions=(0.85, 0.15), prerequisites=(0.51, 0.85),
standard_name='toa_bidirectional_reflectance')
res = comp((self.c01, self.c03))
assert isinstance(res, xr.DataArray)
assert isinstance(res.data, da.Array)
assert res.attrs['name'] == 'blended_channel'
assert res.attrs['standard_name'] == 'toa_bidirectional_reflectance'
data = res.compute()
np.testing.assert_allclose(data, 0.23)
def test_nonliniear_scaling(self):

Check warning on line 107 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L107

Added line #L107 was not covered by tests
"""Test non-linear scaling using `strength` term."""
comp = NDVIHybridGreen('ndvi_hybrid_green', limits=(0.15, 0.05), strength=2.0, prerequisites=(0.51, 0.65, 0.85),

Check warning on line 109 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L109

Added line #L109 was not covered by tests
standard_name='toa_bidirectional_reflectance')

res = comp((self.c01, self.c02, self.c03))
np.testing.assert_array_almost_equal(res.values, np.array([[0.2646, 0.3075], [0.2120, 0.3471]]), decimal=4)

Check warning on line 113 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L112-L113

Added lines #L112 - L113 were not covered by tests

def test_invalid_strength(self):

Check warning on line 115 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L115

Added line #L115 was not covered by tests
"""Test using invalid `strength` term for non-linear scaling."""
with pytest.raises(ValueError):
_ = NDVIHybridGreen('ndvi_hybrid_green', strength=0.0, prerequisites=(0.51, 0.65, 0.85),

Check warning on line 118 in satpy/tests/compositor_tests/test_spectral.py

View check run for this annotation

Codecov / codecov/patch

satpy/tests/compositor_tests/test_spectral.py#L117-L118

Added lines #L117 - L118 were not covered by tests
standard_name='toa_bidirectional_reflectance')
Loading