#!/usr/bin/env python
# coding: utf-8
# # Flood Impact Mapping with UP42
# In the following tutorial we will map the impact of the flooding in urban area using UP42 python SDK and OpenStreetMap data.
# This notebook is intended to show how your existing GIS analysis and workflows can seemlessly be integrated with UP42 Python SDK in a few lines of code.
# The notebook is divided in following sections:
# 1. Download Sentinel-2 AOI clipped GeoTiff with [Sentinel-2 L1C MSI AOI clipped data block](https://marketplace.up42.com/block/3a381e6b-acb7-4cec-ae65-50798ce80e64)
# 2. Calculate Modified Normalized Water Index (MNDWI)
# 3. Convert MNDWI raster mask to vector mask
# 4. Extract building footprints polygons from OSM using [UP42 OSM Data Block](https://marketplace.up42.com/block/df2ec03a-50c4-47ac-8a83-2db613869cf9)
# 5. Plot the impacted buildings with Folium
# In[1]:
# imports
import os
from functools import partial
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pyproj
import rasterio as rio
from rasterio import features
from rasterio.plot import reshape_as_raster, show
from shapely.geometry import LineString, MultiPolygon, Point, Polygon, box
from shapely.geometry import shape as shapely_shp
from shapely.ops import cascaded_union, transform
import folium
import up42
# In[2]:
# allows for ignoring errors + division by zero
np.seterr(divide='ignore', invalid='ignore')
# Set-up data directory to work with. This is optional though. The SDK creates a directory in the project folder for you!
# In[3]:
sentinel_dir = "./results/sentinel/"
# ## Download data from UP42 platform
# In[4]:
# authenticate
up42.authenticate(cfg_file="flood_config.json")
# In[5]:
project = up42.initialize_project()
# init workflow
workflow = project.create_workflow(name="flooding_sentinel")
# Following will fetch the data from UP42 platform corresponding the AOI and parameters we passed into the function.
# The area of interest is based on 2019 flooding events in Mid-West, USA in 2019. [Bellevue](https://goo.gl/maps/8UTHGfrLp8KuKk617)
# In[6]:
input_tasks = ['sentinelhub-s2-aoiclipped']
# Update workflow object with our desired data block as input_task(s)
workflow.add_workflow_tasks(input_tasks=input_tasks)
# read aoi
aoi = up42.read_vector_file("data/aoi_bellevue_US.geojson", as_dataframe=True)
# construct input parameters
input_parameters = workflow.construct_parameters(geometry=aoi,
geometry_operation="contains",
start_date="2019-03-21",
end_date="2019-03-21",
limit=1)
# run the actual job
job = workflow.run_job(input_parameters=input_parameters, track_status=True)
# In[7]:
# download results and quicklooks to results/sentinel/ folder in current directory
job.download_results(sentinel_dir)
job.download_quicklooks(sentinel_dir)
# You should see a new folder named `result/sentinel/` being created in the current working directory with `.tif` and meatadata file `data.json`. Additionally, we downloaded quicklooks `.jpg` with `job.download_quicklooks()`. This will come handy later while visualising on a folium map.
# > **NOTE:** The SDK automatically creates the download directory if not already exists.
# In[8]:
# store results and quicklook paths to separate variables
raster_path = [i for i in job.results if i.endswith('.tif')][0]
metadata_path = [i for i in job.results if i.endswith('.json')][0]
# In[9]:
# similarly store path for quicklook
ql_path = job.quicklooks[0]
# Now, that we have downloaded the necessary data let's move on to the next steps!
# In[10]:
raster_path, metadata_path
# ## Calculate MNDWI
# The Modified Normalized Difference Water Index (MNDWI) uses green and SWIR bands for the enhancement of open water features. It also diminishes built-up area features that are often correlated with open water in other indices.
#
# MNDWI = (Green - SWIR) / (Green + SWIR)
#
# - Green = pixel values from the green band
# - SWIR = pixel values from the short-wave infrared band
# > **Reference**: http://space4water.org/taxonomy/term/1246
# In[11]:
def normalize(array):
"""Normalizes numpy arrays into scale 0.0 - 1.0"""
array_min, array_max = array.min(), array.max()
return ((array - array_min)/(array_max - array_min))
# In[12]:
def calc_mndwi(green_band, swir_band):
mndwi = (green_band - swir_band) / (green_band + swir_band)
return mndwi.astype(np.float32)
# In[13]:
with rio.open(raster_path) as src:
green = src.read(3)
swir = src.read(11)
# normalize band arrays
green_n = normalize(green)
swir_n = normalize(swir)
# get bounds,crs and transform
src_bounds = src.bounds
src_crs = src.crs
src_transform = src.transform
# compute MNDWI
mndwi = calc_mndwi(green_band=green_n, swir_band=swir_n)
# Let's quickly plot and see how it looks like
# In[14]:
plt.figure(figsize=(10, 7))
plt.imshow(mndwi, cmap='terrain_r')
# Add colorbar to show the index
plt.colorbar();
# As we can see the water area is depicted in blue where as land area is brown(-ish) color
# ## Create Vector Mask
# Now that we have computed MNDWI, next step is to isolate boundaries of flooded area from whole image.
# This means,
# - Apply a deterministic decision boundary / threshold to NDWI values
# - Translate the threshold values into boolean value mask (1/0)
# - Convert raster into vector boundary (multipolygon)
# According the [NDWI-wikipedia](https://en.wikipedia.org/wiki/Normalized_difference_water_index):
#
# > For the second variant of the NDWI, another threshold can also be found in that avoids creating false alarms in urban areas:
# > - < 0.3 - Non-water
# > - >= 0.3 - Water
# Meaning all the values that are `>= 0.3` will be mapped to value `true` and `< 0.3` to `false` respectively.
# In[15]:
# create numpy mask
mndwi_msk = (mndwi >= 0.3)
# ### Pixel to shapely geom
# In[16]:
mypoly=[]
for vec, val in features.shapes(source=mndwi_msk.astype(np.float32), transform=src_transform):
mypoly.append({'geom': shapely_shp(vec), 'value': val})
# Now, let's convert this to `GeoDataFrame` as it will be easy to do vector based operations as well as to create viz.
# In[17]:
# to gdf
submerged = gpd.GeoDataFrame(mypoly, crs=src_crs, geometry='geom')
# In[18]:
# value counts
submerged['value'].value_counts()
# We see that `submerged` have two unique values (0.0, 1.0). However, `1.0` corresponds to `water` so it makes sense to filter out land-area from gdf.
# In[19]:
# filter by water area
submerged = submerged[submerged['value'] > 0]
# In[20]:
# shape should be 203 based on value count cell above
submerged.shape[0]
# In[21]:
# quick plot
submerged.plot(figsize=(15, 10));
# Now that we have vectorized boundaries of flooded area which we will use to intersect with building footprint polygons from OSM. It is better to collapse all individual polygons into a single Multipolygon geometry.
# Also, note the x and y axis values!! (**HINT:** crs)
# In[22]:
boundary = cascaded_union(list(submerged['geom']))
# ## Retrieve Building Footprints from OSM
# In this section, we will now retrieve building footprints from OpenStreetMap. At UP42, we recently developed an [OpenStreetMap Data Block](https://marketplace.up42.com/block/df2ec03a-50c4-47ac-8a83-2db613869cf9) which is available on our marketplace.
# As hinted before, the coordinate system of Sentinel-2 data is in `EPSG:3857` and the OSM query is done in `EPSG:4326`. Therefore, we'll need to perform some coordinate transformation here for both the OSM data retrieval as well as plotting the results with Folium. Let's define the transform.
# In[23]:
# transform crs
crs_project = partial(
pyproj.transform,
pyproj.Proj(init='epsg:3857'), # source coordinate system
pyproj.Proj(init='epsg:4326') # destination coordinate system
)
# In[24]:
# read bounds from dataset and transform to wgs84
osm_poly = transform(crs_project, box(*src_bounds))
# In the next part, we will create a new workflow within the scope of same project that we initialized and authenticated in earlier part.
# In[25]:
workflow_osm = project.create_workflow(name="flooding_osm")
# input task name for the OSM
input_tasks_osm = ['openstreetmap']
# Update workflow object with our desired data block as input_task(s)
workflow_osm.add_workflow_tasks(input_tasks=input_tasks_osm)
# read aoi
aoi = up42.read_vector_file("data/aoi_bellevue_US.geojson", as_dataframe=True)
# construct input parameters
input_parameters = workflow_osm.construct_parameters(geometry=aoi,
geometry_operation="bbox",
start_date="2019-03-21",
end_date="2021-03-21")
# The job is almost ready to be run. If you notice carefully, the `end_date` parameter is in the future. The block converts the future date to today's date.
#
# We have constructed the `input_parameters` but we haven't yet mentioned that we want to retrieve `building_footprints` from OSM. This block offers all possible combinations of the map features offerred by OSM. Full list can be found [here](https://wiki.openstreetmap.org/wiki/Map_Features). Table below lists some examples of constructing tags.
#
# |OSM feature |Input Parameter (`osm_tags`) |
# |--------------------|-----------------------------|
# |Roads |`highway=*` |
# |Water bodies |`natural=water` |
# |Building footprints |`building=*` |
# |Land use |`landuse=*` |
#
# Here, the variable `input_parameters` is nothing but a native python `dict`. We can add `osm_tag = ["building=*"]` to this dictionary. The key `osm_tag` can be a list of all above tags as well.
# In[26]:
# add osm_tags to input_parameters
input_parameters['openstreetmap:1']["osm_tags"] = ["building=*"]
# In[27]:
# run the job
job_osm = workflow_osm.run_job(input_parameters=input_parameters, track_status=True)
# In[28]:
# define download directory
osm_dir = "./results/osm/"
# download results
job_osm.download_results(osm_dir)
# In[29]:
building_footprints_path = [i for i in job_osm.results if "building=*" in i][0]
building_footprints_path
# In[30]:
gdf = gpd.read_file(building_footprints_path)
# reproject to EPSG:3857
gdf_proj = gdf.to_crs(src_crs)
# In[31]:
gdf_proj.plot(figsize=(15, 10));
# Notice that the coordinates, here, are in (lat, lon). Now, that data is extracted, we can transform it to the crs of the dataset.
# Doing this right now prevents any inconsistencies (with plotting, intersecting etc.) at later point as well as ensures sanity!!
# In[32]:
gdf_proj.head(1)
# ## Extract Flood Impacted buildings
# Until this point, we set the stage for actual task. Basically, the building footprints that intersects the Multipolygon (one we created from MNDWI) are effected building and those that are not are not-effected buildings!
# In[33]:
effected = gdf_proj[gdf_proj['geometry'].intersects(boundary)]
# change coordinate system for plotting
effected.to_crs(crs='EPSG:4326', inplace=True)
# In[34]:
not_effected = gdf_proj[~gdf_proj['geometry'].intersects(boundary)]
# change coordinate system for plotting
not_effected.to_crs(crs='EPSG:4326', inplace=True)
# In[35]:
# quick sanity check
gdf_proj.shape[0] == (effected.shape[0] + not_effected.shape[0])
# It's time for plotting. We will use Folium library for displaying our results. Of course, it requires a bit of massaging before we get to the plotting
# ### Plotting
# In[36]:
# bbox centroid serves as the center point for the folium map
bbox_centroid = list((osm_poly.centroid).coords[:][0])
bbox_centroid = [bbox_centroid[-1], bbox_centroid[0]]
bbox_centroid
# In[37]:
# extracts bounds for image overlay
lon_min, lat_min, lon_max, lat_max = osm_poly.bounds
# In[38]:
style1 = {'fillColor': '#228B22', 'color': 'red'}
style2 = {'fillColor': '#00FFFFFF', 'color': '#00FFFFFF'}
# In[39]:
# init folium map
m = folium.Map(bbox_centroid, zoom_start=15)
# add effected buildings
folium.GeoJson(effected.to_json(), style_function=lambda x:style1).add_to(m);
# add not_effected buildings
folium.GeoJson(not_effected.to_json(), style_function=lambda x:style2).add_to(m);
# add raster png quicklook
folium.raster_layers.ImageOverlay(image=ql_path, bounds=[[lat_min, lon_min], [lat_max, lon_max]], opacity=0.8).add_to(m);
# In[40]:
folium.LayerControl().add_to(m)
m
# As we can see the buildings impacted by flooding in red and those that are not impacted are in blue.
# It should be noted that success of the analysis depends on the availability of the data in OSM!!
# In[41]:
m.save("/Users/prayag.thakkar/Desktop/folium_map.html")
# In[ ]: