Cloud Masking

Clouds contaminate optical imagery and produce false spectral signatures. Before compositing or analysis, you must identify and mask cloudy pixels. This module teaches you how to use quality bands to create clean images.

Learning objectives

  • Understand why cloud masking is essential for optical imagery.
  • Read and interpret QA (quality assurance) band bit structures.
  • Write cloud mask functions for Landsat and Sentinel-2.
  • Map cloud mask functions over image collections.
  • Create clean composites from masked collections.

Why it matters

Clouds appear bright in visible bands and can be misclassified as snow, urban areas, or bare soil. Shadows appear dark and get confused with water or vegetation. Removing these pixels before analysis prevents garbage-in, garbage-out results.

Key vocabulary

QA Band
Quality Assurance band containing bit-packed flags for clouds, shadows, water, snow, etc.
Bit Flag
A single bit (0 or 1) in a QA band that indicates presence/absence of a condition.
Bitwise AND
Operation to extract specific bits from a QA band (.bitwiseAnd()).
Mask
A binary image where 0 = hide pixel, 1 = show pixel.

Quick win: Landsat 8/9 cloud mask

The QA_PIXEL band contains cloud and shadow flags. Here's a complete example:

// Cloud masking function for Landsat 8/9 Collection 2 Surface Reflectance
function maskL8sr(image) {
  // Bit 3: Cloud, Bit 4: Cloud Shadow
  var cloudShadowBitMask = (1 << 4);
  var cloudsBitMask = (1 << 3);
  
  // Get the QA band
  var qa = image.select('QA_PIXEL');
  
  // Both flags should be zero (clear conditions)
  var mask = qa.bitwiseAnd(cloudShadowBitMask).eq(0)
      .and(qa.bitwiseAnd(cloudsBitMask).eq(0));
  
  // Apply mask and scale the image
  return image.updateMask(mask)
      .multiply(0.0000275).add(-0.2);
}

// Load and filter collection
var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(ee.Geometry.Point([-82.3, 29.6]))
  .map(maskL8sr);

// Create median composite
var composite = collection.median();

// Visualize
Map.centerObject(collection, 9);
Map.addLayer(composite, {
  bands: ['SR_B4', 'SR_B3', 'SR_B2'],
  min: 0,
  max: 0.3
}, 'Cloud-free Composite');

print('Images in collection:', collection.size());

What you should see

A cloud-free summer composite. Compare to a non-masked composite to see the difference.

Understanding QA band bit structure

QA bands pack multiple flags into a single integer using bits:

Bit Landsat QA_PIXEL Value when set
0 Fill 1
1 Dilated Cloud 2
2 Cirrus (high confidence) 4
3 Cloud 8
4 Cloud Shadow 16
5 Snow 32
6 Clear 64
7 Water 128

To check if bit 3 (cloud) is set: qa.bitwiseAnd(1 << 3) or qa.bitwiseAnd(8)

Sentinel-2 cloud mask

Sentinel-2 uses the SCL (Scene Classification Layer) band:

// Cloud masking function for Sentinel-2 SR
function maskS2clouds(image) {
  var scl = image.select('SCL');
  
  // SCL classes to mask:
  // 3 = Cloud Shadow, 8 = Cloud Medium Probability, 
  // 9 = Cloud High Probability, 10 = Thin Cirrus
  var mask = scl.neq(3).and(scl.neq(8)).and(scl.neq(9)).and(scl.neq(10));
  
  // Apply mask and scale
  return image.updateMask(mask).divide(10000);
}

// Apply to collection
var s2Collection = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(ee.Geometry.Point([-82.3, 29.6]))
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 30))
  .map(maskS2clouds);

var s2Composite = s2Collection.median();

Map.addLayer(s2Composite, {
  bands: ['B4', 'B3', 'B2'],
  min: 0,
  max: 0.3
}, 'Sentinel-2 Clean');

Sensor band reference

Sensor QA Band Scale Factor Cloud Bits/Values
Landsat 8/9 SR QA_PIXEL 0.0000275, offset -0.2 Bit 3 (cloud), Bit 4 (shadow)
Landsat 5/7 SR QA_PIXEL 0.0000275, offset -0.2 Bit 3 (cloud), Bit 4 (shadow)
Sentinel-2 SR SCL 1/10000 Values 3, 8, 9, 10
MODIS state_1km varies by product Bits 0-1 (cloud state)

Pro tips

  • Always pre-filter by cloud cover metadata (CLOUD_COVER or CLOUDY_PIXEL_PERCENTAGE) to speed up processing.
  • For aggressive masking, include dilated cloud (bit 1) and cirrus (bit 2).
  • Test your mask on a single image before mapping over the collection.
  • In persistently cloudy regions, use longer time windows or radar data.

Try it: Compare masked vs unmasked

Add an unmasked composite to see the difference:

// Unmasked composite for comparison
var unmasked = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(ee.Geometry.Point([-82.3, 29.6]))
  .median()
  .multiply(0.0000275).add(-0.2);

Map.addLayer(unmasked, {
  bands: ['SR_B4', 'SR_B3', 'SR_B2'],
  min: 0, max: 0.3
}, 'Unmasked (with clouds)', false);

Toggle between layers to see how clouds contaminate the composite.

Common mistakes

  • Forgetting to map the mask function over the collection (.map(maskL8sr)).
  • Using wrong bit positions for different Landsat collections.
  • Not applying scale factors after masking.
  • Masking too aggressively and losing valid pixels.
  • Expecting perfect results - some cloud edges always slip through.

Quick self-check

  1. Why do we need to mask clouds before calculating NDVI?
  2. What does bitwiseAnd(1 << 3) extract?
  3. What is the difference between Landsat's QA_PIXEL and Sentinel-2's SCL?
  4. Why pre-filter by cloud cover percentage before applying a pixel mask?

This module builds on

Next steps