Зачем дрону языковая модель? (Потому что классическое CV уже не катит)
Пять лет назад автономный дрон означал кучу кода на OpenCV, детекторы ArUco маркеров и попытки заставить его не врезаться в стену. Сегодня все проще и сложнее одновременно. Vision-Language Models (VLM) - это не просто "распознавание объектов". Это понимание сцены. Дрон видит не "квадратный объект коричневого цвета", а "коричневая дверь, приоткрытая на 30 градусов". Разница колоссальная.
Важно: на момент написания (январь 2026) VLM-модели переживают взрывной рост. Если в 2024 году мы радовались BLIP-2, то сейчас уже есть десятки специализированных моделей для робототехники. В этом гайде я буду использовать LLaVA-NeXT-Video - потому что она умеет работать с видеопотоком, а не с одиночными кадрами.
Что получится в итоге (без прикрас)
Дрон, который:
- Понимает голосовые команды: "облети квартиру по периметру и найди черную кошку"
- Сам строит маршрут, избегая препятствий
- Умеет импровизировать: если на пути внезапно появился человек, дрон остановится и спросит (текстом), можно ли продолжить
- Работает полностью локально - никаких облаков, никаких подписок
- Стоит в 3-4 раза дешевле коммерческих решений с аналогичными возможностями
Звучит как фантастика? Это потому что вы еще не видели, как современные VLM справляются с пространственным reasoning. Они реально понимают "слева от", "над", "между". И это меняет все.
Железо: что покупать и почему именно это
Здесь главная ошибка - пытаться запихнуть на дрон полноценную RTX 4090. Не надо. Наша архитектура распределенная:
| Компонент | Модель | Цена (руб) | Зачем |
|---|---|---|---|
| Дрон | DJI Tello (б/у) | 8 000-10 000 | Дешевый, программируемый, стабильный в полете |
| Одноплатник на дроне | Jetson Nano 4GB | 15 000 | Запускает легкую модель для экстренных решений |
| Носимый компьютер | Intel NUC 13 Pro | 45 000 | Основная VLM, коммуникация с дроном по Wi-Fi |
| Камера доп. | Raspberry Pi HQ Camera | 6 000 | Высокое разрешение для детального анализа |
Почему такая схема? Потому что latency. Если вся обработка будет на NUC, а дрон только передает видео, мы получим задержку 200-300 мс. Критично для полета в помещении. Поэтому на Jetson Nano стоит крошечная модель (например, MobileVLM 1.7B), которая занимается только одним: "впереди препятствие? Да/Нет". Основная же VLM на NUC получает видео с задержкой, но зато может планировать сложные маршруты.
Софтверный стек: от прошивки до нейросети
Собираем по слоям, как бутерброд:
1 Базовый слой: прошивка дрона
DJI Tello работает на проприетарной прошивке, но есть хаки. Качаем DJITelloPy - это Python библиотека, которая дает низкоуровневый доступ к контроллерам полета. Без этого дальше не сдвинемся.
# Устанавливаем на Jetson Nano
sudo apt update
sudo apt install python3-pip
pip3 install djitellopy opencv-python
Тестовая программа для проверки связи:
from djitellopy import Tello
import cv2
# Подключаемся к дрону
tello = Tello()
tello.connect()
# Включаем видеопоток
tello.streamon()
frame_read = tello.get_frame_read()
# Просто взлетаем и садимся
print(f"Батарея: {tello.get_battery()}%")
tello.takeoff()
tello.land()
2 Средний слой: коммуникация
Jetson Nano (на дроне) и NUC (на земле) общаются через ZeroMQ. Почему не ROS2? Потому что ROS2 для такой простой связки - это как пулемет для охоты на мух. К тому же, в моем гайде про мобильного робота-манипулятора я уже показывал, как ROS2 может усложнить жизнь.
# На Jetson Nano (паблишер видео)
import zmq
import cv2
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://*:5555")
cap = cv2.VideoCapture(0) # Камера дрона
while True:
ret, frame = cap.read()
if ret:
# Ресайзим для экономии трафика
frame_small = cv2.resize(frame, (640, 480))
socket.send(frame_small.tobytes())
# На NUC (сабскрайбер)
import zmq
import cv2
import numpy as np
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://[IP_JETSON]:5555")
socket.setsockopt_string(zmq.SUBSCRIBE, '')
while True:
frame_bytes = socket.recv()
frame = np.frombuffer(frame_bytes, dtype=np.uint8)
frame = frame.reshape((480, 640, 3))
# Теперь frame готов для обработки VLM
3 Верхний слой: VLM и принятие решений
Вот здесь начинается магия. Устанавливаем LLaVA-NeXT-Video - на январь 2026 это одна из лучших open-source моделей для видеоанализа.
Внимание: LLaVA-NeXT-Video требует минимум 16GB VRAM для комфортной работы в 7B параметров. Если GPU слабее - используйте Qwen2-VL-2B, она менее точная, но работает на 8GB.
# На NUC с NVIDIA GPU
git clone https://github.com/haotian-liu/LLaVA-NeXT
cd LLaVA-NeXT
pip install -e .
# Скачиваем веса модели (7B параметров)
huggingface-cli download llava-hf/llava-v1.6-vicuna-7b --local-dir ./weights
Базовый скрипт для анализа сцены:
from llava.model.builder import load_pretrained_model
from llava.mm_utils import process_images, tokenizer_image_token
from llava.constants import IMAGE_TOKEN_INDEX
import torch
# Загружаем модель
model_name = "./weights"
tokenizer, model, image_processor, context_len = load_pretrained_model(
model_name=model_name,
model_base=None,
tokenizer_name=model_name
)
model = model.cuda() # На GPU
# Функция для анализа кадра
def analyze_scene(image_frame, question):
"""
image_frame: numpy array (H, W, 3) от камеры
question: строка, например "Что находится в центре кадра?"
"""
# Подготовка изображения
image_tensor = process_images([image_frame], image_processor, model.config)
image_tensor = [t.cuda() for t in image_tensor]
# Формируем промпт
prompt = f"USER: \n{question}\nASSISTANT:"
# Токенизация
input_ids = tokenizer_image_token(
prompt, tokenizer, IMAGE_TOKEN_INDEX, return_tensors='pt'
).unsqueeze(0).cuda()
# Генерация ответа
with torch.inference_mode():
output_ids = model.generate(
input_ids,
images=image_tensor,
do_sample=True,
temperature=0.2,
max_new_tokens=100
)
# Декодируем ответ
answer = tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0]
return answer
Архитектура принятия решений: как дрон думает
Самая интересная часть. У нас не одна нейросеть, а целый конвейер:
- Экстренный контроллер (на Jetson Nano): MobileVLM 1.7B, вопрос всегда один: "Есть ли прямо по курсу препятствие на расстоянии менее 2 метров? Ответ только YES или NO." Задержка: 15-20 мс.
- Планировщик маршрута (на NUC): LLaVA-NeXT 7B, получает стоп-кадр раз в секунду, анализирует: "Какие пути свободны для движения? Опиши их относительно текущего направления."
- Интерпретатор команд (тоже на NUC): та же LLaVA, но для текста. Превращает "найди кошку" в последовательность действий: "1. Осмотреть комнату по секторам 2. Искать небольшие движущиеся объекты 3. Приблизиться для идентификации"
Fine-tuning на своих данных: когда стандартной модели недостаточно
LLaVA-NeXT тренировалась на общих данных. Но ваша квартира - особенная. Нужно научить модель:
- Узнавать вашу мебель ("это диван Ивановых, а не просто диван")
- Понимать планировку ("коридор ведет в спальню, а не в стену")
- Избегать опасных зон ("рядом с аквариумом не летать"
Делаем так:
# 1. Собираем датасет
import json
from pathlib import Path
# Структура для supervised fine-tuning
dataset = []
# Для каждого помещения делаем 10-15 снимков
for room in ["kitchen", "living_room", "bedroom"]:
for i in range(15):
# Запускаем дрон, делаем снимок
# Вручную размечаем (или используем автоматическую разметку через GPT-4)
item = {
"id": f"{room}_{i}",
"image": f"images/{room}_{i}.jpg",
"conversations": [
{
"from": "human",
"value": "Опиши, что ты видишь. Где можно пролететь?"
},
{
"from": "gpt",
"value": "Я вижу кухню. Слева - стол, справа - окно. Свободное пространство в центре комнаты. Можно лететь прямо или немного влево."
}
]
}
dataset.append(item)
# Сохраняем
with open("drone_dataset.json", "w", encoding="utf-8") as f:
json.dump(dataset, f, ensure_ascii=False, indent=2)
# 2. Fine-tuning с LoRA (чтобы не перетренить всю модель)
python llava/train/train_mem.py \
--model_name_or_path ./weights \
--version v1 \
--data_path drone_dataset.json \
--image_folder images/ \
--vision_tower openai/clip-vit-large-patch14-336 \
--mm_projector_type mlp2x_gelu \
--tune_mm_mlp_adapter True \
--bf16 True \
--output_dir ./checkpoints \
--num_train_epochs 3 \
--per_device_train_batch_size 4 \
--gradient_accumulation_steps 4 \
--save_steps 100 \
--save_total_limit 3 \
--learning_rate 2e-4 \
--weight_decay 0. \
--warmup_ratio 0.03 \
--lr_scheduler_type cosine \
--logging_steps 1 \
--model_max_length 2048 \
--gradient_checkpointing True \
--lazy_preprocess True
После 3 эпох (примерно 6 часов на RTX 4070) модель уже будет знать вашу квартиру лучше, чем вы сами. Шутка. Но ориентироваться будет точно лучше.
Интеграция всего: код, который все связывает
Теперь собираем все компоненты в одну систему. Главный скрипт на NUC:
import threading
import queue
import time
from collections import deque
class AutonomousDrone:
def __init__(self):
# Очереди для межпоточного обмена
self.video_queue = queue.Queue(maxsize=10)
self.command_queue = queue.Queue()
# История кадров для видеоанализа (последние 5 кадров)
self.frame_buffer = deque(maxlen=5)
# Состояние дрона
self.state = {
"battery": 100,
"position": (0, 0, 0), # x, y, z
"obstacle_near": False
}
def video_receiver_thread(self):
"""Получает видео от Jetson Nano"""
while True:
frame = receive_frame_from_zmq() # Функция из примера выше
if frame is not None:
try:
self.video_queue.put_nowait(frame)
except queue.Full:
pass # Пропускаем кадр, если очередь переполнена
def emergency_check_thread(self):
"""Быстрая проверка препятствий (на основе MobileVLM на Jetson)"""
while True:
if not self.video_queue.empty():
frame = self.video_queue.get()
# Отправляем на Jetson для быстрого анализа
# (реализация через REST API к Jetson)
response = requests.post(
"http://jetson.local:8080/check_obstacle",
json={"frame": frame.tolist()}
)
if response.json().get("obstacle"):
self.state["obstacle_near"] = True
# Немедленная команда стоп
self.send_emergency_stop()
else:
self.state["obstacle_near"] = False
def planning_thread(self):
"""Планирование маршрута с помощью LLaVA"""
while True:
time.sleep(1) # Планируем раз в секунду
if not self.video_queue.empty():
# Берем последний кадр
frames = list(self.video_queue.queue)
latest_frame = frames[-1] if frames else None
if latest_frame is not None:
# Анализируем сцену
prompt = """
Ты - пилот дрона. Проанализируй сцену:
1. Какие препятствия видишь?
2. Какие свободные пути для движения?
3. Если бы тебе нужно было лететь вперед, насколько это безопасно?
Ответ дай в формате JSON.
"""
analysis = self.llava_analyze(latest_frame, prompt)
# Парсим ответ и планируем
free_paths = analysis.get("free_paths", [])
if free_paths:
# Выбираем лучший путь
best_path = self.choose_best_path(free_paths)
# Конвертируем в команды дрону
commands = self.path_to_commands(best_path)
for cmd in commands:
self.command_queue.put(cmd)
def command_executor_thread(self):
"""Исполнитель команд"""
while True:
if not self.command_queue.empty():
cmd = self.command_queue.get()
# Проверяем, нет ли экстренной ситуации
if not self.state["obstacle_near"]:
self.execute_command(cmd)
time.sleep(0.1) # 10 команд в секунду максимум
def voice_command_listener(self):
"""Слушатель голосовых команд"""
# Используем whisper.cpp для распознавания
# или более легкую модель для локальной работы
while True:
command = listen_for_voice_command()
if command:
# Интерпретируем команду через VLM
prompt = f"""
Пользователь сказал: "{command}"
Ты - система управления дроном. Преобразуй эту команду в последовательность действий.
Формат ответа:
{
"goal": "цель команды",
"steps": ["шаг1", "шаг2", ...],
"constraints": ["ограничение1", ...]
}
"""
plan = self.llava_text_only(prompt)
# Добавляем шаги в очередь команд
for step in plan["steps"]:
self.command_queue.put(step)
def run(self):
"""Запуск всех потоков"""
threads = [
threading.Thread(target=self.video_receiver_thread),
threading.Thread(target=self.emergency_check_thread),
threading.Thread(target=self.planning_thread),
threading.Thread(target=self.command_executor_thread),
threading.Thread(target=self.voice_command_listener)
]
for thread in threads:
thread.daemon = True
thread.start()
# Главный цикл
try:
while True:
time.sleep(1)
# Мониторинг состояния
self.log_state()
except KeyboardInterrupt:
print("\nЗавершение работы...")
self.land_and_disconnect()
Типичные грабли (чтобы вы не наступили)
Грабли №1: Wi-Fi лагает
Решение: используйте Wi-Fi 6 роутер в одном помещении с дроном. Или лучше - выделенную точку доступа на NUC. В статье про локальный умный дом я показывал, как настроить стабильную локальную сеть.
Грабли №2: VLM глючит на однородных текстурах
Белые стены, однотонный ковер - модель теряется. Решение: добавляем в fine-tuning данные с такими сценами, учим модель говорить "не вижу ориентиров, нужна осторожность" вместо случайных ответов.
Грабли №3: Батарея садится за 10 минут
Дополнительная камера и Jetson Nano жрут энергию. Решение: внешний power bank на 10000 mAh, примотанный скотчем к дрону. Неэлегантно, но работает.
Что дальше? (Когда базовый вариант уже работает)
Самые интересные апгрейды:
- Мультиагентность: несколько дронов, которые координируются через общую VLM. Один летает, второй страхует, третий снимает видео.
- Обучение с подкреплением: вместо fine-tuning на статических данных, даем дрону возможность учиться на своих ошибках. Ударился о стену - запомнил, что так делать не надо.
- Интеграция с домашней автоматизацией: дрон не просто летает, а взаимодействует с умным домом. "Найди кошку и включи свет в той комнате, где она сидит" - вот это уже уровень.
- Лонгриды на борту: сохранять не просто логи, а полноценные отчеты о полете с анализом сцен. Потом можно анализировать, как менялась обстановка в квартире за день.
Финальный совет (который сэкономит вам неделю)
Не пытайтесь сделать все идеально с первого раза. Сначала добейтесь, чтобы дрон просто взлетел и передавал видео. Потом добавьте экстренную остановку при препятствиях. Потом - простой планировщик маршрута. И только потом - полноценную VLM.
Каждый этап тестируйте отдельно. VLM без дрона (просто анализируйте сохраненные видео). Дрон без VLM (ручное управление). Иначе вы никогда не поймете, где именно проблема: в железе, в коде или в модели.
И последнее: ваш дрон будет глупым. Первые недели он будет врезаться в стены, путать дверь с окном и садиться на кошку. Это нормально. Главное - чтобы вы понимали, ПОЧЕМУ он так делает. А с VLM это понимание приходит гораздо быстрее, чем с классическим CV. Потому что вместо "ошибка в строке 243" вы получаете "я думал, что эта тень - это проход, но оказалось, что это просто тень". И это - огромный прогресс.