API demonstration
Table of contents
This notebook is to show and demonstrate the use of Application
Program Interface (API) developed in the frame of the PYRAWS
project to open and process Sentinel-2 Raw data
, corresponding to
a decompressed version of Sentinel-2 L0
data
with additional metada. The API are demonstrated on the
Temperature Hotspots RAW Sentinel-2 (THRAWS)
dataset. We will
introduce the use of the Raw_event
and raw_granule
classes to
process Raw granules
and Raw events
containing images of
volcanic eruptions. It will show how to stack different
Raw granules
acquired during the movement of the satellite along
track and how to perform a coarse onboard coregistration of Raw
bands. Furthermore, it will introduce the APIs to extract specific
bands coordinates. Finally, after introducing the equivalent
L1C_tiles
and L1C_event
, the notebook will show the API to
mosaic the L1C
tiles and crop them around the specific
raw_granule
bands coordinats to have both the L1c
and Raw
products looking at the same area. Finally, it will show how to
process the L1C
information to doublechek the presence of an
eruption by exploiting an algorithm developed on L1c
data that
would work for Raw
data.
1) Imports, paths and variables
Limit CUDA visible devices
import os
os.environ['CUDA_VISIBLE_DEVICES']='5'
Autoreload
%load_ext autoreload
%autoreload 2
Imports
import sys
sys.path.insert(1, os.path.join("..", ".."))
sys.path.insert(1, os.path.join("..", "..", "scripts_and_studies", "hta_detection_algorithms"))
from pyraws.raw.raw_event import Raw_event
from pyraws.l1.l1_event import L1C_event
from pyraws.utils.l1_utils import read_L1C_image_from_tif
from pyraws.utils.visualization_utils import plot_img1_vs_img2_bands
from s2pix_detector import s2pix_detector
from functools import partial
from geopy.geocoders import Nominatim
import geopandas
from geopandas import GeoSeries
import matplotlib.pyplot as plt
from shapely.geometry import Polygon
from termcolor import colored
import torch
from skimage.measure import label, regionprops
import matplotlib.patches as patches
This import is to remove odd errors on libiomp5md.dll
. If you do
not have them, you can skip it
os.environ['KMP_DUPLICATE_LIB_OK']='True'
Set torch device. Use “CUDA” as default if available.
if torch.cuda.is_available():
device=torch.device("cuda")
else:
device=torch.device("cpu")
Set size of figure plots.
plt.rcParams['figure.figsize'] = [10, 10]
2) - Raw_event and raw_granule
To request Raw
files it is necessary to query the database by
specifying a polygon (area) and a date range (start - end). For the
event Etna_00
, shown in the image above, the blue rectangular is
the polygon used to query the database (the eruption is the blue spot
in the image in the center of the rectangular). Upon a query, the
database will download the collection of Raw granules
whose
reference band (B02
) intersects the blue polygon in the specified
date range . An Raw granule
(red rectangulars) corresponds to
the area acquired by the all 13 Sentinel-2 bands
of a single
detector over a single acquisition (lasting 3.6 s). The various Raw
granules in the collection might be produced in different instants
and by different detectors (Sentinel 2 has 12 detectors staggered
across track). We named this collection of Raw granules
referred
to a specific event (Etna_00
) an Raw event
. Such concepts of
Raw granule
and Raw event
(collection of the
Raw granules
) are made through the classes Raw_granule
and
Raw_event
. When an object Raw_event
is created, it
instatiates a collection of Raw_granule
objects each one
containing the information related to each Raw granule
.
To create an Raw_event
object, please specify the
requested_bands
and the requested event_name
.
requested_bands=["B8A","B11","B12"]
event_name="Etna_00"
The next lines will parse query the thraw_db.csv
database with
the requested event_name
, enabling the creation of the
Raw_event
with the requested bands.
event=Raw_event(device=device)
event.from_database(event_name, requested_bands)
3) - Showing Raw granules info
The next lines will show the information related to the granules that
compose the instantiated Raw_event
.
event.show_granules_info()
Interpretation of granules information. As you can see, the
Raw_event
is composed of a collection of Raw_granule
objects,
matching the Raw_granules
whose reference band interesects the
area used to request for Raw data. The method
show_granules_info()
of the class Raw_event
prints all the
granules composing an Raw_event. For each of the granules, the
function shows the granule name
, sensing time
,
Creation time
, detector number
, originality
, parents
,
polygon coordinates
(of vertices), cloud coverage
(percentage/100). originality
and parents
are needed in case
the granule is created through some processing of other granules
(such as stacking or coregistration, see next cells). If this is not
the case, originality
will be True
and the list of granules
parents will be empty. If the granule is created by stacking two
granules, originality
will be False
and parents
will
contain the name of the granules used for stacking. In this case, all
the information are also shown for parents
.
In the next lines, we will select the granule 0
and will show the
bands requested when the Raw_event
was created.
raw_granule=event.get_granule(0)
raw_granule.show_bands(downsampling=True)
4) - Compose granules
The APIs offer utils to compose granules along and across track.
However, the granules from an event cannot composed arbitrarily.
Indeed, to compose two granules along track they must have the
same detector number and the sensing-time to be different of
3.6 s (3 or 4 seconds). For the event Etna_00
, granules [0,2],
[1,3], [3,5] can be stacked along tracks. The next line will stack
granules [0,2] along track. The string “T” means that the granule 0
will be stacked on top of 2.
raw_granule_0_2_stacked=event.stack_granules([0,2], "T")
Showing stacked granule info. By using the method
get_granule_info()
of the classs raw_granule
, you can get the
granule information. You can see that granule is now marked as not
original (originality
is set to False
). This is because the
raw_granule_0_2_stacked
is the result of combination of two
granules. In this case, the get_granule_info()
function will show
will print sensing time
, acquisition time
,
detector number
for the granule parents. originality
will be
False
and the list of granules parents will be not None
. You
can notice that the granule name is composed by the parents’name
separated by the keyword STACKED_T, where T means the first
granule is stacked at the top of the second one.
raw_granule_0_2_stacked.show_granule_info()
The same effect can be used by using the Raw_event
method
get_stackable_granules()
, which permits extracting the couples of
granules that can be stacked along-track automatically.
stackable_granules, stackable_couples=event.stack_granules_couples()
raw_granule_0_2_stacked=stackable_granules[0]
raw_granule_0_2_stacked.show_granule_info()
You can now see by superimposing the bands of stacked granules. As
you can see that bands do not look coregistered. This is because the
pushbroom nature of Sentinel-2, for which every band is looking at
different areas during a single acquisition (granule) (and SWIR
and visible
bands are respectively rotated).
raw_granule_0_2_stacked.coarse_coregistration(crop_empty_pixels=False, verbose=True).show_bands_superimposition(equalize=False)
raw_granule_0_2_stacked.coarse_coregistration_old(crop_empty_pixels=True, verbose=True).show_bands_superimposition(equalize=False)
5) - Coarse bands coregistration
In some cases, the event (fire/volcanic eruption) will be not contained in a single granule. In other cases, if the eruption is located close to the top/bottom margin, the information of some of the bands could be missing because of lack of bands registration. Therefore, we stack to granules along track to try to overcome to this limitation.
Bands can now be roughly coregistered. Coregistration is perfomed
by shifting the bands of a number of pixel
S_{k,l}|_{(S,D)}=\([N_{B_k,B_l}, M_{B_k,B_l}]|_{(S,D)}\)
specific for the couple of bands \((B_k,B_l)\) produced by the
detector having detector number \(D\) in the satellite \(S\)
(S2A or S2B). \(N_{B_k,B_l}\) is the number of along-track shift
pixels, used to compensate the systematic band shifts due to the
pushbroom nature of the sensor. Similarly, \(M_{B_k,B_l}\) is the
average number of across-track pixels in the THRAW
dataset for a
certain couple \((S,D)\). To this aim, \(S_{k,l}|_{(S,D)}\)
are stored in a Look Up Table
and used regardelss the position of
the satellite. It shall be noted that \(S_{k,l}|_{(S,D)}\)
indicates the number of pixels shift for which the band \(B_l\)
shall be moved to match the band \(B_k\). Since \((B_k,B_l)\)
could have different resolution, \(S_{k,l}|_{(S,D)}\) is
expressed with respect to \(B_l\) resolution. Having more than 2
bands leads to coregister al the bands with respect to the first one.
For instance, when using [B8A
, B11
,B12
] bands B12
and B11
are coregistered with respect to B8A
.
The next line extracts the granule 2 from the event and performs the coregistration.
# Get granule 2
raw_granule=event.get_granule(2)
# Get granule 2 from the Raw event and perform coarse coregistration
raw_granule_registered=event.coarse_coregistration(granules_idx=[2])
Showing unregistered vs coarse registered granule.
# Plotting unregistered vs registered granule
fig, ax = plt.subplots(1,2)
ax[0].set_title("Unregistered")
raw_granule_tensor=raw_granule.as_tensor(downsampling=True)
# Plot normalizing on the max
ax[0].imshow(raw_granule_tensor/raw_granule_tensor.max())
ax[1].set_title("Coarse coregistered")
raw_granule_registered_tensor=raw_granule_registered.as_tensor(downsampling=True)
# Plot normalizing on the max
ax[1].imshow(raw_granule_registered_tensor/raw_granule_registered_tensor.max())
plt.show()
As you can see in the image above, on the bottom of the registered
images there is an image where only one band is not null. This is due
to the fact that B11
and B12
are shifted to match B8A
,
leaving some area uncovered. This could create a problem every time
the high temperature anomaly (fire/volcanic eruption) will be
placed in that area. To overcome to this limitation, it is possible
to fill the missing pixels by filling the missing pixels with the
ones of adjacent granules. To this aim, you can call the
coarse_coregistration
function with the flag
use_complementary_granules
set to True
. In this way, if
appropriate adjacent filler granules are available, they will be
retrieved and adjacent pixels used to fill the missing pixels.
FIlling will be performed across-track as well.
# Get granule 2 from the Raw event and perform coarse coregistration with filling elements
raw_granule_registered_filled=event.coarse_coregistration(granules_idx=[2], use_complementary_granules=True)
As you can see in the image below, missing pixels are now filled.
# Plotting unregistered vs registered granule
fig, ax = plt.subplots(1,2)
ax[0].set_title("Unregistered")
raw_granule_tensor=raw_granule.as_tensor(downsampling=True)
# Plot normalizing on the max
ax[0].imshow(raw_granule_tensor/raw_granule_tensor.max())
ax[1].set_title("Coarse coregistered (filled)")
raw_granule_registered_filled_tensor=raw_granule_registered_filled.as_tensor(downsampling=True)
# Plot normalizing on the max
ax[1].imshow(raw_granule_registered_filled_tensor/raw_granule_registered_filled_tensor.max())
Alternatively, it is also possible to crop those empty pixels. To
this aim, you can call the coarse_coregistration
function with
the flag crop_empty_pixels
set to True
. If you set both the
use_complementary_granules
and crop_empty_pixels
flags to
True
, filler elements will be used when available, otherwise
missing pixels will be cropped.
# Get granule 2 from the L0 event and perform coarse coregistration with filling elements
raw_granule_registered_cropped=event.coarse_coregistration(granules_idx=[2], crop_empty_pixels=True)
The next plot shows all the possible cases.
raw_granule_registered_cropped_tensor=raw_granule_registered_cropped.as_tensor(downsampling=True)
fig, ax = plt.subplots(2,2)
ax[0,0].set_title("Unregistered")
ax[0,0].imshow(raw_granule_tensor/raw_granule_tensor.max())
ax[0,1].set_title("Coarse coregistered")
# Plot normalizing on the max
ax[0,1].imshow(raw_granule_registered_tensor/raw_granule_registered_tensor.max())
ax[1,0].set_title("Coarse coregistered (filled)")
ax[1,0].imshow(raw_granule_registered_filled_tensor/raw_granule_registered_filled_tensor.max())
ax[1,1].set_title("Coarse coregistered (cropped)")
ax[1,1].imshow(raw_granule_registered_cropped_tensor/raw_granule_registered_cropped_tensor.max())
plt.show()
6) - Getting Raw granule and bands coordinates
Get image coordinates.
Granules coordinates are stored InventoryMetadata.xml
. The
ploygon represents the Granule Footprint
, meaning the area
covered by all the bands of one detector. The next lines extract the
granule 2 from the Raw_event and performs the coregistration with
filling elements. Then, it extract the coordinates of the entire
granule.
raw_granule_coregistered=event.coarse_coregistration(granules_idx=[2], use_complementary_granules=True, downsampling=False)
coordinates=raw_granule_coregistered.get_granule_coordinates()
The next lines show the area covered by the granule.
def plot_granule_coordinates(coordinates):
try:
world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))
world=world.to_crs("EPSG:4326")
geolocator = Nominatim(user_agent="google")
reverse = partial(geolocator.reverse, language="en")
address=reverse(coordinates[0])[0]
country=address[-address[::-1].find(","):][1:]
coordinates=[(y,x) for (x,y) in coordinates]
poly=GeoSeries([Polygon([x for x in coordinates +[coordinates[0]]])])
poly=poly.set_crs("EPSG:4326")
ax=world.query('name == \"'+country+'\"').plot()
poly.plot(facecolor='red', edgecolor='red',ax=ax)
print("Address: ", colored(address, "red"))
except:
print("Impossible to plot granule over the requested area.")
plot_granule_coordinates(coordinates)
Moreover, it is also possible to perform georeferencing of the single bands. The next lines shows how to get the coordinates of the coregistered bands.
band_shifted_dict=raw_granule_coregistered.get_bands_coordinates()
band_shifted_dict
Showing bands coregistered bands positions.
def plot_granule_coordinates(coordinates, coordinates_shifted_dict):
world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))
world=world.to_crs("EPSG:4326")
geolocator = Nominatim(user_agent="google")
reverse = partial(geolocator.reverse, language="en")
address=reverse(coordinates[0])[0]
country=address[-address[::-1].find(","):][1:]
coordinates=[(y,x) for (x,y) in coordinates]
poly=GeoSeries([Polygon([x for x in coordinates +[coordinates[0]]])])
poly=poly.set_crs("EPSG:4326")
band_names=list(coordinates_shifted_dict.keys())
color_mask=['blue', 'yellow', 'green']
ax=world.query('name == \"'+country+'\"').plot()
poly.plot(facecolor='red', edgecolor='red',ax=ax)
for n in range(len(coordinates_shifted_dict)):
coordinates_shifted_band=coordinates_shifted_dict[band_names[n]]
address_shifted=reverse(coordinates_shifted_dict[band_names[n]][0])[0]
country_shifted=address_shifted[-address_shifted[::-1].find(","):][1:]
coordinates_shifted_band=[(y,x) for (x,y) in coordinates_shifted_band]
poly_shifted=GeoSeries([Polygon([x for x in coordinates_shifted_band +[coordinates_shifted_band[0]]])])
poly_shifted.plot(facecolor=color_mask[n%3], edgecolor=color_mask[n%3],ax=ax)
print("Address: ", colored(address, "red"))
plot_granule_coordinates(coordinates,band_shifted_dict)
To compare the coordinates of your the different bands, you can use
the create_geee_polygon
function and use the polygons on
Google Earth Engine
and visually compare them with respect the
bands images and with respect to the polygon area.
def create_geee_polygon(coordinates, polygon_name, switch_lat_lon=True):
if switch_lat_lon:
pol_coordinates=[[y,x] for (x,y) in coordinates]
else:
pol_coordinates=coordinates
pol_string="var "+polygon_name + " = ee.Geometry.Polygon("+str(pol_coordinates)+");"
print(pol_string)
return pol_string
7) - Open and crop L1C tiles
7.a) - Open L1C tiles and events
Getting L1C
event corresponding to the L0
event.
l1c_event=L1C_event(device=device)
l1c_event.from_database(event_name, requested_bands)
Printing L1C
tiles info.
l1c_event.show_tiles_info()
7.b) Mosaicing and cropping L1C tiles on an L0 band coordinates.
It is now possible to mosaic L1C tiles and crop them over the area of
the previous L0 granules. In this way, it is possible to process L1C
tiles and reproject the retrieved information (e.g., event bounding
boxes) on the correspondent L0 granule. The cropped L1C file is a
“TIF” file. An ending
is added to keep track of the indices of
the granules used(i.e. ending = “2”).
ending="2"
output_cropped_tile_path=l1c_event.crop_tile(band_shifted_dict[requested_bands[0]], None,out_name_ending=ending, lat_lon_format=True)
output_cropped_tile_path
Plotting results.
Raw granule
raw_granule_coregistered.show_bands_superimposition()
L1C cropped tile
l1c_tif, coords_dict, expected_class=read_L1C_image_from_tif(event_name, ending, device=device)
plt.imshow(l1c_tif)
raw_granule_coregistered.show_bands_superimposition()
8) - Processing L1C tiles
It is now possible to process the cropped L1C
data to spot a
volcanic eruption by using a simplified version of the algorithm
Massimetti, Francesco, et
al..
Get hotmap.
_, l1c_filtered_alert_matrix, l1c_alert_matrix=s2pix_detector(l1c_tif)
Show alert matrix and filtered_alert_map.
plot_img1_vs_img2_bands(l1c_tif, l1c_tif, ["alert_map", "filtered_alert_map"], l1c_alert_matrix, l1c_filtered_alert_matrix)
Show Raw granule and equivalent L1C cropped area with the correspondent filtere_alert_map.
raw_granule_tensor=raw_granule_coregistered.as_tensor()
plot_img1_vs_img2_bands(raw_granule_tensor/raw_granule_tensor.max(), l1c_tif, ["L0", "L1C + Hotmap"], None, l1c_filtered_alert_matrix)
Show L1C cropped area with the correspondent filtere_alert_map and bounding boxes.
mask=l1c_filtered_alert_matrix.numpy()
lbl = label(mask)
props = regionprops(lbl)
l1c_numpy=l1c_tif.numpy()
fig, ax = plt.subplots()
ax.imshow(l1c_tif)
for prop in props:
print('Found bbox', prop.bbox)
bbox = prop.bbox # x, y, width, height
rect = patches.Rectangle((bbox[1], bbox[0]), abs(bbox[1]-bbox[3]), abs(bbox[0]-bbox[2]), linewidth=2, edgecolor='y', facecolor='none')
ax.add_patch(rect)
plt.show()