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
27 changes: 23 additions & 4 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,24 +139,42 @@
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):
"""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}.")

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.
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]}, "
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)

# Apply non-linearity to the ndvi for a non-linear conversion from ndvi to 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)
mraspaud marked this conversation as resolved.
Show resolved Hide resolved

# Compute blending fraction from ndvi
fraction = (ndvi - self.ndvi_min) / (self.ndvi_max - self.ndvi_min) * (self.limits[1] - self.limits[0]) \
+ self.limits[0]
self.fractions = (1 - fraction, fraction)
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
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
20 changes: 15 additions & 5 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 @@ -78,6 +77,7 @@ def test_ndvi_hybrid_green(self):
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,12 +86,22 @@ def test_ndvi_hybrid_green(self):
data = res.values
np.testing.assert_array_almost_equal(data, np.array([[0.2633, 0.3071], [0.2115, 0.3420]]), decimal=4)

# Test invalid strength
with pytest.raises(ValueError):
_ = NDVIHybridGreen('ndvi_hybrid_green', strength=0.0, prerequisites=(0.51, 0.65, 0.85),
standard_name='toa_bidirectional_reflectance')

# Test non-linear strength
comp = NDVIHybridGreen('ndvi_hybrid_green', limits=(0.15, 0.05), strength=2.0, prerequisites=(0.51, 0.65, 0.85),
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)
mraspaud marked this conversation as resolved.
Show resolved Hide resolved

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')
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)
Expand Down
Loading