для распознавания спутниковых изображений

Фон

U-Nets, несомненно, покорили область алгоритмов машинного обучения компьютерного зрения своей увлекательной архитектурой. Они не только визуально привлекательны и просты для понимания, но и демонстрируют феноменальную эффективность. Представьте себе огромную ценность, которую мы могли бы использовать, применяя эту архитектуру модели для решения широкого круга реальных проблем.

Проблема

Однако, как и многие проблемы в науке о данных, использование U-Net для решения более широкого круга проблем представляет собой серьезное препятствие: нехватка хорошо помеченных данных. Получение или создание тщательно аннотированного набора данных — сложная и трудоемкая задача. Уровень детализации, требуемый в этих помеченных наборах данных, часто требует недель, если не месяцев работы специально обученных экспертов, которые тщательно просматривают фотографии для пометки. Следовательно, первоначальная стоимость аннотирования данных ограничивает доступность обучающих наборов данных для U-Nets, ограничивая широкое использование модели.

Наше решение

В этой статье мы рассмотрим новый подход к преодолению этого ограничения за счет использования шейп-файлов, широко используемой и хорошо зарекомендовавшей себя структуры данных, в сочетании со спутниковыми изображениями. Заменив скудные помеченные фотографии и маски этим альтернативным подходом, мы можем расширить применимость моделей U-Net для решения сотен проблем, которые ранее были ограничены данными.

Шейп-файлы обычно используются профессионалами в области географических информационных систем (ГИС) для описания географических объектов на карте. Эти файлы многочисленны, и их относительно легко создать с помощью популярных программ, таких как ArcGIS или QGIS. Более того, в Интернете есть множество репозиториев с тысячами уже существующих шейп-файлов. Ярким примером является проект OpenStreetMap, в котором хранится одна из самых больших коллекций шейп-файлов.

Конкретный пример методологии

Чтобы проиллюстрировать эту методологию, давайте рассмотрим шейп-файл парковки, полученный из OpenStreetMap, содержащий контуры тысяч парковок по всей стране. Комбинируя этот шейп-файл со спутниковыми снимками соответствующих областей, мы можем создавать составные изображения и маски, подходящие для обучения моделей U-Net. В последующих разделах этой статьи будет подробно рассмотрен процесс интеграции этих компонентов, открывающий доступ ко множеству приложений U-Net.

Используя обилие шейп-файлов и богатство информации, предлагаемой спутниковыми снимками, мы можем обойти проблему получения тщательно помеченных фотографий. Этот инновационный подход не только расширяет универсальность U-Net, но и позволяет исследователям и практикам решать более широкий спектр реальных проблем.

Техническое описание

Первый шаг – подготовка данных. Мы прочитаем данные и отфильтруем парковки. Найдем центр пространственного объекта. Используйте библиотеку фолиума, чтобы подготовить снимок этого места со спутника.

Читать во всех библиотеках

import folium
import time
import pickle   
import matplotlib.pyplot as plt
import os
##Export the file
##https://nagasudhir.blogspot.com/2021/07/save-folium-map-as-png-image-using.htmlfrom selenium.webdriver.firefox.options import Options
from selenium import webdriver
import os

from selenium.webdriver.firefox.options import Options
import imageio as iio
import numpy as np
import geopandas as gpd
import numpy
print(numpy.version.version)

Создайте функцию для нормализации изображений, которые мы будем использовать позже.

def normalize(img):
    min = img.min()
    max = img.max()
    return 2.0 * (img - min) / (max - min) - 1.0

Читать в наборе данных OpenStreetMap

shapefile_full = gpd.read_file('data/gis_osm_traffic_a_free_1.shp')
shapefile_full.head()

На этом этапе я проведу вас через каждый этап подготовки спутникового снимка и файла маски отдельно для 1 местоположения. А позже мы объединим все это в цикл, чтобы создать целый семпл.

  1. На основе наших данных у нас есть полигональная геометрия парковки. Нам нужно преобразовать его в точку для подачи в библиотеку фолиума.
id='14574538'
st = time.time()
#get the item outof the dataset
shapefile_parking_one = shapefile_full[shapefile_full['osm_id'] == id]
df=shapefile_parking_one
df.crs
df = df.to_crs(epsg=4326)
##location
location=[df.geometry.centroid.y.mean(), df.geometry.centroid.x.mean()]

2. теперь готовим основное изображение с помощью фолиума, на основе этого блога

m_map= folium.Map(location, width=600,height=600, zoom_start=17, tiles='CartoDB positron')
#Map
#m = folium.Map(location,  zoom_start=20, tiles='CartoDB positron')
tile = folium.TileLayer( tiles = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',         attr = 'Esri',        name = 'Esri Satellite',        overlay = False,        control = False       ).add_to(m_map)

mapFname = 'map.html'
m_map.save(mapFname)
mapUrl = 'file://{0}\\{1}'.format(os.getcwd(), mapFname)
m_map

3. Теперь воспользуемся этой же библиотекой для добавления нашего пространственного объекта на карту и подготовки объединенного изображения. Я специально делаю его суперкрасным, чтобы позже можно было разделить его на маску.

#mask
m = folium.Map(location, width=600,height=600, zoom_start=17, tiles='CartoDB positron')

tile = folium.TileLayer( tiles = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',         attr = 'Esri',        name = 'Esri Satellite',        overlay = False,        control = False       ).add_to(m)

style = {"color": "red", "fillOpacity": 0.89}

for _, r in df.iterrows():
    # Without simplifying the representation of each borough,
    # the map might not be displayed
    sim_geo = gpd.GeoSeries(r['geometry']).simplify(tolerance=.5)
    geo_j = sim_geo.to_json()
    geo_j = folium.GeoJson(data=geo_j, style_function=lambda x:style)
    folium.Popup(r['name']).add_to(geo_j)
    geo_j.add_to(m)


maskFname = 'mask.html'
m.save(maskFname)
maskUrl = 'file://{0}\\{1}'.format(os.getcwd(), maskFname)
m

4. Теперь мы берем изображение и маску и экспортируем их с помощью драйвера Selenium Firefox. Это было немного сложно настроить, но оно отлично работает.

#### export map and mask
##### You can find both of these images in the folder of 

# download gecko driver for  from here - https://github.com/mozilla/geckodriver/releases

options = Options()
options.headless = True
##options = Options()
options.binary_location = r'C:\Program Files\Mozilla Firefox\firefox.exe'#### you may need to update this path
driver = webdriver.Firefox(options=options)



###Map_export_portion
# use selenium to save the html as png image
driver.get(mapUrl)
# wait for 5 seconds for the maps and other assets to be loaded in the browser
time.sleep(5)
driver.save_screenshot('map.png')

####mask_export_portion
driver.get(maskUrl)
# wait for 5 seconds for the maps and other assets to be loaded in the browser
time.sleep(5)
driver.save_screenshot('mask.png')
driver.quit()

Это создает файлы в вашем рабочем каталоге, которые вы можете использовать для своего кода распознавания изображений.

5. Теперь мы можем прочитать изображение маски и код Focus на красном слое и преобразовать его в бинарную маску. Последние несколько строк этого кода читаются на необработанном спутниковом снимке сайта.

img = iio.imread('mask.png')
##portion = china[120:250, 110:230]
img =img [0:600, 0:600]

r = img[:,:,0]
b = img[:,:,1]
g = img[:,:,2]
img_red=r>150 
   ## plt.imshow(img_red)
img_blue=      b>100
img_green=      g>100
   ## plt.imshow(img_blue)
img_other = (img_red.astype(np.float32) * img_blue.astype(np.float32)).astype(np.bool)
img_mask = (img_red.astype(np.float32) - img_other.astype(np.float32)).astype(np.bool)

plt.imshow(img_mask)
#import map 
img_map= iio.imread('map.png')
##portion = china[120:250, 110:230]
img_map =img_map [0:600, 0:600]
img_map=normalize(img_map)
plt.imshow(img_mask)

Наконец, вы можете выполнить все эти 5 шагов, объединить их вместе и создать цикл для просмотра каждой записи и добавления изображения и маски в два массива, которые впоследствии можно передать в нейронную сеть. Кроме того, я подмножил свой фрейм данных, чтобы получить 600 изображений только класса парковки.

###create a subset
shapefile_subset=shapefile_full[shapefile_full['fclass'] =="parking"].reset_index() 
shapefile_subset=shapefile_subset[:300]
len(shapefile_subset)
### running a loop on the above code to download a mask and a picture of every location

img_map_array=[]
img_mask_array=[]
st = time.time()

options = Options()
options.headless = True
##options = Options()
options.binary_location = r'C:\Program Files\Mozilla Firefox\firefox.exe'
driver = webdriver.Firefox(options=options)

for i in range(len(shapefile_subset)):
    print ([i])
    id=(shapefile_subset['osm_id'][i])
    #get the item outof the dataset
    shapefile_parking_one = shapefile_full[shapefile_full['osm_id'] == id]
    df=shapefile_parking_one
    df.crs
    df = df.to_crs(epsg=4326)
    
    ##location
    location=[df.geometry.centroid.y.mean(), df.geometry.centroid.x.mean()]
    m_map= folium.Map(location, width=600,height=600, zoom_start=20, tiles='CartoDB positron')
    #Map
    #m = folium.Map(location,  zoom_start=17, tiles='CartoDB positron')
    tile = folium.TileLayer( tiles = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',         attr = 'Esri',        name = 'Esri Satellite',        overlay = False,        control = False       ).add_to(m_map)
    
    mapFname = 'map.html'
    m_map.save(mapFname)
    mapUrl = 'file://{0}\\{1}'.format(os.getcwd(), mapFname)
    ###m_map
    
    #mask
    m = folium.Map(location, width=600,height=600, zoom_start=17, tiles='CartoDB positron')
    ###m = folium.Map(location,  zoom_start=20, tiles='CartoDB positron')
    tile = folium.TileLayer( tiles = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',         attr = 'Esri',        name = 'Esri Satellite',        overlay = False,        control = False       ).add_to(m)
    
    style = {"color": "red", "fillOpacity": 0.89}
    
    for _, r in df.iterrows():
        # Without simplifying the representation of each borough,
        # the map might not be displayed
        sim_geo = gpd.GeoSeries(r['geometry']).simplify(tolerance=.5)
        geo_j = sim_geo.to_json()
        geo_j = folium.GeoJson(data=geo_j, style_function=lambda x:style)
        folium.Popup(r['name']).add_to(geo_j)
        geo_j.add_to(m)
    
    
    maskFname = 'mask.html'
    m.save(maskFname)
    maskUrl = 'file://{0}\\{1}'.format(os.getcwd(), maskFname)
    ##m
    
    #### export map and mask

    
    
    # download gecko driver for  from here - https://github.com/mozilla/geckodriver/releases
    ###Map_export_portion
    # use selenium to save the html as png image
    driver.get(mapUrl)
    # wait for 5 seconds for the maps and other assets to be loaded in the browser
    time.sleep(5)
    driver.save_screenshot('map.png')
    
    ####mask_export_portion
    driver.get(maskUrl)
    # wait for 5 seconds for the maps and other assets to be loaded in the browser
    time.sleep(5)
    driver.save_screenshot('mask.png')
    
    ##convert mask into binary mask
    
    img = iio.imread('mask.png')
    ##portion = china[120:250, 110:230]
    img =img [0:600, 0:600]
    
    r = img[:,:,0]
    b = img[:,:,1]
    g = img[:,:,2]
    img_red=r>150 
       ## plt.imshow(img_red)
    img_blue=      b>100
    img_green=      g>100
       ## plt.imshow(img_blue)
    img_other = (img_red.astype(np.float32) * img_blue.astype(np.float32)).astype(np.bool)
    img_mask = (img_red.astype(np.float32) - img_other.astype(np.float32)).astype(np.bool)
    
    plt.imshow(img_mask)
    
    #import map 
    img_map= iio.imread('map.png')
    ##portion = china[120:250, 110:230]
    img_map =img_map [0:600, 0:600]
    img_map=normalize(img_map)
    plt.imshow(img_map)

    
    
    ### reshape the mask
    
    mask_final=[]
    img_mask0=img_mask*-1+1
    img_mask1=img_mask.astype(int)
    mask_final.append(img_mask0)
    mask_final.append(img_mask1)
    mask_final = np.array(mask_final)
    mask_final=mask_final.transpose([1, 2, 0])
    
    img_map_array.append(img_map)
    img_mask_array.append(mask_final)
et = time.time()
elapsed_time = et - st
print('loop time:', elapsed_time, 'seconds')    
driver.quit()
img_map_array=np.array(img_map_array)
img_mask_array=np.array(img_mask_array)

Эта статья в основном посвящена созданию набора данных выше. Но чтобы завершить рабочий процесс, я добавил простую модель U-net ниже, чтобы показать вам, как можно использовать эти данные.

Построить модель

Вы можете отключить сеть или шейп-файл и принять этот алгоритм для собственного использования. Давайте сначала настроим библиотеки, которые нам понадобятся для этой модели.

###from tensorflow import keras
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, concatenate, Conv2DTranspose
from tensorflow.keras.optimizers import Adam
from keras.callbacks import ModelCheckpoint
from keras.callbacks import CSVLogger
from keras.callbacks import TensorBoard

Создайте модель

## Create the model
def simple_unet_model(n_classes=2, im_sz=600, n_channels=4, n_filters_start=4, growth_factor=2):
    # Creating network model using functional API:
    n_filters = n_filters_start
    inputs = Input((im_sz, im_sz, n_channels))
    conv1 = Conv2D(n_filters, (3, 3), activation='relu', padding='same')(inputs)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
    n_filters *= growth_factor  # increase number of filters when going down the U-Net
    conv2 = Conv2D(n_filters, (3, 3), activation='relu', padding='same')(pool1)
    n_filters //= growth_factor  # decrease number of filters when going up the U-Net
    upconv = Conv2DTranspose(n_filters, (2, 2), strides=(2, 2), padding='same')(conv2)
    concat = concatenate([conv1, upconv])
    conv3 = Conv2D(n_filters, (3, 3), activation='relu', padding='same')(concat)
    output = Conv2D(n_classes, (1, 1), activation='sigmoid')(conv3)
    model = Model(inputs=inputs, outputs=output)
    # Compiling model with ADAM optimizer and logloss (aka binary crossentropy) as loss function
    model.compile(optimizer=Adam(), loss='binary_crossentropy')
    return model

model = simple_unet_model()
model.summary()

Создайте Holdout и обучите модель и взгляните на наши показатели производительности:

x_train=img_map_array[0:200]
x_test=img_map_array[200:]
y_train=img_mask_array[0:200]
y_test=img_mask_array[200:]
x_train.shape
# Now training the model:
st = time.time()
N_EPOCHS =50
BATCH_SIZE = 32
# ask Keras to save best weights (in terms of validation loss) into file:
model_checkpoint = ModelCheckpoint(filepath='weights_simple_unet.hdf5', monitor='val_loss', save_best_only=True)
# ask Keras to log each epoch loss:
csv_logger = CSVLogger('log.csv', append=True, separator=';')
# ask Keras to log info in TensorBoard format:
tensorboard = TensorBoard(log_dir='tensorboard_simple_unet/', write_graph=True, write_images=True)
# Fit:
model.fit(x_train, y_train, batch_size=BATCH_SIZE, epochs=N_EPOCHS,
          verbose=2, shuffle=True,
          callbacks=[model_checkpoint, csv_logger, tensorboard],
          validation_data=(x_test, y_test))

et = time.time()
elapsed_time = et - st
print('loop time:', elapsed_time, 'seconds')   

Полученные результаты

Созданная нами модель U-сети имеет высокую точность только после 50 эпох. Мы сравнили несколько изображений с масками и предсказаниями. Первое, что мы заметили, это то, что маски не всегда хорошо очерчивают полную парковку. Мы можем превратить наши шейп-файлы в маски, но выбранный нами шейп-файл — не лучший пример контуров парковки. Прогнозы довольно хорошо выделяют светлый бетон, поэтому мы часто ошибочно классифицируем дороги как парковки, но это понятно. Мы можем использовать точно такую ​​же методологию и, надеюсь, с лучшими данными создадим еще лучшие прогнозы для парковок или любого другого шейп-файла, к которому вы можете применить это.

В заключение, объединение шейп-файлов и спутниковых изображений с моделями U-Net открывает новые горизонты в компьютерном зрении. Сочетание легкодоступных географических данных и изображений дистанционного зондирования обеспечивает жизнеспособную альтернативу трудоемкому процессу аннотирования данных. В результате теперь мы можем решить множество проблем, которые раньше сдерживались нехваткой наборов данных с тегами. Благодаря этой новообретенной возможности потенциал U-Net для революции в различных областях безграничен, что делает их незаменимым инструментом в арсенале как специалистов по данным, так и энтузиастов машинного обучения.