Calculating Image Overlap and Ground Sample Distance for Site Survey
Before a serpentine grid can be laid or a corridor buffered, the survey needs a single quantitative anchor: the ground sample distance that the flight will actually achieve, and the flight-line spacing that holds the planned overlap around it. This guide, part of Drone Flight Planning for Photogrammetric Survey, turns the camera specification and a target resolution into the three numbers every mission needs — flying height, ground footprint, and line spacing — as one copy-paste function. It arises constantly in fieldwork because the person planning the flight is rarely the person who chose the drone, and the sensor on the airframe is frequently not the one the last plan assumed.
The ground sample distance is the real-world size of one pixel on the ground. For a nadir frame it is fixed by the sensor width , the flying height , the focal length , and the image width in pixels :
Everything else in this guide is algebra on that identity. Invert it for height when you have a resolution target; multiply it by pixel counts for the footprint; apply overlap fractions to the footprint for spacing and shutter timing.
Context & When to Use
Reach for these calculations whenever the airframe, lens, or resolution requirement changes — which in practice is every new site. The trade-offs are concrete. Flying lower tightens the GSD but multiplies flight lines, battery swaps, and processing time; a 5 mm/px masonry survey can be an order of magnitude more imagery than a 20 mm/px landscape pass over the same footprint. Raising overlap strengthens the tie-point network and rescues low-texture surfaces, but again inflates image count and matching cost. For heritage work the deciding factor is usually the smallest feature that must be legible in the final orthomosaic: tooling marks and inscription strokes demand a GSD at or below half their width, whereas earthwork and enclosure mapping tolerates far coarser pixels. Compute the numbers first, then let the flight grid generator consume the spacing this function returns.
Implementation
The function below takes a camera specification and a target GSD and returns a complete geometric summary. It works in SI throughout (metres, seconds) with millimetre inputs for the sensor and lens, mirroring how manufacturers publish specifications.
# requirements.txt
# numpy==1.26.4
from dataclasses import dataclass, asdict
import numpy as np
@dataclass
class Camera:
sensor_w_mm: float # physical sensor width
sensor_h_mm: float # physical sensor height
focal_mm: float # lens focal length
img_w_px: int # image width in pixels
img_h_px: int # image height in pixels
def survey_geometry(cam: Camera, target_gsd_m: float,
forward_overlap: float = 0.80,
side_overlap: float = 0.70,
ground_speed_ms: float = 5.0) -> dict:
"""
From a camera and a target GSD, return flying height, ground footprint,
flight-line spacing, and shutter interval for an archaeological survey.
GSD = (sensor_w * H) / (focal * img_w) -> H = GSD * focal * img_w / sensor_w
All distances are metres; sensor and focal length given in mm.
"""
if not (0 < forward_overlap < 1) or not (0 < side_overlap < 1):
raise ValueError("Overlaps must be fractions strictly between 0 and 1.")
sensor_w_m = cam.sensor_w_mm / 1000.0
sensor_h_m = cam.sensor_h_mm / 1000.0
focal_m = cam.focal_mm / 1000.0
# Invert the GSD identity for flying height above the surface.
height_m = target_gsd_m * focal_m * cam.img_w_px / sensor_w_m
# Ground footprint of a single frame at that height.
footprint_across_m = target_gsd_m * cam.img_w_px # perpendicular to flight
footprint_along_m = target_gsd_m * cam.img_h_px # along flight line
# Overlap -> geometry. Side overlap is between adjacent lines;
# forward overlap governs spacing between successive exposures.
line_spacing_m = footprint_across_m * (1.0 - side_overlap)
photo_advance_m = footprint_along_m * (1.0 - forward_overlap)
shutter_interval_s = photo_advance_m / ground_speed_ms
return {
"target_gsd_mm": round(target_gsd_m * 1000, 2),
"flying_height_m": round(height_m, 2),
"footprint_across_m": round(footprint_across_m, 2),
"footprint_along_m": round(footprint_along_m, 2),
"line_spacing_m": round(line_spacing_m, 2),
"shutter_interval_s": round(shutter_interval_s, 3),
"photos_per_100m_line": int(np.ceil(100.0 / photo_advance_m)),
}
if __name__ == "__main__":
# DJI Zenmuse P1 full-frame, 35 mm lens, 5 mm/px masonry survey.
p1 = Camera(sensor_w_mm=35.9, sensor_h_mm=24.0, focal_mm=35,
img_w_px=8192, img_h_px=5460)
result = survey_geometry(p1, target_gsd_m=0.005,
forward_overlap=0.80, side_overlap=0.70,
ground_speed_ms=4.0)
for k, v in result.items():
print(f"{k:>22}: {v}")
The archaeological logic lives in the defaults: 80/70 overlap is a conservative floor that survives bare soil and rubble, and a 4 m/s ground speed keeps motion blur below one pixel at typical shutter speeds when the GSD is fine. Loosen these only when the surface is richly textured.
Verification
Run the module and check the printed geometry against a hand calculation. For the P1 example at 5 mm/px the height should land near 22 m, and the footprint across track near 41 m. Assert the invariant that recomputing GSD from the returned height reproduces the target:
def test_roundtrip():
p1 = Camera(35.9, 24.0, 35, 8192, 5460)
g = survey_geometry(p1, 0.005)
# Recover GSD from height and confirm it matches within 0.1 mm.
gsd_check = (p1.sensor_w_mm / 1000) * g["flying_height_m"] \
/ ((p1.focal_mm / 1000) * p1.img_w_px)
assert abs(gsd_check - 0.005) < 1e-4, gsd_check
assert g["line_spacing_m"] < g["footprint_across_m"] # overlap > 0
print("geometry self-consistent")
test_roundtrip()
Expected output is geometry self-consistent with no AssertionError. If line_spacing_m ever equals or exceeds footprint_across_m, the side overlap fell to zero or below and adjacent strips will not tie.
Common Errors & Fixes
ZeroDivisionError: float division by zero—ground_speed_mswas passed as0, orsensor_w_mmwas left unset. A stationary aircraft has no shutter interval; supply the real cruise speed and confirm the sensor width came from the manufacturer sheet, not the crop-factor “equivalent”.- Computed height is roughly 1.5× or 2.3× the expected value — a crop-sensor focal length was used with a full-frame sensor width (or vice versa). The and must describe the same physical camera; do not mix a 35 mm-equivalent focal length with the true sensor width.
ValueError: Overlaps must be fractions strictly between 0 and 1— overlap was passed as a percentage (80) rather than a fraction (0.80). The function guards against this because an 80.0 overlap silently produces catastrophic negative spacing.
Related
- Drone Flight Planning for Photogrammetric Survey — the section overview that frames these calculations.
- Automating flight grid generation with Python — consumes the line spacing this function returns.
- Planning safe flight corridors over heritage sites — clearances and geofences around the grid.
- Automated drone image processing workflows — where imagery at this GSD is reconstructed.