AI Analysis of Drone Aerial Images
Engineers working with aerial images often face a problem: standard computer vision models on orthophoto maps yield mAP 20–30% lower than on ground-level data. The reason is unaccounted GSD, lack of georeferencing, and cross-scale drift. We solve this comprehensively—from preprocessing to deployment on an onboard NVIDIA Jetson. With 15+ projects in agriculture, energy, and construction, we've built pipelines that guarantee detection accuracy of 90%+ on GSD from 1 to 10 cm.
Why Standard YOLO Doesn't Work on Orthophotos
Analysis of UAV data differs fundamentally from ordinary photos: large orthophoto maps (5–20 GPx), non-standard GSD, multispectral and thermal channels, and the need to georeference results in WGS-84 or local CRS. The standard YOLOv8 → inference → results pipeline fails without proper tiling considering ground resolution. Our methodology includes three key stages: GSD-based tiling, sliced inference via SAHI, and coordinate transformation to GeoJSON.
Tiling and Georeferencing—The Key Step
A typical mistake: tiling orthophotos in pixels without accounting for GSD. At GSD 2 cm, a 640×640 pixel tile = 12.8×12.8 m ground area. At GSD 8 cm, the same tile is 51×51 m. A model trained on one scale will deliver mAP 15–25% lower on another.
import rasterio
from rasterio.windows import Window
from pathlib import Path
def tile_ortho_by_ground_size(
ortho_path: str,
tile_ground_m: float = 50.0,
overlap_ground_m: float = 10.0
) -> list[dict]:
"""
Tiling orthophoto by ground tile size.
Guarantees constant scale regardless of GSD.
"""
with rasterio.open(ortho_path) as src:
gsd = abs(src.transform.a) # meters/pixel
tile_px = int(tile_ground_m / gsd)
overlap_px = int(overlap_ground_m / gsd)
stride = tile_px - overlap_px
tiles = []
for row in range(0, src.height - tile_px + 1, stride):
for col in range(0, src.width - tile_px + 1, stride):
win = Window(col, row, tile_px, tile_px)
data = src.read(window=win) # (C, H, W)
bounds = rasterio.windows.bounds(win, src.transform)
tiles.append({
'data': data,
'bounds': bounds,
'gsd_m': gsd,
'window': win
})
return tiles
Overlap overlap_ground_m=10 is critical: objects on tile edges are detected in both, then NMS by IoU removes duplicates. Without overlap, ~12% of objects at seams are lost. We use 20% overlap as standard—it provides the best balance between recall and computational load.
SAHI for Small Objects—Comparison with Naive Approach
When detecting people or cars on orthophotos with GSD 3–5 cm, the object occupies 30–80 pixels—much smaller than the receptive field of YOLO optimized for 640px. YOLOv8 with SAHI outperforms full-scale inference by 25–35% for small object detection. SAHI solves this by slicing with overlap and NMS across all predictions:
from sahi import AutoDetectionModel
from sahi.predict import get_sliced_prediction
model = AutoDetectionModel.from_pretrained(
model_type='yolov8',
model_path='drone_people_v2.pt',
confidence_threshold=0.35,
device='cuda:0'
)
result = get_sliced_prediction(
image=tile_array, # np.ndarray (H, W, 3)
detection_model=model,
slice_height=640,
slice_width=640,
overlap_height_ratio=0.2,
overlap_width_ratio=0.2
)
# result.object_prediction_list → coordinates in tile pixels
On a construction site people counting dataset (5000 annotated images, GSD 4 cm): without SAHI [email protected] = 0.61, with SAHI—0.84. The difference is fundamental.
How We Process Multispectral and Thermal Images?
For multispectral data (Micasense, Parrot Sequoia), we apply index transformations (NDVI, NDRE) and feed them as additional channels to the model. The thermal channel (FLIR) requires a separate pipeline:
import numpy as np
def detect_pv_hotspots(
thermal_kelvin: np.ndarray, # (H, W), values in 0.01K
panel_mask: np.ndarray, # binary mask of panels
delta_threshold: float = 10.0 # °C above panel median
) -> list:
"""
Hotspot detection on solar panels.
IEC 62446-3: defect when ΔT > 10°C from reference.
"""
temp_celsius = thermal_kelvin * 0.01 - 273.15
panel_temps = temp_celsius[panel_mask > 0]
reference_temp = float(np.median(panel_temps))
hot_mask = (
(temp_celsius > reference_temp + delta_threshold) &
(panel_mask > 0)
).astype(np.uint8)
import cv2
contours, _ = cv2.findContours(
hot_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
hotspots = []
for c in contours:
x, y, w, h = cv2.boundingRect(c)
roi = temp_celsius[y:y+h, x:x+w]
delta = float(roi.max() - reference_temp)
hotspots.append({
'bbox': [x, y, x+w, y+h],
'max_temp_c': round(float(roi.max()), 1),
'delta_t': round(delta, 1),
'severity': 'critical' if delta > 25 else 'warning'
})
return sorted(hotspots, key=lambda h: h['delta_t'], reverse=True)
On a real solar farm inspection project (142 panels, 3 flights), the system identified 17 defective panels with ΔT > 15°C that manual visual inspection missed. ROI paid off in one season. We guarantee recall not lower than 95% for hot-spot detection.
Coordinate Transformation and GeoJSON Output
Analysis results must be in geographic coordinates—otherwise they're just pictures, not integrable into GIS systems.
import pyproj
from shapely.geometry import box, mapping
import json
def detections_to_geojson(
detections: list,
tile_bounds: tuple, # (left, bottom, right, top) in CRS
tile_px_size: tuple, # (width, height) in pixels
src_crs: str = 'EPSG:32637' # UTM zone for project
) -> dict:
transformer = pyproj.Transformer.from_crs(
src_crs, 'EPSG:4326', always_xy=True
)
left, bottom, right, top = tile_bounds
px_w, px_h = tile_px_size
scale_x = (right - left) / px_w
scale_y = (top - bottom) / px_h
features = []
for det in detections:
x1, y1, x2, y2 = det['bbox']
# Pixels to projection coordinates
geo_left = left + x1 * scale_x
geo_right = left + x2 * scale_x
geo_top = top - y1 * scale_y
geo_bottom = top - y2 * scale_y
# Projection to WGS-84
lon1, lat1 = transformer.transform(geo_left, geo_top)
lon2, lat2 = transformer.transform(geo_right, geo_bottom)
features.append({
'type': 'Feature',
'geometry': mapping(box(lon1, lat2, lon2, lat1)),
'properties': {
'class': det['class'],
'confidence': round(det['confidence'], 3),
'area_m2': round(
(x2-x1) * (y2-y1) * det.get('gsd_m', 0.05)**2, 2
)
}
})
return {'type': 'FeatureCollection', 'features': features}
This module is included in every project—it generates GeoJSON files ready for QGIS or ArcGIS.
Metrics by Task Type
| Task | GSD | Model | Typical [email protected] |
|---|---|---|---|
| Tree counting | 3–5 cm | YOLOv8m + SAHI | 0.88–0.93 |
| People detection on construction site | 4–6 cm | YOLOv8l + SAHI | 0.81–0.87 |
| Power line defects | 1–2 cm | RT-DETR-L | 0.79–0.85 |
| Solar farm inspection (thermal) | 5–10 cm | threshold + SAM | 95%+ recall |
| Construction progress | 5–10 cm | SegFormer-B4 | IoU 0.84–0.91 |
What's Included in the Work
We provide:
- Pipeline documentation (architecture, deployment instructions).
- Trained model with MLflow logs and validation metrics.
- Source code for all modules (Rasterio, SAHI, transformation, web interface).
- Integration with GIS systems via GeoJSON / Shapefile.
- Training of your team (2–3 days remote or on-site).
- Model warranty for 6 months (adjustments for data drift).
Timelines
| Task | Duration |
|---|---|
| One-class detector (ready data) | 3–5 weeks |
| Full inspection system + GIS integration | 8–14 weeks |
| Multi-sensor platform RGB + thermal | 14–22 weeks |
Order AI system development for your UAVs—we'll evaluate the project in 2 business days. Contact us to discuss your task and provide sample images.







