Для кого эта статья?
Текст написан для энтузиастов дроностроения и рубрики "сделай сам". Сразу отмечу, что описана любительская конструкция. Если вам нужен квадрокоптер с профессиональным тепловизором, то смотрите в сторону специализированных решений. Как и в своей прошлой публикации о любительском дронострое, я предполагаю что читатель хоть немного знает тему.
Ради чего?
Ради интереса. Описанное решение имеет ряд недостатков в сравнении с заводскими тепловизионными камерами:
более высокая сложность изготовления и настройки
бо́льшая хрупкость всей конструкции
избыточный вес и энергопотребление
Что нам понадобится?
FPV-квадрокоптер с аналоговым VTX, желательно 3,5" или больше, т. к. потребуется дополнительное место для установки одноплатника и запас тяги; в данном случае использован Happymodel Crux35
Raspberry Pi Zero 2W
тепловизионная приставка InfiRay P2 Pro (или другая совместимая, об этом ниже)
переходник для подключения приставки к одноплатнику (USB Type-C "мама" → Micro-USB "папа"), лучше самодельный, т. к. он выйдет легче заводского
переключатель между камерами VIFLY Cam Switcher
понижающий преобразователь на 5 Впаяльник и прямые руки
Что будем делать?
Будем заниматься понемногу настройкой "малины", программированием на питоне и bash, пайкой. Общая схема и последовательность действий описана в каноническом видео-руководстве:
Обновление 1
По состоянию на сегодня канал удалён, к счастью, я успел сохранить видео и перезалил его в облако. Качать по ссылке: https://drive.google.com/file/d/1GuERpf3BbVYePvchFw25uc7j8MgrUGeN/view?usp=sharing
Обновление 2
Пока запись была в отложке автор воссоздал канал и перезалил видео
На мой взгляд, многое в нём осталось за кадром (например, годное руководство по настройке переключения между камерами появилось буквально пару месяцев назад), поэтому здесь я опишу процесс с самого начала.
Основой для проекта стала работа некоего LeoDJ, который на форуме eevblog поделился своими наработками по обратной инженерии протокола камеры Infiray P2 Pro, на основании которых @leswright1977 написал скрипт, использующий библиотеку cv2 для обработки изобажения и выводящий картинку с камеры на экран:

Упомянутый выше @catch22mania творчески развил эту идею, приспособив скрипт для работы на Raspberry Pi Zero 2W, который отправляет полученную картинку на выход аналогового видео, откуда её забирает VTX и по радиоканалу передаёт в очки пилота. Возможно, источником вдохновения послужил проект Connecting FPV transmitter to Pi Zero (2017 год), автор которого использовал аналоговый передатчик вместо WiFi для достижения большей дальности передачи видео с камеры наблюдения.
"Малина"
Начнём с настройки отноплатника. Для этого нужны компьютер и карта памяти, на которую мы установим ОС Raspbian Bullseye 64.
Для подготовки оси нам потребуется Raspberry Pi Imager. Подключаем карту памяти к компьютеру и запускаем установщик. Сама установка предельно проста: нужно выбрать устройство, ОС и указать хранилище.
Установка пошагово, опытные пользователи могут пропустить этот раздел




Для полноценной работы нужна сеть, поэтому когда установщик спросит о дополнительных настройках, выбираем "EDIT SETTINGS". Тут укажем имя пользователя и пароль (нужны для удалённого входа, не мудрствуя лукаво оставил admin:admin), а также настройки точки доступа:

На первых порах одноплатник я настраивал в headless-режиме (управление через SSH без подключения к "малине" монитора и клавиатуры), поэтому переходим в "Службы" и включаем его (аутентификация по имени и паролю):

Отлично, настройка завершена, нажимаем сохранить и далее. Установщик выдаст предупреждение о полном стирании данных с карты памяти, соглашаемся и ждём окончания записи.
Запуск
Вставив карту памяти можно подключать питание. Для начала я использовал обычную банку:

Загрузка занимает минуту-полторы. Если всё сделано правильно, то "малина" подключится к указанной ранее сети и мы увидим её в настройках роутера (для DLink это меню DHCP → Список клиентов DHCP):

Зная адрес берём PuTTY (или любой другой клиент) и подсоединяемся:

При успешном подключении появляется полноценный терминал, для входа используем указанные ранее имя и пароль (admin:admin):

Для просмотра картинки с тепловизора на компьютере нужен доступ к удалённому рабочему столу, с этим поможет VNC Viewer, так что устанавливаем его не отходя от кассы:
sudo apt update && sudo apt -y upgrade && sudo apt -y autoremove sudo apt -y install realvnc-vnc-server realvnc-vnc-viewer
Настройка сервера VNC на стороне одноплатника также не составит труда:
sudo raspi-config


Теперь скачиваем на компьютер сам VNC Viewer. Я не стал заморачиваться с установкой и использую отдельное (standalone) приложение, запуская VNC-Viewer-7.13.1-Windows-64bit.exe:

Выбираем созданное подключение и запускаем его:

Вводим пароль:

Теперь у нас есть полноценный удалённый рабочий стол:

Также нам потребуется включить вывод аналогового видео, которое будет передаваться полётному контроллеру (далее ПК) и данее на VTX. Для этого снова выполняем
sudo raspi-config

Далее выбираем D6 Composite


На этом установка завершена, переходим к настройке ПО для взаимодействия с тепловизором.
Создаём окружение
Делая настройку впервые я выполнял действия в той же последовательности, что и автор видео, однако, в самый первый раз установочный скрипт упал из-за нехватки оперативной памяти (думаю, @catch22mania разобрался с этим заранее и просто забыл упомянуть в своём видео).
Естественно, увеличить объём оперативной памяти "малины" (512 Мб) с кондачка мы не можем, зато нам доступно расширение файла подкачки (он же SWAP-память) с заводских 100 до 2048 Мб. Дабы не делать всю работу руками, можно воспользоваться заранее подготовленной заплаткой:
#скачиваем wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1llGY-OybNOsNHqaNSDE41J3_K0kga2zj' -O raspberry-swap-file-2048.diff #выключаем подкачку sudo dphys-swapfile swapoff #накатываем изменения sudo patch /etc/dphys-swapfile < raspberry-swap-file-2048.diff #пересоздаём файл подкачки sudo dphys-swapfile setup #включаем подкачку sudo dphys-swapfile swapon
Теперь установка должна пройти без сучка без задоринки:
#скачиваем и распаковываем архив wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1DUug7rpHFosw0n5j6SoHpa5NtMTPPydg' -O fpv-diy-thermal-camera-pi-installer-cerritos-v003b-catch22mania.tar.gz tar -xvzf fpv-diy-thermal-camera-pi-installer-cerritos-v003b-catch22mania.tar.gz #переходим в нужную папку и запускаем установочные скрипты cd fpv-diy-thermal-camera-pi-installer-cerritos-v003b-catch22mania bash installer-cerritos-v003b.sh bash update-ptc-script-testing-cerritos-003b.sh bash update-read_sbus-testing-cerritos-003b.sh
На всякий случай вот установочный скрипт целиком
#!/bin/bash #скачиваем wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1llGY-OybNOsNHqaNSDE41J3_K0kga2zj' -O raspberry-swap-file-2048.diff #выключаем подкачку sudo dphys-swapfile swapoff #накатываем изменения sudo patch /etc/dphys-swapfile < raspberry-swap-file-2048.diff #пересоздаём файл sudo dphys-swapfile setup #включаем подкачку sudo dphys-swapfile swapon #скачиваем и распаковываем архив wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=18z9PWzRiaDQAiC88VzYxKeBgzEfpdpB2' -O fpv-diy-thermal-camera-pi-installer-cerritos-v003b-catch22mania.tar.gz tar -xvzf fpv-diy-thermal-camera-pi-installer-cerritos-v003b-catch22mania.tar.gz #переходим в нужную папку и запускаем установочные скрипты cd fpv-diy-thermal-camera-pi-installer-cerritos-v003b-catch22mania bash installer-cerritos-v003b.sh bash update-ptc-script-testing-cerritos-003b.sh bash update-read_sbus-testing-cerritos-003b.sh
Скачать и установить его можно с помощью команды
wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1XUnnOhHUMqBvGSxotxQI0_1ok1tmWJLa' -O 'prepare-raspberry-pi-zero2w.bash' && bash ~/prepare-raspberry-pi-zero2w.bash
После успешного завершения в домашней папке появится много нового:

Итак, всё нужное для работы со стороны "малины" имеется, а значит займёмся железом.
Переходник
Для передачи изображения нужно связать USB Type-C (тепловизор) и Micro-USB ("малина"). Сделать его несложно, в моём случае использовалось гнездо JRC-B008 ("мама") и безымянный разъём Micro-USB ("папа"), а также 4 провода (желательно гибкий и тонкий 26-28 (или даже 30) AWG):

По возможности берите гнездо с укороченной платой, например, такое. Для небольших птиц важен каждый грамм

Это же касается "папы", чем меньше, тем лучше.
В итоге получился вот такой переходник:

Красный и чёрный провода – это +5 В и земля, а жёлтый и голубой, соответственно, – Д+ и Д-.
Питание и выход аналогового видео
Раз уж у нас в руках паяльник, доработаем "малину":
Припаиваем выводы питания и передачи аналогового видео согласно схеме:


Запуск
Убедившись, что переходник и питание подключены, повторно подключаемся к одноплатнику и переходим в домашнюю папку. Точкой входа является скрипт starter.sh, выполняющийся сразу после загрузки ОС. В нём находим и раскоментируем команду
# ptc script - InfiRay, etc. (testing) - options see ~/starter.sh.ptc.examples python /home/admin/testing/ptc-patched-c22-014.py --fullscreen --colormap 20 & # 256x192; options see ~/starter.sh.ptc.examples
Она запускает скрипт ~/testing/ptc-patched-c22-014.py с параметрами fullscreen и colormap 20, что развернёт окно с тепловым изображением на весь экран и применит к нему палитру №20 (полный список см. в коде скрипта).
Для проверки работоспособности идём в терминал и выполняем bash ~/starter.sh, спустя пару секунд на экране появляется заветное окно:

Важно!
Может случиться так, что вместо вменяемой картинки вы увидите вот это:

По своему опыту могу сказать, что причина до смешного проста: кабель, которым вы запитываете "малину" не подходит и его нужно заменить.
Ещё одно неочевидное наблюдение: качество картинки зависит от напряжения источника питания, а точнее от его пульсации. Ниже будет сравнение 3 источников:
Банка и две зарядки от разных производителей
Первый пример: "малина" запитана от банки:
Второй пример: "малина" запитана от зарядки к DJI Mini 2
Третий пример: "малина" запитана от зарядки к телефону
Для меня эта зависимость несколько неочевидна, т. к. на данном этапе мы работаем с цифровым, а не аналоговым изображением (насколько я понимаю). А причина, похоже в понижающем импульсном преобразователе, который отдаёт пульсирующее напряжение. Эти пульсации можно сгладить выходными фильтрами и/или конденсаторами. Внутрь банки и зарядных устройств я не смотрел, думаю, что причина как раз в отличиях ёмкости и ЭПС выходных конденсаторов, а также индуктивности дросселя, образующих вместе LC-фильтр (поправьте, если ошибаюсь).
Для сглаживания пульсаций к выводам шины питания "малины" я припаял два параллельно соединённых танталовых конденсатора T520D227M010ATE018 ёмкостью 220 мкФ каждый.
Обновление 1
На самом деле это лишнее, всё прекрасно запиталось от полётного контроллера, отдающего чистые 5 вольт. Так что в итоге конденсаторы я выпаял.


Поскольку всё это будет на квадрокоптере с шумящими двигателями, то по совету ИИ для дополнительного шумоподавления к выводам питания тепловизора припаял электролитический и керамический конденсаторы:

Обновление 2
Их я тоже в итоге снял по причине ненужности при питании от полётника.
В итоге картинка стала значительно устойчивее даже при питании от "шумной" банки, хотя до идеальной ещё далеко (она будет ниже):
Над скриптом и настройками удобнее работать напрямую с ПК. Для этого скопируем скрипт ~/testing/ptc-patched-c22-014.py на компьютер и сразу же создадим под него проект в PyCharm.
Теперь с помощью идущего в комплекте удлинителя Type-C подключаем тепловизор к ноутбуку и запускаем скрипт. Если вы работаете на Windows, то в первый раз он ожидаемо упадёт.
Для исправления нужно правильно определить устройство, из которого cv2 будет считывать изображение, т. к. у нас Windows, то вместо
cap = cv2.VideoCapture('/dev/video' + str(dev), cv2.CAP_V4L)
пишем
cap = cv2.VideoCapture(dev)
Также нужно убрать вот эту часть, т. к. мы не на "малине":
if isPi == True: cap.set(cv2.CAP_PROP_CONVERT_RGB, 0.0) else: cap.set(cv2.CAP_PROP_CONVERT_RGB, False)
Теперь скрипт работает:

Если вдруг вместо вменяемой картинки вы увидите что-то такое

то вы читаете данные с другого устройства (скорее всего это веб-камера вашего ноутбука) и нужно сменить порядковый номер устройства (по умолчанию dev = 0, т. к. на одноплатнике всего одна камера):
cap = cv2.VideoCapture(dev)
Запускаем и наслаждаемся картинкой превосходного качества
Для удобства весь код выложен на ГитХабе, всё, что нужно для его работы – это интерпретатор питона.
Питание на птице
Следующий этап – установка всего этого на Crux35. Помним о том, что теперь у нас нет банки, так что запитать одноплатник придётся либо от батареи, либо от полётника. В моём случае свободных гнёзд питания уже не осталось, поэтому выбор пал на Pololu S13V15F5, отдющий 5 В и до 1,5 А с КПД до 95% (при входном напряжении 6-9 В, что соответсвует 2S на основе 18650) и весе всего 0,7 г.
А вот как надо сделать на самом деле
Правильно решение – это использование шины питания поисковой пищалки, т. к. она нужна только в экстренной ситуации и обычно простаивает:

В итоге я просто припаял выводы питания параллельно с питанием пищалки.
Если вы используете другой полётник, то перед подключением желательно всё-таки проверить напряжение на них (помните, что для этого нужно подключить батарею, т. к. без неё питание на пищалку не подаётся). Да, Raspberry Pi Zero 2W может работать и от 3,3 В, но лучше перестраховаться и подключаться к 5 В.
Ещё одно возможное решение
В современных полётниках производители стали делать выводы для припаивания выводов под сторонний USB. Теоретически запитать "малину" можно и от него, т. к. при 5 вольтах потребляемый ей ток составляет около 120 мА при включенном WiFi и 100 мА без него (в полёте он нам всё равно не понадобится), т. е. мощности вполне хватит. Этот вариант я пока не проверял, можно оставить его в качестве направления дальнейших изысканий.

Но это нужно обязательно проверять, т.к. у меня есть подозрение, что напряжения на VUSB при подключенной батарее (без самого USB) не будет.
С питанием вроде бы разобрались, теперь переходим к камерам.
Переключатель камер
Критически важным этапом является настройка переключателя между обычной и тепловизионной камерами. Для этого применён упомянутый выше VIFLY Cam Switcher, руководство по которому доступно здесь и который де-факто является стандартом подобного рода устройств. В первую очередь делаем подключение:

И если с подсоединением камер и полётника всё однозначно, то для управления нам предложены целых 4 варианта:

В моём случае на плате полётного контроллера свободным остался контакт для светодиодной ленты (LED_STRIP), а поскольку Betaflight поддерживает сервоприводы и позволяет парой команд переназначить ресурс, то было решено пойти этим путём. Для этого нужно:
определить ресурс, выделенный для светодиодной ленты (физически это контакт, на который подаётся управляющий сигнал)
освободить ресурс и выделить его под управление сервоприводом
настроить управление сервоприводом с пульта и подавать сигналы на контакт
D/Iпереключателя камер
Если этот способ вам не подходит, то вот неплохое англоязычное руководство по настройке.
Начнём с подключения всех частей:

Аналоговый сигнал от "малины" передаётся на С2 (вспомогательная камера). Обратите внимание, что к С2 подводятся только два провода – сигнал и земля. Голубой провод – управляющий, одним концом он припаян к выводу LED, другим – к контакту D/I переключателя:

Теперь подлючаем коптер к компьютеру и открываем Betaflight. Обязательно сделаем две вещи:
сохраним пользовательские настройки (пригодятся, если светодиодная лента и сервоприводы не включены в сборку и придётся перепрошивать контроллер)
сделаем полный дамп (на случай если напутаем что-то и нужно будет откатиться)
Перво-наперво проверим, влючены ли у нас нужные фичи, а именно LED_STRIP и SERVOS, и если нет – включаем их и перепрошиваем полётник. Этот процесс тривиален и подробно расписывать его не будем, каноническое руководство (также объясняющее, почему для контроля над сервоприводом подходит именно контакт светодиодной ленты) можно посмотреть у Джошуа Бардвелла.
Открываем консоль Betaflight и выводим список всех ресурсов командой resource. Ищем среди них LED_STRIP (не перепутайте с LED!):
# name: Crux35 # resource resource BEEPER 1 C15 resource MOTOR 1 B06 ... resource MOTOR 4 A02 resource LED_STRIP 1 B01 <------ нас интересует вот это ...
Запоминаем значение B01 и выполняем следующие команды:
resource LED_STRIP 1 none resource servo 1 B01 save
Обратите внимание: значение ресурса у разных полётников может быть разным, поэтому копипаста из статьи в вашем случае может не сработать! Обязательно выполняйте команду resource и проверяйте её вывод! Например, вывод этой же команды для контроллера JHEMCU F405:
# name: Rekon35 # resources resource BEEPER 1 C13 resource MOTOR 1 B00 ... resource MOTOR 8 C08 resource LED_STRIP 1 A09 <----- ! ...
Теперь нужно привязать управление servo 1 к кнопке на пульте. У меня RadioMaster Pocket и я хочу переключаться между камерами с помощью кнопки SD:

Она соответствует каналу AUX4, следовательно, в Betaflight во вкладке Servos делаем так:

Теперь при нажатии на СД управляющий сигнал подаётся на Серво1:

На данном этапе птица выглядит жутковато:

И тем не менее, всё отлично работает!
Перед полётом нужно убедиться, что камера, OSD и "малина" настроены на работу в режиме PAL. Если у одного из компонентов режим не совпадает (например, камера и OSD настроены на NTSC), то переключившись с обычной камеры на тепловизионную, вы рискуете остаться без OSD.
И несколько завершающих штрихов к скрипту:
упрощение скрипта, т. к. в моём случае я отказался от интерактива (переключения палитры, вывода макисмальной и минимальной температуры). В полёте он мне просто не нужен, да и свободных переключателей для управления на моём пульте нет
запись видео в файл одновременно с запуском скрипта. Это во-первых даёт уникальную возможность писать картинку с тепловизора без помех и OSD, т. к. в файл попадает изображение, взятое напрямую с датчика (в отличие от записи DVR, на которой вы будете видеть ту же картинку, что и в очках), а во-вторых вести запись постоянно, т. е. даже в тот момент, когда в VTX передаётся картинка с курсовой камеры
Скрипт. Итоги
import cv2 import time import numpy as np width = 256 height = 192 scale = 3 dev = 0 newWidth = width * scale newHeight = height * scale # Инициализация видеопотока cap = cv2.VideoCapture(dev) # Настройка окна cv2.namedWindow('ThermalWindow', cv2.WND_PROP_FULLSCREEN) cv2.setWindowProperty('ThermalWindow', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) # Настройка видеозаписи current_time_str = time.strftime("%Y%m%d--%H%M%S") fourcc = cv2.VideoWriter_fourcc(*'XVID') out = cv2.VideoWriter(f"thermal_output-{current_time_str}.avi", fourcc, 25, (newWidth, newHeight)) while cap.isOpened(): ret, frame = cap.read() if ret: # Разделение на визуальную и тепловую части imdata, thdata = np.array_split(frame, 2) bgr = cv2.cvtColor(imdata, cv2.COLOR_BGR2RGB) bgr = cv2.resize(bgr, (newWidth, newHeight), interpolation=cv2.INTER_CUBIC) heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_DEEPGREEN) cv2.imshow('ThermalWindow', heatmap) # Запись кадра в файл out.write(heatmap) keyPress = cv2.waitKey(1) if keyPress == ord('q'): break else: break # Очистка ресурсов cap.release() out.release() cv2.destroyAllWindows()
После завершения работы в папке появится файл с записью:

Следующая задача – крепление для тепловизора. Конечно, можно заморочиться и напечатать его, но т. к. проект любительский я просто слепил его из термопластика (он же полиморфус).
В итоге первое поколение получилось таким:




Всё готово, осталось поднять дрон в воздух и посмотреть картинку:
Итак, что показал первый полёт:
возникли проблемы с навигацией, что неудивительно, т. к. на "малине" всё ещё был включен ваф-фай, антенна которого находилась прямо около антенны приёмника ГНСС, а поскольку я летал возле дома, то одноплатник подлючился к домашней точке доступа и начал давить своим сигналом навигацию
энергопотребление невзведённого дрона выросло незначительно, но в полёте потребляемая пощность растёт, т.к. с добавлением малины и камеры вырос общий вес
расположение одноплатника крайне неудачное
Недолго думая я начал переделку. Сперва я подправил starter.sh, добавив в него отключение вай-фая переда запуском скрипта:
sudo rfkill block wifi
Правда, теперь мы не сможем работать в headless-режиме, поэтому потребуются переходники для подключения монитора и клавиатуры:

Дальше сама плата: здесь всего два пункта:
т. к. разъём Micro-USB перестал помещаться, то я отпаял его и подключил выход переходника непосредственно к плате (питание к питанию, шину данных к DP/DM)
гнёзда под питание и аналовое видео я припаял прямо к плате, получилось убористее и с меньшим количеством проводов:

С размещением тоже получилось сделать проще и изящнее: отдоплатник уехал под "верхнюю палубу", что позволило скрыть провода и улучшить аэродинамику (особенно при встречном ветре). У этого решения есть минусы (теплоотвод), зато антенна ГНСС задышала полной грудью.
Второе поколение




Вот так получилась маленькая, относительно тихая и симпатичная жужжалка весом всего 130 грамм, которая может переключаться между обычным и тепловизионным изображениями, а также записывать картинку в 4К.
Что по питанию?
Здесь есть целых три варианта:
заводской 3S/4S
самодельный 3S/4S на ёмких 18650
самодельный 4S на литий-полимерных банках
Я остановился на двух последних вариантах: 3S на основе Murata US18650 VTC6 для полётов на дальняк и 4S на основе GNB 850mAh.


В целом оба решения компромиссные и оба имеют право на существование. Для снижения веса батарея крепится к нижней раме обычной стяжкой.
Направление дальнейших изысканий:
снижение энергопоребления "малины". Годную статью об этом можно почитать здесь
прямой вывод видеопотока с тепловизора в composite output. По идее, это позволит использовать облегчённую и более проворную версию ОС без графического интерфейса, что увеличит производительность и снизит энергопотребление (читай увеличит дальность и время полёта). В принципе, самое простой решение – это летать на курсовой камере, а картинку с тепловизора писать в файл
допиливание самого скрипта (например, с помощью ChatGPT я без особых усилий добавил на экран шкалу температур)
Надеюсь, вам было интересно и вы почерпнули для себя что-то новое. До новых встреч!
