- PVSM.RU - https://www.pvsm.ru -
В белом-белом городе на белой-белой улице стояли белые-белые дома… А как быстро вы можете найти все крыши домов на этой фотографии?
Все чаще можно слышать про планы правительства провести полную инвентаризацию объектов недвижимости с целью уточнения кадастровых данных. Для первичного решения этой задачи можно применить простой способ, основанный на расчете площади крыш капитальных строений по аэрофотоснимкам и дальнейшее сопоставление с кадастровыми данными. К сожалению, ручной поиск и расчет занимает много времени, а поскольку новые дома сносятся и строятся непрерывно, то расчет требуется повторять снова и снова. Сразу возникает гипотеза, что этот процесс можно автоматизировать с помощью алгоритмов машинного обучения, в частности, Computer Vision. В этой статье я расскажу о том, как мы в «НОРБИТ» [1] решали эту задачу и с какими сложностями столкнулись.
Спойлер — у нас получилось [2]. В основе разработанного ML сервиса находится модель глубокого машинного обучения, основанная на свёрточных (convolution) нейронных сетях. Сервис принимает на вход снимки с беспилотных летательных аппаратов, на выходе генерирует GeoJSON файл с разметкой найденных объектов капитального строительства с привязкой к географическим координатам.
В результате это выглядит так:
Начнем с технических проблем, с которыми мы столкнулись:
А еще беспилотники иногда приносят вот такие фотографии:
Также хотелось бы отметить и проблемы, которые могли бы быть, но нас не коснулись:
Несколько слов о том, какие инструменты были использованы нами для создания решения.
В качестве ОС использовали Ubuntu 18.04. С драйверами на GPU (NVIDIA) в Ubuntu все в порядке, поэтому задача обычно решается одной командой:
> sudo apt install nvidia-cuda-toolkit
Первой задачей, с которой мы столкнулись, было разбиение изображений облета на тайлы (2048х2048 px). Можно было бы написать свой скрипт, но тогда бы пришлось думать о сохранении географической привязки каждого тайла. Проще было использовать готовое решение, например, GeoServer — это ПО с открытым исходным кодом, позволяющее публиковать на сервере геоданные. Помимо этого GeoServer решил для нас еще одну задачу – удобное отображение результата автоматической разметки на карте. Это можно сделать локально, например, и в qGIS, но для распределенной команды и демонстрации удобнее web-ресурс.
Для выполнения разбиения на тайлы нужно в настройках указать требуемый масштаб и размер.
Для переводов между системами координат мы использовали библиотеку pyproj:
from pyproj import Proj, transform
class Converter:
P3857 = Proj(init='epsg:3857')
P4326 = Proj(init='epsg:4326')
...
def from_3857_to_GPS(self, point):
x, y = point
return transform(self.P3857, self.P4326, x, y)
def from_GPS_to_3857(self, point):
x, y = point
return transform(self.P4326, self.P3857, x, y)
...
В результате удалось легко сформировать из всех полигонов один большой слой и наложить его сверху на подложку.
/usr/share/geoserver
echo «export GEOSERVER_HOME=/usr/share/geoserver» >> ~/.profile
sudo groupadd geoserver
sudo usermod -a -G geoserver <user_name>
sudo chown -R :geoserver /usr/share/geoserver/
sudo chmod -R g+rwx /usr/share/geoserver/
cd geoserver/bin && sh startup.sh
GeoServer – не единственное приложение, позволяющее решить нашу задачу. В качестве альтернативы, например, можно рассмотреть ArcGIS for Server, однако этот продукт является проприетарным, потому использовать его мы не стали.
Далее предстояло на каждом тайле находить все видимые крыши. Первым подходом к решению задачи было использование object_detection [4] из набора models/research Tensorflow. Этим способом можно находить и локализовывать прямоугольным выделением (boundary box) классы на изображениях.
Очевидно, для обучения модели нужен размеченный датасет. По счастливой случайности, помимо облетов, в наших закромах сохранился датасет на 50 тыс. крыш со старых добрых времен, когда все датасеты для обучения еще повсеместно лежали в открытом доступе.
Точный размер обучающей выборки, требующийся для получения приемлемой точности модели, заранее предсказать довольно сложно. Он может варьироваться в зависимости от качества изображений, степени их непохожести друг на друга и условий, в которых модель будет эксплуатироваться в production. У нас были кейсы, когда хватало 200 шт., а иногда не хватало и 50 тыс. размеченных образцов. В случае дефицита размеченных изображений мы обычно добавляем методы аугментации: повороты, зеркальные отражения, цветокоррекции и др.
Сейчас доступно множество сервисов, позволяющих произвести разметку изображений – как с открытым исходным кодом для установки на свой компьютер/сервер, так и корпоративные решения, включающие работу внешних асессоров, например Яндекс.Толока. В этом проекте мы использовали самый простой VGG Image Annotator [5]. В качестве альтернативы ему можно попробовать coco-annotator [6] или label-studio [7]. Последний мы обычно используем для разметки текста и аудиофайлов.
Для обучения на разметке различных аннотаторов обычно нужно выполнить небольшое перекладывание полей, пример для VGG [8].
Чтобы правильно вычислить площадь крыши, которая попала в область прямоугольного выделения, необходимо, соблюдения нескольких условий:
Для решения второй проблемы можно попробовать обучить отдельную модель, которая бы определяла правильный угол поворота тайла для разметки, но все получилось немного проще. Люди сами стремятся к уменьшению энтропии, поэтому выравнивают все рукотворные сооружения по отношению друг к другу, особенно при плотной застройке. Если посмотреть сверху, то на локализованном участке заборы, дорожки, посадки, теплицы, беседки будут параллельны или перпендикулярны границам крыш. Остается только найти все четкие линии и вычислить самый часто встречающийся угол наклона к вертикали. Для этого в OpenCV есть замечательный инструмент HoughLinesP.
...
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 50, minLineLength=minLineLength, maxLineGap=5)
if lines is not None:
length = image.shape[0]
angles = []
for x1, y1, x2, y2 in lines[0]:
angle = math.degrees(math.atan2(y2 — y1, x2 - x1))
angles.append(angle)
parts_angles.append(angles)
median_angle = np.median(angles)
...
# разбиение тайла на кропы
for x in range(0, image.shape[0]-1, image.shape[0] // count_crops):
for y in range(0, image.shape[1]-1, image.shape[1] // count_crops):
get_line(image[x:x+image.shape[0]//count_crops, y:y+image.shape[1]//count_crops, :])
...
# вычисление медианы угла на всех кропах
np.median([a if a>0 else 90+a for a in np.array(parts_angles).flatten()])
После нахождения угла изображение поворачиваем с помощью аффинного преобразования:
h, w = image.shape[:2]
image_center = (w/2, h/2)
if size is None:
radians = math.radians(angle)
sin = math.sin(radians)
cos = math.cos(radians)
size = (int((h * abs(sin)) + (w * abs(cos))), int((h * abs(cos)) + (w * abs(sin))))
rotation_matrix = cv2.getRotationMatrix2D(image_center, angle, 1)
rotation_matrix[0, 2] += ((size[0] / 2) — image_center[0])
rotation_matrix[1, 2] += ((size[1] / 2) — image_center[1])
else:
rotation_matrix = cv2.getRotationMatrix2D(image_center, angle, 1)
cv2.warpAffine(image, rotation_matrix, size)
Полный код примера тут [9]. Вот как это выглядит:
Метод поворота тайлов и разметка прямоугольниками работает быстрее, чем разметка масками, практически все крыши находятся, но в продакшене этот метод используется только как вспомогательный из-за нескольких недостатков:
Второй попыткой была реализация поиска и выделения крыш масками попиксельно, а затем автоматическая обводка контуров найденных масок и создание векторных полигонов.
Материалов о принципах работы, видах и задачах свёрточных нейронных сетей, в том числе и на русском языке, уже предостаточно, поэтому мы не станем углубляться в них в этой статье. Остановимся только на одной конкретной реализации, Mask-RCNN – архитектуры для локализации и выделения контуров объектов на изображениях. Есть и другие отличные решения со своими достоинствами и недостатками, например, UNet, но лучшего качества удалось добиться именно на Mask-RCNN.
В процессе своего развития она прошла несколько стадий. Первая версия R-CNN была разработана в 2014 году. Принцип ее работы заключается в выделении на изображении небольших областей, для каждой из которых производится оценка вероятности наличия в этой области целевого объекта. R-CNN отлично справлялась с поставленной задачей, однако скорость ее работы оставляла желать лучшего. Закономерным развитием стали сети Fast R-CNN и Faster R-CNN, получившие улучшения в алгоритме обхода изображений, что позволило значительно повысить быстродействие. На выходе в Faster R-CNN появляется разметка прямоугольным выделением, обозначающим границы объекта, что не всегда достаточно для решения задачи.
В Mask R-CNN добавляется еще и попиксельное наложение маски, позволяющее получить точное очертание объекта.
Наглядно boundary box и маски можно увидеть на результате работы модели (включен фильтр по минимальной площади здания):
Условно можно выделить 4 этапа в работе этой сети:
Помимо всего, сеть оказалась очень гибкой в конфигурировании, и нам удалось перестроить ее на обработку изображений с дополнительными информационными слоями.
Использование исключительно только RGB изображений не позволило добиться необходимой точности распознавания (модель пропускала целые здания, была средняя ошибка в 15% в вычислении площади крыш), поэтому мы подавали на вход модели дополнительные полезные данные, например карты высот, полученные методом фотограмметрии. [10]
При определении качества моделей мы чаще всего пользовались метрикой Intersection over Union (IoU)
Пример кода для вычисления IoU с использованием библиотеки geometry.shapely:
from shapely.geometry import Polygon
true_polygon = Polygon([(2, 2), (2, 6), (5, 6), (5, 2)])
predicted_polygon = Polygon([(3, 3), (3, 7), (6, 7), (6, 3)])
print(true_polygon.intersection(predicted_polygon).area / true_polygon.union(predicted_polygon).area)
>>> 0.3333333333333333
Отслеживание процесса обучения моделей удобно контролировать с помощью Tensorboard — удобного инструмента для контроля метрик, позволяющего в режиме реального времени получать данные о качестве модели и сопоставлять их с другими моделями.
Tensorboard предоставляет данные по множеству различных метрик. Для нас наиболее интересными являются:
При обучении нами применялась стандартная практика случайного разделения датасета на 3 части – обучающей, валидационной и тестовой. В процессе обучения качество модели оценивается на валидационной выборке, а по завершении проходит финальное испытание на тестовых данных, которые были от нее закрыты в процессе обучения.
Наши первые запуски обучения мы делали на небольшом наборе летних снимков и, решив проверить, насколько хороша будет наша модель зимой, ожидаемо получили разочаровывающий результат. Вариант с использованием разных моделей для разных сезонов, безусловно, является отличным выходом из ситуации, однако он повлек бы за собой ряд неудобств, поэтому решили попытаться сделать модель универсальной. Поэкспериментировав с различными конфигурациями слоев, а также закрывая от изменений веса отдельных слоев, мы нашли оптимальную стратегию обучения модели, подавая на вход попеременно летние и зимние снимки.
Теперь, когда у нас есть функционирующая модель, можно сделать из скрипта распознавания фоновый API сервис, который принимает на вход изображение, а на выходе генерит json с найденными полигонами крыш. Это непосредственно не влияет на решение задачи, но может быть кому-то будет полезно.
Ubuntu использует systemd, и пример будет приведен именно для этой системы. Код самого сервиса можно посмотреть здесь [11]. Пользовательские юниты находятся в директории /etc/systemd/system, там мы и создадим файл нашего сервиса.
cd /etc/systemd/system
sudo touch my_srv.service
Отредактируем файл:
sudo vim my_srv.service
Юнит systemd состоит из трех секций:
В результате наш файл будет выглядеть таким образом:
[Unit]
Description=my_test_unit
[Service]
WorkingDirectory=/home/user/test_project
User=root
ExecStart=/home/user/test_project/venv/bin/python3 /home/user/test_project/script.py
[Install]
WantedBy=multi-user.target
Теперь перезагрузим конфигурацию systemd и запустим наш сервис:
sudo systemctl daemon-reload
sudo systemctl start my_srv.service
Это простой пример фонового процесса, systemd поддерживает множество различных параметров, позволяющих гибко конфигурировать поведение сервиса, но для нашей задачи ничего более сложного и не требуется.
Главным результатом проекта стала возможность автоматически выявлять несоответствия фактической застройки и информации, содержащейся в кадастровых данных.
В результате оценки точности модели на тестовых данных получены следующие значения: количество найденных крыш – 91%, точность обводки крыш полигонами – 94%.
Удалось добиться приемлемого качества работы моделей на летних и зимних облётах, но качество распознавания может снижаться на снимках сразу после снегопада.
Теперь даже Сиднейский оперный театр не ускользнет от глаз нашей модели.
Мы планируем выложить этот сервис с обученной моделью на нашем демостенде. Если интересно попробовать работу сервиса на своих собственных фото, присылайте заявки на ai@norbit.ru.
Автор: Дмитрий Тимаков
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/352952
Ссылки в тексте:
[1] «НОРБИТ»: https://www.norbit.ru/
[2] получилось: https://www.vedomosti.ru/press_releases/2020/03/11/norbit-i-n-sistems-razrabotali-intellektualnuyu-model-klassifikatsii-geodannih-dlya-kompanii-shahti
[3] официальном сайте: http://geoserver.org/release/stable/
[4] object_detection: https://github.com/tensorflow/models/tree/master/research/object_detection
[5] VGG Image Annotator: http://www.robots.ox.ac.uk/~vgg/software/via/via.html
[6] coco-annotator: https://github.com/jsbroks/coco-annotator
[7] label-studio: https://labelstud.io/
[8] VGG: https://github.com/timakov-dmitry/roof_area/blob/master/dataset.py
[9] тут: https://github.com/timakov-dmitry/roof_area/blob/master/tile_rotate.py
[10] фотограмметрии.: https://ru.wikipedia.org/wiki/%D0%A4%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%8F
[11] здесь: https://github.com/timakov-dmitry/roof_area/blob/master/roofs_detector.py
[12] Источник: https://habr.com/ru/post/500752/?utm_source=habrahabr&utm_medium=rss&utm_campaign=500752
Нажмите здесь для печати.