Skip to main content
NSF NEON, Operated by Battelle

Main navigation

  • About
    • NEON Overview
      • Vision and Management
      • Spatial and Temporal Design
      • History
    • About the NEON Biorepository
      • ASU NEON Biorepository Staff
      • Contact the NEON Biorepository
    • Observatory Blog
    • Newsletters
    • Staff
    • FAQ
    • User Accounts
    • Contact Us

    About

  • Data
    • Data Portal
      • Data Availability Charts
      • API & GraphQL
      • Prototype Data
      • Externally Hosted Data
    • Data Collection Methods
      • Airborne Observation Platform (AOP)
      • Instrument System (IS)
        • Instrumented Collection Types
        • Aquatic Instrument System (AIS)
        • Terrestrial Instrument System (TIS)
      • Observational System (OS)
        • Observation Types
        • Observational Sampling Design
        • Sampling Schedules
        • Taxonomic Lists Used by Field Staff
        • Optimizing the Observational Sampling Designs
      • Protocols & Standardized Methods
    • Getting Started with NEON Data
      • neonUtilities for R and Python
      • Learning Hub
      • Code Hub
    • Using Data
      • Data Formats and Conventions
      • Released, Provisional, and Revised Data
      • Data Product Bundles
      • Usage Policies
      • Acknowledging and Citing NEON
      • Publishing Research Outputs
    • Data Notifications
    • NEON Data Management
      • Data Availability
      • Data Processing
      • Data Quality

    Data

  • Samples & Specimens
    • NEON Biorepository Sample Portal at ASU
    • About Samples
      • Sample Types
      • Sample Repositories
      • Megapit and Distributed Initial Characterization Soil Archives
    • Finding and Accessing Sample Data
      • Species Checklists
      • Sample Explorer - Relationships and Data
      • Biorepository API
    • Requesting and Using Samples
      • Requesting Samples from the NEON Biorepository
      • Request Megapit and Initial Characterization Soil
      • Sample Use Guidelines
      • Sample Use Policy
      • Acknowledging and Citing the NEON Biorepository

    Samples & Specimens

  • Field Sites
    • Field Site Map and Info
    • Spatial Data Layers & Maps

    Field Sites

  • Resources
    • Getting Started with NEON Data
    • Research Support Services
      • Field Site Coordination
      • Letters of Support
      • Permits and Permissions
      • AOP Flight Campaigns
      • Research Support FAQs
      • Research Support Projects
    • Code Hub
      • neonUtilities for R and Python
      • Code Resources Guidelines
      • Code Resources Submission
      • NEON's GitHub Organization Homepage
    • Learning Hub
      • Tutorials
      • Workshops & Courses
      • Science Videos
      • Teaching Modules
    • Science Seminars and Data Skills Webinars
    • Document Library
    • Funding Opportunities

    Resources

  • Impact
    • Research Highlights
    • Papers & Publications
    • NEON in the News

    Impact

  • Get Involved
    • Upcoming Events
    • Past Events
    • Research and Collaborations
      • Environmental Data Science Innovation and Inclusion Lab
      • Collaboration with DOE BER User Facilities and Programs
      • EFI-NEON Ecological Forecasting Challenge
      • NEON Great Lakes User Group
      • NCAR-NEON-Community Collaborations
    • Advisory Groups
      • Science, Technology & Education Advisory Committee (STEAC)
      • Innovation Advisory Committee (IAC)
      • Technical Working Groups (TWG)
    • NEON Ambassador Program
      • Exploring NEON-Derived Data Products Workshop Series
    • Partnerships
    • Community Engagement
    • Work Opportunities

    Get Involved

  • My Account
  • Search

Search

Learning Hub

  • Tutorials
  • Workshops & Courses
  • Science Videos
  • Teaching Modules

Breadcrumb

  1. Resources
  2. Learning Hub
  3. Tutorials
  4. Introduction to NEON Discrete Lidar Data in Python

Tutorial

Introduction to NEON Discrete Lidar Data in Python

Authors: Bridget Hass

Last Updated: Jun 30, 2026

Objectives

After completing this tutorial, you will be able to:

  • Use Python functions to programmatically download NEON AOP data from the API
  • Download and plot shapefiles and kmls (included as lidar metadata) to visualize coverage for a given year
  • Explore and plot the AOP discrete lidar point cloud contents in Python using the laspy package
  • Read in and plot the AOP L3 raster data products (CHM, DTM, DSM) in Python using the rasterio package

Things You’ll Need To Complete This Tutorial

To complete this tutorial, you will need:

  • Python version 3.9 or higher

  • Create a NEON user account

  • Generate an API token for downloading data

  • Install required Python packages

    • requests
    • gdal
    • fiona
    • geopandas
    • rasterio
    • laspy
    • lazrs
    • neonutilities
    • python-dotenv

Additional Resources

If you are interested in learning more about the neonutilities package or the NEON API, or want a deeper dive in how this works with the Python requests package, please refer to the tutorial and webpages linked below.

  • Understanding AOP Data Releases and Best Practices for AOP Data Management
  • NEON Data API

For a handy resource on Jupyter Notebook tips, tricks and shortcuts, check out the DataQuest blog linked below.

  • 28 Jupyter Notebook Tips, Tricks, and Shortcuts

Overview of AOP Discrete Lidar Data Products

AOP generates several Level-1 and Level-3 (derived) data products. The Level 1 lidar data is the point cloud data, provided in laz (or zipped las) format, while the Level 3 data is provided in geotiff (.tif) raster format. Images of these two data types are shown in the figures below.

Lidar Point Cloud
Lidar Elevation Raster

The table below summarizes the NEON AOP Lidar data products. This lesson will give a brief introduction to both the Level 1 Classified Point Cloud data, as well as the Level 3 raster geotiff data products (DTM, DSM, and CHM). For more detailed information on these data products, please refer to: Airborne Remote Sensing Lidar, and/or click on the linked data product pages in the table.

Acronym / Short Name Data Product Name Data Product ID Link to ATBD
Point Cloud Discrete return LiDAR point cloud DP1.30003.001 NEON L0-to-L1 Discrete Return LiDAR ATBD
CHM Ecosystem structure DP3.30015.001 NEON Ecosystem Structure ATBD
DSM/DTM Elevation - LiDAR DP3.30024.001 NEON Elevation (DTM and DSM) ATBD
Slope/Aspect Slope and Aspect - LiDAR DP3.30025.001 NEON Elevation (Slope and Aspect) ATBD

The Data Product page contains important information, as well as linked Algorithm Theoretical Basis Documents (ATBDs) which provide necessary information for understanding how the data products were generated, uncertainty associated with the data, and other essential contextual information.

Lidar Point Clouds

The first part of this tutorial will be working with the Discrete Return LiDAR Point Cloud data product (DP3.30003.001) at the NEON site Guanica Forest (GUAN) in Domain 04, Puerto Rico.

Before we dig into the data, we'll need to import the required packages.

Import Packages

First, we need to import the required Python packages.

Reminder: If you haven't installed these packages (see more detailed installation instructions above), you can install them in the notebook as shown below. If the install doesn't work with a simple pip install, download the appropriate wheel file and substitute the package name with the wheel file name (including the full path). We recommend installing these packages one by one so you can make sure each package installs successfully.

!pip install requests
!pip install gdal
!pip install fiona
!pip install geopandas
!pip install rasterio
!pip install laspy
!pip install lazrs
!pip install neonutilities
!pip install python-dotenv

Once all packages are successfully installed, import them as follows. Note we will import some of them a bit later.

#import required packages
import os
import requests
import numpy as np
import matplotlib.pyplot as plt
import geopandas as gpd
import laspy
import neonutilities as nu
import dotenv

As of June 2026, NEON requires an API token for data downloads, to reduce bot scraping and improve user support. Tokens can be generated in NEON data portal user accounts - log in to your account or create one, and go to the API Tokens section. For best practices in storing and using tokens, follow the instructions here. Once you've set up your token as an environment variable, you can load it using the python-dotenv package as follows, optionally specifying the path to the .env file in load_dotenv().

dotenv.load_dotenv()
token = os.environ.get("NEON_TOKEN")

Now that we've pulled in the packages needed for the first part of the tutorial, we can download a dataset from the NEON API.

First we'll start by defining variables that specify the NEON data product ID (dpID), site, and year. You can change the site code to look at a different site of your interest (for more detailed information about the NEON sites, please check out the Explore Field Sites webpage.

dpID='DP1.30003.001' 
site = 'GUAN'

We can use the function nu.list_available_dates to see what data is available for this data product and site. This function requires two inputs: the data product ID dpID and the site ID, site.

Optionally run help to see the required inputs and more information about this function. help(nu.list_available_dates)

nu.list_available_dates(dpID,site)
RELEASE-2026 Available Dates: 2018-05, 2022-10

The AOP has only flown Puerto Rico (D04) in 2018 and 2022 so far (as of 2026). D04 is only on the AOP schedule every 4-5 years, unlike most domains in the continental United States. For this tutorial, we'll look at data from 2022.

year='2022'

On your own, you can use help to take a look at the nu.by_file_aop and nu.by_tile_aop functions, which we'll use to download the CHM data for all tiles, and the lidar classified point cloud data for a single tile that we want to explore.

The required inputs for this function are the product, site, year and token. Optionally we can specify the savepath to save the files. By default, the function will display the size of the files, and prompt the user to continue the download (by typing y); any other response will halt the download. This is to prevent an accidental download of a large volume of data.

Lidar Metadata

We'll start by exploring some of the metadata, including the pdf documentation, and shapefiles that provide geographic information corresponding to the data. Because AOP data can be pretty large for an entire site, and you may only need to work with a subset of the data for a given site, we recommend starting with the metadata.

Lidar Documentation - QA html and pdf reports

AOP data provides summary html or pdf documents, which include information about the sensors used to collect the lidar data, acquisition parameters, processing parameters, and QA information. When working with any AOP data, we recommend reviewing this documentation, as well as referencing the relevant ATBDs. The QA reports are saved in the "Metadata" folder. We can start by downloading the CHM data for this site and take a look at the reports (on your own) and shapefiles.

# download the CHM data to the C:/data directory - change this if desired
nu.by_file_aop(dpid='DP3.30015.001',
               site='GUAN',
               year=year,
               token=token,
               savepath=r'C:\data',
               check_size=False)
Downloading 177 NEON data files totaling approximately 286.4 MB

100%|███████████████████████████████████| 177/177 [01:26<00:00,  2.04it/s]

Use the code cell below to walk through all the directories and display what was downloaded.

# Display end level folders in the savepath
for root, dirs, files in os.walk(r'C:\Data\DP3.30015.001'):
    if not dirs:
        print(root)
C:\Data\DP3.30015.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\L3\DiscreteLidar\CanopyHeightModelGtif
C:\Data\DP3.30015.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\Metadata\DiscreteLidar\Reports
C:\Data\DP3.30015.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\Metadata\DiscreteLidar\TileBoundary\geojson
C:\Data\DP3.30015.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\Metadata\DiscreteLidar\TileBoundary\kmls
C:\Data\DP3.30015.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\Metadata\DiscreteLidar\TileBoundary\shps
C:\Data\DP3.30015.001\neon-publication\release\tag\RELEASE-2026\NEON.DOM.SITE.DP3.30015.001\GUAN\20221001T000000--20221101T000000\basic

You can see there are a number of files in the Metadata folder, including Reports and TileBoundary files including geojson, kmls, and shps. Please explore the Reports on your own time.

Plot Shapefile Boundaries

Next let's display the path to the tile boundary shapefile, and save it to the variable shp_file:

# display .shp data in the savepath
for root, dirs, files in os.walk(r'C:\Data\DP3.30015.001'):
    for file in files:
        if file.endswith(".shp"):
             shp_file = os.path.join(root, file)
             print(shp_file)
C:\Data\DP3.30015.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\Metadata\DiscreteLidar\TileBoundary\shps\NEON_D04_DPQA_2022_GUAN_2_lidar_tile_boundary.shp

To plot the boundary shapefile, use geopandas (imported as gpd) as follows:

gdf = gpd.read_file(shp_file)
gdf.plot(alpha=0.5, edgecolor='black');
ax = plt.gca(); ax.ticklabel_format(style='plain') 
ax.set_title('AOP Coverage of ' + site + ' in ' + year);
plt.xticks(rotation=90); #optionally rotate the xtick labels

png

Now that we can see the extent of the tiles, we'll pick a single tile in this area to download. For this example, specify the tile 726000_1986000 towards the southern part of the site, including both land and sea.

nu.by_tile_aop(dpid='DP1.30003.001',
               site=site,
               year=year,
               easting=726000,
               northing=1986000,
               savepath='C:\data',
               token=token)
Provisional NEON data are not included. To download provisional data, use input parameter include_provisional=True.


Continuing will download 2 NEON data files totaling approximately 413.8 MB. Do you want to proceed? (y/n)  y


Downloading 2 NEON data files totaling approximately 413.8 MB

100%|███████████████████████████████████████| 2/2 [00:15<00:00,  7.81s/it]

We can check that this file successfully downloaded to the expected location. Alternatively you could look in your file explorer.

# display .shp data in the savepath
for root, dirs, files in os.walk(r'C:\Data\DP1.30003.001'):
    for file in files:
        if file.endswith(".laz"):
             laz_file = os.path.join(root, file)
             print(laz_file)
C:\Data\DP1.30003.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\L1\DiscreteLidar\ClassifiedPointCloud\NEON_D04_GUAN_DP1_726000_1986000_classified_point_cloud_colorized.laz

Exploring point cloud data with laspy

Now that we've successfully downloaded a laz (or zipped las) file, we can use the laspy package to read it in! We'll do that in the next line, reading the lidar file into the variable name point_cloud:

# read the laz file into a LasData object using laspy.read()
point_cloud=laspy.read(laz_file)

Reading in the file with with laspy.read() reads in both the metadata and the raw point cloud data. We can print out the point_cloud variable to show some basic information about what we've read in:

point_cloud
<LasData(1.4, point fmt: <PointFormat(7, 0 bytes of extra dims)>, 60503721 points, 4 vlrs)>

point_format.dimension_names show us the available information stored in this LasData object format:

list(point_cloud.point_format.dimension_names)
['X',
 'Y',
 'Z',
 'intensity',
 'return_number',
 'number_of_returns',
 'synthetic',
 'key_point',
 'withheld',
 'overlap',
 'scanner_channel',
 'scan_direction_flag',
 'edge_of_flight_line',
 'classification',
 'user_data',
 'scan_angle',
 'point_source_id',
 'gps_time',
 'red',
 'green',
 'blue']

In the next few cells, we can explore some of these variables:

point_cloud.classification
array([2, 2, 2, ..., 1, 1, 2], shape=(60503721,), dtype=uint8)

Let's get the set of this list to see all the unique classification values in this file. This may take a little time to run.

set(list(point_cloud.classification))
{np.uint8(1), np.uint8(2), np.uint8(5), np.uint8(6), np.uint8(7)}

We can see that there are a several unique classification values for this site. Las files have "predefined classification schemes defined by the American Society for Photogrammetry and Remote Sensing (ASPRS)". You can refer to the ArcGIS documentation for more details.

The following table lists the LAS classification codes defined by ASPRS for these LAS versions:

Classification value Meaning
0 Never classified
1 Unassigned
2 Ground
3 Low Vegetation
4 Medium Vegetation
5 High Vegetation
6 Building
7 Low Point

Next let's take a look at what we can consider to be the main data - the geographic loation of each point in the point cloud. This can be accessed with point_cloud.xyz. Let's take a look:

xyz = point_cloud.xyz
xyz
array([[7.26249992e+05, 1.98624955e+06, 1.83900000e+00],
       [7.26249966e+05, 1.98624954e+06, 1.85400000e+00],
       [7.26249935e+05, 1.98624954e+06, 1.85700000e+00],
       ...,
       [7.26890362e+05, 1.98675012e+06, 3.28200000e+00],
       [7.26890912e+05, 1.98675024e+06, 1.54700000e+00],
       [7.26890837e+05, 1.98675022e+06, 1.62000000e+00]],
      shape=(60503721, 3))

We can see this is a 3-dimensional array, as we might expect. Let's read this into the variable xyz:

We can see the size (or number of points) in this array using the built-in python function len:

len(xyz)
60503721

There are > 60 million lidar points in this single 1km x 1km tile. For the rest of this exercise, we'll look at a random subset of these points, taking every 100th point (you can change this subset factor, but when we visualize the data in a few steps, subsetting by a larger factor will speed up the time it takes to make the plot).

factor=100
points_dec = xyz[::factor]

These point clouds have been "colorized" by the camera RGB imagery. If you refer back to the dimension names, you can see there are a red, green, and blue attributes. We can pull these into a single array by using np.vstack:

# points = np.vstack((point_cloud.x, point_cloud.y, point_cloud.z)).transpose()
colors = np.vstack((point_cloud.red, point_cloud.green, point_cloud.blue)).transpose()

These colors have been scaled to store the color at a higher resolution, accomodated by the camera, so we'll need to re-scale the values between 0-1 in order to use them in our plot. The code below does this re-scaling, and then subsets the color data to by same factor we used to subset the xyz data.

colors_norm = (colors - np.min(colors))/np.ptp(colors)
colors_dec = colors_norm[::factor]

3D Point Cloud Visualization

Lastly, we can visualize this 3D data using matplotlib to see what the point cloud looks like.

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
# Plot the las data in 3D
fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(points_dec[:,0],points_dec[:,1],points_dec[:,2],color=colors_dec,s=4)
ax.set_zlim3d(-10,50)
plt.show()

png

We can see a mix of both land and sea here, with slightly fewer returns over the ocean. The z scale is stretched considerably compared to x and y. A lot of the energy from the laser beam is absorbed in water, so it is typical to see low point density over bodies of water. Remember this plot only displays 1/100th of the data, so there is a lot more data in the las file than is shown here.

Lidar Raster Data - CHM

Lastly, we'll take a look at a derived (Level-3, or L3) data product generated from this point cloud data. NEON generates 5 different derived L3 products from the discrete data: Digital Terrain Model (DTM), Digital Surface Model (DSM), Canopy Height Model (CHM), Slope, and Aspect.

In the last part of this lesson, we'll show how to read in and visualize the CHM (downloaded previously) using Python package rasterio. First we'll import the package and sub-package that's used to display the data

import rasterio
from rasterio.plot import show

In the next couple cells, we'll find the CHM tile corresponding to the las file that we read in, and then plot it.

# display .shp data in the savepath
for root, dirs, files in os.walk(r'C:\Data\DP3.30015.001'):
    for file in files:
        if file.endswith("726000_1986000_CHM.tif"):
             chm_file = os.path.join(root, file)
             print(chm_file)
C:\Data\DP3.30015.001\neon-aop-products\2022\FullSite\D04\2022_GUAN_2\L3\DiscreteLidar\CanopyHeightModelGtif\NEON_D04_GUAN_DP3_726000_1986000_CHM.tif

Next we'll read this in, using rasterio, as follows:

chm = rasterio.open(chm_file)

And finally, we can plot the data using the rasterio show function that we imported earlier.

fig, ax = plt.subplots(figsize=(6, 6))
show(chm, ax=ax)
ax.ticklabel_format(style='plain', axis='y') # turn off scientific notation for the y-axis (or both axes)

png

Additional Resources

  • L1 Point Clouds (.laz): If you'd like to continue exploring the point cloud data in Python using laspy, laspy website has some nice examples you can follow, now that you know how to download NEON point cloud data and read it into Python.
  • L3 Rasters (.tif): Refer to the rasterio documentation for more options on plotting, and beyond in rasterio.

Python and Beyond - Other Options for working with Point Cloud Data

There are also a number of open-source tools for working with point-cloud data. Python may not be the best option for developing more rigourous processing workflows, for example. The resources below show some other recommended tools that can be integrated with Python for your analysis:

  • LAStools
  • PDAL (Point Data Abstraction Library)
  • plas.io (free, interactive, web-based point cloud visualization)
  • lidR (R package for point cloud data)

Get Lesson Code

intro_discrete_point_clouds.ipynb

Questions?

If you have questions or comments on this content, please contact us.

Contact Us
NSF NEON, Operated by Battelle

Follow Us:

Join Our Newsletter

Get updates on events, opportunities, and how NEON is being used today.

Subscribe Now

Footer

  • About Us
  • Contact Us
  • Terms & Conditions
  • Careers
  • Code of Conduct

Copyright © Battelle, 2026

The National Ecological Observatory Network is a major facility fully funded by the U.S. National Science Foundation.

Any opinions, findings and conclusions or recommendations expressed in this material do not necessarily reflect the views of the U.S. National Science Foundation.