Несколько дней назад я наткнулся на yt video, в котором обсуждалась программа ESA Copernicus, европейская инициатива по наблюдению за Землей через созвездие спутников. Это созвездие состоит из множества спутников, называемых Sentinels, которые ежедневно сканируют Землю по различным параметрам.
Что меня удивило, так это тот факт, что все полученные данные открыты для общественности и обновляются каждый день, так что любой может скачать и использовать его.

Итак, в довольно унылый понедельник я приступил к делу: создать что-то, используя эти данные. Учитывая мою любовь к кораблям, я решил создать что-то связанное с кораблями — программу для загрузки и распознавания кораблей с помощью машинного обучения.

Поскольку изображения Sentinel-2 имеют наилучшее разрешение 1 пиксель x 10 метров, я решил обнаруживать только движущиеся корабли, потому что их следы легче обнаружить и с меньшей вероятностью они приведут к ложному срабатыванию.

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

Шаг 1: Получение данных

Первое, что нужно сделать, это программно загрузить данные. В качестве источника данных я выбрал спутники Sentinel-2, которые каждые пять дней охватывают всю Землю и предлагают готовое к использованию изображение True Color (TCI) в формате jp2 (jpeg 2000).

Загрузка данных из Copernicus проста. Просто зарегистрируйтесь на https://scihub.copernicus.eu, выберите область и скачайте. Использовать код также легко, как видно из следующего фрагмента.

import fnmatch
from sentinelsat import SentinelAPI, make_path_filter

sapi = SentinelAPI(SUSER, SPASSWORD, 'https://scihub.copernicus.eu/dhus/')

# Query scihub for Sentinel2 data from the last 24h
p = sapi.query(date=('NOW-1DAYS', 'NOW'), area_relation="Intersects", platformname='Sentinel-2',
     processinglevel='Level-2A')

# Create a pathfilter for downloading only TCI at 10m resolution
def path_filter(a):
 npath = a['node_path']
 return fnmatch.fnmatch(npath, '*_TCI_10m.jp2')

# Download only if the cloud coverage is less than 30%
for key, value in p.items():
 cov = value['cloudcoverpercentage']
 if cov > 30.:
  print ('skipping for cloud coverage', size, cov)
  continue
 sapi.download(key, './tempdataset', nodefilter=path_filter)

Как только вы запустите его, у вас будут изображения jp2 из Sentinel-2 за последний день.

Шаг 2: Создание обучающего набора данных

Для этого шага я использовал QGIS, который поддерживает растры JP2. Сначала откройте файл jp2, создайте слой полигонального шейп-файла, переключитесь в режим редактирования и нажмите «Добавить полигон». Теперь нам нужно выделить границу корабля (включая кильватер); найти их проще, чем вы думаете.

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

Шаг 3: Обучение и проверка

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

Для проверки концепции я использовал его в качестве основы следующим образом: я определил две сцены, одну для обучения, а другую для проверки результатов. Затем я создал все конфигурации для обучения, вернув объект ObjectDetectionConfig.

from os.path import join, dirname

from rastervision.core.rv_pipeline import (ObjectDetectionConfig,
                                           ObjectDetectionChipOptions,
                                           ObjectDetectionPredictOptions)
from rastervision.core.data import (
    ClassConfig, ObjectDetectionLabelSourceConfig, GeoJSONVectorSourceConfig,
    RasterioSourceConfig, SceneConfig, DatasetConfig, ClassInferenceTransformerConfig)
from rastervision.pytorch_backend import PyTorchObjectDetectionConfig
from rastervision.pytorch_learner import (
    Backbone, SolverConfig, ObjectDetectionModelConfig,
    ObjectDetectionImageDataConfig, ObjectDetectionGeoDataConfig,
    ObjectDetectionGeoDataWindowConfig, GeoDataWindowMethod)

import os


def get_config(runner, data_uri='./dataset', full_train=True, nochip=False):
    def get_path(part):
        return join(data_uri, part)

    class_config = ClassConfig(
        names=['ship'], colors=['red'])

    def make_scene(scene_id, img_path, label_path):
        raster_source = RasterioSourceConfig(
            channel_order=[0, 1, 2], uris=[img_path])
        label_source = ObjectDetectionLabelSourceConfig(
            vector_source=GeoJSONVectorSourceConfig( 
                uri=label_path, 
                ignore_crs_field=True,
                transformers=[
                    ClassInferenceTransformerConfig(default_class_id=0)
                ])
        )
        return SceneConfig(
            id=scene_id,
            raster_source=raster_source,
            label_source=label_source)

    chip_sz = 300
    img_sz = chip_sz

    scenes = [
        make_scene('od_test', get_path('t1.jp2'),
                   get_path('t1.geojson')),
        make_scene('od_test-2', get_path('t2.jp2'),
                   get_path('t2.geojson'))
                   
    ]
    scene_dataset = DatasetConfig(
        class_config=class_config,
        train_scenes=scenes[0:1],
        validation_scenes=scenes[1:])

    chip_options = ObjectDetectionChipOptions(neg_ratio=1.0, ioa_thresh=1.0)

    if nochip:
        window_opts = ObjectDetectionGeoDataWindowConfig(
            method=GeoDataWindowMethod.sliding,
            stride=chip_sz,
            size=chip_sz,
            neg_ratio=chip_options.neg_ratio,
            ioa_thresh=chip_options.ioa_thresh)

        data = ObjectDetectionGeoDataConfig(
            scene_dataset=scene_dataset,
            window_opts=window_opts,
            img_sz=img_sz,
            augmentors=[])
    else:
        data = ObjectDetectionImageDataConfig(img_sz=img_sz, augmentors=[])

    model = ObjectDetectionModelConfig(backbone=Backbone.resnet18)
    solver = SolverConfig(
            lr=1e-4,
            num_epochs=12,
            batch_sz=8,
            one_cycle=True,
            sync_interval=300)
    backend = PyTorchObjectDetectionConfig(
        data=data,
        model=model,
        solver=solver,
        log_tensorboard=True,
        run_tensorboard=True)

    predict_options = ObjectDetectionPredictOptions(
        merge_thresh=0.1, score_thresh=0.5)

    return ObjectDetectionConfig(
        root_uri='.',
        dataset=scene_dataset,
        backend=backend,
        train_chip_sz=chip_sz,
        predict_chip_sz=chip_sz,
        chip_options=chip_options,
        predict_options=predict_options)

И, наконец, я запускаю его с помощью:

rastervision run sentiship.py

После 12 эпох обучения он уже мог обнаруживать корабли — легко!

Двигаясь вперед, я очень хочу изучить, как использовать эту модель в дальнейшем. Одна интригующая возможность, которая приходит на ум, — использовать его для обнаружения «кораблей-призраков» — судов, у которых нет автоматической системы идентификации (АИС). Эти неуловимые суда, часто занятые незаконной деятельностью или брошенные, создают серьезные проблемы для морских властей. Используя возможности машинного обучения и огромное количество данных, предоставляемых программой Copernicus, мы сможем пролить больше света на эти морские тайны. Следите за обновлениями, пока я углубляюсь в это захватывающее приключение!

Репозиторий Github: https://github.com/dakk/sentiship

Твиттер: @dagide