QuickFire 1.0 – Visualizing Fires in the Sentinel Hub EO Browser

A custom script suitable for non-experts to produce nice-looking satellite images of fires and other hot spots, with options for advanced users

Visualizing fires (and other hot spots)

Some of you may know my older custom script to visualize fires in the Sentinel Hub EO Browser. It has been used to a degree surpassing my expectations and a simplified, hardcoded version of it has even been added to the EO Browser as integrated visualization in the wildfire theme.

Remote Sensing everywhere

Fires near the Zaphorizhia Nuclear Power Plant in Ukraine – 24 August 2022

During the last years, remote sensing made a giant leap forward when it comes to being used by non-experts. Especially people interested in the OSINT (open source intelligence) field and journalists/media outlets are making heavy use of it, but even the general public is using satellite images now to take a look at things.

Getting good (or at least useable) images isn’t always easy for non-experts. So with this custom script, I tried to make it as painless as possible, even for inexperienced users of the EO Browser. While it does not offer the wide range of possibilities my older fire visualization script offers, this new one is easier to use and still holds some possibilities to tweak images for more experienced users. It also delivers better much better results when it comes to the most used visualization style (especially in media) natural colors with infrared overlay. Unlike the old script, the new script pretty much automatically prevents (at least if so desired) white clipping, making it far easier to get good images, and it shows nicer-looking hot spots.

So let us take a look at the script here, download link, some documentation, and examples you’ll find further down.

The Script

// VERSION=3
// QuickFire V1.0.0 by Pierre Markuse (https://twitter.com/Pierre_Markuse)
// Made for use in the Sentinel Hub EO Browser (https://apps.sentinel-hub.com/eo-browser/?)
// CC BY 4.0 International (https://creativecommons.org/licenses/by/4.0/)

function setup() {
  return {
    input: ["B01","B02","B03","B04","B08","B8A","B11","B12","CLP", "dataMask"],
    output: { bands: 4 }
  };
}

function stretch(val, min, max) {return (val - min) / (max - min);} 

function satEnh(arr, s) {
   var avg = arr.reduce((a, b) => a + b, 0) / arr.length;
   return arr.map(a => avg * (1 - s) + a * s);
}

 function layerBlend(lay1, lay2, lay3, op1, op2, op3) {
    return lay1.map(function(num, index) {
     return (num / 100 * op1 + (lay2[index] / 100 * op2) + (lay3[index] / 100 * op3));
    });
  }  

function evaluatePixel(sample) {
  const hsThreshold = [2.0, 1.5, 1.25, 1.0];
  const hotspot = 1;
  const style = 1;
  const hsSensitivity = 1.0;
  const boost = 1;
  
  const cloudAvoidance = 1;
  const cloudAvoidanceThreshold = 245;
  const avoidanceHelper = 0.8;

  const offset = -0.000;
  const saturation = 1.10;
  const brightness = 1.00;
  const sMin = 0.01;
  const sMax = 0.99;
  
  const showBurnscars = 0;
  const burnscarThreshold = -0.25;
  const burnscarStrength = 0.3;

  const NDWI = (sample.B03-sample.B08)/(sample.B03+sample.B08);
  const NDVI = (sample.B08-sample.B04)/(sample.B08+sample.B04);
  const waterHighlight = 0;
  const waterBoost = 2.0;
  const NDVI_threshold = -0.15;
  const NDWI_threshold = 0.15;
  const waterHelper = 0.2;
  
  const Black = [0, 0, 0];
  const NBRindex = (sample.B08-sample.B12) / (sample.B08+sample.B12); 
  const naturalColorsCC = [Math.sqrt(brightness * sample.B04 + offset), Math.sqrt(brightness * sample.B03 + offset), Math.sqrt(brightness * sample.B02 + offset)];
  const naturalColors = [(2.5 * brightness * sample.B04 + offset), (2.5 * brightness * sample.B03 + offset), (2.5 * brightness * sample.B02 + offset)];
  const URBAN = [Math.sqrt(brightness * sample.B12 * 1.2 + offset), Math.sqrt(brightness * sample.B11 * 1.4 + offset), Math.sqrt(brightness * sample.B04 + offset)];
  const SWIR = [Math.sqrt(brightness * sample.B12 + offset), Math.sqrt(brightness * sample.B8A + offset), Math.sqrt(brightness * sample.B04 + offset)];
  const NIRblue = colorBlend(sample.B08, [0, 0.25, 1], [[0/255, 0/255, 0/255],[0/255, 100/255, 175/255],[150/255, 230/255, 255/255]]);
  const classicFalse = [sample.B08 * brightness, sample.B04 * brightness, sample.B03 * brightness];
  const NIR = [sample.B08 * brightness, sample.B08 * brightness, sample.B08 * brightness];
  const atmoPen = [sample.B12 * brightness, sample.B11 * brightness, sample.B08 * brightness];
  var enhNaturalColors = [0, 0, 0];
  for (let i = 0; i < 3; i += 1) { enhNaturalColors[i] = (brightness * ((naturalColors[i] + naturalColorsCC[i]) / 2) + (URBAN[i] / 10)); }
  
  const manualCorrection = [0.00, 0.00, 0.00];
  
  var Viz = layerBlend(URBAN, naturalColors, naturalColorsCC, 10, 40, 50); // Choose visualization(s) and opacity here

  if (waterHighlight) {
    if ((NDVI < NDVI_threshold) && (NDWI > NDWI_threshold) && (sample.B04 < waterHelper)) {
     Viz[1] = Viz[1] * 1.2 * waterBoost + 0.1;
     Viz[2] = Viz[2] * 1.5 * waterBoost + 0.2;
    }
  } 
  
  Viz = satEnh(Viz, saturation);
  for (let i = 0; i < 3; i += 1) {
    Viz[i] = stretch(Viz[i], sMin, sMax); 
    Viz[i] += manualCorrection[i];  
  }

  if (hotspot) {  
    if ((!cloudAvoidance) || ((sample.CLP<cloudAvoidanceThreshold) && (sample.B02<avoidanceHelper))) { switch (style) { case 1: if ((sample.B12 + sample.B11) > (hsThreshold[0] / hsSensitivity)) return [((boost * 0.50 * sample.B12)+Viz[0]), ((boost * 0.50 * sample.B11)+Viz[1]), Viz[2], sample.dataMask]; 
        if ((sample.B12 + sample.B11) > (hsThreshold[1] / hsSensitivity)) return [((boost * 0.50 * sample.B12)+Viz[0]), ((boost * 0.20 * sample.B11)+Viz[1]), Viz[2], sample.dataMask]; 
        if ((sample.B12 + sample.B11) > (hsThreshold[2] / hsSensitivity)) return [((boost * 0.50 * sample.B12)+Viz[0]), ((boost * 0.10 * sample.B11)+Viz[1]), Viz[2], sample.dataMask];  
        if ((sample.B12 + sample.B11) > (hsThreshold[3] / hsSensitivity)) return [((boost * 0.50 * sample.B12)+Viz[0]), ((boost * 0.00 * sample.B11)+Viz[1]), Viz[2], sample.dataMask]; 
       break;
       case 2:
        if ((sample.B12 + sample.B11) > (hsThreshold[3] / hsSensitivity)) return [1, 0, 0, sample.dataMask]; 
       break;
       case 3:
        if ((sample.B12 + sample.B11) > (hsThreshold[3] / hsSensitivity)) return [1, 1, 0, sample.dataMask]; 
       break;
       case 4:  
        if ((sample.B12 + sample.B11) > (hsThreshold[3] / hsSensitivity)) return [Viz[0] + 0.2, Viz[1] - 0.2, Viz[2] - 0.2, sample.dataMask];
       break;
       default:
      }
    }
  }

  if (showBurnscars) {
   if (NBRindex<burnscarThreshold) {
     Viz[0] = Viz[0] + burnscarStrength;
     Viz[1] = Viz[1] + burnscarStrength;
   }
  }

  return [Viz[0], Viz[1], Viz[2], sample.dataMask];
}
DownloadYou can download the script here or in the Sentinel Hub Custom Script Repository.

How to use it

I will assume you know how to use a custom script in the EO Browser, so I will only talk about how to use this script. Feel free to not listen to anything I write here and experiment yourself though.

Let me begin with the most important line:

var Viz = layerBlend(URBAN, naturalColors, naturalColorsCC, 10, 40, 50);

Here you can choose which visualization is used. You can choose three different ones and define their strength from 100 (full result is used) down to 0 (nothing of it is used). Those three visualizations will be added together to the final visualization. This allows for a lot of flexibility in combining them and getting different results. The combination I use here is a good starting point if you have no idea what to do.

Here is a brief description of the predefined options:

  • naturalColors = A simple 4-3-2 natural color band combination
  • naturalColorsCC = A clipping-controlled natural color band combination
  • enhNaturalColors = A natural color band combination incorporating some SWIR data
  • URBAN = Urban false color 12-11-4 combination, good for hot spots
  • SWIR = Classic SWIR 12-8A-4 combination, good at showing burn scars
  • classicFalse = The classic 8-4-3 false color combination
  • NIR = Monochromatic band 8 view
  • NIRblue = Monochromatic blue-tinted band 8 view for better visibility
  • atmoPen = 12-11-8 with good atmospheric penetration especially in smoky areas
  • BLACK = Well, black. Useful if you want to create a modifying layer for your GIS app.
  • NBRindex = Normalized Burn Ration index
TipYou can easily add your own visualizations or change the predefined ones. This way you can add any index of your choice for example.

Especially the naturalColorsCC option will help you with controlling too bright areas in clouded scenes. This is helpful to get images that are looking more natural. But don’t hesitate to experiment with the other ones, natural colors might not always be the best way to show something.

AttentionL2A processing can introduce some artifacts in smoky scenes (bright streaks). If this is the case better try using L1C data.

Fine-tuning

const offset = -0.000;
const saturation = 1.10;
const brightness = 1.00;
const sMin = 0.01;
const sMax = 0.99;

This code block can be used to influence the look of your visualization. sMin and sMax define the black and white point of it, this can be used to enhance contrast or visually “flatten” the image. brightness is used to darken or brighten up the image, saturation can be used to get weaker or stronger colors. offset can be used to shift the histogram of the image to the left (negative values) or right (positive values). Especially images using the naturalColorsCC visualization might need a boost in contrast and saturation as otherwise, they look pretty flat.

Hot spots

const hsThreshold = [2.0, 1.5, 1.25, 1.0];
const hotspot = 1;
const style = 1;
const hsSensitivity = 1.0;
const boost = 1;

This code block is controlling the hot spot visualization. Use hotspot to turn it on (1) or off (0). style is defining which style is used, 1 is red-to-yellow, 2 is solid red, 3 is solid yellow, and 4 is a red tint. With hsSensitivity you can adjust the sensitivity, higher sensitivity will catch more (weaker) hot spots, but lead to more false positives. boost is only used for style 1 and defines the strength of the red-yellow overlay.

hsThreshold defines the cut-offs for the different levels of visualization when using style 1, they are usually fine this way, but if you know what you’re doing play around. Actually, play around even if you don’t know what you’re doing. That’s the fun in learning stuff.

Cloud avoidance

const cloudAvoidance = 1;
const cloudAvoidanceThreshold = 245;
const avoidanceHelper = 0.8;

My older fire scripts did not include clipping control, so clouds were usually white clipping. The algorithm used to detect hot spots often gets false positives in bright clouds, but with clouds already completely white, this wasn’t a problem. This new script, however, does not wash out the clouds completely (at least not if used correctly), so we need a method to avoid hot spots appearing in clouds. Here we use the cloud probability layer (CLP) provided by the EO Browser and also use the blue band to help make a better distinction between smoke and clouds. This is necessary, as the CLP often flags darker smoke as clouds as well, but here we want hot spots to appear. cloudAvoidance is turning all of this on (1), or off (0). Use cloudAvoidanceThreshold and avoidanceHelper to influence what registers as cloud, with lower values leading to more areas registered as cloudy.

AttentionKeep in mind, that it is totally possible to have hot spots under thick clouds or very thick smoke cover, they just can’t always be detected. In those cases try using to visualize the scene using the SWIR or atmoPen preset, as those have the best chance to mitigate the effects of smoke and clouds.

Visualizing burn scars

const showBurnscars = 0;
const burnscarThreshold = -0.25;
const burnscarStrength = 0.3;

This can be used to mark burn scars, it uses a simple NBR index comparison to do so, which, depending on the area, atmospheric conditions, and time since the fire was burning can yield anything from near perfect to absolutely unusable results. This is just a little extra that might come in handy at times. Consider using a SWIR visualization if you are keen on showing burn scars, as they usually show up quite nicely in it. showBurnscars is turned on by setting it to (1), and off by setting it to (0). burnscarThreshold defines the sensitivity, and burnscarStrength how visibly the burn scars are marked.

Water highlighting

const waterHighlight = 0;
const waterBoost = 2.0;
const NDVI_threshold = -0.15;
const NDWI_threshold = 0.15;
const waterHelper = 0.2;

This might be seen as a gimmick, but sometimes it can be useful to highlight water in images. Results depend a lot on atmospheric conditions, but in good scenes, it provides near-perfect results. waterHighlight can be turned on (1) or off (0). waterBoost defines how strong water is highlighted, NDVI_threshold and NDWI_threshold can be adjusted to get better results depending on your scene, waterHelper is using band 4 to further help distinguish water from other things. I can only tell you to experiment with all those settings, but if it works it can really make water pop.

Attentionoffset should be set to 0 when using this, otherwise, the sky might fall on your head.

Manual correction

const manualCorrection = [0.00, 0.00, 0.00];

Here you can influence the result manually by adding or subtracting any amount from the red, green, and blue channels. This can be especially useful when artifacts in the smoke are forcing you to use L1C instead of L2A data. You can, at least partially, correct the atmospheric effects. Try reducing the amount of blue and adding some reds for a very basic correction. Just give it a try.

Getting started and some remarks

If you are totally new to all of this maybe this older blog post can be helpful to you: “Looking at Wildfires (and more…) – An Introduction

You should also take a look at the education pages of the Sentinel Hub EO Browser.

To be fair I should also mention that while the EO Browser can deliver excellent results and sometimes it is hard to believe they were rendered automatically by a custom script you should not shy away from further processing those results. I find it extremely useful to get images through the EO Browser which I then manually process. I usually download the same scene with different settings and then combine those in Photoshop.

Examples

A few examples to get you started, but I encourage you to try the script as you like, experiment, and see what is possible. All these images are rendered in the EO Browser only, no post-processing. Click them to get to the image in the EO Browser and have a look at the settings used in the script.

Battleship Mountain Fire, B.C., Canada - September 10th, 2022

Battleship Mountain Fire, B.C., Canada – September 10th, 2022

One of the most liked visualizations of fires is natural colors with infrared overlay. It looks familiar to the audience (pretty much like from a plane) and usually creates a visually pleasing image. Especially when used in media make sure to explain what can be seen, what looks like flames are just hot spots derived from infrared data.

Battleship Mountain Fire, B.C., Canada - September 10th, 2022

Battleship Mountain Fire, B.C., Canada – September 10th, 2022

A very basic visualization, but I love the clarity it has, hot spots do “pop” and are easily visible.

Selenga River Delta, Russia - August 8th, 2022

Selenga River Delta, Russia – August 8th, 2022

An example of the water highlighting at work.

Chicago, Illinois, USA - July 9th, 2022

Chicago, Illinois, USA – July 9th, 2022

Chicago, Illinois, and The Lake they call Michigan with water highlighting.

Fires in Siberia, Russia. June 28th, 2020.

Fires in Siberia, Russia – June 28th, 2020.

Natural colors with some IR mixed in.

Feedback / Questions

If you have some kind of feedback or questions regarding this script, or ideas about how it could be made better, feel free to share. Comment on this post or contact me via email or on . I would also very much like to see images you create with the script, so in case you do, let me know where I can find them or tag me in your images on Twitter. You might also want to follow for news about the EO Browser. In case you have general questions about the EO Browser you should read this FAQ and register at the Sentinel Hub Forum here to ask questions or maybe contribute your own scripts for the EO Browser. Should you find settings and combinations that work particularly well, maybe consider posting an example image along with the settings used in the Sentinel Hub Forum, so other users can benefit from it as well.

Acknowledgements

I would like to thank the team at Sentinel Hub for providing this excellent service. Their enthusiasm and expertise are big drivers in making satellite images available not only to experts but to journalists/media and the general public. They have been and still are a major driver when it comes to pushing the field of remote sensing.

I’d also like to thank for his idea of saturation refinement[1] and Marko Repše from whom I adapted the way to implement a simple but effective method of white-clipping control[2].

Lastly, I want to thank all people who are going to use the script to get images. Access to open data is pointless if we don’t use it. Looking forward to seeing cool images and hopefully some stories going along with them. The Earth is changing in front of our eyes. Satellite images are proof and witness of these changes at the same time.

References

1. ^ Kadunc M. (2017), Color Correction with JavaScript, Sentinel Hub Blog. https://medium.com/sentinel-hub/color-correction-with-javascript-d721e12a919.
2. ^ Repše M. (2020), Highlight Optimized Natural Color Script, Sentinel Hub Custom Scripts. https://custom-scripts.sentinel-hub.com/sentinel-2/highlight_optimized_natural_color/#

Images if not otherwise stated: Contains modified Copernicus Sentinel data [2022], processed by Pierre Markuse, CC BY 4.0 license

One thought on “QuickFire 1.0 – Visualizing Fires in the Sentinel Hub EO Browser

  1. Pingback: Visualizing Wildfires and Burn Scars with the Sentinel Hub EO Browser V2 - Pierre Markuse

Leave a Reply

Your email address will not be published. Required fields are marked *