Orthorectify DJI nadir close-up drone photos and project Labelbox annotations to ground coordinates.
Left: raw close-up photo with Labelbox bounding box. Centre: all 105 photo footprints over the DSM, with 84 projected label polygons (red). Right: the same photo georeferenced as a COG GeoTIFF with its label polygon in the DSM CRS.
closeup-ortho performs a simple planar projection of DJI nadir close-up photos onto a DSM. Each photo is placed on the ground as a rectangle whose:
- 📍 position comes from the photo's RTK GPS (WGS84 lat/lon)
- 🧭 heading comes from the gimbal yaw
- 📐 size comes from the camera-to-ground distance —
AbsoluteAltitude − max(DSM elevation in a small buffer)— combined with the lens geometry (calibrated focal length for the wide camera,FocalLengthIn35mmFilmfor the tele camera)
It assumes photos are taken straight down (gimbal pitch ≈ −90°) and that the DSM uses ellipsoidal heights, matching the drone's AbsoluteAltitude. All outputs are in the CRS of the input DSM.
Two commands:
| Command | What it does |
|---|---|
orthorectify |
Projects photos onto a DSM → GeoPackage footprint polygons + optional COG GeoTIFFs |
labels |
Reprojects Labelbox bounding-box annotations to ground coordinates, linked to footprints |
A single georeferenced close-up photo (COG GeoTIFF) with its projected label polygon, displayed in QGIS over the DSM.
python -m venv .venv
# Windows
.venv\Scripts\python -m pip install -e .
# macOS / Linux
.venv/bin/python -m pip install -e .Dependencies: rasterio, geopandas, pyproj, shapely, numpy, Pillow
closeup-ortho orthorectify \
--photos data/examples/enearehab1/closeup/20260127_enearehab1_wpt169_m3e \
--dsm data/examples/enearehab1/dsm/20260225_enearehab1_10m169_m3e_aoi.tif \
--out out/examples/enearehab1/footprints.gpkg \
--geotiffs out/examples/enearehab1/tiff| Option | Default | Description |
|---|---|---|
--lenses |
tele |
Which lenses to process: tele, wide, tele,wide, or all |
--geotiffs DIR |
— | Also write projected COG GeoTIFFs to this folder |
--buffer-radius M |
auto | DSM sampling buffer radius in meters (default: ~50–75 cm) |
--no-recursive |
— | Scan only the top folder, not subfolders |
from closeup_ortho import orthorectify_folder
gdf = orthorectify_folder(
photo_dir="data/examples/enearehab1/closeup/20260127_enearehab1_wpt169_m3e",
dsm_path="data/examples/enearehab1/dsm/20260225_enearehab1_10m169_m3e_aoi.tif",
out_gpkg="out/examples/enearehab1/footprints.gpkg",
lenses={"tele"},
write_geotiffs=True,
geotiff_dir="out/examples/enearehab1/tiff",
)closeup-ortho labels \
--ndjson "data/examples/enearehab1/labelbox/2025_wa_roberge - 6_12_2026.ndjson" \
--footprints out/examples/enearehab1/footprints.gpkg \
--out out/examples/enearehab1/labels.gpkgfrom closeup_ortho import project_labels
gdf = project_labels(
ndjson_path="data/examples/enearehab1/labelbox/2025_wa_roberge - 6_12_2026.ndjson",
footprints_gpkg="out/examples/enearehab1/footprints.gpkg",
out_gpkg="out/examples/enearehab1/labels.gpkg",
)The output layer contains one polygon per bounding box. Join it back to footprints via footprint_filename → filename.
| Column | Description |
|---|---|
path |
Full path to the photo |
filename |
Filename with extension |
directory |
Parent directory of the photo |
camera |
tele or wide |
longitude, latitude |
Camera position, WGS84 |
abs_alt_m |
AbsoluteAltitude (ellipsoidal), meters |
dist_m |
Camera-to-ground distance used for projection, meters |
gsd_m |
Ground sample distance, meters/pixel |
footprint_w_m, footprint_h_m |
Footprint dimensions on the ground, meters |
yaw, pitch, roll |
Gimbal angles, degrees |
nadir_ok |
Gimbal within nadir tolerance |
focal_source |
calib (wide camera's CalibratedFocalLength) or fl35 (FocalLengthIn35mmFilm) |
timestamp |
Capture time |
| Column | Description |
|---|---|
footprint_filename |
🔑 Foreign key → filename in the footprints layer |
datarow_id |
Labelbox data row ID |
label_id |
Labelbox label ID |
feature_id |
Labelbox feature ID for this bounding box |
url |
Full URL of the source JPEG on the object store |
object_name |
Annotation class name (e.g. Plants) |
object_value |
Annotation class value (e.g. plants) |
taxon_name |
Taxon name from the radio classification |
taxon_value |
Taxon ID (PlantNet ID) |
bbox_top_px, bbox_left_px, bbox_height_px, bbox_width_px |
Original bbox in image pixels |
dataset_name |
Labelbox dataset name |
project_name |
Labelbox project name |
workflow_status |
Labelbox workflow status (e.g. IN_REVIEW, TO_LABEL) |
label_created_by |
Annotator email |
label_created_at |
Annotation timestamp |
lb_created_by |
Data row creator email |
- Using the Matrice 4 Enterprise laser rangefinder for the distance term (currently DSM-derived for all drones)
- Handling photos that fall outside the DSM extent (currently skipped with a warning)

