Данные лидара для обнаружения линии электропередачи
Вступление
В последние годы был достигнут значительный прогресс в разработке детекторов LIDAR, что привело к получению огромных объемов необработанных данных. Лидары теперь более точны и обеспечивают гораздо более высокое разрешение, чем даже 10 лет назад. Лидары с воздушным базированием стали эффективным методом получения информации об окружающей среде. Тем не менее, данные, которые вы получаете, на самом деле представляют собой всего лишь набор разреженных точек, которые могут обеспечить хорошую визуализацию, но требуют обширной обработки, когда дело доходит до более практических целей. К сожалению, на сегодняшний день развитие компьютерного зрения в основном касается структурированных двумерных данных (фото, видео). Текущие методы не распространяются на многомерные разреженные данные, такие как облако точек, которое мы получаем после базовой предварительной обработки данных LIDAR. В этой области проводятся обширные исследования. Мы
Особо следует упомянуть PCL - набор библиотек, разработанный великим международным сообществом для предоставления инструментов как для 2D, так и для 3D-данных для самых разных приложений. К сожалению, на данный момент применить библиотеку к какому-либо интересующему решению - непростая задача. Часто это означает, что вам нужно слишком глубоко погрузиться в библиотеку. Это имеет смысл для продуктов промышленного уровня, которым требуется высокая масштабируемость. Но это может оказаться слишком дорогостоящим для разработки PoC. Поэтому я решил попробовать, что можно сделать с данными облака точек, используя простой подход и довольно стандартные библиотеки Python (PCL можно использовать из Python, но только пока, поскольку только небольшие подмножества могут быть легко интегрированы).
Данные для эксперимента
В качестве примера возьмем данные, генерируемые антенным лидаром для обнаружения линий электропередачи. Линии электропередач часто хорошо видны при визуализации облака точек. Однако нанесение на карту точек, принадлежащих линии электропередачи, требует больших усилий вручную. С другой стороны, простое геометрическое рассмотрение может предоставить нам средства для значительного упрощения или даже автоматизации такой обработки.
Линия электропередачи на картинке на самом деле представляет собой набор точек, которые образуют определенные геометрические узоры. Чтобы упростить дальнейшую категоризацию, я решил проверить, как мы можем формировать кластеры из этих точек.
Программное обеспечение, использованное для эксперимента
Я буду использовать библиотеки NumPy, Sklearn, Laspy и SciPy для формирования кластеров и matplotlib для визуализации.
import laspy import scipy import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import DBSCAN from sklearn import metrics from sklearn import preprocessing from mpl_toolkits.mplot3d import Axes3D from matplotlib import path
Laspy отлично подходит для обработки данных облака точек в Python. Мы читаем данные облака точек из las-файла и проверяем форму фактического набора данных.
# Open a file in read mode: inFile = laspy.file.File(“./LAS/simple.las”) # Grab a numpy dataset of our clustering dimensions: dataset = np.vstack([inFile.x, inFile.y, inFile.z]).transpose() dataset.shape
(5942479, 3)
- наше облако точек состоит из 5942479 точек. Недостаточно, если вы хотите перейти к мелким деталям. Но число будет слишком большим, если вы попытаетесь преобразовать этот DataFrame в трехмерный массив NumPy, так как в этом случае мы получим огромный массив 5942479³ = 2,09 * 10²⁰. Он будет использовать огромный объем оперативной памяти для хранения действительно разреженных данных. Очевидно, что нужно использовать разреженный массив NumPy. Но на самом деле разреженный массив отлично подходит для 2D, но не работает с 3D-данными. Функции обработки матриц не полностью совместимы с разреженными трехмерными матрицами. Мы должны работать с DataFrame вместо массива NumPy из-за требований к памяти.
Устранение точек вне зоны видимости
Нам нужно найти способ устранить те точки, которые не являются линиями электропередач. Линии электропередач размещаются высоко над землей из соображений безопасности и для обеспечения их оптимальной работы. Но для пересеченной местности мы должны учитывать, что некоторые точки земли могут быть выше линий электропередач в разных частях изображения из-за наклона земли. Чтобы избежать этого, давайте разделим наше облако точек на небольшие вертикальные части.
%%time def frange(start, stop, step): i = start while i < stop: yield i i += step #ground points grid filter n = 100 #grid step dataset_Z_filtered = dataset[[0]] zfiltered = (dataset[:, 2].max() — dataset[:, 2].min())/10 #setting height filtered from ground print(‘zfiltered =’, zfiltered) xstep = (dataset[:, 0].max() — dataset[:, 0].min())/n ystep = (dataset[:, 1].max() — dataset[:, 1].min())/n for x in frange (dataset[:, 0].min(), dataset[:, 0].max(), xstep): for y in frange (dataset[:, 1].min(), dataset[:, 1].max(), ystep): datasetfiltered = dataset[(dataset[:,0] > x) &(dataset[:, 0] < x+xstep) &(dataset[:, 1] > y) &(dataset[:, 1] < y+ystep)] if datasetfiltered.shape[0] > 0: datasetfiltered = datasetfiltered[datasetfiltered[:, 2] >(datasetfiltered[:, 2].min()+ zfiltered)] if datasetfiltered.shape[0] > 0: dataset_Z_filtered = np.concatenate((dataset_Z_filtered, datasetfiltered)) print(‘dataset_Z_filtered shape’, dataset_Z_filtered.shape)
С помощью этого простого подхода мы можем значительно сократить количество точек в облаке в кратчайшие сроки, даже используя умеренную вычислительную мощность. В нашем случае это было уменьшение количества точек на порядок за 3 минуты - неплохо для нескольких строк кода, учитывая, что мы не приложили реальных усилий для какой-либо оптимизации.
dataset_Z_filtered shape (169862, 3) CPU times: user 3min 16s, sys: 7.14 ms, total: 3min 16s Wall time: 3min 16s
Теперь мы будем использовать гораздо меньший отфильтрованный набор данных.
Изучение наших данных
Давайте исследуем наши данные:
print(“Examining Point Format: “) pointformat = inFile.point_format for spec in inFile.point_format: print(spec.name) Examining Point Format: X Y Z intensity flag_byte raw_classification scan_angle_rank user_data pt_src_id gps_time
Во время своих экспериментов я стараюсь использовать 4D-представление данных (X, Y, Z и интенсивность), но результаты не улучшаются по сравнению с 3D (X, Y, Z), поэтому давайте придерживаться последнего подмножества данных.
print(‘Z range =’, dataset[:, 2].max() — dataset[:, 2].min()) print(‘Z max =’, dataset[:, 2].max(), ‘Z min =’, dataset[:, 2].min()) print(‘Y range =’, dataset[:, 1].max() — dataset[:, 1].min()) print(‘Y max =’, dataset[:, 1].max(), ‘Y min =’, dataset[:, 1].min()) print(‘X range =’, dataset[:, 0].max() — dataset[:, 0].min()) print(‘X max =’, dataset[:, 0].max(), ‘X min =’, dataset[:, 0].min()) Z range = 149.81 Z max = 181.78999908447264 Z min = 31.979999084472652 Y range = 622.9700000002049 Y max = 2576396.509974365 Y min = 2575773.539974365 X range = 556.4400000000605 X max = 711882.7199987792 X min = 711326.2799987792
Нормализация данных
Как видите, значения находятся в разных диапазонах. Для лучших результатов мы должны нормализовать набор данных.
dataset = preprocessing.normalize(dataset)
Кластеризация геометрически близких точек
Теперь мы готовы обрабатывать наши данные. Наши линии электропередач на самом деле представляют собой пространственные кластеры точек, поэтому естественно попробовать алгоритм кластеризации. После непродолжительного исследования я обнаружил, что DBSCAN, предоставляемый библиотекой Sklearn, лучше всего работает «из коробки».
clustering = DBSCAN(eps=2, min_samples=5, leaf_size=30).fit(dataset)
Теперь давайте визуализируем наши результаты.
core_samples_mask = np.zeros_like(clustering.labels_, dtype=bool) core_samples_mask[clustering.core_sample_indices_] = True labels = clustering.labels_ # Number of clusters in labels, ignoring noise if present. n_clusters_ = len(set(labels)) — (1 if -1 in labels else 0) n_noise_ = list(labels).count(-1) print(‘Estimated number of clusters: %d’ % n_clusters_) print(‘Estimated number of noise points: %d’ % n_noise_) Estimated number of clusters: 501 Estimated number of noise points: 1065
Визуализация
Большинство наших точек были сгруппированы в кластеры. Посмотрим, как это выглядит на практике:
# Black removed and is used for noise instead. fig = plt.figure(figsize=[100, 50]) ax = fig.add_subplot(111, projection=’3d’) unique_labels = set(labels) colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))] for k, col in zip(unique_labels, colors): if k == -1: # Black used for noise. col = [0, 0, 0, 1] class_member_mask = (labels == k) xyz = dataset[class_member_mask & core_samples_mask] ax.scatter(xyz[:, 0], xyz[:, 1], xyz[:, 2], c=col, marker=”.”) plt.title(‘Estimated number of cluster: %d’ % n_clusters_) plt.show()
Теперь ясно, что простое геометрическое рассмотрение и довольно стандартный метод кластеризации помогают нам упростить категоризацию точек с использованием умеренных вычислительных ресурсов.
При необходимости каждый кластер точек можно разделить на категории.
Полученные результаты
Наш эксперимент показал, что сочетание геометрических соображений и стандартных библиотек Python может привести к значительному сокращению усилий, необходимых для категоризации необработанных данных облака точек для дальнейшего использования.
Благодарности
Я хочу поблагодарить своих коллег Энди Босый, Николай Козленко, Владимира Сендецкого за их обсуждения, сотрудничество и полезные советы, а также за всю команде MindCraft.ai за постоянную поддержку.