В прошлом году попалась интересная задача по GEOINT на L3akCTF 2025. Нужно было найти гнездо аиста в поле. Дана была только панорама места без опознавательных знаков. Ниже — 4 скрина панорамы с обзором на каждую сторону света.
Из полутора тысяч команд во время соревнования только три смогли найти нужное место. Соревнования закончились, но загадка покоя не давала — посидев несколько вечеров, всё же удалось придумать решение.
Проанализировав окрестности, смогли определить, что это Эстония. Поняли по форме дорожного знака. Перечень знаков ПДД Эстонии можно посмотреть тут: ссылка.
Угол дороги на знаке заострен
После определения страны начали искать информацию об аистах и нашли сайт с мониторингом всех гнёзд белых аистов: https://www.eoy.ee/valgetoonekurg/. На сайте есть карта всех гнёзд, их там 3366 штук.
Мониторинг гнёзд белых аистов в Эстонии
Чисто теоретически задачу можно решить и в ручном режиме, просто перебрав все гнёзда Эстонии. Но на это может уйти не один день. Поэтому искали автоматизированное решение или какие-то дополнительные подсказки, чтобы сузить поиски. В итоге удалось придумать автоматизацию, и гнездо находится скриптами за 20 минут.
Для автоматизации нужны отформатированные и структурированные данные. Их легко вытащить с сайта мониторинга гнёзд. Даже парсить ничего не нужно, поскольку координаты приходят по API в JSON одним объектом. Ответ API тут: https://www.eoy.ee/valgetoonekurg/?load_events. Из всего объекта нужны только координаты, остальное — мусор.
Но когда начал писать парсер, нашёл поле «added» — это признак, где размещено гнездо. И среди прочего оказалось полезное: «На бетонных столбах без проводов». Такая фильтрация сократила количество точек до 429 штук — в 8 раз меньше, такое вполне реально и «руками» обработать. Однако признак я нашёл уже после реализации основного скрипта поиска, да и автоматизировать всё равно интереснее.
Идея автоматизированного поиска простая: пишем бота, который будет бегать по всем известным координатам и смотреть обстановку. Оглядывается по сторонам и сравнивает местность с известными фотками. Достаточно перейти в панораму Google Карт и сделать четыре скрина: дефолтная панорама, которая открывается сразу, — обычно это положение, параллельное дороге. А также ещё три фотки с шагом 90 градусов. На Картах Google, если кликать по компасу в углу экрана, обзор поворачивается ровно на 90 градусов.
Запускаем бота по всем точкам, чтобы скринить панораму с четырёх сторон света. А чтобы удобнее было обрабатывать результаты, делаем скрины с именем lat_lng_degree.jpg.
Когда насобирали скрины всех мест, нужно сравнить с исходными фото. Просто попиксельное сравнение тут не годится из-за разных масштабов и ракурсов. Но справится машинное зрение; для этого удобно использовать библиотеку OpenCV — она есть под множество языков, но, как по мне, в Python проще использовать. Базовое сравнение в OpenCV не учитывает цвет, так как картинки сравниваются в оттенках серого, а алгоритм производит сравнение по контрольным точкам и объектам, которые считает важными. Это даёт хорошие данные, но в нашем случае недостаточно.
Поэтому пришлось немного помудрить и добавить три уровня проверки:
Сравнение в шести разных масштабах — с шагом 10% шаблон уменьшается и увеличивается перед сверкой, а в итоге берётся лучший результат.
Сравнение на основе ORB — поиск ключевых точек. OpenCV ищет точки на изображениях, которые по «его мнению» являются значимыми, и сравнивает наличие и расположение таких точек на обоих изображениях.
Сравнение гистограмм — это сравнение по распределению цветов и яркости. Такая проверка может напортачить, если оригиналы сделаны зимой, а на панорамах будет лето или наоборот. Но если сезон один и тот же, то поможет отбросить лишние варианты.
Этими тремя алгоритмами сравниваем шаблонные картинки с каждой стороной света из набора одних и тех же координат. Выбираем лучшие варианты по всем сторонам света и высчитываем итоговый результат схожести шаблона и места с панорамы.
В итоге бот пробегается по всем местам, делает фотки с четырёх сторон и сравнивает их с шаблонными изображениями. Все результаты сортируем, и после выполнения нужное место оказалось на первом месте, набрав 32,5% схожести. На втором и третьем местах — по 29%, далее — ниже 25%.
Результат сверки мест на карте
Первая версия скрипта «бегала» по панорамам 13 часов, а потом ещё около часа сравнивала результаты. Но после фильтрации точек до 430 дело пошло шустрее — 2,5 часа. Это всё равно много, пришлось запустить бота на 10 потоков, больше мой ноут не вытянул (это 10 открытых в фоне браузеров). Скрипт сверки фоток тоже разделил на потоки. Итоговое время работы составило 19 минут: 13,5 минут — сбор фоток, и 5,5 минут — сверка с исходными фотками.
Код писал, безжалостно мучая нейронку, объясняя, что дописывать в мои алгоритмы, потом руками до ума доводил. Поэтому качество может немного хромать, но для баловства — самый раз, и понять идею можно. Все скрипты — в репозитории на GitHub.
Посмотреть на аиста можно тут: карта с панорамой.