Image Operations

Before we can compute indices, classify land cover, or detect change, we need to understand the building blocks. Think of this module as learning your tools before building the house. We will cover how a single image differs from a collection, how to pick the bands you need, clip to a study area, and attach computed layers. Let's jump in.

Learning objectives

  • Distinguish ee.Image from ee.ImageCollection and know when to use each.
  • Use .clip(), .mosaic(), .select(), and .addBands() for core image manipulation.
  • Read image metadata and properties using .get(), .propertyNames(), and .bandNames().
  • Understand band structure, projections, and how images store multi-layer data.

Why it matters

Here is one of the most common beginner mistakes: confusing an Image with an ImageCollection. It leads to cryptic errors, and it will happen to you at least once. Understanding the difference (along with core operations like band selection and clipping) gives you a reliable foundation for everything we build in this course.

Key vocabulary

ee.Image
A single raster image composed of one or more bands. Think of it as a single photograph with multiple color channels.
ee.ImageCollection
An ordered stack of images, typically organized by acquisition date. Think of it as a photo album.
Band
One layer of data within an image. Each band stores values for a specific wavelength, index, or measurement.
Metadata
Properties attached to an image (acquisition date, cloud cover, sensor ID) that describe the data without being pixel values.

Your first win: load, select, clip, and add NDVI

Let's get a quick win. This script loads a single Landsat 8 image over San Francisco, selects the visible bands, clips it to a county boundary, and adds a computed NDVI band. All four core operations in under 20 lines.

// Load a specific Landsat 8 image by ID
var image = ee.Image('LANDSAT/LC08/C02/T1_L2/LC08_044034_20230715');

// Apply scale factors for surface reflectance
var scaled = image.select('SR_B.*').multiply(0.0000275).add(-0.2);

// Select RGB bands for display
var rgb = scaled.select(['SR_B4', 'SR_B3', 'SR_B2']);

// Clip to San Francisco County
var sfCounty = ee.FeatureCollection('TIGER/2018/Counties')
  .filter(ee.Filter.eq('NAME', 'San Francisco')).first().geometry();
var clipped = rgb.clip(sfCounty);

// Compute and add NDVI
var ndvi = scaled.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI');
var withNDVI = clipped.addBands(ndvi.clip(sfCounty));

// Display results
Map.centerObject(sfCounty, 12);
Map.addLayer(clipped, {min: 0, max: 0.3}, 'RGB');
Map.addLayer(withNDVI.select('NDVI'), {min: 0, max: 0.8, palette: ['brown', 'yellow', 'green']}, 'NDVI');

What you should see

A true-color image of San Francisco clipped to the county boundary, plus an NDVI layer showing green areas (parks, Presidio) in bright green and urban areas in brown/yellow.

What's the difference? Image vs. ImageCollection

Think of an ee.Image as a single photograph: one acquisition, one date, one set of bands. An ee.ImageCollection is the whole photo album: many images grouped together, usually by sensor and time. This analogy is simple, but it is critical. Functions that work on one type often do not work on the other.

Feature ee.Image ee.ImageCollection
What it holds One raster (one date, one scene) Many rasters (many dates or scenes)
How to load ee.Image('ID') ee.ImageCollection('ID')
Common operations .select(), .clip(), .addBands() .filterDate(), .map(), .median()
Extract one image Already is one .first(), .sort().first()
Combine into one N/A .median(), .mosaic(), .mean()

Key rule: If you loaded with ee.ImageCollection(), you must reduce or extract before you can use single-image operations like .clip() or .normalizedDifference().

Peeking inside an image

Before we process anything, let's peek inside. GEE images carry rich metadata and band information that tells us exactly what data is available.

var image = ee.Image('LANDSAT/LC08/C02/T1_L2/LC08_044034_20230715');

// What bands does this image have?
print('Band names:', image.bandNames());

// What properties (metadata) are available?
print('Property names:', image.propertyNames());

// Read specific properties
print('Cloud cover:', image.get('CLOUD_COVER'));
print('Acquisition date:', image.date().format('YYYY-MM-dd'));
print('Sensor ID:', image.get('SENSOR_ID'));

// Check the projection and scale of a band
print('B4 projection:', image.select('SR_B4').projection());
print('B4 scale (meters):', image.select('SR_B4').projection().nominalScale());

What you should see

The console shows a list of band names (SR_B1 through SR_B7, plus thermal and QA bands), a long list of metadata properties, the cloud cover percentage, the date (2023-07-15), and the native scale of 30 meters for the visible bands.

Picking just the bands you need

Most images contain way more bands than we need. The .select() method pulls out only the ones we want, keeping scripts clean and fast.

var image = ee.Image('LANDSAT/LC08/C02/T1_L2/LC08_044034_20230715');

// Select by exact name (most common)
var rgb = image.select(['SR_B4', 'SR_B3', 'SR_B2']);
print('RGB band count:', rgb.bandNames().size()); // 3

// Select by pattern (regular expression)
var allSR = image.select('SR_B.*');
print('All SR bands:', allSR.bandNames()); // SR_B1 through SR_B7

// Select by index (0-based)
var firstTwo = image.select([0, 1]);
print('First two bands:', firstTwo.bandNames());

// Select and rename in one step
var renamed = image.select(['SR_B5', 'SR_B4'], ['NIR', 'RED']);
print('Renamed bands:', renamed.bandNames()); // NIR, RED

Best practice: Select by name, not by index. Band order can change between datasets, but names are consistent within a collection.

Cutting to your study area

The .clip() method masks an image to a geometry or feature boundary. Pixels outside become transparent. It is great for clean map displays and focusing on our area of interest.

// Clip to a simple geometry
var roi = ee.Geometry.Rectangle([-122.5, 37.6, -122.3, 37.8]);
var clipped = image.clip(roi);

// Clip to a feature from a FeatureCollection
var state = ee.FeatureCollection('TIGER/2018/States')
  .filter(ee.Filter.eq('NAME', 'California')).first();
var clippedToState = image.clip(state.geometry());

Map.addLayer(clippedToState.select(['SR_B4', 'SR_B3', 'SR_B2'])
  .multiply(0.0000275).add(-0.2),
  {min: 0, max: 0.3}, 'California');

Performance note: Clipping is computationally expensive on large or complex geometries. For analysis (reducers, exports), prefer .filterBounds() on the collection and specify the geometry in reduceRegion() instead of clipping first. Save .clip() for display purposes.

Stitching scenes together with mosaic

When our study area spans multiple satellite scenes, .mosaic() stitches them into a single seamless image. Here is the catch: the last image in the collection "wins" where scenes overlap, so sort order matters.

// Create a mosaic from multiple Landsat scenes
var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2023-07-01', '2023-07-31')
  .filterBounds(ee.Geometry.Point([-122.4, 37.7]))
  .filter(ee.Filter.lt('CLOUD_COVER', 15));

// Sort by cloud cover so cleanest image is on top
var mosaic = collection.sort('CLOUD_COVER').mosaic();

// Apply scale factors
var scaled = mosaic.select('SR_B.*').multiply(0.0000275).add(-0.2);

Map.setCenter(-122.4, 37.7, 8);
Map.addLayer(scaled, {bands: ['SR_B4', 'SR_B3', 'SR_B2'], min: 0, max: 0.3}, 'Mosaic');

For a statistically robust composite (better cloud removal), use .median() or .mean() instead of .mosaic(). Mosaicking is best when you want speed or need to preserve the original pixel values from a specific scene.

Stacking on new layers with addBands

The .addBands() method attaches new layers to an existing image. This is how we include computed indices (NDVI, NDWI), elevation data, or any derived layer alongside the original bands. Think of it like adding a new sheet to a stack of transparencies.

var image = ee.Image('LANDSAT/LC08/C02/T1_L2/LC08_044034_20230715');
var scaled = image.select('SR_B.*').multiply(0.0000275).add(-0.2);

// Compute NDVI and NDWI
var ndvi = scaled.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI');
var ndwi = scaled.normalizedDifference(['SR_B3', 'SR_B5']).rename('NDWI');

// Add both to the original image
var enriched = scaled.addBands(ndvi).addBands(ndwi);
print('Enriched band names:', enriched.bandNames());
// SR_B1, SR_B2, ..., SR_B7, NDVI, NDWI

// Add elevation from a different source
var dem = ee.Image('USGS/SRTMGL1_003').rename('elevation');
var withElevation = enriched.addBands(dem);
print('With elevation:', withElevation.bandNames());

By default, .addBands() keeps all bands. If the new band has the same name as an existing one, you can use .addBands(newBand, null, true) to overwrite the existing band.

Reading the fine print: image metadata

Metadata properties tell us when, where, and how an image was captured. Think of it as the EXIF data on your phone photos, but for satellites. This information is essential for quality control, filtering, and documenting our analyses.

var image = ee.Image('LANDSAT/LC08/C02/T1_L2/LC08_044034_20230715');

// Key metadata fields for Landsat 8
print('Date acquired:', image.get('DATE_ACQUIRED'));
print('Cloud cover (%):', image.get('CLOUD_COVER'));
print('Cloud cover (land):', image.get('CLOUD_COVER_LAND'));
print('Sun elevation:', image.get('SUN_ELEVATION'));
print('WRS Path:', image.get('WRS_PATH'));
print('WRS Row:', image.get('WRS_ROW'));

// The system timestamp (milliseconds since epoch)
print('System time:', image.get('system:time_start'));
print('Formatted date:', image.date().format('YYYY-MM-dd'));

// Image footprint geometry
var footprint = image.geometry();
Map.addLayer(footprint, {color: 'blue'}, 'Image footprint');
Map.centerObject(footprint, 7);

Use .getInfo() sparingly. It forces a synchronous server call, which blocks your script. For display purposes, print() is asynchronous and much safer.

Pro tips

  • Always check band names first. Different collections use different naming conventions. Landsat 8 SR uses SR_B4, while Sentinel-2 uses B4.
  • Select early. Drop unneeded bands at the start of your script. Fewer bands means less memory and faster processing.
  • Use print() instead of .getInfo() for inspection. The .getInfo() method blocks execution and can time out on large objects.
  • Clip only for display. For analysis, pass the geometry to reduceRegion() or Export instead of clipping the image.

Try it: build a multi-index image

Load a Sentinel-2 image, apply scale factors (.divide(10000)), compute NDVI and EVI, and add both as new bands. Print the final band list to verify.

// Starter code
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterBounds(ee.Geometry.Point([-122.4, 37.7]))
  .filterDate('2023-07-01', '2023-08-01')
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10))
  .first();

var scaled = s2.select(['B2', 'B3', 'B4', 'B8']).divide(10000);

// YOUR CODE HERE:
// 1. Compute NDVI: (B8 - B4) / (B8 + B4)
// 2. Compute EVI: 2.5 * (B8 - B4) / (B8 + 6*B4 - 7.5*B2 + 1)
// 3. Add both bands to the scaled image
// 4. Print the final band names

Common mistakes

  • Calling .clip() on an ImageCollection. Collections do not have a .clip() method. Either extract a single image first, or use .map() to clip each image in the collection.
  • Forgetting scale factors. Landsat 8/9 SR values are stored as integers. Always apply .multiply(0.0000275).add(-0.2) before analysis or display.
  • Selecting bands by index. Band order can differ across images in a collection. Always select by name.
  • Using .mosaic() without sorting. The last image wins in overlap zones. If you do not sort by quality, a cloudy image could end up on top.

Quick self-check

  1. What is the key difference between ee.Image and ee.ImageCollection?
  2. How do you extract a single image from a collection?
  3. Why should you avoid clipping for analysis workflows?
  4. What does .addBands() return: a new image, or does it modify the original?
  5. How would you find the native resolution of a Landsat band?

Going deeper

This module covers the essential image operations you will use in every GEE script. For a deeper exploration of image structure, visualization, and manipulation, see:

Next Steps

  • Band Arithmetic - perform mathematical operations between bands.
  • NDVI - compute and interpret the Normalized Difference Vegetation Index.
  • Cloud Masking - remove cloud-contaminated pixels before analysis.
  • Image Collections - filter, sort, and composite multi-image stacks.