Привет! Меня зовут Елена, я занимаюсь ресерчем и обучением моделей машинного обучения в компании NtechLab.
В прошлом году мне захотелось поучаствовать в крупнейшем российском хакатоне “Лидеры Цифровой трансформации”. И, собрав команду, неожиданно, мы заняли призовое место. О том, как мы сформировали команду, как проходил хакатон, о наших эмоциях и настрое вы можете прочитать в первой части статьи . Я же хочу более детально рассказать о технической стороне решения.
Как вы помните, на выбор у нас было 20 кейсов и мы выбрали задание Департамента природопользования Москвы – «Разработка ПО для определения состояния зеленых насаждений города по фотографиям».
Перед нами стояла задача превратить обычную фотографию зелёных насаждений в детальный отчёт о здоровье каждого дерева. Для ее решения недостаточно было просто применить готовые алгоритмы. Нашей целью стала система, способная с точностью выше 90% выполнять сегментацию деревьев, классифицировать их породы, выявлять дефекты и формулировать сводное описание состояния — и всё это должно было работать быстро и стабильно. Разберем последовательно все этапы разработки этого проекта.
Разметка данных
Как уже было сказано ранее, задача подразумевала наличие разметки. Однако организаторы предоставили только порядка тысячи фотографий с пораженными деревьями без лейблов. При этом само ТЗ подразумевало не просто создание бинарной классификации (здоровое/больное дерево), но и определение конкретных заболеваний и поражений, включая наклон деревьев и пр.
Мы пробовали как самостоятельно разметить датасет (но не успели), так и использовать предразметку llm (качество получилось среднее). В общем, эти данные получилось использовать только разве что для слабенького файнтюна и тестирования в качестве целевого домена.
Датасеты для остальных задач были благополучно найдены в интернете, не идеальные, но для прототипа подошли.
Сегментация
Модель как для сегментации, так и для классификации решили выбрать из семейства YOLO (текущее SOTA-решение). По ТЗ на обработку одного изображения у нас было максимум 30 секунд, и мы могли развернуться только на CPU. Поэтому мы выбирали между самыми легкими моделями — YOLO26s-seg и YOLO26n-seg. Решили взять YOLO26s-seg, хоть она работает примерно в два раза дольше (118.4 мс против 53.3 мс), но все равно очень быстро, и при этом у нее выше качество. YOLO26n-seg же скорее подходит для задач в реальном времени, что в нашем случае было не нужно.
Для сегментации нашли хороший датасет Urban Street Tree с масками стволов и деревьев, с листвой и без.

У этого датасета есть много минусов:
он содержит деревья не нашего региона
не все зеленые насаждения размечены, а только 1-3 главных дерева/куста
в масках отсутствует окрашенная белым часть ствола
практически нет сценариев, где кроны деревьев переплетаются/накладываются друг на друга, что совершенно не похоже на насаждения в Москве и МО (рисунок 2)*
*В машинном обучении это называют domain shift.

Чтобы сгладить эти недостатки, мы постарались обучить модель с добавлением аугментаций (например, mosaic - склейка нескольких изображений в одно). Но полученное качество нас не устроило и мы решили взять наиболее репрезентативные данные из полученного датасета и самостоятельно разметить их. Работали в CVAT с помощью инструментов авторазметки - использовали встроенную модель сегментации. Для разметки таких сложных структур, как дерево, эта модель очень помогла, но все равно пришлось потратить время на сглаживание контуров и фильтрацию полученных данных, поскольку сами по себе маски включали в себя оторванные контуры (конечно, этой проблемы не было бы, если бы мы вручную делали и перепроверяли разметку, но времени на это не было), и другие сомнительные моменты, которые, опять же, убирались с помощью opencv и алгоритмов. Таким образом, мы относительно быстро получили почти 300 изображений с разметкой.

В итоге мы дообучили модель на найденном датасете Urban Street Tree и после этого еще немного обучили на нашей разметке с теми же лейблами. Примеры работы сетки можно видеть на рис.3. Модель все равно не распознавала прямо все деревья в кадре, но в целом нас удовлетворил полученный результат.
Кстати, вот такую замечательную сегментацию делает оpen-source модель SAM2. Мы даже изначально не хотели ничего обучать, а просто сделать классификатор полученных контуров. Но, на сложных случаях, модель выделяла слишком маленькие контуры и пришлось бы добавлять алгоритмы по их сопоставлению в целое дерево. А это уже выглядит больше как костыль, чем идея.

Классификация пород
Итак, после сегментации у нас есть маски деревьев и есть порядка 20 видов (по задаче было достаточно определять наиболее популярные в Москве и МО), которые мы хотим классифицировать. Нужные виды определили по статье.

Open-source сеток не нашли (хотя есть много сеток по классификации листьев/коры, но не всего дерева в профиль). Решили подсобрать датасет сами, в доступе было очень много спутниковых снимков леса, а нужны были обычные фото де��ева в полный рост. Для этого написали пайплайны выгрузки данных с платформы iNaturalist. Собранный датасет состоял из совершенно разных ракурсов деревьев - от отдельных изображений листьев и коры до дерева в полный рост. На этом моменте отфильтровали датасет уже готовым сегментатором и обучили также небольшую модель семейства YOLO. Но несмотря на потраченные усилия, качество модели нас сильно не устраивало.
Мы подумали и решили добавить в интерфейс уточнение вида дерева по отдельному фото листа. Таким образом мы увеличили качество определения породы с 60-70% до более 90%, как и требовалось по ТЗ. Такой подход также в будущем обеспечил бы более точное определение заболеваний дерева.
Определение заболевания
Первостепенной задачей мы поставили именно сегментацию, что по логике правильно, ведь “нет дерева - нет болезни”, но только в самом конце поняли, что ключевой в этой задаче являлась сетка для определения поражений дерева (на которую оставили слишком мало ресурсов).
Одной из первых мы нашли уже готовую модель по классификации патологий дерева на основе Swin Transformer (OttoYu/Tree-Condition) и успокоились. Однако, она выдавала вероятность наличия одного из 22 поражений, куда входили нужные лейблы (наросты, раны или механические повреждения, гниль, наличие дупла и так далее), но не выделяла их bbox-ом на изображении.
Конечно идеальное решение подразумевало добавление сегментации наростов, повреждений и прочее. Но под конец на разметку ресурсов не хватало, поэтому решили испытать автоматическую разметку изображений с помощью VLLM.
Идея была следующая: использовать в качестве первичного эксперта мультимодальную модель из семейства QwenVL, которая понимает и изображения, и текст.
Ключевой промпт модели выглядел примерно так
prompt = """
Ты — эксперт по анализу деревьев. Проанализируй изображение и верни ответ в формате JSON.
Требования к анализу:
1. defects: если есть дерево, опиши его дефекты. Используй конкретные термины: трещины, стволовые гнили, дупла, повреждения ствола, повреждения кроны, плодовые тела грибов, болезни, сухие ветви (определяй ТОЛЬКО в вегетационный период (весна/лето), в невегетационный период (осень/зима) ставь 'не определяется'). Если дефектов нет, укажи "нет"
2. bbox: для КАЖДОГО найденного дефекта укажи координаты bounding box в формате [x_min, y_min, x_max, y_max] где координаты нормализованы от 0 до 1. Если несколько дефектов - верни список bbox: [[x1,y1,x2,y2], [x1,y1,x2,y2]]
ВАЖНО:
- Координаты bbox должны быть точными и охватывать область дефекта
- Всегда возвращай bbox для дефектов, даже если это приблизительная область
- Если дефектов несколько, верни несколько bbox
- Следи за тем, чтобы bbox не был рядом с дефектом. Он должен именно выделять дефект
- Верни ответ ТОЛЬКО в формате JSON без дополнительного текста
Пример правильного ответа:
{
"defects": "трещины на стволе, сухие ветви",
"bbox": [[0.35, 0.45, 0.55, 0.75], [0.15, 0.25, 0.30, 0.40]]
}
"""
Таким образом нам удалось получить неплохую разметку примерно для 500 изображений.

Как видим из рисунка 6, получились как очень классные кейсы (картинки 3-4), так и неправильная разметка картинки 1-2). В итоге, так как классов поражений было много, данных получилось все равно мало, модель вышла плохая, а самостоятельно отправлять QwenVL искать повреждения прямо в сервисе мы не решились (хотя зря, для демо она справляется недурно).
Создание итогового описания
По ТЗ к готовому решению необходимо было приложить таблицу с параметрами для каждого дерева на картинке и текстовое описание. Его решили генерировать тоже с помощью QwenVL. Модель не разворачивали самостоятельно и использовали по hugging face API.
В промпт для модели уже передавали полученные ранее результаты (порода, болезни) и давали указания на самостоятельный анализ.
Вот такие описания мы получили

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

На дереве наблюдается значительное повреждение в виде крупного дупла на стволе, что может ослабить его структурную целостность. Наклон дерева отсутствует.
Дополнительные наработки
После выхода в полуфинал, организаторы попросили доработать решение. А именно: добавить визуализацию наклона (для этого с помощью opencv2 определяли ось маски дерева и рассчитывали угол отклонения от горизонта, для упрощения задачи считали, что на фото он не завален), исправить некоторые баги, поработать над визуализацией результатов на картинке.
Мы слишком сосредоточились на технологиях и совсем забыли о том, как видят наш проект организаторы, поэтому пришлось срочно дорабатывать все визуализации и презентацию.
Сборка решения
Как было уже сказано в первой статье, мы писали веб-сервис без фуллстак-разработчика, поэтому он получился простенький, но рабочий (использовали FAST API,sqlalchemy и VUEJS).
Сервера оформили на Yandex Cloud и даже вошли по аренде в приветственную квоту (на 2025 год это 4к рублей на которые мы смогли арендовать машинку с CPU на несколько недель). Поскольку все модели поддерживали CPU, а LLM мы использовали через HF API, то арендовать видеокарты не пришлось (с этим бы возникло много проблем, во-первых, потому что по квоте они не предоставлялись, и во-вторых, для аренды GPU нужно ждать согласования от платформы какое-то количество времени).
Хакатон также предоставлял квоты на аренду машинок по предварительным заявкам.
Командный дух
Изначально мы не думали, что получится закончить сервис и занять призовое место. Я настраивала команду на то, что мы спокойно и размеренно сделаем решение как сможем, не забросим и доведем все до логического конца. Было страшно требовать от людей соответствовать твоему идеальному видению и придираться, не хотелось портить со старыми друзьями отношения или пугать новых знакомых. Но в то же время брать на себя все задачи тоже неправильно. Пришлось ловить баланс между пользой для проекта и пользой для людей, которые в нем участвуют.
Кроме того, играть роль «знающего», не чувствуя себя экспертом, — занятие сомнительное. Ты вроде и несёшь ответственность, но при этом становишься "бутылочным горлышком": без тебя процесс не движется, а посоветоваться или поспорить — не с кем. Нет самокритики, нет и стороннего взгляда — в итоге часть идей высасывает ценное время, а по-настоящему сильные решения остаются в тени.
Несмотря на то, что хакатон отнял уйму энергии и ресурсов, сейчас я с уверенностью могу сказать: это было выгодное вложение. Время окупилось новыми знаниями и удовлетворением от проделанной работы.

