Skip to content

Implement FCI "Flames" fire visualisation #3129

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

Merged
merged 10 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions satpy/composites/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2036,3 +2036,45 @@ def __call__(self, projectables, nonprojectables=None, **info):

masked_projectable = projectable.where(lon_min_max)
return super().__call__([masked_projectable], **info)


class SimpleFireMaskCompositor(CompositeBase):
"""Class for a simple fire detection compositor."""

def __call__(self, projectables, nonprojectables=None, **attrs):
"""Compute a simple fire detection to create a boolean mask to be used in "flames" composites.

Expects 4 channel inputs, calibrated to BT/reflectances, in this order [µm]: 10.x, 3.x, 2.x, 0.6.

It applies 4 spectral tests, for which the thresholds must be provided in the yaml as "test_thresholds":
- Test 0: 10.x > thr0 (clouds filter)
- Test 1: 3.x-10.x > thr1 (hotspot)
- Test 2: 0.6 > thr2 (clouds, sunglint filter)
- Test 3: 3.x+2.x > thr3 (hotspot)

.. warning::
This fire detection algorithm is extremely simple, so it is prone to false alarms and missed detections.
It is intended only for PR-like visualisation of large fires, not for any other use.
The tests have been designed for MTG-FCI.

"""
projectables = self.match_data_arrays(projectables)
info = combine_metadata(*projectables)
info["name"] = self.attrs["name"]
info.update(self.attrs)

# fire spectral tests

# test 0: # window channel should be warm (no clouds)
ir_105_temp = projectables[0] > self.attrs["test_thresholds"][0]
# test 1: # 3.8-10.5µm should be high (hotspot)
temp_diff = projectables[1] - projectables[0] > self.attrs["test_thresholds"][1]
# test 2: vis_06 should be low (no clouds, no sunglint)
vis_06_bright = projectables[3] < self.attrs["test_thresholds"][2]
# test 3: 3.8+2.2µm should be high (hotspot)
ir38_plus_nir22 = projectables[1] + projectables[2] >= self.attrs["test_thresholds"][3]

res = ir_105_temp & temp_diff & vis_06_bright & ir38_plus_nir22 # combine all tests

res.attrs = info
return res
55 changes: 53 additions & 2 deletions satpy/etc/composites/fci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ composites:

true_color_with_night_ir:
description: >
True Color during daytime, and a simple IR105 layer during nighttime.
True Color during daytime, and the night_ir during nighttime.
compositor: !!python/name:satpy.composites.DayNightCompositor
standard_name: fci_day_night_blend
lim_low: 78
Expand All @@ -162,7 +162,7 @@ composites:

true_color_with_night_ir_hires:
description: >
True Color during daytime, and a simple IR105 layer during nighttime.
True Color during daytime, and the night_ir during nighttime.
compositor: !!python/name:satpy.composites.DayNightCompositor
standard_name: fci_day_night_blend
lim_low: 78
Expand Down Expand Up @@ -246,6 +246,57 @@ composites:
- flash_age
- true_color_with_night_ir105

### True Color with Fires
true_color_flames_with_night_ir105:
Copy link
Member

Choose a reason for hiding this comment

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

Wasn't there a recommendation from the RGB workshop to name all day-only products with a day prefix?

Copy link
Member Author

@ameraner ameraner May 15, 2025

Choose a reason for hiding this comment

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

the fire overlay works day&night, so also the background composite is day&night, so we can probably leave it like this? Naming of complex composites gets ugly quite quickly...

description: >
True Color with a simple night layer overlayed with a fire visualisation. Works best with HRFI+FDHSI inputs.
Originally inspired by the work of Pierre Markuse
on S2 https://custom-scripts.sentinel-hub.com/custom-scripts/sentinel-2/markuse_fire/
WARNING: This composite uses an extremely simple fire detector to identify fire pixels, so it is prone
to false alarms and missed detection. It is intended solely for PR-like visualisation purposes of large fires,
and no other (operational) use.
compositor: !!python/name:satpy.composites.BackgroundCompositor
standard_name: true_color_flames
prerequisites:
- flames_masked
- true_color_with_night_ir105

flames_masked:
compositor: !!python/name:satpy.composites.MaskingCompositor
standard_name: fci_flames_colorised
conditions:
- method: less
value: 0.5
transparency: 100
- method: isnan
transparency: 100
prerequisites:
# Composite
- name: fci_fire_channels_sum
# Data used in masking
- name: simple_fci_fire_mask

simple_fci_fire_mask:
compositor: !!python/name:satpy.composites.SimpleFireMaskCompositor
standard_name: simple_fci_fire_mask
prerequisites:
- ir_105
- ir_38
- nir_22
- vis_06
test_thresholds:
- 293
- 20
- 15
- 340

fci_fire_channels_sum:
standard_name: fci_fire_channels_sum
compositor: !!python/name:satpy.composites.SumCompositor
prerequisites:
- name: ir_38
- name: nir_22

### GeoColor
geo_color:
compositor: !!python/name:satpy.composites.DayNightCompositor
Expand Down
71 changes: 71 additions & 0 deletions satpy/etc/composites/viirs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,74 @@ composites:
- name: I01
- name: I03
standard_name: cimss_cloud_type


### True Color with Fires
true_color_flames_with_night_ir105:
description: >
True Color with a simple night layer overlayed with a fire visualisation. Works best with HRFI+FDHSI inputs.
Originally inspired by the work of Pierre Markuse
on S2 https://custom-scripts.sentinel-hub.com/custom-scripts/sentinel-2/markuse_fire/
WARNING: This composite uses an extremely simple fire detector to identify fire pixels, so it is prone
to false alarms and missed detection. It is intended solely for PR-like visualisation purposes of large fires,
and no other (operational) use.
compositor: !!python/name:satpy.composites.BackgroundCompositor
standard_name: image_ready
prerequisites:
- flames_masked
- true_color_with_night_ir105

flames_masked:
compositor: !!python/name:satpy.composites.MaskingCompositor
standard_name: viirs_flames_colorised
conditions:
- method: less
value: 0.5
transparency: 100
- method: isnan
transparency: 100
prerequisites:
# Composite
- name: viirs_fire_channels_sum
# Data used in masking
- name: simple_viirs_fire_mask

simple_viirs_fire_mask:
compositor: !!python/name:satpy.composites.SimpleFireMaskCompositor
standard_name: simple_viirs_fire_mask
prerequisites:
- I05
- I04
- M11
- I01
test_thresholds:
- 293
- 20
- 15
- 340

viirs_fire_channels_sum:
standard_name: image_ready
compositor: !!python/name:satpy.composites.SumCompositor
prerequisites:
- name: I04
- name: M11


true_color_with_night_ir105:
description: >
True Color during daytime, and a simple IR105 layer during nighttime.
compositor: !!python/name:satpy.composites.DayNightCompositor
standard_name: image_ready
lim_low: 78
lim_high: 88
prerequisites:
- true_color
- night_ir105


night_ir105:
compositor: !!python/name:satpy.composites.SingleBandCompositor
prerequisites:
- name: I05
standard_name: night_ir105
17 changes: 17 additions & 0 deletions satpy/etc/enhancements/fci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ enhancements:
method: !!python/name:satpy.enhancements.gamma
kwargs: {gamma: [1.0, 1.0, 1.0]}

fci_flames_colorised:
standard_name: fci_flames_colorised
operations:
- name: colorize
method: !!python/name:satpy.enhancements.colorize
kwargs:
palettes:
- { colors: ylorrd, min_value: 330, max_value: 430 , reverse: true}

fci_fire_temperature_sum:
standard_name: fci_fire_temperature_sum
operations: []

true_color_flames:
standard_name: true_color_flames
operations: []

volcanic_emissions:
standard_name: volcanic_emissions
operations:
Expand Down
19 changes: 19 additions & 0 deletions satpy/etc/enhancements/viirs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,22 @@ enhancements:
],
min_value: 0,
max_value: 201}

viirs_flames_colorised:
standard_name: viirs_flames_colorised
operations:
- name: colorize
method: !!python/name:satpy.enhancements.colorize
kwargs:
palettes:
- { colors: ylorrd, min_value: 330, max_value: 430 , reverse: true}


night_ir105:
standard_name: night_ir105
operations:
- name: colorize
method: !!python/name:satpy.enhancements.colorize
kwargs:
palettes:
- { colors: greys, min_value: 190, max_value: 295 }
40 changes: 40 additions & 0 deletions satpy/tests/test_composites.py
Original file line number Diff line number Diff line change
Expand Up @@ -2171,3 +2171,43 @@ def test_realistic_colors(self):
np.testing.assert_allclose(arr[0, :, :], expected_red)
np.testing.assert_allclose(arr[1, :, :], expected_green)
np.testing.assert_allclose(arr[2, :, :], 3.0)


class TestFireMaskCompositor:
"""Test fire mask compositors."""

def test_SimpleFireMaskCompositor(self):
"""Test the SimpleFireMaskCompositor class."""
from satpy.composites import SimpleFireMaskCompositor
rows = 2
cols = 2
ir_105 = xr.DataArray(da.zeros((rows, cols), dtype=np.float32), dims=("y", "x"),
attrs={"name": "ir_105"})
ir_105[0, 0] = 300
ir_38 = xr.DataArray(da.zeros((rows, cols), dtype=np.float32), dims=("y", "x"),
attrs={"name": "ir_38"})
ir_38[0, 0] = 400
nir_22 = xr.DataArray(da.zeros((rows, cols), dtype=np.float32), dims=("y", "x"),
attrs={"name": "nir_22"})
nir_22[0, 0] = 100
vis_06 = xr.DataArray(da.zeros((rows, cols), dtype=np.float32), dims=("y", "x"),
attrs={"name": "vis_06"})
vis_06[0, 0] = 5

projectables = (ir_105, ir_38, nir_22, vis_06)

with dask.config.set(scheduler=CustomScheduler(max_computes=0)):
comp = SimpleFireMaskCompositor(
"simple_fci_fire_mask",
prerequisites=("ir_105", "ir_38", "nir_22", "vis_06"),
standard_name="simple_fci_fire_mask",
test_thresholds=[293, 20, 15, 340])
res = comp(projectables)

assert isinstance(res, xr.DataArray)
assert isinstance(res.data, da.Array)
assert res.attrs["name"] == "simple_fci_fire_mask"
assert res.data.dtype == bool

assert np.array_equal(res.data.compute(),
np.array([[True, False], [False, False]]))
Loading