LCMS Viewer Intro Notebook

  • Based on https://github.com/google/earthengine-community/blob/master/datasets/scripts/LCMS_Visualization.js Copyright 2025 The Google Earth Engine Community Authors

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

  • Example script for visualizing LCMS change summaries, land cover, and land use.

  • A more in-depth visualization of LCMS products is available at: https://apps.fs.usda.gov/lcms-viewer/index.html

  • Contact sm.fs.lcms@usda.gov with any questions or specific data requests.

github github

#Boiler plate
#Import modules
import os,sys
sys.path.append(os.getcwd())


try:
    from  geeViz.geeView import *
except:
    !python -m pip install geeViz
    from  geeViz.geeView import *


print('Done')

First, we’ll take a look at the foundation of the geeViz.geeView module - the LCMS Data Explorer

  • This viewer framework serves as the foundation for this module

#This notebook breaks down various pieces of LCMS data visualization available in this interactive viewer
IFrame(src='https://apps.fs.usda.gov/lcms-viewer/', width='100%', height='500px')

Create your own LCMS Viewer

  • You can create your own instance of the LCMS Data Explorer using geeViz.geeView

#Clear any layers added to Map object
#If map is not cleared, layers are simply appended to the existing list of layers if layers have been added previously
Map.clearMap()

#############################################################################
### Define visualization parameters ###
#############################################################################
startYear = 1985
endYear = 2023
lossYearPalette = ['ffffe5', 'fff7bc', 'fee391', 'fec44f', 'fe9929',
                   'ec7014', 'cc4c02']
gainYearPalette = ['c5ee93', '00a398']
durationPalette = ['BD1600', 'E2F400','0C2780']

lossYearViz = {'min': startYear, 'max': endYear, 'palette': lossYearPalette};
gainYearViz = {'min': startYear, 'max': endYear, 'palette': gainYearPalette};
durationViz = {'min': 1, 'max': 5, 'palette': durationPalette};

#############################################################################
### Define functions ###
#############################################################################

#Convert given code to year that number was present in the image.
def getMostRecentChange(c, code):
	def wrapper(img):
		yr = ee.Date(img.get('system:time_start')).get('year')
		return ee.Image(yr).int16().rename(['year']).updateMask(img.eq(code)).copyProperties(img,['system:time_start'])
	return c.map(wrapper)
#############################################################################
### Bring in LCMS annual outputs ###
#############################################################################

lcms = ee.ImageCollection('USFS/GTAC/LCMS/v2023-9')
props = lcms.first().toDictionary().getInfo()
print('Properties:',props)
bandNames =  lcms.first().bandNames().getInfo()
print('Available study areas:', lcms.aggregate_histogram('study_area').keys().getInfo())
print('Available LCMS products',bandNames)
print('Learn more about visualization of LCMS products here', 'https://apps.fs.usda.gov/lcms-viewer/')

#Filter out study area
# lcms = lcms.filter(ee.Filter.eq('study_area','CONUS'))

#Set up time periods to compare land cover and land use
earlySpan = [startYear, startYear+4]
lateSpan = [endYear-4, endYear]

  • Raw LCMS outputs are available for more customized analysis

  • Double click anywhere within CONUS to plot the time series of raw LCMS outputs

#############################################################################
### Add full raw model outputs ###
#############################################################################
Map.clearMap()
#Separate products
raw_change = lcms.select(['Change_Raw_.*'])
raw_land_cover = lcms.select(['Land_Cover_Raw_.*'])
raw_land_use = lcms.select(['Land_Use_Raw_.*'])

#Shorten names
raw_change_bns = [bn for bn in bandNames if bn.find('Change_Raw_') > -1]
raw_land_cover_bns = [bn for bn in bandNames if bn.find('Land_Cover_Raw_') > -1]
raw_land_use_bns = [bn for bn in bandNames if bn.find('Land_Use_Raw_') > -1]

raw_change_bns_short = [i.split('_Probability_')[-1] for i in raw_change_bns]
raw_land_cover_bns_short = [i.split('_Probability_')[-1] for i in raw_land_cover_bns]
raw_land_use_bns_short = [i.split('_Probability_')[-1] for i in raw_land_use_bns]

raw_change = raw_change.select(raw_change_bns,raw_change_bns_short)
raw_land_cover = raw_land_cover.select(raw_land_cover_bns,raw_land_cover_bns_short)
raw_land_use = raw_land_use.select(raw_land_use_bns,raw_land_use_bns_short)


#Add to map
Map.addLayer(raw_land_use,{'reducer':ee.Reducer.max(),'min':0,'max':30,'opacity':1,'addToLegend':False,'queryParams':{'palette':props['Land_Use_class_palette'],'yLabel':'Model Confidence'}},'Raw LCMS Land Use Model Probability',True) 
Map.addLayer(raw_land_cover,{'reducer':ee.Reducer.max(),'min':0,'max':30,'opacity':1,'addToLegend':False,'queryParams':{'palette':props['Land_Cover_class_palette'],'yLabel':'Model Confidence'}},'Raw LCMS Land Cover Model Probability',True) 
Map.addLayer(raw_change,{'reducer':ee.Reducer.max(),'min':0,'max':30,'bands':'Fast_Loss,Gain,Slow_Loss','opacity':1,'addToLegend':False,'queryParams':{'palette':props['Change_class_palette'],'yLabel':'Model Confidence'}},'Raw LCMS Change Model Probability',True) 


Map.centerObject(lcms.first())
Map.turnOnInspector()
Map.setQueryDateFormat('YYYY')
Map.view()
Map.clearMap()
#############################################################################
### Visualize Land Use change ###
#############################################################################
lu = lcms.select(['Land_Use'])
earlyLu = lu.filter(ee.Filter.calendarRange(earlySpan[0], earlySpan[1], 'year'))
lateLu = lu.filter(ee.Filter.calendarRange(lateSpan[0], lateSpan[1], 'year'))
Map.addLayer(earlyLu, {'reducer':ee.Reducer.mode(),'autoViz':True}, 'Early Land Use Mode ({}-{})'.format(earlySpan[0],earlySpan[1]), True)
Map.addLayer(lateLu, {'reducer':ee.Reducer.mode(),'autoViz':True,'opacity':0}, 'Recent Land Use Mode ({}-{})'.format(lateSpan[0],lateSpan[1]), True);



#############################################################################
### Visualize Land Cover change ###
#############################################################################
lc = lcms.select(['Land_Cover'])
earlyLc = lc.filter(ee.Filter.calendarRange(earlySpan[0], earlySpan[1], 'year'))
lateLc = lc.filter(ee.Filter.calendarRange(lateSpan[0], lateSpan[1], 'year'))
Map.addLayer(earlyLc, {'reducer':ee.Reducer.mode(),'autoViz':True}, 'Early Land Cover Mode ({}-{})'.format(earlySpan[0],earlySpan[1]), True);
Map.addLayer(lateLc, {'reducer':ee.Reducer.mode(),'autoViz':True,'opacity':0}, 'Recent Land Cover Mode ({}-{})'.format(lateSpan[0],lateSpan[1]), True);


Map.turnOnInspector()
Map.setQueryDateFormat('YYYY')
Map.view()
Map.clearMap()
#############################################################################
### Visualize Change products ###
#############################################################################

#Select the change band. Land_Cover and Land_Use are also available.
change = lcms.select(['Change'])

#Convert to year collection for a given code.
slowLossYears = getMostRecentChange(change, 2)
fastLossYears = getMostRecentChange(change, 3)
gainYears = getMostRecentChange(change, 4)

#Find the most recent year.
mostRecentSlowLossYear = slowLossYears.max()
mostRecentFastLossYear = fastLossYears.max()
mostRecentGainYear = gainYears.max()

#Find the duration.
slowLossDuration = slowLossYears.count()
fastLossDuration = fastLossYears.count()
gainDuration = gainYears.count()

#Add year summaries to the map.
Map.addLayer(mostRecentSlowLossYear, lossYearViz, 'Most Recent Slow Loss Year', True)
Map.addLayer(mostRecentFastLossYear, lossYearViz, 'Most Recent Fast Loss Year', True)
Map.addLayer(mostRecentGainYear, gainYearViz, 'Most Recent Gain Year', True)

#Add durations to the map.
Map.addLayer(slowLossDuration,durationViz, 'Slow Loss Duration', False);
Map.addLayer(fastLossDuration,durationViz, 'Fast Loss Duration', False);
Map.addLayer(gainDuration,durationViz, 'Gain Duration', False);


Map.turnOnInspector()
Map.view()

Time lapses enable quick visualization of multi-temporal imageCollections

# Since any collection can be viewed as a time lapse, let's bring in the change outputs as a timelapse
# These can take time to load since each frame is an individual tile map service
Map.clearMap()
justChange = change.map(lambda img:img.updateMask(ee.Image(img.gt(1).And(img.lt(5))).copyProperties(lcms.first())))
Map.addTimeLapse(justChange,{'autoViz':True},'LCMS Change Time Lapse',False)

Map.setQueryDateFormat('YYYY')
Map.turnOnInspector()
Map.view()