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

Add a blend method to create temporal RGB from MultiScene #2488

Merged
merged 9 commits into from
Oct 11, 2023
2 changes: 1 addition & 1 deletion satpy/multiscene/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Functions and classes related to MultiScene functionality."""

from ._blend_funcs import stack, timeseries # noqa
from ._blend_funcs import stack, temporal_rgb, timeseries # noqa
from ._multiscene import MultiScene # noqa
17 changes: 17 additions & 0 deletions satpy/multiscene/_blend_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,20 @@ def timeseries(datasets):
res = xr.concat(expanded_ds, dim="time")
res.attrs = combine_metadata(*[x.attrs for x in expanded_ds])
return res


def temporal_rgb(
data_arrays: Sequence[xr.DataArray],
) -> xr.DataArray:
"""Combine a series of datasets as a temporal RGB.

The first dataset is used as the Red component of the new composite, the second as Green and the third as Blue.
All the other datasets are discarded.
"""
from satpy.composites import GenericCompositor

compositor = GenericCompositor("temporal_composite")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenericCompositor removes units and calibration information, which we probably don't want if we produce an RGB from three times the same channel. Maybe GenericCompositor contains other (implicit) assumptions that make it not optimal for temporal composites.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. GenericCompositor is really "generic image compositor" so it assumes things no longer represent "real physical data" anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Units don't make sense for the returned RGB, so that's fine.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would units not make sense if I combine three times HRV or three times 10.8 µm?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is creating an RGB, we are already implicitely renaming the time dimension with bands, so I get we already moved out of the scientific realm to move into the imaging one.

composite = compositor((data_arrays[0], data_arrays[1], data_arrays[2]))
Comment on lines +193 to +194
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is time metadata available, this would probably fail with IncompatibleTimes, due to the check in composites.check_times.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea which data has that so I could test? SEVIRI HRIT and AVHRR L1b AAPP worked.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in a lot of cases we started removing the time metadata in the readers because it was so inconsistent. I could be completely misremembering though.

composite.attrs = data_arrays[2].attrs

return composite
2 changes: 1 addition & 1 deletion satpy/multiscene/_multiscene.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ def blend(
then assigns those datasets to the blended scene.

Blending functions provided in this module are :func:`stack`
(the default) and :func:`timeseries`, but the Python built-in
(the default), :func:`timeseries`, and :func:`temporal_rgb`, but the Python built-in
function :func:`sum` also works and may be appropriate for
some types of data.

Expand Down
44 changes: 43 additions & 1 deletion satpy/tests/multiscene_tests/test_blend.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ def test_blend_two_scenes_bad_blend_type(self, multi_scene_and_weights, groups):

simple_groups = {DataQuery(name='CloudType'): groups[DataQuery(name='CloudType')]}
multi_scene.group(simple_groups)

weights = [weights[0][0], weights[1][0]]
stack_func = partial(stack, weights=weights, blend_type="i_dont_exist")
with pytest.raises(ValueError):
Expand Down Expand Up @@ -390,3 +389,46 @@ def _check_stacked_metadata(data_arr: xr.DataArray, exp_name: str) -> None:
assert 'sensor' not in data_arr.attrs
assert 'platform_name' not in data_arr.attrs
assert 'long_name' not in data_arr.attrs


class TestTemporalRGB:
"""Test the temporal RGB blending method."""

@pytest.fixture
def nominal_data(self):
"""Return the input arrays for the nominal use case."""
da1 = xr.DataArray([1, 0, 0], attrs={'start_time': datetime(2023, 5, 22, 9, 0, 0)})
da2 = xr.DataArray([0, 1, 0], attrs={'start_time': datetime(2023, 5, 22, 10, 0, 0)})
da3 = xr.DataArray([0, 0, 1], attrs={'start_time': datetime(2023, 5, 22, 11, 0, 0)})

return [da1, da2, da3]

@pytest.fixture
def expected_result(self):
"""Return the expected result arrays."""
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

@staticmethod
def _assert_results(res, expected_start_time, expected_result):
assert res.attrs['start_time'] == expected_start_time
np.testing.assert_equal(res.data[0, :], expected_result[0])
np.testing.assert_equal(res.data[1, :], expected_result[1])
np.testing.assert_equal(res.data[2, :], expected_result[2])

def test_nominal(self, nominal_data, expected_result):
"""Test that nominal usage with 3 datasets works."""
from satpy.multiscene import temporal_rgb

res = temporal_rgb(nominal_data)

self._assert_results(res, nominal_data[-1].attrs['start_time'], expected_result)

def test_extra_datasets(self, nominal_data, expected_result):
"""Test that only the first three arrays affect the usage."""
from satpy.multiscene import temporal_rgb

da4 = xr.DataArray([0, 0, 1], attrs={'start_time': datetime(2023, 5, 22, 12, 0, 0)})

res = temporal_rgb(nominal_data + [da4,])

self._assert_results(res, nominal_data[-1].attrs['start_time'], expected_result)