Working with QA Bands and Bitmasks in Google Earth Engine

Most optical satellite imagery products come with one or more QA-bands that allows the user to assess quality of each pixel and extract pixels that meet their requirements. The most common application for QA-bands is to extract information about cloudy pixels and mask them. But the QA bands contain a wealth of other information that can help you remove low quality data from your analysis. Typically the information contained in QA bands is stored as Bitwise Flags. In this post, I will cover basic concepts related to Bitwise operations and how to extract and mask with specific quality indicators using Bitmasks.

For this post, we will work with MOD11A1.006 Terra Land Surface Temperature and Emissivity Daily Global 1km dataset. But the concepts and code snippet can be applied to any other dataset easily. Looking at the bands metadata, we see that the LST_Day_1km band is accompanied by a QC_Day band containing LST Quality Indicators.

Understanding QA flags

The QC_day band contains 8-bits of information. That means the pixel values can range from 0 to 255. There are 4 different indicators stored in this band, with each having 4 possible values. Each combination of these values result in a different 8-bit number that is stored in the output. We can learn about about how these QA bits can be interpreted using a concrete example. Let’s say, you added the QC_Day band to the map and inspected a pixel. The pixel has a value of 145. What does it mean? Well, the value 145 is the result of each QA flag being set to a specific value.

Tip: Use this handy decimal to binary converter to know the binary representation of a number and vice-versa.

The figure below breaks down the value into bit pairs and shows how they can be interpreted.

Understanding BitMasks

Let’s say you want to query the QA band for all pixels where Bits 4-5 are set to a specific value. You will need to extract the information stored in Bits 4-5 while ignoring all other bits. This can be achieved using a Bitmask. Simply put, a bitmask will retain information in the bits of interest and set all other values to 0.

Bitmasks can be created using the left-shift (<<) and right-shift (>>) operators. The GEE API provides leftShift() and rightShift() functions for ee.Image() and ee.Number(). Once a bitmask is created, it can be applied on the input image using the bitwiseAnd() function. Below is a GEE API function that extracts the specific bits.

// Helper function to extract the values from specific bits
// The input parameter can be a ee.Number() or ee.Image()
// Code adapted from https://gis.stackexchange.com/a/349401/5160
var bitwiseExtract = function(input, fromBit, toBit) {
  var maskSize = ee.Number(1).add(toBit).subtract(fromBit)
  var mask = ee.Number(1).leftShift(maskSize).subtract(1)
  return input.rightShift(fromBit).bitwiseAnd(mask)
}

To understand this code, take a look at the figure below. We will continue the example from the previous section and try to see how this algorithm works by answering the question: What is the value of bits 4 and 5 in the number 145?

Masking Low Quality Data

Let’s now put all of this together and see how we can mask low-quality data from the MODIS LST images. Our goal is to use the QC_day band and extract all pixels from the LST_Day_1km image where

  • QA Flag (Bits 0-1) is 0 or 1 (LST produced of both good and other quality)
  • Data Quality Flag (Bits 2-3) is 0 (Good data quality)
  • LST Error Flag (Bits 6-7) is 0 (Average LST error ≤ 1K)
var modisLST = ee.ImageCollection("MODIS/006/MOD11A1")
var lsib = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
var australia = lsib.filter(ee.Filter.eq('country_na', 'Australia'))
var geometry = australia.geometry()
var terra = modisLST
  .filter(ee.Filter.date('2001-01-01', '2010-01-01'))
  .select('LST_Day_1km','QC_Day');
  
// Get a single image for testing
var image = ee.Image(terra.first())
var lstDay = image.select('LST_Day_1km')
var qcDay = image.select('QC_Day')
// Let's extract all pixels from the input image where
// Bits 0-1 <= 1 (LST produced of both good and other quality)
// Bits 2-3 = 0 (Good data quality)
// Bits 4-5 Ignore, any value is ok
// Bits 6-7 = 0 (Average LST error ≤ 1K)
var qaMask = bitwiseExtract(qcDay, 0, 1).lte(1)
var dataQualityMask = bitwiseExtract(qcDay, 2, 3).eq(0)
var lstErrorMask = bitwiseExtract(qcDay, 6, 7).eq(0)
var mask = qaMask.and(dataQualityMask).and(lstErrorMask)
var lstDayMasked = lstDay.updateMask(mask)  
var visParams = {min:13000, max:16000, palette: ['green', 'yellow', 'red']}
Map.addLayer(lstDay.clip(geometry), visParams, 'Original LST Image');
Map.addLayer(lstDayMasked.clip(geometry), visParams, 'LST Masked');
Original vs. Masked Image

You can also wrap this code in a function and map() it over a collection, to mask all images with the same criteria. Here’s the full script showing how to do it with additional comments.

If you are new to Earth Engine and want to master it, check out my course End-to-End Google Earth Engine.

Leave a Reply