Short answer

An elevation profile is the line you get by walking a path across a DEM and recording the ground height at every step, then plotting height against distance travelled. To build one well: define the profile line, densify it into evenly spaced sample points, read the DEM at each point (interpolating between cells), and plot elevation versus cumulative distance. The quality of the result hinges on three choices — sampling interval, interpolation method, and the vertical units and exaggeration of the plot.

A profile is a measurement product, not just a picture. Done carefully it yields real gradients, true distances, and defensible vertical relief; done carelessly it under-samples peaks, mislabels gradients, and exaggerates steepness without saying so.

What a profile actually samples

The DEM is a grid of elevation cells. A profile line crosses that grid; at each sample point along the line you must convert a continuous (x, y) position into an elevation, even though the point rarely sits exactly on a cell centre. Two things govern the answer.

  • The source DEM resolution and accuracy. A profile can never resolve detail finer than the grid. A 30 m Copernicus DEM cannot show a 5 m gully; a 1 m lidar DTM can. Vertical accuracy (often ~0.1-0.3 m for lidar, several metres for global radar DEMs) sets the noise floor on every height you read.
  • DTM vs DSM. Profile a bare-earth DTM for ground elevation. A DSM profile follows tree canopy and rooftops, producing spikes that are not ground.

Confirm horizontal units before measuring distance. A profile measured across an EPSG:4326 grid reports distance in degrees, not metres, and the horizontal axis will be wrong. Reproject to a projected CRS (UTM, national grid) first so both axes are in metres.

Sampling interval: the first decision

You densify the profile line into points before reading elevations. The interval should be near the DEM cell size:

  • Too coarse (interval ≫ cell size) steps over ridges and incised valleys, clipping the true maximum and minimum — a 50 m interval on a 5 m DEM can miss a sharp summit entirely.
  • Too fine (interval ≪ cell size) re-reads the same cell many times under nearest-neighbour, adding no information; under bilinear it produces a smoothly interpolated but artificially detailed line.

A practical rule is to sample at the cell size or at half of it. For a 10 m DEM, points every 5-10 m. In QGIS, densify the line with "Points along geometry" (Processing) at the chosen spacing; in PostGIS, ST_DumpPoints(ST_Segmentize(line, 5)).

Interpolation: nearest-neighbour vs bilinear

Once you have sample points, you read the raster.

  • Nearest-neighbour returns the value of the cell the point falls in. It is correct for categorical rasters and exact for the grid, but produces a stair-stepped profile because adjacent points snap to the same cell.
  • Bilinear interpolates the four surrounding cell centres weighted by distance, giving a smooth, more physically plausible elevation for a continuous surface. This is the right default for elevation profiles.

GDAL's gdallocationinfo reads single points; for a smooth profile, sample with Python (rasterio.sample with bilinear, or scipy interpolation), or use a purpose-built tool. PostGIS exposes both: ST_Value(rast, geom) defaults to nearest-neighbour, and ST_Value(rast, band, geom, true, 'bilinear') interpolates (the true is exclude_nodata_value; the resample argument, added in PostGIS 3.2, accepts 'nearest' or 'bilinear').

Tools that build the profile for you

  • QGIS Elevation Profile panel (View → Elevation Profile, modern QGIS): add the DEM as an elevation source, draw the line interactively, and read the chart live. It samples along the line automatically.
  • QGIS "Profile tool" / "Terrain profile" plugin for quick interactive cross-sections with export to CSV.
  • GDAL + Python for repeatable, scripted profiles where you control interval and interpolation exactly.
  • PostGIS when the DEM lives in the database and you want profiles as part of an analytical query.

Reading gradient and distance correctly

Two derived quantities are where profiles are most often misused.

Distance is along-line (cumulative 2D distance), not straight-line start-to-end, and it is planimetric — it ignores the up-and-down travel. If you need true surface (slope) distance, integrate sqrt(dx² + dz²) over segments; it is always longer than planimetric distance.

Gradient between two points is Δelevation / Δhorizontal-distance, expressed as percent (rise/run × 100) or degrees (atan(rise/run)). These are not interchangeable: a 100% grade is 45°, not 100°. State which you mean. Compute gradient from the underlying data, never by reading the slope of the plotted line, because the plot's apparent slope depends on the exaggeration.

Vertical exaggeration

To make subtle relief visible, profiles are usually drawn with the vertical axis at a larger scale than the horizontal — vertical exaggeration (VE). A profile 10 km long with 200 m of relief is nearly flat at 1:1, so a VE of 5× or 10× is common. The rule is simple: always label the VE factor on the plot. An unlabelled exaggerated profile makes gentle terrain look like an alpine ridge, and readers will misjudge steepness. The true gradient is in the numbers, not the visual.

Worked example — profile across a valley in QGIS, scripted in PostGIS

A geologist needs a cross-valley profile for a cross-section. The DEM is 10 m, reprojected to UTM zone 33N (EPSG:32633). Draw the line A-B across the valley. In PostGIS:

WITH line AS (
  SELECT ST_Segmentize(ST_GeomFromText(
    'LINESTRING(500000 5600000, 503000 5600000)', 32633), 10) AS geom
),
pts AS (
  SELECT (ST_DumpPoints(geom)).geom AS pt,
         (ST_DumpPoints(geom)).path[1] AS seq
  FROM line
)
SELECT seq,
       (seq-1)*10 AS dist_m,
       ST_Value(r.rast, p.pt) AS elev_m
FROM pts p
JOIN dem r ON ST_Intersects(r.rast, p.pt)
ORDER BY seq;

That yields a distance/elevation table to plot. Compute valley-floor-to-shoulder gradient as (elev_shoulder − elev_floor) / dist between them, reported in percent. When the chart is drawn at VE 5×, label it so the steep-looking shoulders are not mistaken for cliffs.

Handling NoData, voids, and edges

Profiles often cross cells with no valid elevation: voids in radar DEMs, water-body masks, or the area beyond the tile edge. Sampling these returns the DEM's NoData value (frequently -9999 or -32768), and if it is not recognised as NoData it plots as a wild spike or trench. Before profiling, confirm the NoData value is correctly declared (gdalinfo shows it) so tools skip those samples rather than plotting them. For small interior voids, fill them first — gdal_fillnodata.py interpolates across gaps — and note in the caption that the filled segment is interpolated, not measured.

Edges deserve the same caution: a profile that runs to the boundary of a tile will hit NoData at the end, and any cumulative-distance or relief statistic computed naively will be corrupted by that last point. Trim the profile to the valid data extent.

From a single profile to swath and longitudinal profiles

A single line samples one transect, which can be unrepresentative if the terrain is rough or the line happens to clip a boulder or void. Two refinements help:

  • Swath profiles sample a band either side of the line and report the minimum, maximum, and mean elevation at each distance. The envelope between min and max shows how variable the terrain is across the swath, which a single line hides. They are standard in tectonic geomorphology for comparing ridge crests against valley floors.
  • Longitudinal (stream) profiles follow a drainage line from source to mouth and are read for knickpoints — abrupt steepening that can signal lithological contacts, faulting, or base-level change. Extract the channel by flow routing, order points by downstream distance, and plot elevation against distance; the concavity and any breaks are the geological signal.

Common pitfalls and why they happen

  • Profiling a DSM. Canopy and buildings create false spikes; the profile is not ground. Use a DTM.
  • Distance in degrees. Sampling on an EPSG:4326 DEM makes the horizontal axis degrees, not metres. Reproject to a metric CRS first.
  • Sampling interval too coarse. Wide spacing clips peaks and troughs, understating relief and gradient. Sample near the cell size.
  • Confusing percent and degrees of gradient. A frequent reporting error; 100% ≠ 100°. State the unit.
  • Unlabelled vertical exaggeration. Readers misjudge steepness. Always print the VE factor and derive gradient from data.

Quality checks

  • Verify the start and end elevations against a contour map or a known benchmark.
  • Confirm the cumulative distance at B matches the planimetric length of line A-B (ST_Length).
  • Inspect the profile for vertical spikes — they usually signal NoData hits or a DSM, not real terrain.
  • Re-sample at half the interval; if the profile changes materially, your interval was too coarse.
  • Record DEM source, resolution, CRS, sampling interval, interpolation method, and VE in the figure caption.

Bathyl perspective

We treat an elevation profile as an evidence layer with stated assumptions: the DEM and its resolution, the CRS, the sampling interval, the interpolation method, and the vertical exaggeration all travel with the figure. That lets a reviewer reproduce the gradient we report and tells a decision-maker exactly how much detail the profile can actually support.

Related reading

Sources