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_COVERorCLOUDY_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
- Why do we need to mask clouds before calculating NDVI?
- What does
bitwiseAnd(1 << 3)extract? - What is the difference between Landsat's QA_PIXEL and Sentinel-2's SCL?
- Why pre-filter by cloud cover percentage before applying a pixel mask?
This module builds on
- Image Collections - filtering and mapping over collections
- Functions - writing reusable mask functions
Next steps
- Temporal Compositing - create cloud-free composites
- NDVI - calculate vegetation indices on clean images
- Band Arithmetic - perform calculations on clean data
- Classification - classify clean imagery