What Cities Look Like from Space

Here's a wild fact: cities cover less than 3% of Earth's land, yet more than half of all humans live in them, and they gobble up 75% of the world's energy. In this module, we'll learn to spot built-up areas from orbit, light up urban footprints with nighttime imagery, and measure how cities have been sprawling over time.

Learning objectives

  • Map built-up areas using the Normalized Difference Built-up Index (NDBI)
  • Analyze nighttime lights for urbanization using VIIRS data
  • Combine NDVI, NDBI, and NDWI for urban land use classification
  • Measure urban expansion over time by comparing multi-date imagery

Why should we care?

The United Nations projects that 68% of the world's population will live in cities by 2050. That's a lot of concrete. All that rapid urbanization drives habitat loss, cranks up urban heat islands, strains water resources, and reshapes entire ecosystems.

Planners, governments, and researchers need accurate, up-to-date maps of where cities are and where they're headed. Remote sensing gives us exactly that, at scales from individual neighborhoods to entire continents, with records stretching back decades.

Key vocabulary

NDBI (Normalized Difference Built-up Index)
A spectral index that highlights built-up and impervious surfaces. It uses the SWIR and NIR bands: (SWIR - NIR) / (SWIR + NIR).
VIIRS DNB (Visible Infrared Imaging Radiometer Suite, Day/Night Band)
A sensor on the Suomi NPP and NOAA-20 satellites that measures nighttime light emissions, revealing patterns of human settlement and economic activity.
Urban heat island
The phenomenon where cities are significantly warmer than surrounding rural areas due to impervious surfaces, reduced vegetation, and waste heat.
Impervious surface
Any surface that does not allow water to infiltrate, such as roads, buildings, and parking lots. A key indicator of urbanization.
JRC Global Human Settlement Layer (GHSL)
A European Commission dataset mapping built-up areas globally from 1975 to present using Landsat and Sentinel imagery.

Your first urban map in 60 seconds

Let's jump right in. This script calculates NDBI from Landsat 8 and maps Houston's built-up areas. Bright pixels? Those are roads, buildings, and parking lots.

// Define Houston area
var houston = ee.Geometry.Point([-95.37, 29.76]);

// Load Landsat 8 SR, summer 2023
var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(houston)
  .filter(ee.Filter.lt('CLOUD_COVER', 20))
  .median();

// Apply scale factors
var scaled = l8.multiply(0.0000275).add(-0.2);

// Calculate NDBI = (SWIR1 - NIR) / (SWIR1 + NIR)
var ndbi = scaled.normalizedDifference(['SR_B6', 'SR_B5']).rename('NDBI');

// Visualize
Map.addLayer(ndbi, {min: -0.5, max: 0.5, palette: ['blue', 'white', 'red']}, 'NDBI');
Map.centerObject(houston, 11);
print('NDBI calculated for Houston');

What you should see

A map centered on Houston where red and warm tones light up built-up areas (downtown, highways, commercial zones). White shows mixed land cover, and blue tones mark vegetation and water. The urban core and major transportation corridors pop right out. Pretty cool, right?

Why are cities tricky to map?

Cities are a unique challenge for remote sensing. Unlike a nice uniform forest or a field of corn, urban areas are a wild mosaic of materials: concrete, asphalt, metal roofs, glass, patches of trees, and water bodies, all packed within a few hundred meters of each other.

So how do we make sense of all that? We have several tools in our toolkit:

  • Spectral indices: NDBI highlights impervious surfaces; NDVI reveals urban vegetation; NDWI identifies water bodies. Used together, they separate the major urban land cover types.
  • Nighttime lights: Artificial light emissions from cities give us an independent measure of urbanization that correlates with population density and economic output.
  • Multi-temporal analysis: Comparing images from different years reveals where cities are growing, which neighborhoods are densifying, and where green space is disappearing.
  • Global datasets: Pre-built products like the JRC Global Human Settlement Layer give us ready-to-use urban maps spanning decades.

How NDBI spots concrete from space

The Normalized Difference Built-up Index takes advantage of a neat spectral trick: built-up surfaces (concrete, asphalt) reflect more SWIR energy relative to NIR compared to vegetation. The formula is:

NDBI = (SWIR1 - NIR) / (SWIR1 + NIR)
// For Landsat 8: SWIR1 = SR_B6, NIR = SR_B5

NDBI values range from -1 to +1. Built-up areas typically produce positive values (0.0 to 0.3), while dense vegetation produces negative values. By applying a threshold, we can create a binary urban mask. Let's see it in action:

// Create a binary urban mask from NDBI
var houston = ee.Geometry.Point([-95.37, 29.76]);

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(houston)
  .filter(ee.Filter.lt('CLOUD_COVER', 20))
  .median()
  .multiply(0.0000275).add(-0.2);

var ndbi = l8.normalizedDifference(['SR_B6', 'SR_B5']).rename('NDBI');

// Threshold: pixels with NDBI > 0 are classified as built-up
var urbanMask = ndbi.gt(0).selfMask();

Map.addLayer(urbanMask, {palette: ['red']}, 'Built-up Areas');
Map.centerObject(houston, 11);

A threshold of 0 is a solid starting point, but you may need to adjust it for your study area. Here's something to watch out for: arid regions with bare soil can produce false positives because dry soil also reflects strongly in SWIR. Don't worry, we'll fix that in the next section.

Cities that glow in the dark

Here's where it gets really interesting. The VIIRS Day/Night Band sensor captures visible light emitted at night from street lights, buildings, vehicles, and factories. This gives us a completely different view of urbanization compared to daytime spectral indices.

// Load VIIRS monthly nighttime lights
var ntl = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG')
  .filterDate('2023-06-01', '2023-06-30')
  .first()
  .select('avg_rad');

// Visualize nighttime lights (radiance in nanoWatts/cm2/sr)
Map.addLayer(ntl, {
  min: 0, max: 60,
  palette: ['black', 'blue', 'purple', 'orange', 'yellow', 'white']
}, 'Nighttime Lights - June 2023');
Map.setCenter(-95.37, 29.76, 8);

Nighttime lights open the door to some fascinating urban analyses:

  • Urban footprint delineation: Light emissions define the functional extent of a city, including suburban areas that barely show up in daytime imagery.
  • Economic activity estimation: Nighttime light intensity correlates with GDP, which is especially powerful in regions where economic statistics are unreliable.
  • Disaster assessment: Comparing pre-event and post-event nighttime lights reveals the extent of power outages after hurricanes, earthquakes, or conflict.

What you should see

A dark background with bright clusters showing populated areas. Houston's urban core appears white or yellow, surrounded by a gradient of orange and purple representing suburban and exurban areas. Look closely and you can even see highways connecting cities as thin bright lines.

Three indices are better than one

No single index captures the full complexity of urban land cover. Here's the cool part: by combining NDVI, NDBI, and NDWI, we can build a simple land use classification that separates vegetation, built-up areas, and water.

// Combined urban land use classification
var houston = ee.Geometry.Point([-95.37, 29.76]);

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-06-01', '2023-08-31')
  .filterBounds(houston)
  .filter(ee.Filter.lt('CLOUD_COVER', 20))
  .median()
  .multiply(0.0000275).add(-0.2);

// Calculate three indices
var ndvi = l8.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI');
var ndbi = l8.normalizedDifference(['SR_B6', 'SR_B5']).rename('NDBI');
var ndwi = l8.normalizedDifference(['SR_B3', 'SR_B5']).rename('NDWI');

// Simple classification using index thresholds
// Water: NDWI > 0; Vegetation: NDVI > 0.3; Built-up: NDBI > 0
var water = ndwi.gt(0);
var vegetation = ndvi.gt(0.3).and(water.not());
var builtUp = ndbi.gt(0).and(water.not()).and(vegetation.not());

// Combine into a single classification image
var classification = ee.Image(0)
  .where(water, 1)       // 1 = Water
  .where(vegetation, 2)  // 2 = Vegetation
  .where(builtUp, 3)     // 3 = Built-up
  .selfMask();

Map.addLayer(classification, {
  min: 1, max: 3,
  palette: ['blue', 'green', 'red']
}, 'Land Use Classification');
Map.centerObject(houston, 11);

This threshold-based approach is fast and interpretable. Think of it like sorting laundry: each index acts as a filter that catches a different material. For more nuanced classification, consider supervised classification methods (covered in earlier modules) that can distinguish commercial, residential, and industrial land uses.

Watching cities grow over time

This is a powerful insight: one of the most compelling applications of urban remote sensing is measuring how cities expand. By comparing NDBI or nighttime lights between two dates, we can map exactly where new development has popped up.

// Compare nighttime lights: 2014 vs 2023
var ntl2014 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG')
  .filterDate('2014-06-01', '2014-06-30')
  .first().select('avg_rad');

var ntl2023 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG')
  .filterDate('2023-06-01', '2023-06-30')
  .first().select('avg_rad');

// Calculate change
var ntlChange = ntl2023.subtract(ntl2014).rename('NTL_Change');

// Visualize: red = brighter (growth), blue = dimmer
Map.addLayer(ntlChange, {
  min: -10, max: 30,
  palette: ['blue', 'white', 'orange', 'red']
}, 'Nighttime Light Change (2014-2023)');
Map.setCenter(-95.37, 29.76, 9);

// Calculate urban area using pixel area
var roi = ee.Geometry.Rectangle([-95.8, 29.5, -95.0, 30.0]);
var urban2023 = ntl2023.gt(5).selfMask(); // Threshold for "urban"

var urbanArea = urban2023.multiply(ee.Image.pixelArea()).reduceRegion({
  reducer: ee.Reducer.sum(),
  geometry: roi,
  scale: 500,
  maxPixels: 1e9
});
print('Urban area (sq m):', urbanArea);

What you should see

A change map where red and orange areas mark locations that became brighter between 2014 and 2023 (new development). In the Houston metro area, look for strong growth signals along the Katy Freeway corridor, in Sugar Land, and in The Woodlands. Blue areas (dimming) are rare in growing cities.

Pro tips

  • NDBI limitations: Bare soil and dry grassland can produce positive NDBI values similar to built-up areas. Always combine NDBI with NDVI to separate urban from barren land.
  • Nighttime lights for comparison: When comparing cities across countries, normalize nighttime light values by area to account for differences in city size.
  • JRC GHSL: For peer-reviewed built-up area maps, use ee.ImageCollection('JRC/GHSL/P2023A/GHS_BUILT_S'). This product provides multi-epoch urban extent from 1975 to 2030.
  • Seasonal effects: Use summer imagery for NDBI to minimize confusion with deciduous forests that lose leaves in winter and may look "built-up" in spectral indices.

Try it: compare nighttime lights for your city

Pick a city you know well and compare its nighttime light footprint between 2014 and 2023. Look for expansion along highways and in suburban fringe areas. It's like watching a city breathe outward.

// Replace the coordinates with your city
var myCity = ee.Geometry.Point([-96.80, 32.78]); // Dallas, TX example

var ntl2014 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG')
  .filterDate('2014-06-01', '2014-06-30')
  .first().select('avg_rad');

var ntl2023 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG')
  .filterDate('2023-06-01', '2023-06-30')
  .first().select('avg_rad');

Map.addLayer(ntl2014, {min: 0, max: 60, palette: ['black', 'yellow']}, '2014');
Map.addLayer(ntl2023, {min: 0, max: 60, palette: ['black', 'yellow']}, '2023');
Map.centerObject(myCity, 9);

Common mistakes

  • Using the wrong SWIR band: Landsat 8 has two SWIR bands. NDBI uses SR_B6 (SWIR1, 1.57 to 1.65 micrometers), not SR_B7 (SWIR2).
  • Forgetting scale factors: Landsat Collection 2 SR data requires .multiply(0.0000275).add(-0.2) before computing indices. Raw DN values will give you nonsense.
  • Comparing different months: When doing multi-temporal urban analysis, use the same month in each year to avoid seasonal vegetation differences that affect NDBI.
  • Ignoring cloud contamination: Unfiltered clouds produce false NDBI values. Always filter by CLOUD_COVER and use a median composite.

Quick self-check

  1. What two bands does NDBI use, and which Landsat 8 band names correspond to them?
  2. Why might bare soil produce a false positive in NDBI analysis?
  3. Name two applications of nighttime lights data beyond urban mapping.
  4. In a combined NDVI-NDBI-NDWI classification, how do we distinguish vegetation from built-up areas?
  5. What function calculates the area of each pixel in square meters?

Going deeper

We've covered urban remote sensing at a foundational level here. For advanced techniques including sub-pixel urban mapping, urban climate analysis, and built environment characterization, check out:

  • EEFA Book - Chapter A1.2: Urban Environments
  • EEFA Book - Chapter A1.3: Built Environments

Next Steps