Del 20 al 21 de octubre, se realizó el hackathon internacional
NASA Space Apps Challenge en Moscú. Sus organizadores en Rusia eran muchachos de la comunidad
rusa de hackers . Como parte del evento, se les pidió a los participantes que resolvieran 20 casos sobre varios temas: desde filmar una película sobre el hackathon hasta desarrollar aplicaciones de monitoreo y diseñar aviones autónomos. La lista completa de temas se puede estudiar por
referencia o en un
artículo sobre Habré .

Nuestro equipo "Space Monkeys", que incluía a Oleg Borodin (desarrollador front-end en el laboratorio Singularis), Vladislav Plotnikov (ingeniero de control de calidad en el laboratorio Singularis), Yegor Shvetsov, Dmitry Petrov, Yuri Bederov y Nikolai Denisenko, decidieron resolver el problema bajo el pegadizo titulado “¡Detecta ese fuego!”, que está redactado de la siguiente manera: “
Aplica crowdsourcing para que las personas puedan contribuir a la detección, confirmación y seguimiento de incendios forestales. La solución podría ser una aplicación móvil o web. "
Debido al hecho de que el equipo reunió a 5 desarrolladores con experiencia en el desarrollo para varias plataformas, se decidió de inmediato que el prototipo de nuestra aplicación se implementará para plataformas web y móviles.
¿Qué datos de la NASA usamos?
Aún así, el hackathon se llevó a cabo bajo los auspicios de la Administración Nacional de Aeronáutica y del Espacio, por lo que sería un error no utilizar datos abiertos de las despensas de la NASA. Además, de inmediato encontramos el conjunto de datos Active Fire Data que necesitábamos. Este conjunto de datos contiene información sobre las coordenadas de incendios en todo el mundo (puede descargar información sobre un continente específico). Los datos se actualizan todos los días (puede recibir datos durante 24 horas, 48 horas, 7 días).
El archivo contiene información sobre los siguientes campos: latitud, longitud, brillo, escaneo, seguimiento, fecha_aq, hora_aq, satélite, confianza, versión, bright_t31, frp, día, de los cuales utilizamos solo las coordenadas de los puntos de fuego (latitud y longitud).
El principio de la aplicación.
Dado que la aplicación es de crowdsourcing, idealmente debería ser utilizada por un gran número de usuarios. El principio de la aplicación es el siguiente:
El usuario, después de detectar un incendio, le toma una foto (con geoetiquetado) y lo carga utilizando el servicio. Las fotos con geoetiquetas y coordenadas de envío van al servidor de aplicaciones. La fotografía se puede descargar desde la versión web o móvil de la aplicación.
La foto resultante es procesada en el servidor por una red neuronal capacitada para confirmar que la foto está realmente en llamas. El resultado del guión es la precisión de la predicción, si> 0.7, entonces la foto realmente se dispara. De lo contrario, no registramos esta información y le pedimos al usuario que cargue otra foto.
Si el script de análisis de imágenes dio un resultado positivo, entonces las coordenadas de la geoetiqueta se agregan al conjunto de datos con todas las coordenadas. A continuación, se calculan las distancias entre el i- ésimo punto del conjunto de datos de la NASA y el punto del usuario. Si la distancia entre los puntos es ≤ 3 km, entonces el punto del conjunto de la NASA se agrega al diccionario. Entonces pasamos por todos los puntos. Después de eso, devuelva json con coordenadas que satisfagan la condición al lado del cliente de la aplicación. Si no se encuentran coordenadas por la condición dada, entonces devolvemos el único punto que recibimos del usuario.
Si el servidor devuelve una serie de puntos, la parte del cliente de la aplicación dibuja una zona de fuego en el mapa. En caso de que el servidor devuelva un punto, está marcado en el mapa con una etiqueta especial.
Pila de tecnología usada
Parte frontal de la aplicación web
La aplicación web, accesible desde un navegador, se centró en las pantallas de las computadoras y no era adaptativa, sin embargo, las tecnologías utilizadas permitieron refinar fácilmente este aspecto para dispositivos móviles. Utilizamos la siguiente pila de tecnología en el lado web:
Escenario de trabajo
El usuario abre la aplicación y ve su ubicación:

Inicialización del mapa y geo usuario:
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();
Si hay un incendio en el radio n (variable personalizada) de kilómetros, se mostrará en forma de polígono con un resumen de información adicional:

El usuario selecciona una ubicación de incendio en el mapa:

Configuración de etiqueta de fuego:
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); });
A continuación, el usuario carga una foto del incendio usando ng2-file-upload .
Como resultado de estas acciones, los siguientes datos se transfieren al servidor:
- coordenadas de usuario
- coordenadas del fuego especificado
- foto de fuego
El resultado de la aplicación es el resultado del reconocimiento.
Aplicaciones de aplicaciones móviles
Tecnologías utilizadas
- React native: un marco para desarrollar aplicaciones multiplataforma para iOS y Android
- Redux - control de flujo de datos de la aplicación
- Redux-saga - biblioteca que usa efectos secundarios en Redux
Escenario de trabajo
Selección de fotos de fuego

| Comentario del usuario

| Marca de fuego

|
Parte de fondo de la aplicación
Lenguaje de programación - JAVA 8
Plataforma en la nube: Microsoft Azure
Marco de aplicaciones web - Play Framework
Mapeo relacional de objetos - marco Ebean
El servidor tiene 2 scripts escritos en Python: predict.py y getZone.py, las siguientes bibliotecas de Python se instalaron para su trabajo:
- pandas - para procesamiento y análisis de datos
- geopandas - para trabajar con geodatos
- numpy - para trabajar con matrices multidimensionales
- matplotlib: para la visualización de datos de gráficos bidimensionales (2D) (también se admiten gráficos 3D)
- bien proporcionado: para la manipulación y el análisis de objetos geométricos planos.
API del servidor: fire.iconx.app/api
post /pictures {} return { id }
post /pictures/:id
Script predict.py
Una secuencia de comandos de entrada recibió una imagen, se realizó un simple preprocesamiento de la imagen (más sobre esto en la sección "Capacitación de modelos") y, según el archivo guardado con pesas, que también se encuentra en el servidor, se emitió una predicción. Si el modelo produce una precisión> 0.7, entonces el fuego es fijo, de lo contrario, no.
El script se ejecuta de manera clásica.
$ python predict.py image.jpg
Listado de código: 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))
Script getZone.py
La entrada al script son las coordenadas del punto que vino del lado del cliente de la aplicación. El guión ajusta todas las coordenadas de la NASA, agrega una nueva latitud y longitud a este archivo, sobrescribe el archivo original y comienza a buscar los puntos más cercanos. La distancia entre puntos se calcula utilizando la fórmula de Haversine .
Para hacer esto, la latitud y longitud de los puntos se convierten en radianes:
pt1_lon, pt1_lat, pt2_lon, pt2_lat = map(radians, [pt1_lon, pt1_lat, pt2_lon, pt2_lat])
Existen diferencias entre la latitud y la longitud para cada uno de los puntos:
d_lon = pt2_lon - pt1_lon d_lat = pt2_lat - pt1_lat
Todo esto se sustituye en la fórmula de haversina:
a = sin(d_lat/2)**2 + cos(pt1_lat) * cos(pt2_lat) * sin(d_lon/2)**2
Tomamos la raíz del resultado del cálculo, calculamos el arcoseno y multiplicamos el resultado por 2.
c = 2 * asin(sqrt(a))
La distancia será el producto del radio de la Tierra (6371 km) y el resultado del cálculo anterior.
Entrenamiento modelo
Para analizar la imagen de un incendio, necesitábamos un conjunto de fotos de entrenamiento con incendios. Las fotos fueron recolectadas por un script del sitio https://www.flickr.com/ y etiquetadas manualmente.
La descarga se realizó con FlikerAPI. El script realizó operaciones de preprocesamiento estándar con imágenes: recorte - cuadrado con centrado (relación 1: 1) y cambio de tamaño a formato 256 × 256.
Listado de código: 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
Naturalmente, la arquitectura convolucional de la red neuronal, en la que se utilizó el modelo pre-entrenado, se utilizó para trabajar con imágenes. La elección recayó (esperado) en MobileNet , porque:
- Ligero: es importante que el tiempo de respuesta de la aplicación sea mínimo.
- Rápido: es importante que el tiempo de respuesta de la aplicación sea mínimo.
- Exactamente: MobileNet predice con la precisión necesaria.
Después del entrenamiento, la red produjo una precisión de ~ 0.85.
Para construir el modelo, entrenamiento y predicción, se utilizó un montón de Keras + Tensorflow . El trabajo con datos se realizó a través de Pandas .
Dado que el DataSet de la NASA son datos geográficos, queríamos usar la biblioteca GeoPandas . Esta biblioteca es una extensión de las capacidades de Pandas para proporcionar métodos espaciales y operaciones en tipos geométricos. Las operaciones geométricas se implementan a través de la biblioteca bien proporcionada, funcionan con archivos (fiona, gráficos), matplotlib.
Después de pasar casi un día y medio para descubrir esta biblioteca, la abandonamos porque no pudimos encontrar dónde podría darnos una ventaja real al trabajar con ella. Nuestra tarea de calcular las coordenadas era muy pequeña, por lo que al final, todo se implementó de forma nativa.
Que sigue
Naturalmente, todo lo que obtuvimos como resultado es una aplicación extremadamente inestable y cruda, que tiene derecho a ser finalizada.
Hemos tenido éxito:
- Implemente prototipos de aplicaciones móviles y web que puedan tomar fotos (solo versión móvil), subirlas y enviarlas al servidor. Además, las coordenadas de envío con éxito llegan al servidor.
- En el servidor, fue posible implementar 2 scripts que implementan la lógica principal de la aplicación. Se organizó el flujo de datos de entrada a estos scripts y la recepción de datos de salida con el envío posterior a la parte del cliente.
- Implemente el verdadero "prototipo" de nuestra aplicación.
No logramos implementarlo, pero me gustaría resolver los siguientes problemas y agregar funciones (los elementos van de acuerdo con la prioridad de la tarea):
- Organice la grabación de todas las coordenadas desde el conjunto de datos a la base de datos para interactuar directamente con la base de datos.
- Organice la carga automática de un nuevo archivo desde el sitio web de la NASA, es decir. organizar actualizaciones automáticas diarias de coordenadas.
- Agregue notificaciones a los usuarios ubicados en el área cercana al fuego.
- Agregar registro (necesario para implementar el primer párrafo).
- Reescribe el algoritmo de cálculo de la zona de fuego.
- Resuelva tareas de diseño: brinde belleza a las versiones móvil y web de la aplicación.