Данные лидара для обнаружения линии электропередачи

Вступление

В последние годы был достигнут значительный прогресс в разработке детекторов 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 за постоянную поддержку.