Как мы писали приложение на хакатоне NASA Space Apps Challenge

    20 — 21 октября в Москве проходил международный хакатон NASA Space Apps Challenge. Его организаторами в России выступили ребята из сообщества Russian.Hackers. В рамках мероприятия участникам было предложено решить 20 кейсов по различным тематикам: от съемки фильма о хакатоне до разработки приложений мониторинга и проектирования автономных летательных аппаратов. Полный список тем можно изучить по ссылке или в статье на Хабре.

    Наша команда “Space Monkeys”, в которую входили Олег Бородин (Front-end developer в Singularis lab), Владислав Плотников (QA engineer в Singularis lab), Егор Швецов, Дмитрий Петров, Юрий Бедеров и Николай Денисенко, выбрала для решения проблему под броским названием “Spot that fire!”, которая сформулирована следующим образом: “Применить краудсорсинг, чтобы люди могли вносить свой вклад в обнаружение, подтверждение и отслеживание лесных пожаров. Решением может быть мобильное или веб приложение.

    В силу того, что в команде было собрано 5 разработчиков с опытом разработки под различные платформы, сразу же было решено, что прототип нашего приложения будет реализовываться под Web и Mobile платформы.

    Какие данные NASA мы использовали?


    Все-таки хакатон проводился под эгидой Национального управления по аэронавтике и исследованию космического пространства, поэтому не использовать открытые данные из кладовых NASA было бы неправильно. Кроме того, мы сразу же нашли нужный нам датасет Active Fire Data. Данный датасет содержит в себе информацию о координатах пожаров по всему миру (можно скачать информацию по конкретному континенту). Данные обновляются каждые сутки (можно получать данные за 24 часа, 48 часов, 7 дней).


    Файл содержит информацию по следующим полям: latitude,longitude, brightness, scan, track, acq_date, acq_time,satellite, confidence, version, bright_t31, frp, daynight, из которых мы использовали только координаты точек пожаров (latitude и longitude).


    Принцип работы приложения


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


    1. Пользователь, обнаружив пожар, фотографирует его (с геометкой) и загружает с помощью сервиса. Фото с геометкой и координаты отправки уходят на сервер приложения. Фотография может загружаться с Web или Mobile версии приложения.

    2. Полученная фотография обрабатывается на сервере обученной нейросетью для подтверждения того, что на фотографии действительно пожар. Результат выполнения скрипта — точность предсказания, если >0.7, то на фото действительно пожар. Иначе не фиксируем данную информацию и просим пользователя загрузить другую фотографию.

    3. В случае, если скрипт анализа картинки дал положительный результат, то координаты из геометки добавляются в датасет со всеми координатами. Далее рассчитываются расстояния между i-ой точкой из датасета NASA и точкой от пользователя. Если расстояние между точками 3 км, то точка из сета NASA добавляется в словарь. Так проходим по всем точкам. После этого на клиентскую часть приложения возвращаем json с координатами, удовлетворяющими условию. Если координат, находящихся по заданному условию, не нашлось, то возвращаем обратно единственную точку, которую мы получили от пользователя.

    4. Если сервер возвращает массив точек, то клиентская часть приложения отрисовывает зону пожара на карте. В случае, когда сервер вернул одну точку, она отмечается на карте специальной меткой.


    Используемый стек технологий


    Front-end часть Web-приложения


    Web-приложение, доступное из браузера, ориентировалось на экраны компьютеров, и не было адаптивно, однако, используемые технологии легко позволяли доработать этот аспект и для мобильных устройств. Мы использовали следующий стек технологий на web-стороне:


    • фреймворк Angular 6 от компании Google на языке TypeScript
    • CSS&JS фреймворк Materialize
    • модуль для загрузки файлов ng2-file-upload
    • карты OpenStreetMap, библиотека Leaflet

    Сценарий работы


    Пользователь открывает приложение и видит своё расположение:




    Инициализация карты и геометки пользователя:


    this.map = L.map('map').setView([latitude, longitude], 17);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '& copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          }).addTo(this.map);
    
    L.circle([latitude, longitude]).addTo(this.map)
            .bindPopup('You are here')
            .openPopup();
    

    Если в радиусе n (настраиваемая переменная) километров есть пожар, то он будет отображен в виде полигона со сводкой дополнительной информации:




    Пользователь выбирает место пожара на карте:




    Установка метки пожара:


    let marker; 
    this.map.on('click', function (e) {
           if (marker) {
              self.map.removeLayer(marker);
            }
            marker = L.circle([e.latlng.lat, e.latlng.lng], { 
              color: 'red',
              fillColor: '#f03',
              fillOpacity: 0.5,
              radius: 15
            }).addTo(self.map)
              .bindPopup('Метка пожара')
              .openPopup();
            self.appService.coordinatesStorage.latitude = e.latlng.lat;
            self.appService.coordinatesStorage.longitude = e.latlng.lng;
            console.log('fire', self.appService.coordinatesStorage); 
          });
    

    Далее пользователь загружает фото пожара с помощью ng2-file-upload.


    В результате этих действий серверу передаются следующие данные:


    • координаты пользователя
    • координаты указанного пожара
    • фото пожара

    Выходными данными приложения является результат распознавания.



    Mobile-app приложения


    Используемые технологии


    • React native — фреймворк для разработки кроссплатформенных приложений для iOS и Android
    • Redux — управление потоком данных в приложениях
    • Redux-saga — библиотека использующая side эффекты в Redux

    Сценарий работы


    Выбор фото пожара


    Комментарий от пользователя


    Метка для пожара



    Back-end часть приложения


    • Язык программирования — JAVA 8

    • Облачная платформа — Microsoft Azure

    • Web application framework — Play Framework

    • Object-relational mapping — Ebean framework


    На сервере располагаются 2 скрипта, написанные на Python: predict.py и getZone.py, для их работы были установлены следующие Python-библиотеки:


    • pandas — для обработки и анализа данных
    • geopandas — для работы с геоданными
    • numpy — для работы с многомерными массивами
    • matplotlib — для визуализации данных двумерной (2D) графикой (3D графика также поддерживается)
    • shapely — для манипулирования и анализа плоских геометрических объектов.

    API сервера: fire.iconx.app/api


    • загрузка координат

    post /pictures {}
    return { id }
    

    • загрузка картинки

    post /pictures/:id
    

    Скрипт predict.py


    Скрипт на вход получал картинку, происходил простой препроцессинг картинки (о нем подробнее в блоке “Обучение модели”) и на основе сохраненного файла с весами, который также находится на сервере, выдавалось предсказание. Если модель выдала точность > 0.7, то пожар фиксируется, иначе — нет.


    Скрипт запускается классическим образом

    $ python predict.py image.jpg 

    Листинг кода:
    import keras
    import sys
    from keras.layers import Dense
    from keras.models import model_from_json
    from sklearn.externals import joblib
    from PIL import Image
    import numpy as np
    from keras import models, layers, optimizers
    from keras.applications import MobileNet
    from keras.models import Sequential
    from keras.layers import Dense, Dropout, Flatten
    from keras.layers import Conv2D, MaxPooling2D
    
    def crop_resize(img_path, img_size_square): 
        # Get dimensions      
        mysize = img_size_square
        image = Image.open(img_path) 
        width, height = image.size
        # resize
        if (width and height) >= img_size_square:
            if width > height:
                wpercent = (mysize/float(image.size[1]))
                vsize = int((float(image.size[0])*float(wpercent)))
                image = image.resize((vsize, mysize), Image.ANTIALIAS)
            else:
                wpercent = (mysize/float(image.size[0]))
                hsize = int((float(image.size[1])*float(wpercent)))
                image = image.resize((mysize, hsize), Image.ANTIALIAS)
            # crop
            width, height = image.size
            left = (width - mysize)/2
            top = (height - mysize)/2
            right = (width + mysize)/2
            bottom = (height + mysize)/2
            image=image.crop((left, top, right, bottom))
      
            return image
    
    conv_base = MobileNet(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
    
    def build_model():
        model = models.Sequential()
        model.add(conv_base)
        model.add(layers.Flatten())
        model.add(layers.Dense(256, activation='relu'))
        model.add(layers.Dense(64, activation='relu'))
        model.add(layers.Dense(1, activation='sigmoid'))
        model.compile(loss='binary_crossentropy',
        optimizer=optimizers.RMSprop(lr=2e-5),
        metrics=['acc'])
        return model
    image=crop_resize(sys.argv[1],224)
    image = np.reshape(image,[1,224,224,3])
    
    #Loading models and text processing
    model = build_model()
    print('building a model')
    model.load_weights('./models/mobile_weights.h5')
    print('model loaded')
    pred_cat=model.predict(image)
    if pred_cat > 0.7:
      print('fire {}'.format(pred_cat))
    else: print('no fire {}'.format(pred_cat))
    



    Скрипт getZone.py


    Входными данными скрипта служат координаты точки, пришедшие с клиентской части приложения. Скрипт подтягивает все координаты от NASA, добавляет в этот файл новую широту и долготу, перезаписывает исходный файл и начинает искать ближайшие точки. Расстояние между точками считается по формуле гаверсинуса (англ. Haversine formula).


    Для этого широта и долгота точек переводятся в радианы:


    pt1_lon, pt1_lat, pt2_lon, pt2_lat = map(radians, [pt1_lon, pt1_lat, pt2_lon, pt2_lat])
    

    Находятся разности между широтой и долготой для каждой из точек:


    d_lon = pt2_lon - pt1_lon
    d_lat = pt2_lat - pt1_lat
    

    Все это подставляется в формулу гаверсинуса:


    a = sin(d_lat/2)**2 + cos(pt1_lat) * cos(pt2_lat) * sin(d_lon/2)**2
    

    Берем корень от результата вычислений, вычисляем арксинус и умножаем результат на 2.


    c = 2 * asin(sqrt(a))
    

    Расстоянием будет произведение радиуса Земли (6371 км) на результат предыдущего вычисления.


    Обучение модели


    Для анализа картинки на предмет пожара нам потребовался тренировочный набор фотографий с пожарами. Фотографии собирались скриптом с сайта https://www.flickr.com/ и размечались вручную.


    Скачивание происходило при помощи FlikerAPI. В скрипте производились стандартные операции препроцессинга с картинками: кадрирование — квадратное с центровкой (ratio 1: 1), и изменение размеров до формата 256 × 256.


    Листинг кода:
    import flickrapi
    import urllib.request
    from PIL import Image
    import pathlib
    import os
    from tqdm import tqdm
    # Flickr api access key
    flickr=flickrapi.FlickrAPI('your API key', 'your secret key', cache=True)
    def get_links():
        search_term = input("Input keywords for images: ")
        keyword = search_term
        max_pics=2000
    
        photos = flickr.walk(text=keyword,
                             tag_mode='all',
                             tags=keyword,
                             extras='url_c',
                             per_page=500, # mb you can try different numbers..
                             sort='relevance')
        urls = []
        for i, photo in enumerate(photos):
            url = photo.get('url_c')
            if url is not None:
                urls.append(url)
            if i > max_pics:
                break
        num_of_pics=len(urls)
        print('total urls:',len(urls)) # print number of images available for a keywords
        return urls, keyword, num_of_pics
    #resizing  and cropping output images will be besquare
    def crop_resize(img_path, img_size_square):
        # Get dimensions
        mysize = img_size_square
        image = Image.open(img_path)
        width, height = image.size
        # resize
        if (width and height) >= img_size_square:
            if width > height:
                wpercent = (mysize/float(image.size[1]))
                vsize = int((float(image.size[0])*float(wpercent)))
                image = image.resize((vsize, mysize), Image.ANTIALIAS)
            else:
                wpercent = (mysize/float(image.size[0]))
                hsize = int((float(image.size[1])*float(wpercent)))
                image = image.resize((mysize, hsize), Image.ANTIALIAS)
            # crop
            width, height = image.size
            left = (width - mysize)/2
            top = (height - mysize)/2
            right = (width + mysize)/2
            bottom = (height + mysize)/2
            image=image.crop((left, top, right, bottom))
            return image
    def download_images(urls_,keyword_, num_of_pics_):
        num_of_pics=num_of_pics_
        keyword=keyword_
        urls=urls_
        i=0
        base_path='./flickr_data/' # your base folder to save pics
        for item in tqdm(urls):
            name=''.join([keyword,'_',str(i),'.jpg'])
            i+=1
            keyword_=''.join([keyword,'_',str(num_of_pics)])
            dir_path= os.path.join(base_path,keyword_)
            file_path=os.path.join(dir_path,name)
            pathlib.Path(dir_path).mkdir(parents=True, exist_ok=True)
            urllib.request.urlretrieve(item, file_path)
            resized_img=crop_resize(file_path, 256) #set output image size
            try:
                resized_img.save(file_path)
            except:
                pass
    urls, keyword, num_of_pics =get_links()
    continue = input("continue  or try other keywords (y,n): ")
    if continue =='y':
        download_images(urls, keyword, num_of_pics)
    elif continue =='n':
        get_links()
    else:
        pass
    


    Естественно, для работы с картинками использовалась сверточная архитектура нейронной сети, в которой использовалась предобученная модель. Выбор пал (ожидаемо) на MobileNet, потому что:


    • Легковесно — важно, чтобы время отклика приложения было минимально.
    • Быстро — важно, чтобы время отклика приложения было минимально.
    • Точно — MobileNet предсказывает с необходимой точностью.

    После обучения сеть выдавала точность ~ 0.85.


    Для построения модели, обучения и предсказания использовалась связка Keras + Tensorflow. Работа с данными осуществлялась через Pandas.


    Так как NASA DataSet представляет собой географические данные, то мы захотели использовать библиотеку GeoPandas. Данная библиотека — расширение возможностей Pandas для обеспечения пространственных методов и операция над геометрическими типами. Геометрические операции реализуются через библиотеку shapely, работа с файлами — fiona, построение графиков — matplotlib.


    Потратив почти полтора дня на то, чтобы разобраться с этой библиотекой, мы отказались от нее, потому что не смогли найти, в чем она может дать нам реальное преимущество от работы с ней. Наша задача расчета координат была совсем небольшой, поэтому в итоге все реализовали нативно.


    Что дальше?


    Естественно, все, что мы получили в итоге — крайне нестабильное и сырое приложение, которое имеет право на то, чтобы его доработали.


    У нас получилось:


    1. Реализовать прототипы мобильного и Web-приложений, которые были способны делать фото (только для мобильной версии), загружать и отправлять их на сервер. Также на сервер успешно приходят координаты отправки.
    2. На сервере удалось развернуть 2 скрипта, которые реализуют основную логику приложения. Была налажена подача данных на вход этим скриптам и получение выходных данных с последующей отправкой на клиентскую часть.
    3. Реализовать самый настоящий “прототип” нашего приложения.

    У нас не получилось реализовать, но хотелось бы решить следующие проблемы и добавить фичи (пункты идут в соответствии с приоритетом задачи):


    1. Организовать запись всех координат из датасета в базу данных, чтобы взаимодействовать напрямую с БД.
    2. Организовать автоматическую подгрузку нового файла с сайта NASA, т.е. организовать автоматическое ежедневное обновление координат.
    3. Добавить нотификацию пользователей, находящихся в зоне, близкой к пожару.
    4. Добавить регистрацию (необходимо для реализации первого пункта).
    5. Переписать алгоритм расчета зоны пожара.
    6. Решить дизайнерские задачи — навести красоту в мобильной и веб-версии приложения.

    Singularis

    60,00

    Компания

    Поделиться публикацией
    Комментарии 1
      0
      Есть одна идея… Если вы обнаружили пожар, то необходимо оповестить о нём пожарную службу. Оповещать можно только живых работников. Зачем нейросеть, если она повторяет работу пожарника и не даёт никакой новой информации? Значит, этот процесс можно вычеркнуть.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое