Learn to crosswalk LCMS datasets to different levels

  • Currently, all LCMS deliverables are delivered at the highest level (largest number of classes)

  • This notebook facilitates crosswalking of LCMS deliverables to different levels

  • Lower levels provide greater accuracy, while higher levels provide greater thematic detail

  • Use this notebook to find the level that suits your data needs and tolerance for map error

Copyright 2025 Ian Housman

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

http://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.

github github

#Boiler plate
#Import modules

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

import geeViz.examples.lcmsLevelLookup as ll
import pandas as pd
import numpy as np
import glob
from IPython.display import Markdown
print('Done')

First, we’ll take a look at the various levels for LCMS data

  • This is a standard way of crosswalking LCMS data to an appropriate level of thematic detail for your needs

  • You can also crosswalk LCMS data in many other ways by combining different sets of Change, Land Cover, and Land Use classes in various manners

# Bring in the lookup dictionary and convert it to a Pandas dataframe for easy viewing
products = list(ll.all_lookup.keys())

color_lookup = {}
def color_cells(val):
    if val in color_lookup:
        color = color_lookup[val]
        return f'background-color: #{color};color:#1b1716;border-top: 1px solid #1b1716;text-shadow:1px 1px 0 #bfb7b0,-1px 1px 0 #bfb7b0,-1px -1px 0 #bfb7b0,1px -1px 0 #bfb7b0;'
    else:
        return ''
for product in products:
    product_title = product.replace('_',' ')
    product_lookup = ll.all_lookup[product]
    available_levels = ll.product_levels[product]
    
    highest_level = max(available_levels)
  
    
    highest_level = [n for n in product_lookup.keys() if len(n.split("-")) == highest_level]
    table = [highest_level]

    for level in available_levels[1:]:
        table.append(['-'.join(l.split('-')[:level]) for l in highest_level])
    
    table = np.transpose(table)
    

    color_lookup = {}
    out_table = [[product_lookup[v][2] for v in r] for r in table]
    for r in table:
        for v in r:
            color_lookup[product_lookup[v][2]] = product_lookup[v][1]
    
    df = pd.DataFrame(out_table,index= None)
    blankIndex=[''] * len(df)
    
    df.columns = [f'Level {l}' for l in available_levels]
    df =df[df.columns[::-1]]
    # Apply the styling to the DataFrame
    df.index +=1
    df = df.style.applymap(color_cells)

    
    display(Markdown(f'<h1>{product_title} Levels</h1>'))
    display(df)

Learn how to crosswalk and symbolize LCMS products at a specific level

  • You need to crosswalk (remap) values and provide the relevant symbology to render the maps properly

  • The code below will show different products and levels and their respective crosswalk (remap) class numbers and symbology properties

for product in ll.product_levels.keys():
    product_title = product.replace('_',' ')
    for level in ll.product_levels[product]:
        remap_dict = ll.getLevelNRemap(level, bandName=product)
        print('Product:',product_title, 'Level:',level, remap_dict)

Crosswalk and visualize all LCMS products and levels

  • This will apply the crosswalk (remap) and update the symbology for all products and levels

  • A map viewer will then open to visualize the resulting layers

Map.clearMap()
lcms = ee.ImageCollection("USFS/GTAC/LCMS/v2023-9")


for product in ll.product_levels.keys():
    product_title = product.replace('_',' ')
    lc = lcms.select([product])
    isFirst = True
    reducer = ee.Reducer.mode() if product != 'Change' else ee.Reducer.max()
    levels = ll.product_levels[product]
    
    for level in levels:
        remap_dict = ll.getLevelNRemap(level, bandName=product)
        lcT = lc.map(lambda img: img.remap(remap_dict["remap_from"], remap_dict["remap_to"]).rename([product]).set(remap_dict["viz_dict"])) # Crosswalk and set symbology
        Map.addLayer(lcT, {"autoViz": True, "canAreaChart": True, "includeClassValues": False,"reducer":reducer}, f"{product_title} Level {level}", isFirst) # Visualize output
        isFirst = False

Map.setCenter(-111.83856, 40.73678, 11)
Map.turnOnAutoAreaCharting()
Map.view()

The v2024-10 release introduces a new set of classes for Change

  • This will illustrate how the new classes and the different levels relate

# New 2024.10 release levels
# Bring in the lookup dictionary and convert it to a Pandas dataframe for easy viewing
# Function to apply color based on lookup

html = ''
products = ['Change','Land_Cover','Land_Use']
for product in products:
    product_title = product.replace('_',' ')
    product_lookup = ll.all_lookup_2024_10[product]
    available_levels = ll.product_levels_2024_10[product]
    
    highest_level = max(available_levels)
  
    
    highest_level = [n for n in product_lookup.keys() if len(n.split("-")) == highest_level]
   
    table = [highest_level]

    for level in available_levels[1:]:
        table.append(['-'.join(l.split('-')[:level]) for l in highest_level])
  
    table = np.transpose(table)
    
    color_lookup = {}
    out_table = [[product_lookup[v][2] for v in r] for r in table]
    for r in table:
        for v in r:
            color_lookup[product_lookup[v][2]] = product_lookup[v][1]
    
    df = pd.DataFrame(out_table,index= None)
    blankIndex=[''] * len(df)
    
    df.columns = [f'Level {l}' for l in available_levels]
    df =df[df.columns[::-1]]
   
    # Apply the styling to the DataFrame
    df.index +=1
    df = df.style.applymap(color_cells)
    display(Markdown(f'<h1>{product_title} Levels</h1>'))
    display(df)
    html += f"""<h3>{product_title}</h3>\n{df.to_html()}"""
print(html)

Now, we’ll look at how to crosswalk and symbolize the various products and levels of LCMS data

  • This will present the crosswalk classes for each prduct and each level

  • It will then present the JSON that can be used to symbolize the outputs in GEE. This can easily be adapted to be used in other environments

# Provide crosswalk and symbology for each level of each product
products = ['Change','Land_Cover','Land_Use']
out_html = ''
for product in products:
    
    product_title = product.replace('_',' ')
    out_html += f"""<h3>{product_title}</h3>\n"""
    for level in ll.product_levels_2024_10[product][1:]:
        remap_dict = ll.getLevelNRemap(level, bandName=product,lookup=ll.all_lookup_2024_10)
        
        out_html += f"""<strong>Level {level}:</strong>\
            <p>Remap From:  <code>{remap_dict['remap_from']}</code></p>\
            <p>Remap To:  <code>{remap_dict['remap_to']}</code></p>\
            <p>Visualization JSON: <code>{remap_dict['viz_dict']}</code></p><br>\n"""
    out_html += """<hr>\n"""
display(Markdown(out_html))
print(out_html)

Next, we’ll take a look at the accuracy of LCMS at different levels

  • This method will parse our text file outputs into a more shareable html format

  • Notice that accuracy generally decreases as the number of classes increase

# Parse Accuracy outputs
acc_files = 'data/LCMS_2024-10_Accuracy_Tables/*.txt'
version = '2024-10'
out_html = ''
files = glob.glob(acc_files)
for file in files:
    fn = os.path.splitext(os.path.basename(file))[0]
    product = fn.split('_stats_')[1].split('_Level')[0].replace('_',' ')
    sa = fn.split('_')[-1]
    level = fn.split('Level_')[1].split('_')[0]
    title = f"""LCMS v{version} {sa} {product} Level {level} Accuracy """
 
    o = open(file,'r')
    lines = o.read()
    o.close()
    # print(lines)
    first_lines = lines.split('#------------------------------------------------------\n')[0]
    first_lines = first_lines.split('\n')[2:]
    first_lines = '\n'.join(first_lines)
    cm = lines.split('#------------------------------------------------------\n')[1]
   
    first_lines=first_lines.replace('\n','<br>\n')
    first_lines = f"""<p>{first_lines}</p>"""
    
    cm = cm.replace(' ',',').split('\n')
    ns = list(range(2,100))
    ns.reverse()
    for i in ns:
        cm = [l.replace(','*i,',') for l in cm]
    cm[1] = cm[1]+','*(len(cm[2].split(','))-2)
    cm[2] = ','+cm[2]
    cm = [l.replace('_',' ').split(',') for l in cm if l != '']
   
    df = pd.DataFrame(cm[1:],index=None,columns=None )
    out_html += f"""<h3>{title}</h3>{first_lines}{df.to_html(index=False,header=False)}<br>"""
    
   
display(Markdown(out_html))
print(out_html)