Skip to content

Canopy Height Models

An integral part of any analysis is the production of a canopy height model, or CHM. The CHM is a rasterized representation of the canopy of the forest. The creation and filtering of CHMs play a large role in tree detection algorithms and are an interpretable way to display information.

A basic canopy height model can be created using a convenience wrapper:

tile = pyfor.cloud.Cloud('my_tile.las')
tile.normalize(1)
chm = tile.chm(0.5)

The above block will load the las file, my_tile.las, remove the ground elevation (normalize) and compute a basic canopy height model. Here, we specify a resolution of 0.5 units.

A raster is computed on a grid, not on the extent of the data. The origin of that grid is snapped to a multiple of the cell size, the target aligned pixels convention, the same one gdal_translate -tap, terra, and lidR use. This is what lets a canopy height model of one tile line up with its neighbour: both tiles put their cells on the same lattice, so the rasters can be mosaicked without resampling, and comparing them cell by cell is meaningful.

chm.grid.spec
GridSpec(origin_x=405000.0, origin_y=3276300.0, cell_size=0.5, n=400, m=400)

When a raster has to be produced on a particular grid, for example to match an existing product or a project wide grid, build the spec yourself and pass it to any of the gridding methods:

spec = pyfor.rasterizer.GridSpec.covering(405000, 3276300, 405200, 3276500, 0.5)
chm = tile.chm(0.5, spec=spec)

The spec has to cover the cloud, and pyfor raises a ValueError if it does not rather than quietly dropping points. See GridSpec for the details, and CloudDataFrame.grid_spec for building one that covers a whole collection.

The values pyfor computes for a cell do not depend on the grid it was handed: the maximum z of a cell, and every other reduction, is the same whichever spec is used, and a spec only decides which cells exist. Interpolation is the exception. When interp_method is given, cells that contain no points are filled from a nearby cell, and a cell sitting exactly between two occupied cells can take its value from either of them. Which one it takes is not specified, so pit_filter and interp_method can produce a slightly different raster at those cells from one grid to the next. Without interpolation, empty cells stay empty.

Often times, raw CHMs are not adequate for analysis. They contain many issues, such as missing values and data pits. We can add some extra arguments to add NaN interpolation and pit filtering.

better_chm = tile.chm(0.5, interp_method="nearest", pit_filter="median")

Here, we interpolate missing values using a nearest neighbor interpolator, and pass a median filter over the canopy height model to smooth pits.

We can display our CHM with the .plot method:

better_chm.plot()

A canopy height model with interpolation and pit filtering applied.

A canopy height model is a Raster object. And can be written out in the same way.

better_chm.write('my_chm.tif')