AI-анализ аэрофотоснимков с дронов
Анализ данных с БПЛА отличается от работы с обычными фотографиями несколькими фундаментальными особенностями: большие ортофотопланы (5–20 Гпкс), нестандартный GSD (1–10 см/пиксель), мультиспектральные и тепловые каналы, необходимость геопривязки результатов в WGS-84 или локальной CRS. Стандартный пайплайн YOLOv8 → inference → результаты здесь не работает без предварительного тайлинга с учётом наземного разрешения.
Тайлинг и геопривязка — ключевой этап
Типичная ошибка: нарезка ортофото в пикселях без учёта GSD. При GSD 2 см тайл 640×640 пикселей = 12.8×12.8 м наземной площади. При GSD 8 см тот же тайл — уже 51×51 м. Модель, обученная на одном масштабе, даст на другом mAP на 15–25% ниже.
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]:
"""
Нарезка ортофото по наземному размеру тайла.
Гарантирует постоянный масштаб независимо от GSD.
"""
with rasterio.open(ortho_path) as src:
gsd = abs(src.transform.a) # метров/пиксель
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_ground_m=10 критично: объекты на границах тайлов детектируются в обоих, затем NMS по IoU убирает дубли. Без перекрытия теряется ~12% объектов на стыках.
SAHI для мелких объектов
При детекции людей или машин на ортофото GSD 3–5 см объект занимает 30–80 пикселей — значительно меньше рецептивного поля YOLO, оптимизированного под 640px. SAHI (Slicing Aided Hyper Inference) решает это нарезкой с перекрытием и NMS по всем предсказаниям:
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 → координаты в пикселях тайла
На датасете подсчёта людей на стройплощадке (5000 аннотированных снимков, GSD 4 см): без SAHI [email protected] = 0.61, с SAHI — 0.84. Разница принципиальная.
Трансформация координат и GeoJSON-вывод
Результаты анализа должны быть в геодезических координатах — иначе это просто картинки, не интегрируемые в ГИС-системы.
import pyproj
from shapely.geometry import box, mapping
import json
def detections_to_geojson(
detections: list,
tile_bounds: tuple, # (left, bottom, right, top) в CRS
tile_px_size: tuple, # (width, height) в пикселях
src_crs: str = 'EPSG:32637' # UTM зона для проекта
) -> 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']
# Пиксели → проекционные координаты
geo_left = left + x1 * scale_x
geo_right = left + x2 * scale_x
geo_top = top - y1 * scale_y
geo_bottom = top - y2 * scale_y
# Проекция → 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}
Тепловой канал (FLIR)
Мультироторные БПЛА с тепловизорами DJI Zenmuse H20T или FLIR Vue Pro дают 16-битные TIFF с температурными значениями. Детекция горячих точек на солнечных панелях — типичный кейс:
import numpy as np
def detect_pv_hotspots(
thermal_kelvin: np.ndarray, # (H, W), значения в 0.01K
panel_mask: np.ndarray, # бинарная маска панелей
delta_threshold: float = 10.0 # °C выше медианы панели
) -> list:
"""
Hotspot detection на солнечных панелях.
IEC 62446-3: дефект при ΔT > 10°C от референса.
"""
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)
На реальном проекте инспекции СЭС (142 панели, 3 полёта) система выявила 17 дефектных панелей с ΔT > 15°C, которые пропустила ручная визуальная проверка. ROI окупился за один сезон.
Metrics по типам задач
| Задача | GSD | Модель | Типичный [email protected] |
|---|---|---|---|
| Подсчёт деревьев | 3–5 см | YOLOv8m + SAHI | 0.88–0.93 |
| Детекция людей на стройке | 4–6 см | YOLOv8l + SAHI | 0.81–0.87 |
| Дефекты ЛЭП | 1–2 см | RT-DETR-L | 0.79–0.85 |
| Инспекция СЭС (тепло) | 5–10 см | threshold + SAM | 95%+ recall |
| Прогресс строительства | 5–10 см | SegFormer-B4 | IoU 0.84–0.91 |
Сроки
| Задача | Срок |
|---|---|
| Детектор одного класса (готовые данные) | 3–5 недель |
| Полная инспекционная система + ГИС-интеграция | 8–14 недель |
| Мультисенсорная платформа RGB + thermal | 14–22 недели |







