From 6d8eed79f4d273193b4b7d1a5177c3809bdd5434 Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Mon, 22 May 2023 13:12:14 +0300 Subject: [PATCH 1/8] Add a blend method to create temporal RGB from MultiScene --- satpy/multiscene/__init__.py | 2 +- satpy/multiscene/_blend_funcs.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/satpy/multiscene/__init__.py b/satpy/multiscene/__init__.py index 3cfa907017..0338f47d77 100644 --- a/satpy/multiscene/__init__.py +++ b/satpy/multiscene/__init__.py @@ -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 diff --git a/satpy/multiscene/_blend_funcs.py b/satpy/multiscene/_blend_funcs.py index 0210cef5cc..e8d25e5f0a 100644 --- a/satpy/multiscene/_blend_funcs.py +++ b/satpy/multiscene/_blend_funcs.py @@ -178,3 +178,22 @@ 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], + weights: Optional[Sequence[xr.DataArray]] = None, + combine_times: bool = True, + blend_type: str = 'select_with_weights' +) -> 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") + composite = compositor((data_arrays[0], data_arrays[1], data_arrays[2]), attrs=data_arrays[2].attrs) + + return composite From 72ccf1f439178fa8d629701b64c8608906ff9def Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Mon, 22 May 2023 13:42:05 +0300 Subject: [PATCH 2/8] Remove extra parameters --- satpy/multiscene/_blend_funcs.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/satpy/multiscene/_blend_funcs.py b/satpy/multiscene/_blend_funcs.py index e8d25e5f0a..ff2749db81 100644 --- a/satpy/multiscene/_blend_funcs.py +++ b/satpy/multiscene/_blend_funcs.py @@ -182,9 +182,6 @@ def timeseries(datasets): def temporal_rgb( data_arrays: Sequence[xr.DataArray], - weights: Optional[Sequence[xr.DataArray]] = None, - combine_times: bool = True, - blend_type: str = 'select_with_weights' ) -> xr.DataArray: """Combine a series of datasets as a temporal RGB. From 3c48c53a1c51a966b6cbd9628f510380cfb491af Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Mon, 22 May 2023 13:49:51 +0300 Subject: [PATCH 3/8] Fix using the attributes from the latest scene --- satpy/multiscene/_blend_funcs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/satpy/multiscene/_blend_funcs.py b/satpy/multiscene/_blend_funcs.py index ff2749db81..82597aa3fc 100644 --- a/satpy/multiscene/_blend_funcs.py +++ b/satpy/multiscene/_blend_funcs.py @@ -191,6 +191,7 @@ def temporal_rgb( from satpy.composites import GenericCompositor compositor = GenericCompositor("temporal_composite") - composite = compositor((data_arrays[0], data_arrays[1], data_arrays[2]), attrs=data_arrays[2].attrs) + composite = compositor((data_arrays[0], data_arrays[1], data_arrays[2])) + composite.attrs = data_arrays[2].attrs return composite From ef355b4e9c9c88dfd968540d7a29a6a3a54906d0 Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Mon, 22 May 2023 14:49:15 +0300 Subject: [PATCH 4/8] Add tests for temporal RGB blending --- satpy/tests/multiscene_tests/test_blend.py | 43 +++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/satpy/tests/multiscene_tests/test_blend.py b/satpy/tests/multiscene_tests/test_blend.py index 6b85dd9d79..a4aca901d2 100644 --- a/satpy/tests/multiscene_tests/test_blend.py +++ b/satpy/tests/multiscene_tests/test_blend.py @@ -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): @@ -390,3 +389,45 @@ 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 + for i in range(3): + np.testing.assert_equal(res.data[i, :], expected_result[i]) + + 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) From 02b971c4d7156853aebe831dfc479d7eab1a964c Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Mon, 22 May 2023 14:55:40 +0300 Subject: [PATCH 5/8] Update MultiScene.blend() docstring --- satpy/multiscene/_multiscene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satpy/multiscene/_multiscene.py b/satpy/multiscene/_multiscene.py index d803758b88..48c8db6b99 100644 --- a/satpy/multiscene/_multiscene.py +++ b/satpy/multiscene/_multiscene.py @@ -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. From 652e32515ac24b524df9c3edde86ca94d820898a Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Tue, 23 May 2023 08:18:32 +0300 Subject: [PATCH 6/8] Update satpy/multiscene/_multiscene.py Co-authored-by: David Hoese --- satpy/multiscene/_multiscene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satpy/multiscene/_multiscene.py b/satpy/multiscene/_multiscene.py index 48c8db6b99..c93f5706bc 100644 --- a/satpy/multiscene/_multiscene.py +++ b/satpy/multiscene/_multiscene.py @@ -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), :func:`timeseries` and :func:`temporal_rgb`, 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. From 6c41a99c7b0f76b92330b09a3b2ba7df1ed1245b Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Tue, 23 May 2023 08:21:41 +0300 Subject: [PATCH 7/8] Use explicit indices in test assertions --- satpy/tests/multiscene_tests/test_blend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/satpy/tests/multiscene_tests/test_blend.py b/satpy/tests/multiscene_tests/test_blend.py index a4aca901d2..2fca990f21 100644 --- a/satpy/tests/multiscene_tests/test_blend.py +++ b/satpy/tests/multiscene_tests/test_blend.py @@ -411,8 +411,9 @@ def expected_result(self): @staticmethod def _assert_results(res, expected_start_time, expected_result): assert res.attrs['start_time'] == expected_start_time - for i in range(3): - np.testing.assert_equal(res.data[i, :], expected_result[i]) + 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 ec911dbe090691246431bcf81a588bf811f47f9f Mon Sep 17 00:00:00 2001 From: Panu Lahtinen Date: Thu, 25 May 2023 10:17:29 +0300 Subject: [PATCH 8/8] Fix syntax error adding an item to a list --- satpy/tests/multiscene_tests/test_blend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satpy/tests/multiscene_tests/test_blend.py b/satpy/tests/multiscene_tests/test_blend.py index 2fca990f21..7140d98c8e 100644 --- a/satpy/tests/multiscene_tests/test_blend.py +++ b/satpy/tests/multiscene_tests/test_blend.py @@ -429,6 +429,6 @@ def test_extra_datasets(self, nominal_data, expected_result): da4 = xr.DataArray([0, 0, 1], attrs={'start_time': datetime(2023, 5, 22, 12, 0, 0)}) - res = temporal_rgb(nominal_data + da4) + res = temporal_rgb(nominal_data + [da4,]) self._assert_results(res, nominal_data[-1].attrs['start_time'], expected_result)