Drone Flight Planning for Photogrammetric Survey: Grid Design, Overlap, and Ground Sample Distance
Every reconstruction failure that surfaces during dense matching can usually be traced back to a decision made before the aircraft ever left the ground. Flight planning is where the resolution, geometric strength, and geodetic control of an archaeological survey are actually fixed — the field session merely executes what the plan encodes. This section, part of the broader Photogrammetry & 3D Site Mapping Pipelines programme, treats the flight plan as a first-class engineering artefact: a deterministic specification of altitude, camera geometry, overlap, and control-point layout that downstream processing can rely on. Get the ground sample distance wrong and no amount of Structure-from-Motion cleverness recovers the detail; get the overlap wrong and tie-point matching collapses over low-texture rubble and bare earth. The three guides in this topic area work through the arithmetic and automation in detail: calculating image overlap and ground sample distance, automating flight grid generation with Python, and planning safe flight corridors over heritage sites.
A well-formed plan produces imagery that flows cleanly into automated drone image processing workflows and, at campaign scale, into batch processing of photogrammetry datasets without per-flight manual intervention. The objective for heritage work is not maximal coverage but reproducible coverage: the same site flown next season at the same GSD, same overlap, and same control geometry, so that multitemporal comparison measures real change rather than acquisition noise.
What the plan must fix before the flight
Six parameters determine whether a survey succeeds, and all six are set at the desk. The target GSD derives from what you need to resolve — a 5 mm/px orthomosaic for masonry tooling marks demands a very different altitude than a 30 mm/px landscape survey of an enclosure system. Camera and sensor choice fixes the focal length, sensor width, and image dimensions that the GSD arithmetic consumes. Altitude follows from GSD once the camera is chosen. Forward and side overlap govern the redundancy of the tie-point network. Nadir versus oblique geometry trades planimetric orthomosaic quality against the façade coverage that standing structures require. And control geometry — the placement of Ground Control Points (GCPs) and independent checkpoints — anchors the block to the site datum and provides the residuals that prove it.
The relationship at the centre of all planning is the ground sample distance. For a nadir camera at flying height above the surface:
where is the physical sensor width, the focal length (in the same units), the image width in pixels, and the height above ground. Because the terrain under an archaeological site is rarely flat, is the height above the local surface, not above the launch point — which is why terrain-following matters on sloping tells and terraced sites.
Prerequisites
| Library / tool | Version | Purpose |
|---|---|---|
python |
3.11 | Runtime for planning scripts |
shapely |
2.0.4 | Site-polygon geometry, buffering, grid clipping |
geopandas |
1.0.1 | Reprojection and vector I/O for waypoints |
pyproj |
3.6.1 | CRS transforms between WGS84 and projected grids |
numpy |
1.26.4 | Vectorised spacing and heading arithmetic |
rasterio |
1.3.9 | Sampling a DEM for terrain-following AGL |
simplekml |
1.3.6 | KML export for Litchi / Google Earth review |
fiona |
1.9.6 | GeoPackage / GeoJSON waypoint output |
Environment assumptions: a projected CRS appropriate to the site (this section uses EPSG:27700, British National Grid — substitute your site’s EPSG), a digital elevation model covering the survey extent for AGL computation, and a UAV whose ground-station software ingests at least one of CSV waypoints, KML, or QGroundControl .plan files. A licensed aeronautical authorisation for the airspace is assumed to be held before any of this runs.
Implementation
The planning sequence runs desk-to-field in a fixed order: fix the GSD, derive the geometry, lay the grid, add control, export, and validate.
Step 1 — Derive altitude and image footprint from the GSD target. Invert the GSD equation to get flying height, then compute the ground footprint of a single frame. These numbers feed every later step.
# requirements.txt
# numpy==1.26.4
import numpy as np
def altitude_and_footprint(gsd_m, sensor_w_mm, sensor_h_mm,
focal_mm, img_w_px, img_h_px):
"""Return flying height (m) and ground footprint (m) for a target GSD."""
# H = GSD * f * image_width / sensor_width (all consistent units)
height_m = gsd_m * focal_mm * img_w_px / (sensor_w_mm / 1000.0) / 1000.0
footprint_w = gsd_m * img_w_px
footprint_h = gsd_m * img_h_px
return round(height_m, 2), (round(footprint_w, 2), round(footprint_h, 2))
# DJI Zenmuse P1, 35 mm lens, targeting 8 mm/px
h, fp = altitude_and_footprint(0.008, 35.9, 24.0, 35, 8192, 5460)
print(f"Fly at {h} m AGL; frame covers {fp[0]} x {fp[1]} m")
Step 2 — Convert overlap percentages into flight-line spacing and shutter interval. Side overlap sets the distance between adjacent lines; forward overlap and ground speed set the shutter interval. The image overlap and GSD guide develops the full derivation.
def line_spacing(footprint_across_m, side_overlap=0.70):
"""Distance between adjacent flight lines for a given side overlap."""
return footprint_across_m * (1.0 - side_overlap)
def shutter_interval(footprint_along_m, ground_speed_ms, forward_overlap=0.80):
"""Seconds between exposures to hold the forward overlap."""
advance_per_frame = footprint_along_m * (1.0 - forward_overlap)
return advance_per_frame / ground_speed_ms
spacing = line_spacing(43.7, 0.70) # ~13.1 m between lines
interval = shutter_interval(29.1, 5.0, 0.80) # ~1.16 s between frames
For heritage blocks over featureless surfaces, plan 80/70 (forward/side) as a floor and lift to 85/75 where vegetation or bare soil starves the matcher of texture.
Step 3 — Generate the serpentine grid over the site polygon. A boustrophedon (serpentine) pattern minimises turns and battery-sapping transits. The flight grid generation guide covers the full generator; the core is laying parallel lines at line_spacing, clipping to the buffered polygon, and reversing alternate lines.
Step 4 — Add oblique passes for standing structures. Nadir-only blocks under-constrain vertical faces. Add a perimeter orbit at 15–25° off-nadir around walls and towers so façades receive convergent imagery; this is what keeps the mesh in mesh generation and optimization for ruins from collapsing along overhangs.
Step 5 — Lay GCPs and checkpoints. Distribute at least five GCPs across the block — corners plus centre — with additional points on any pronounced elevation change, and reserve independent checkpoints (never fed to the bundle adjustment) to measure true accuracy. Log every marker’s EPSG:27700 coordinate at capture.
Step 6 — Export to the ground station. Emit the waypoint set in the format your platform consumes: CSV for Litchi, KML for review in Google Earth, .plan for QGroundControl, or DJI Pilot mission JSON. Carry the CRS metadata alongside — waypoints exported as bare latitude/longitude with no datum note are the single commonest cause of a survey landing tens of metres off its control.
CRS & Geometry Validation
Planning happens in a projected CRS (metres) so that spacing and buffers are Euclidean, but UAV firmware flies in EPSG:4326. The transform must therefore be explicit and logged. Validate three things before export: that the site polygon is valid (shapely is_valid and is_simple), that every generated waypoint falls inside the licensed operating area after reprojection, and that the round-trip transform EPSG:27700 → EPSG:4326 → EPSG:27700 returns each point to within a millimetre.
from pyproj import Transformer
fwd = Transformer.from_crs("EPSG:27700", "EPSG:4326", always_xy=True)
inv = Transformer.from_crs("EPSG:4326", "EPSG:27700", always_xy=True)
def roundtrip_ok(e, n, tol_m=0.001):
lon, lat = fwd.transform(e, n)
e2, n2 = inv.transform(lon, lat)
return abs(e - e2) < tol_m and abs(n - n2) < tol_m
A silent failure mode here is a datum mismatch between the DEM used for terrain-following and the CRS used for planning — sample the DEM in its own CRS, then transform, never the reverse.
Automated QA/QC
Gate the plan before it is flown. A plan that fails any gate is routed to a review queue rather than to the aircraft.
- Coverage gate: the union of frame footprints must cover 100% of the site polygon buffered outward by one footprint width; report any hole.
- Overlap gate: computed forward and side overlap at every line must meet or exceed the planned floor after terrain correction.
- Control gate: at least five GCPs, maximum inter-GCP gap below a configured threshold, and at least two independent checkpoints present.
- Airspace gate: zero waypoints inside any imported no-fly or heritage-restriction polygon (see planning safe flight corridors over heritage sites).
Each gate writes a row to an audit log — plan_id, gate, status, metric, threshold, timestamp — so the acquisition is defensible under later peer review.
Integration with adjacent workflows
The plan’s outputs are the contract with everything downstream. The exported GSD and control geometry are read directly by automated drone image processing workflows, whose geodetic gate rejects blocks whose GCP residuals exceed twice the planned GSD. When many sites are flown in a campaign, the plan manifests become the index that drives batch processing of photogrammetry datasets, letting one configuration process dozens of surveys without hand-editing paths. The captured GCP coordinates and checkpoint residuals also become metadata records in the site database — useful when reconciling the survey against the schemas in the artifact and feature database work.
Troubleshooting
pyproj.exceptions.CRSError: Invalid projection: EPSG:2770— a transposed or truncated EPSG code. British National Grid isEPSG:27700; verify against the EPSG registry before every campaign.shapely.errors.GEOSException: TopologyException: found non-noded intersection— a self-intersecting site polygon (common when a boundary is digitised by hand). Fix withpolygon.buffer(0)before generating the grid.- Waypoints land offset by a constant vector in the ground station — the plan was exported in projected metres but the firmware read them as degrees, or vice versa. Confirm the export CRS matches what the platform expects (
EPSG:4326for DJI/Litchi). rasterio.errors.RasterioIOError: ... not recognized as a supported file format— the DEM path is wrong or the file is a sidecar rather than the raster. Point at the.tifitself and confirm it overlaps the site withrasterio.open(dem).bounds.- Effective overlap far below plan after the flight — ground speed exceeded the shutter interval’s assumption, or terrain rose toward the aircraft. Re-derive the interval for the true speed and re-fly the affected lines.
Compliance notes
Record the acquisition to a recognised standard so it remains discoverable and reusable. Under ISO 19115 capture the lineage (planned GSD, altitude, overlap, camera model, CRS) and the spatial extent as a bounding polygon in the site CRS. MIDAS Heritage expects the event to be tied to a monument identifier, with the survey date, method, and responsible organisation. FAIR principles ask that the plan manifest and the resulting control residuals be archived alongside the imagery under a persistent identifier, so a later team can reproduce the geometry exactly. Retain the checkpoint residuals — they are the objective accuracy statement that a scheduled-monument consent condition will eventually ask for.
Related
- Calculating image overlap and ground sample distance for site survey — the arithmetic linking altitude, sensor, and resolution.
- Automating flight grid generation with Python — serpentine waypoint grids over a site polygon.
- Planning safe flight corridors over heritage sites — buffers, clearances, and geofence exports.
- Automated drone image processing workflows — how planned imagery is ingested and reconstructed.
- Batch processing photogrammetry datasets — running many planned surveys through one configuration.